GraphQL查询的基本单位是字段。每个字段对应一个值或一个嵌套对象,查询的执行结果与查询的结构同构。
{ user(id: "1001") { name email posts(limit: 3) { title likesCount } } }
这个查询返回的JSON结构与查询文本完全对应——这叫"查询-响应同构"(Query-Response Isomorphism)。这是GraphQL最重要的设计特性之一:你看到查询文本就能知道返回数据的形状,不需要查文档。
字段的嵌套深度没有规范层面的限制,但在实践中需要设置合理的深度限制(通常5-7层),防止恶意查询消耗过多资源。
字段可以接收参数,参数在Schema中定义类型和默认值:
type Query { posts(limit: Int = 10, sortBy: SortOrder = NEWEST): [Post!]! user(id: ID!, includeInactive: Boolean = false): User } enum SortOrder { NEWEST OLDEST POPULAR }
查询中使用参数:
{ posts(limit: 5, sortBy: POPULAR) { title likesCount } }
必需参数用 ! 标记(如 id: ID!),省略时查询验证会报错。有默认值的参数可以省略。
别名解决了一个实际需求:在同一个查询中对同一字段做不同的参数请求。比如你想同时获取最新帖子和最热帖子:
{ newest: posts(limit: 5, sortBy: NEWEST) { title createdAt } popular: posts(limit: 5, sortBy: POPULAR) { title likesCount } }
没有别名,两次调用 posts 会产生同名字段冲突。别名 newest 和 popular 让它们在响应中有不同的键名:
{ "data": { "newest": [ { "title": "...", "createdAt": "..." }, ... ], "popular": [ { "title": "...", "likesCount": 42 }, ... ] } }
别名也可以用于给字段起更语义化的名称:
{ currentUser: user(id: "1001") { displayName: name profilePic: avatarUrl } }
当多个查询需要选择相同的字段组合时,片段帮你避免重复。片段定义一组字段,在查询中通过 ...FragmentName 引用。
fragment UserSummary on User { id name avatarUrl } fragment PostPreview on Post { id title createdAt author { ...UserSummary } likesCount }
在查询中使用:
query Feed { feed(limit: 10) { ...PostPreview comments(first: 2) { body author { ...UserSummary } } } } query Profile($userId: ID!) { user(id: $userId) { ...UserSummary email posts(limit: 5) { ...PostPreview } } }
片段的好处是:如果 UserSummary 的字段定义需要变更(比如要加 bio 字段),只改片段定义就行,所有引用该片段的查询自动更新。
片段还有内联写法,适合一次性使用:
{ user(id: "1") { ... on User { name email } } }
变量让查询可复用且类型安全。变量在操作名后声明类型,在查询体内用 $ 引用。
query SearchPosts($query: String!, $category: SearchCategory, $limit: Int = 10) { search(query: $query, category: $category, limit: $limit) { ... on Product { id name price } ... on User { id name avatarUrl } ... on Category { id name productCount } } }
变量值通过HTTP请求体单独传递:
{ "query": "query SearchPosts($query: String!, ...) { ... }", "variables": { "query": "graphql", "category": "TECHNOLOGY", "limit": 5 } }
这种分离有几个好处:查询文本是常量字符串,可以预编译和缓存;变量值是数据,可以安全地从用户输入获取而不用担心注入;变量有类型约束,Schema验证层会拦截类型不匹配的输入。
联合类型和接口需要用内联片段来根据运行时类型选择不同字段:
{ search(query: "iphone") { __typename ... on Product { id name price inStock } ... on User { id name bio postCount } ... on Category { id name slug } } }
__typename 是GraphQL内置的元字段,返回当前对象的实际类型名。这在客户端处理联合类型返回值时非常有用——你知道该按哪种类型的数据结构来渲染。

把以上元素组合起来,一个真实的查询可能是这样的:
query FeedWithPagination( $limit: Int = 10 $cursor: String $userId: ID! ) { user(id: $userId) { ...UserInfo recentPosts: posts(first: $limit, after: $cursor) { pageInfo { hasNextPage endCursor } edges { node { id title ...PostMeta author { ...UserInfo } } } } } } fragment UserInfo on User { id name avatarUrl } fragment PostMeta on Post { createdAt likesCount commentsCount }
这个查询同时用到了:操作名和变量声明(FeedWithPagination、$limit、$cursor)、别名(recentPosts)、片段(UserInfo、PostMeta)、嵌套字段选择、参数传递。这是GraphQL查询语法在真实场景中的典型用法——看起来复杂,但每个元素都有明确的职责。
变量解决了"值不同"的问题,指令解决"结构不同"的问题。@include 和 @skip 是规范内置的两个指令,根据布尔变量决定字段是否出现在查询里:
query Profile($userId: ID!, $withEmail: Boolean!, $showPosts: Boolean!) { user(id: $userId) { name email @include(if: $withEmail) posts(limit: 5) @skip(if: $showPosts) { title } } }
@include(if: true) 等价于把字段写进去,@skip(if: true) 等价于删掉字段。它们很适合"同一个组件在不同端展示不同字段"的场景——服务端 Schema 不用为每个端设计不同接口,客户端用变量控制结构。
别名冲突。同一个 SelectionSet 里两个字段解析后的键名相同就会冲突,比如两个 user: user(id: ...) 不能同时出现,必须换别名。
变量类型必须与 Schema 参数兼容。$limit: Int 不能传给 limit: Int! 参数,可空值传非空参数会在验证阶段失败;枚举参数不能传字符串字面量,必须用枚举值。
片段只能在匹配的返回类型上展开。...UserSummary on User 不能用在返回 Post 的字段上;要处理联合或接口类型,必须用内联片段加类型条件。
多操作时必须有 operationName。一个请求体里出现多个操作时,客户端必须显式指定 operationName,否则服务器不知道执行哪一个。Apollo 等客户端默认只发一个操作,但如果代码生成工具把多个操作拼进一个文档,就要留意这个细节。
REST 里"获取某用户的最近 5 篇帖子"要写 GET /users/1/posts?limit=5,字段裁剪靠服务端实现或 OData 一类的扩展。GraphQL 把"要哪些字段、要多少条、按什么顺序"全部变成查询的一部分,客户端可以精确到字段粒度控制响应。这也是为什么同一个 Schema 能同时服务 Web 端、移动端和管理后台——三个端各自声明自己的字段组合,服务端解析器只需要写一份。