GraphQL 语法速查表 - GraphQL 查询语言与 API 设计大全

这套速查给正在设计 GraphQL Schema、或是从 Apollo/URQL 等客户端调用它的开发者。先讲客户端最常敲的词汇——精准选取所需字段的查询、变更数据、订阅,以及复用片段;再讲服务的另一侧,从类型系统、指令到分页和 __typename 自省。与泛泛的语法表不同,条目按你在一次 API 会话里扮演的角色(请求方/提供方)来分。读完应能写出带参数和片段的精确查询、声明一个字段类型合理的 type,并定义一个连接式分页字段。

编程语言·共 42 条命令·最后更新 2026-07-21
graphqlapi查询语言

典型使用场景

面向需要同时定义 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 | Post
定义联合类型
interface Node { id: ID! }
定义接口
type User implements Node { id: ID! name: String! }
实现接口
scalar DateTime
自定义标量类型
type 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 } } }
Relay 风格分页
{ 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 | Postunion 表示「是其中之一」,必须配内联片段才能取具体字段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. 1确认当前 schema 暴露的入口类型

    grep -n "type Query\|type Mutation" schema.graphqls

    grep 本地表能快速确认字段名与是否拼错。

  2. 2列出全部类型名做对照

    grep -n "^type \|^interface \|^union \|^enum " schema.graphqls

    这是脱离客户端、只靠本地文件核对契约的最快方式。

  3. 3检查是否遗漏了非空标记

    grep -n ": String\|: ID\|: Int\b" schema.graphqls

    缺 ! 的字段会让客户端把本应必有的值当作可空处理。

  4. 4核对深度与复杂度限制是否已开启

    grep -n "maxDepth\|maxComplexity\|cost" server

    没有上限就等于把资源消耗的决定权交给客户端。

  5. 5排查 N+1 是否已批量化

    grep -n "DataLoader\|dataloader" server

    嵌套字段逐条查库是 GraphQL 服务最常见的性能事故。

  6. 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日,内容持续校对官方文档。

联系我们

命令或描述有误?提交反馈、商务合作或产品建议都可发送邮件给我们。

联系我们