GraphQL 语法速查表 - GraphQL 查询语言与 API 设计大全
这套速查给正在设计 GraphQL Schema、或是从 Apollo/URQL 等客户端调用它的开发者。先讲客户端最常敲的词汇——精准选取所需字段的查询、变更数据、订阅,以及复用片段;再讲服务的另一侧,从类型系统、指令到分页和 __typename 自省。与泛泛的语法表不同,条目按你在一次 API 会话里扮演的角色(请求方/提供方)来分。读完应能写出带参数和片段的精确查询、声明一个字段类型合理的 type,并定义一个连接式分页字段。
典型使用场景
面向需要同时定义 API 契约与前端数据形状的全栈工程师,尤其是从 REST 迁移过来、想搞清楚「字段级请求」到底改变了什么的人。GraphQL 的收益是把「客户端要什么就取什么」变成可执行的契约;代价是服务端必须自己管字段级授权、N+1、以及查询复杂度。本表覆盖查询(含别名、片段、@include/@skip、游标分页)、mutation 与 subscription、类型系统(object/input/enum/union/interface/scalar/directive)、以及 introspection(用于客户端代码生成与 schema 探索)。需要建立的心智模型是:一次 HTTP 请求可以触发任意查询,因此没有「URL 少写一个参数」这层天然防护,鉴权与深度限制必须显式加。
查询 Query 9
{ user { id name } }{ user(id: 1) { name } }{ users { id name posts { title } } }query GetUser($id: ID!) { user(id: $id) { name } }{ user { name email } posts { title } }{ user(id: 1) { name @include(if: $withName) } }{ user(id: 1) { name @skip(if: $skipName) } }{ me: user(id: 1) { name } you: user(id: 2) { name } }{ users(first: 10) { edges { node { name } } } }变更 Mutation 5
mutation { createUser(name: "Alice") { id } }mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id } }mutation { updateUser(id: 1, name: "Bob") { id name } }mutation { deleteUser(id: 1) { id } }mutation { bulkCreate(input: [{ name: "A" }, { name: "B" }]) { id } }订阅 Subscription 3
subscription { newMessage { content } }subscription { userUpdated(id: 1) { name } }subscription($roomId: ID!) { messageAdded(roomId: $roomId) { id content } }类型系统 Schema 10
type User { id: ID! name: String! email: String }input CreateUserInput { name: String! email: String! }enum Role { ADMIN USER GUEST }union SearchResult = User | Postinterface Node { id: ID! }type User implements Node { id: ID! name: String! }scalar DateTimetype Query { user(id: ID!): User }type Mutation { createUser(input: CreateUserInput!): User }directive @auth(requires: Role!) on FIELD_DEFINITION片段与指令 5
fragment UserFields on User { id name email }{ user { ...UserFields } }{ user { ...UserFields ... on Admin { permissions } } }@deprecated(reason: "Use newField")@cacheControl(maxAge: 3600)内省与验证 Introspection 5
{ __schema { types { name } } }{ __type(name: "User") { fields { name type { name } } } }{ __schema { queryType { name } mutationType { name } } }{ __type(name: "User") { fields { name args { name type { name } } } } }{ __schema { directives { name locations } } }高级特性 5
query($id: ID!, $withPosts: Boolean!) { user(id: $id) { name posts @include(if: $withPosts) { title } } }{ users(first: 10, after: "cursor") { pageInfo { hasNextPage endCursor } } }{ user { name } } # 期望 user.name,错误会被聚合返回query @cost(complexity: 10) { user { name } }extend type User { age: Int }参数矩阵
| 参数 | 作用 | 示例 |
|---|---|---|
{ user { id name } } | 最简查询:选择集必须至少有一个字段,直接写顶层字段名即等价于 query 关键字 | { user { id name } } |
{ user(id: 1) { name } } | 带参数的字段,参数类型与实参必须严格匹配(ID 不会再接受字符串 "1" 之外的类型) | { user(id: 1) { name } } |
query GetUser($id: ID!) { user(id: $id) { name } } | 具名操作 + 变量:把入参从查询文本里提出来,便于缓存复用与打印调试 | query GetUser($id: ID!) { user(id: $id) { name } } |
me: user(id: 1) { name } you: user(id: 2) { name } | 别名:同一字段取多次时用别名区分,避免字段冲突(Response Field Merging) | { me: user(id: 1) { name } you: user(id: 2) { name } } |
name @include(if: $withName) | 条件包含:变量为 true 才请求该字段,适合可选展开的详情场景 | { user(id: 1) { name @include(if: $withName) } } |
name @skip(if: $skipName) | 条件跳过,与 @include 互补;两者可同时作用同一字段 | { user(id: 1) { name @skip(if: $skipName) } } |
fragment UserFields on User { id name email } | 片段复用:命名片段必须带 on 类型,且必须在查询文本顶层定义 | fragment UserFields on User { id name email } |
{ user { ...UserFields ... on Admin { permissions } } } | 内联片段配合 union/interface 取具体类型的独有字段 | { user { ...UserFields ... on Admin { permissions } } } |
{ users(first: 10) { edges { node { name } } } } | Relay 风格分页的典型形状,edges/node 解耦了「条目」与「分页元信息」 | { users(first: 10) { edges { node { name } } } } |
pageInfo { hasNextPage endCursor } | 配合 first/after 的游标分页,取下一页所需游标,避免深分页的 offset 代价 | { users(first: 10, after: "cursor") { pageInfo { hasNextPage endCursor } } } |
mutation { createUser(name: "Alice") { id } } | 变更操作必须显式写 mutation 关键字,且字段内仍需写明要返回的内容 | mutation { createUser(name: "Alice") { id } } |
mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id } } | 用 input 对象承载参数,是复杂入参的标准写法;! 表示非空 | mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id } } |
bulkCreate(input: [{ name: "A" }, { name: "B" }]) | 列表入参支持批量写入,但服务端必须自行实现事务与逐条错误返回 | mutation { bulkCreate(input: [{ name: "A" }, { name: "B" }]) { id } } |
subscription { newMessage { content } } | 订阅需要长连接与解析器支持,不是所有传输层都实现;服务端要处理断线重连 | subscription { newMessage { content } } |
type User { id: ID! name: String! email: String } | object 类型;非空标记放在服务端才能保证,真正的不变量在这里定义 | type User { id: ID! name: String! email: String } |
input CreateUserInput { name: String! email: String! } | input 类型只能作入参,不能作返回值,这是 GraphQL 强制字段级拆分的原因 | input CreateUserInput { name: String! email: String! } |
enum Role { ADMIN USER GUEST } | 枚举限制取值集合,比 String 自由文本更安全,前端可直接穷举 | enum Role { ADMIN USER GUEST } |
union SearchResult = User | Post | union 表示「是其中之一」,必须配内联片段才能取具体字段 | union SearchResult = User | Post |
interface Node { id: ID! } | interface 抽出多个类型共享的字段契约,object 用 implements 声明实现 | type User implements Node { id: ID! name: String! } |
scalar DateTime | 自定义标量,序列化与解析逻辑由服务端实现,需与客户端约定格式 | scalar DateTime |
directive @auth(requires: Role!) on FIELD_DEFINITION | 自定义指令位置声明,是实现字段级鉴权与复杂度控制的入口 | directive @auth(requires: Role!) on FIELD_DEFINITION |
@deprecated(reason: "Use newField") | 标记字段废弃,客户端代码生成会给出警告而不是直接报错 | @deprecated(reason: "Use newField") |
@cacheControl(maxAge: 3600) | 缓存提示,由网关或服务端据此设置响应缓存头 | @cacheControl(maxAge: 3600) |
{ __schema { types { name } } } | 内省查全部类型,是客户端代码生成与 schema 探索的入口 | { __schema { types { name } } } |
{ __type(name: "User") { fields { name args { name type { name } } } } } | 按名字查某个类型的字段与参数,查错字段名时最直接 | { __type(name: "User") { fields { name name args { name type { name } } } } } |
{ __schema { queryType { name } mutationType { name } } } | 确认入口类型名,避免在只读网关上误发 mutation | { __schema { queryType { name } mutationType { name } } } |
query @cost(complexity: 10) { user { name } } | 声明查询代价,配合服务端上限拦截恶意深层嵌套查询 | query @cost(complexity: 10) { user { name } } |
extend type User { age: Int } | 扩展已有类型,模块化拆分 schema 时用它避免重复声明 | extend type User { age: Int } |
易错点与避坑指南
现象查询报 "Cannot query field \"xxx\" on type \"Query\"",但 schema 里明明有这个字段。
原因schema 有多个 entry 文件,字段定义分散;或 introspection 被网关按路径拦截,客户端拿到的是被裁剪过的子集。
处置用 { __schema { queryType { fields { name } } } } 确认服务端真正暴露的字段,再核对是否需要鉴权才能 introspect。
现象同一请求里查了两个同名字段但参数不同,返回结果只有一个。
原因没有用别名,GraphQL 的响应字段合并规则要求同名字段参数一致,否则报冲突。
处置用 me: user(id: 1) 与 you: user(id: 2) 这样的别名区分。
现象把 GraphQL 当 REST 用,深层嵌套查询直接打垮数据库。
原因字段可以无限嵌套,等于把「一次请求做多少工作」的决定权交给了客户端;没有深度与复杂度限制。
处置服务端设定最大查询深度与复杂度上限(cost 指令配合校验),或用 DataLoader 把嵌套批量合并。
现象字段有值但客户端显示 null,schema 上该字段是 String(可空)却被当成必有。
原因可空字段与必填字段在类型层面就不对称;服务端某条记录该字段为空时返回 null 是符合规范的。
处置让客户端生成代码按可空处理;确实是业务不变量就在 schema 上标 String! 并在服务端保证。
现象mutation 部分成功,接口却整体报错,数据处于中间状态。
原因GraphQL 规范不保证多个 mutation 字段的原子性,成功与错误会被聚合成同一个响应里的不同条目。
处置把需要原子性的操作封装成单个字段并在服务端开事务,失败时整体回滚并抛出错误。
现象subscription 连上后没有任何事件,或频繁断开。
原因订阅依赖长连接,需要传输层与服务端解析器同时支持;代理与网关可能超时切断,或解析器没有把事件注册进去。
处置确认传输层(WebSocket / SSE)与解析器实现,必要时改用轮询;并实现重连与去重。
现象cacheControl 写了但响应头没有缓存,重复请求一直打服务端。
原因缓存提示需要网关或服务端解析器支持才生效,很多实现只把它当作 schema 注释。
处置在网关层显式配置缓存规则,不要假设指令会自动生效。
现象开发环境能 introspect,生产报 introspection is not allowed。
原因生产环境出于安全考虑关闭了内省,客户端代码生成流程因此失败。
处置把 schema 文件纳入版本管理并按需发布;或者在受控网络里保留只读 introspection。
排障路径
1确认当前 schema 暴露的入口类型
grep -n "type Query\|type Mutation" schema.graphqlsgrep 本地表能快速确认字段名与是否拼错。
2列出全部类型名做对照
grep -n "^type \|^interface \|^union \|^enum " schema.graphqls这是脱离客户端、只靠本地文件核对契约的最快方式。
3检查是否遗漏了非空标记
grep -n ": String\|: ID\|: Int\b" schema.graphqls缺
!的字段会让客户端把本应必有的值当作可空处理。4核对深度与复杂度限制是否已开启
grep -n "maxDepth\|maxComplexity\|cost" server没有上限就等于把资源消耗的决定权交给客户端。
5排查 N+1 是否已批量化
grep -n "DataLoader\|dataloader" server嵌套字段逐条查库是 GraphQL 服务最常见的性能事故。
6确认 schema 的分模块拼接是否完整
grep -rn "extend type" schema.graphqls多 entry 方案里漏掉 extend 会让某个模块的字段整批丢失。
命令示例
变量 + 条件字段:一次查询里按开关取不同字段
query GetUser($id: ID!, $withName: Boolean!) {
user(id: $id) {
name @include(if: $withName)
email @skip(if: $withName)
}
}变量在查询体外用 $ 声明并写死类型(ID!、Boolean!),值由请求的 variables 传入,避免把值拼进查询字符串(那既是注入面也让服务端无法复用编译好的查询)。@include 在条件为 true 时保留字段,@skip 在条件为 true 时丢弃字段,两者常配合用来在同一份查询上做两个响应形状。
输出
{
"data": {
"user": {
"name": "Alice",
"email": null
}
}
}别名解决同类型多参数字段的响应合并冲突
{ me: user(id: 1) { name } you: user(id: 2) { name } }同一请求里两次写 user 且参数不同,不加别名会直接报字段冲突(GraphQL 要求同名字段的参数一致才允许合并)。加上 me: 与 you: 之后它们变成两个独立的响应字段,既能并发取两个对象,也绕开了「一个字段只能取一个参数」的硬限制。
输出
{
"data": {
"me": {
"name": "Alice"
},
"you": {
"name": "Bob"
}
}
}Relay 连接式分页:edges/node 加 pageInfo 游标
{ users(first: 10, after: "cursor") { edges { node { name } } pageInfo { hasNextPage endCursor } } }first/after 是 Relay 规范里的游标分页参数,after 传上一页的 endCursor。判断还有没有下一页看 pageInfo.hasNextPage,而不是靠本页条数小于 first 猜测——过滤条件变化时两种判断会给出不同答案。
输出
{
"data": {
"users": {
"edges": [
{ "node": { "name": "Alice" } }
],
"pageInfo": {
"hasNextPage": true,
"endCursor": "Y3Vyc29y"
}
}
}
}用 input 变量提交变更,别把输入拼进 mutation 字符串
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) { id }
}把可变参数放进 CreateUserInput! 这类 input 类型里由 variables 传入:schema 变了会直接暴露成校验错误,而拼进 mutation 字符串的写法在服务端只能靠字符串解析,校验与缓存全失效。返回值只选 { id }:mutation 默认返回类型是整个对象,漏写选择集会把用户邮箱等字段一起下发。
常见坑与注意事项
- 字段级授权缺失就是越权:只要客户端能传任意 id 就能读写他人数据,而 GraphQL 没有「URL 里少写一个参数」这层天然防护。user(id: $id) 这类入口必须在解析器内部校验当前身份与目标对象的关系,schema 上的 @auth 指令不接校验逻辑就只是一段注释。
- 生产环境开着 introspection 等于把全量 schema(类型、参数、枚举值、已废弃字段)免费交给攻击者,便于枚举出未在文档中的内部字段;同时关掉它又会让依赖内省拉 schema 的前端代码生成流程直接断裂,改成把 schema 文件纳入版本管理。
- 嵌套字段若在解析器里逐字段查库,users { posts { comments { author { ... } } } } 会把一次查询放大成成百上千次数据库往返,几条并发请求就能打满连接池,让全站请求一起超时。列表字段必须批量加载(DataLoader 之类)或直接在 SQL 里 join 出来。
- 顶层 mutation 字段不是幂等的,而客户端与网关的自动重试、超时重发都可能让同一次变更执行两遍:重复建用户、重复扣款、重复发消息。写操作要么设计成幂等(带客户端幂等键并在服务端去重),要么在服务端显式拒绝重放。
- 敏感字段(例如带用户维度的 profile / email)与 @cacheControl 一起用时,若网关按字段名而不是按登录态区分缓存键,一个用户的响应会被缓存后发给另一个用户,表现为数据串号而非报错。要么按身份隔离缓存键,要么不给这类字段挂公共缓存。
提示
- GraphQL 查询返回的字段由客户端决定,服务端不需要返回所有字段。
- 变量用 $ 前缀,在查询外部定义,避免字符串拼接。
- Schema 定义语言(SDL)是 GraphQL 的核心,先定义类型再实现解析器。
- 内省查询(__schema/__type)可用于生成 TypeScript 类型与代码提示工具。
- 分页推荐采用 Relay 连接规范(edges/node/pageInfo),兼顾向前与向后翻页。
常见问题
一次想按条件返回部分字段,用 @include 还是 @skip?
两个指令都接收 if: $布尔变量,区别只在意图方向。@include(if: $withName) 是「满足条件才带上」:query($id: ID!, $withPosts: Boolean!) { user(id: $id) { name posts @include(if: $withPosts) { title } } }。@skip(if: $skipName) 是「满足条件就丢弃」,适合某个组合里要屏蔽掉的字段。客户端调用时把变量设为 true/false 即可,不必拼字符串,两者在服务端被同等对待。
联合类型返回的数据不知道是 User 还是 Post,客户端怎么安全取值?
用内联片段按具体类型收窄:先 fragment UserFields on User { id name email } 定义公共字段,再写 { user { ...UserFields ... on Admin { permissions } } },其中 ... on Admin 只在真实类型是 Admin 时生效,其它类型直接跳过这段。配合 SDL 里的 union SearchResult = User | Post,客户端就能拿到 type 之外的差异化字段,而不必猜结构。
分页到底该用 first/after 还是 offset 分页?
表里推荐的是 Relay 连接规范:{ users(first: 10, after: "cursor") { edges { node { name } } pageInfo { hasNextPage endCursor } } },first 决定本页条数,after 传上一页返回的 endCursor 游标,pageInfo.hasNextPage 判断还有没有下一页。它比 offset 分页更稳,因为翻页期间有新数据写入时不会漏行也不会重复。连接式写法 { users(first: 10) { edges { node { name } } } } 是同一套规范的精简形式。
怎么在写查询前就知道某个字段和参数叫什么?
用内省查询直接问服务端要的 schema。{ __schema { types { name } } } 列出全部类型,{ __type(name: "User") { fields { name type { name } } } } 查看某个类型的字段,{ __type(name: "User") { fields { name args { name type { name } } } } } 进一步看到每个字段的参数及其类型,{ __schema { queryType { name } mutationType { name } } } 确认查询根和变更根的类型名,{ __schema { directives { name locations } } } 查可用指令。这些内省结果也是生成 TypeScript 类型和编辑器提示的原料。
字段里的报错会让整个查询失败吗?
不会,GraphQL 允许数据与错误同时返回。表里的写法 { user { name } } # 期望 user.name 标明:即便解析 user.name 时抛错,响应里依然带上能成功解析的字段,并把失败字段归到 errors 数组中说明路径和原因。这正是它与 REST 的差别——客户端要同时处理 data 和 errors 两个通道,而不是假定二者互斥。
由 巧匠 维护
公开更新于 2026年7月21日,内容持续校对官方文档。