关于 sql-plan-audit
sql-plan-audit 是用于审查 sqlc 风格查询目录中 SQLite 查询计划的技能。它生成稳定的计划报告并支持基线检查,帮助团队在代码审查和 CI 中发现计划回退。
主要功能
- 01生成 SQLite EXPLAIN 报告
- 02与 JSON 基线比较计划
- 03在 CI 中标记回退
- 04标示全表扫描和临时排序树
- 05处理 sqlc.arg 和 sqlc.slice 占位符
适用场景
- 01审查架构或查询修改后的计划
- 02确认查询使用新建的索引
- 03定期审计查询目录
- 04在计划相对基线回退时使 CI 失败
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mizchi/ skills --skill sql-plan-audit安装后在 Claude Code 中输入/sql-plan-audit确认已加载。
附带文件README.mdscripts/explain-runner.mjs
详细介绍
SQL 执行计划审计
当项目使用 sqlc 风格的查询目录(或任何包含 -- name: X :type 标记的文件),并希望在代码审查中查看查询计划时使用。
原因
sqlc 会在代码生成时检查 SQL 语法,但不会监控执行计划。列重命名、索引删除,或 WHERE 子句的小幅改动,都可能在不知不觉中让查询从 SEARCH USING INDEX 变为 SCAN TABLE。在没有 EXPLAIN ANALYZE 的 Cloudflare D1 / SQLite 上,规划器输出是唯一的静态信号。此工具会将该输出固定为可供审查的产物。
何时调用
- PR 中更改了架构或查询目录。
- 新增了索引,想确认查询是否会使用它。
- 对现有查询目录进行季度审计。
工作流
- 确定架构文件和查询目录文件。典型布局如下:
<project>/db/schema.sql <project>/db/queries.sql (or db/sqlite/query.sql for sqlc projects) - 运行执行器:
node scripts/explain-runner.mjs \ --schema your-project/db/schema.sql \ --queries your-project/db/queries.sql \ --out your-project/.linters/query-plans.txt - 提交输出。文本格式在多次运行间保持稳定;差异会显示计划变更。
- 如需在 CI 中强制执行,将结果重新生成为 JSON,并与已提交的基线比较:
node scripts/explain-runner.mjs \ --schema your-project/db/schema.sql \ --queries your-project/db/queries.sql \ --baseline your-project/.linters/query-plans.json \ --format json --fail-on regress - 如果有意引入回归(例如,有意弃用了某个索引),请在同一个 PR 中重新生成基线。
成功与失败的表现
执行器在成功时会刻意保持静默。配置 CI 时,了解输出的去向很重要:
| 调用方式 | stdout | stderr | 退出码 |
|---|---|---|---|
--out <path>(重新生成) | 无 | 仅 Node 的 node:sqlite 实验性警告 | 0 |
无 --out(重新生成) | 完整文本/JSON 报告 | 仅实验性警告 | 0 |
--baseline <path> --fail-on regress(CI 检查),无问题 | 完整文本/JSON 报告(若设置了 --out,则无输出) | 仅实验性警告 | 0 |
--baseline <path> --fail-on regress(CI 检查),存在回归 | 完整文本/JSON 报告 | 逐查询的回归详情(变更前后的计划) | 1 |
| 任意路径,发生内部错误(例如架构加载失败) | 无输出或部分输出 | Node 堆栈跟踪 | 1 |
请注意:stderr 中包含通过/失败诊断信息。在 CI 中,请显式捕获 stderr(2>&1、2>artifact.log 或分开捕获流)——只将 stdout 传给解析器会丢失所有回归消息。如果重新生成时“没有输出”,请检查 $? 和 ls <out path> 进行确认;这是文档所述的正常成功情况。
执行器如何处理占位符
运行 EXPLAIN 前,sqlc.arg('x') 和 sqlc.slice('x') 会被改写为 NULL。计划取决于 SQL 结构,而非绑定值,因此这样处理是安全的。? 位置占位符会原样保留——SQLite 接受它们出现在 EXPLAIN 中。
严重性标记
!SCAN — 全表扫描。通常表示缺少索引,或查询有意访问每一行。?TEMP B-TREE —USE TEMP B-TREE FOR ORDER BY / GROUP BY / DISTINCT。由于没有索引覆盖排序,排序会在内存中进行。SEARCH — 命中索引,属于正常情况。
小型表上的一次 SCAN(例如少于 1k 行的 users 表)通常没有问题;此标记只是提示,并非判定结论。
CI 集成
添加一个运行 JSON 变体并在出现回归时失败的步骤。典型的 justfile:
sql-plan-audit:
node scripts/explain-runner.mjs \
--schema db/schema.sql \
--queries db/queries.sql \
--out .linters/query-plans.txt
node scripts/explain-runner.mjs \
--schema db/schema.sql \
--queries db/queries.sql \
--format json --out .linters/query-plans.json
sql-plan-audit-check:
node scripts/explain-runner.mjs \
--schema db/schema.sql \
--queries db/queries.sql \
--baseline .linters/query-plans.json \
--format json --fail-on regress
Pre-push(pkfire / lefthook / pre-commit)是合适的检查时机——每次提交都运行会很慢,也会产生过多噪声。
局限性
- SQLite EXPLAIN 不会报告估算行数,因此执行器无法区分“扫描 10M 行的问题”与“扫描 50 行的正常情况”。请结合人工审查。
- FTS5 虚拟表会显示为
SCAN VIRTUAL TABLE。这会被视为信息提示,不会被标记。 datetime('now')等函数会在计划生成时求值。由于内存数据库的架构相同但没有数据,因此不会执行副作用(写入)。- 子查询和 CTE 可能会在计划中产生额外行;差异比较会稳定地处理这些情况。
引擎扩展性
执行器专用于 SQLite。要支持 Postgres / MySQL,需要替换 node:sqlite 驱动和 EXPLAIN 语法;解析器、基线差异比较和严重性分类器与引擎无关。此处尚未实现——有需要时可添加配套脚本。
要求
- Node 22 或更高版本(使用内置的
node:sqlite模块)。 - sqlc 风格的查询目录(
-- name: X :type标记)。如需支持其他命名查询格式,可调整运行器中的parseQueryCatalog。
文件
scripts/explain-runner.mjs— CLI 入口,无依赖。