你以为省事,其实在赊账

用 GORM 这类 ORM 连数据库,感觉跟请了个"代购管家"差不多——你说"帮我把这个用户的所有订单和商品信息都取出来",管家笑着说没问题,然后悄悄跑了 N+1 趟仓库。账面上你只写了两行代码,但数据库那边已经刷出了几十条查询。

你不用想细节,但性能的账你来付。

大多数 Go 项目接数据库,第一反应是装 GORM,很少有人考虑 sqlc 这条路。理由很充分:文档多、社区大、抄代码快。这没毛病,直到流量上来的那一天。

GORM 除了 N+1 查询,还有一个不那么明显的麻烦:运行时反射。每次请求进来,GORM 都要动态检查你的 struct 标签、字段类型和关联关系。10K RPS 的时候这点开销还撑得住,到了 100K RPS,这个"运行时检查"就变成了压在 p99 延迟上的一块砖,GC 压力跟着蹭蹭往上涨,内存也开始膨胀。

Cloudflare 看过这条路,选择用 sqlc 往反方向走。

sqlc 的逻辑:SQL 直接编译成 Go 代码

sqlc 工作流:SQL文件编译生成类型安全Go代码

sqlc 的核心思路说起来很直接:你写 SQL,它帮你生成 Go 代码。

它直接把 .sql 文件编译成类型安全的 Go 函数和 struct,这是真正的编译期代码生成sqlc generate 跑完代码就出来了,运行期间没有任何反射,没有中间映射层,直接调 Postgres 驱动。

工作流程是这样的:

1. 写 SQL 文件(queries.sql)
2. 运行 sqlc generate → 生成 types.go + queries.go
3. 在业务代码里直接调用生成的函数,IDE 有自动补全
4. 编译期抓到 SQL 与代码不一致的问题,不用等到生产报警

说个具体的场景:你改了数据库字段类型,重新跑 sqlc generate,然后 go build——编译器会直接报错,告诉你第几行用错了。不需要等请求真的打过来才发现数据映射挂了。

这就像出发前对好地图,而不是开到路口才发现路封了。

数字说话:85K vs 42K qps

sqlc vs GORM 性能对比:QPS、p99延迟、内存占用

Cloudflare 的实测数据(10K 并发):

方案QPSp99 延迟内存占用
Go + sqlc + pgx85,0001.2ms180MB
Go + GORM42,0003.8ms1.2GB
Go + Ent38,0004.2ms

吞吐量翻了一倍,p99 延迟从 3.8ms 降到 1.2ms,内存从 1.2GB 缩到了 180MB。

内存那栏的差距最有意思。GORM 在高并发下靠反射维持运转,内存会持续堆积,GC 的频率也随之上涨,进而拖慢 p99——这是个互相咬的循环。sqlc 没有运行时反射,内存占用基本稳定在 180MB 附近不动。

Cloudflare 的生产搭配

技术栈是这样组合的:

Go + sqlc + Postgres 生产技术栈
├── sqlc(queries.sql → 编译生成 queries.go)
├── pgx(原生驱动 + 连接池管理)
├── Zerolog(结构化日志)
└── Prometheus(查询延迟 histogram 指标)

Cloudflare 工程师用来描述这套方案的词是"刻意无聊"(intentionally boring)。SQL 放在版本控制里,migration 先跑,sqlc 重新生成代码,然后部署。没有隐式的 query planner,没有自动生成的 join,出了问题直接看 .sql 文件,不需要反向推 ORM 究竟触发了什么。

每条查询都能干净地挂上 Prometheus 指标,因为生成的代码就是普通 Go 函数,想在哪里加埋点,一眼就看出来。给 ORM 操作加监控往往要绕好几圈才能搞清楚它实际执行了哪些 SQL。

sqlc 的零停机迁移:双读模式

Schema 升级是 ORM 最容易翻车的地方。“自动同步 schema” 听起来方便,但在生产环境里这大概等同于在高速公路上换轮胎。

sqlc 的迁移流程是明确的步骤,不靠魔法:

1. 写新 migration 文件(增加字段或修改类型)
2. sqlc generate(新查询方法自动生成,旧方法保留)
3. 双读期:新旧两套查询同时在线,分批切流量
4. 流量全部切过来之后,删除旧查询文件,重新生成

因为每条查询都有类型检查,双读双写的切换在编译期就能验证对不对。不需要停服,更不用在凌晨 3 点盯着迁移脚本发呆。

什么时候值得从 GORM 换到 sqlc

sqlc 不是没有代价的。它要求你真正写 SQL,不能靠 ORM 帮你生成查询。如果团队对 SQL 不熟,迁移成本会比想象中高。

适合上 sqlc 的场景:

  • 查询复杂,需要 window function、CTE 或者 Postgres 特有的 JSONB 操作
  • 服务延迟敏感,p99 开始影响用户体验
  • 想把数据库层纳入严格的类型系统管控,减少运行时意外
  • 团队已经在写原始 SQL 了,只是缺一层类型检查

暂时不建议换的场景:

  • 项目还在原型期,schema 天天在变
  • 团队 SQL 水平参差不齐,需要 ORM 充当保护层
  • 业务逻辑不复杂,GORM 跑得好好的,p99 也没异常
  • 查询量不大,ORM 的那点开销完全在可接受范围内

一个务实的做法:从最热的接口开始,把对应的查询迁到 sqlc,两套代码共存一段时间。如果数字好看,再扩大范围。全量迁移风险太高,不值得一次性做。

本质:别让工具替你藏 SQL

Cloudflare 这套方案背后的逻辑其实很简单:SQL 是你和数据库之间的合同,应该写清楚,而不是交给 ORM 自动生成一份你看不懂的合同。

sqlc 在这条路上走得很彻底。它的定位不是"更好的 ORM",是另一个方向的选择——代价是你需要真正懂 SQL,好处是代码和数据库之间没有黑盒,出了问题找 SQL 文件就够了。

如果你的服务已经在跑高并发,p99 开始抖动,可以先把最热的几条查询拿出来,用 sqlc 重写,对比一下延迟——85K 还是 42K,那张表放在那儿,自己判断。


常见问题

Q: sqlc 怎么安装和配置?

go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest 安装,然后在项目根目录创建 sqlc.yaml 配置文件,指定数据库类型(postgresql/mysql/sqlite)、schema 路径和 SQL 文件路径。配置完成后跑 sqlc generate,Go 代码自动生成到指定目录。官方文档在 docs.sqlc.dev,安装完基本 15 分钟以内能跑起来第一个例子。

Q: sqlc 能完全替代 GORM 吗?

不能完全替代。sqlc 专注于 SQL 查询的代码生成,不处理 migration(需要配合 golang-migrate 或 Atlas 等工具使用)。GORM 的模型关联自动管理、软删除等功能,sqlc 都要手写 SQL 来实现。两者解决的问题不完全一样。

Q: sqlc 支持哪些数据库?

主要支持 PostgreSQL、MySQL 和 SQLite。PostgreSQL 的支持最完整,包括 JSONB、数组类型、自定义域类型等 Postgres 特有功能,如果你已经在用 Postgres,sqlc 的适配基本是开箱即用。

Q: 已有 GORM 项目怎么迁移?

推荐渐进式迁移:找出慢查询或复杂查询,单独用 sqlc 重写,两套代码共存一段时间,确认无误后再扩大范围。不建议一次性全量迁移,生产风险太高,而且改动范围大了很难定位问题。