云端 Mac 上的 iOS SQLite 查询计划验收:复合索引与扫描预算

云端 Mac 上的 iOS SQLite 查询计划验收:复合索引与扫描预算

客户的订单列表在测试库里瞬间打开,导入历史数据后却明显卡顿。接单工作室在云端 Mac 上交付多个项目时,先选固定数据的查询计划验收,而不是反复换机器计时:前者适合定位索引缺失,后者容易把缓存和负载波动当成优化成果。本文用一条订单查询,建立从命令行预检到 iOS 实际连接复核的流程。

一、先固定查询契约,再讨论索引

约定列表只取某客户的已完成订单,按创建时间倒序,相同时间按编号倒序,每次最多返回二十条。测试同时检查筛选、顺序与数量,不能只看耗时。

准备三组参数:有记录的客户、不存在的客户,以及记录高度集中的客户。小而均匀的数据适合验证逻辑,不适合代表真实业务分布;上线数据中少数大客户可能占据大部分记录。

记录 sqlite_version()、表结构、索引定义和统计信息生成方式。Mac 自带命令行与应用使用的引擎可能不同,尤其项目自行集成 SQLite 时,不能把两者的计划当成同一份结论。

查询计划是优化器的选择,不是稳定的文本接口。不要把整段输出逐字相等作为跨版本门禁。

二、建立不会碰到客户库的夹具

在终端执行下面的脚本。它创建独立临时目录,不连接生产数据库;数据编号固定,重复运行可得到相同逻辑内容。SQL 文件与数据库放在一起,便于交付时重跑。

set -eu
work="$(mktemp -d)"
cat > "$work/check.sql" <<'SQL'
.bail on
SELECT sqlite_version();
CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  customer_id INTEGER NOT NULL,
  status TEXT NOT NULL,
  created_at INTEGER NOT NULL
);
WITH RECURSIVE seq(x) AS (
  SELECT 1
  UNION ALL
  SELECT x + 1 FROM seq WHERE x < 10000
)
INSERT INTO orders
SELECT x, x % 100,
       CASE WHEN x % 3 = 0 THEN 'done' ELSE 'open' END,
       x / 10
FROM seq;
EXPLAIN QUERY PLAN
SELECT id, created_at FROM orders
WHERE customer_id = 7 AND status = 'done'
ORDER BY created_at DESC, id DESC LIMIT 20;
CREATE INDEX orders_customer_status_time
ON orders(customer_id, status, created_at DESC, id DESC);
ANALYZE;
EXPLAIN QUERY PLAN
SELECT id, created_at FROM orders
WHERE customer_id = 7 AND status = 'done'
ORDER BY created_at DESC, id DESC LIMIT 20;
SQL
sqlite3 "$work/fixture.sqlite" < "$work/check.sql"
printf '%s\n' "$work"

先保存终端输出,再运行实际查询检查结果。这里故意制造相同创建时间,以便验证第二排序键确实生效。仅验证最新一条记录,会漏掉同时间记录顺序漂移。

三、读访问路径,不背计划字符串

没有索引时,通常会看到扫描订单表,并为排序建立临时结构。建索引后,检查是否按客户与状态查找,以及是否仍需要额外排序。具体措辞随 SQLite 版本变化,不应假定输出永远相同。

复合索引先放等值条件,再接排序字段,目的是让引擎沿目标范围读取前二十条。它也覆盖了本例的查询列;若后来加入大字段,覆盖关系可能消失,应重新验收,而不是继续引用旧报告。

不要见到 SCAN 就判失败:覆盖索引的顺序扫描与大表逐行过滤不是一回事。反过来,出现 SEARCH 也不保证工作量小,低选择性的条件仍可能读取很多记录。

索引适合高频列表,不适合无差别地加满所有字段。它会增加写入和存储成本;提交修改时同时跑插入、更新与删除用例,确认读优化没有破坏交付预算。

四、用实际连接检查扫描预算

命令行通过后,把夹具导入应用测试,并使用项目真正的数据库封装执行同一条参数化查询。绑定参数类型保持一致,语句必须读到结束,不能取第一行就停止计数。

能访问底层连接时,在执行前用 sqlite3_stmt_status 清零 SQLITE_STMTSTATUS_FULLSCAN_STEP,执行后读取它;另用 SQLITE_STMTSTATUS_SORT 查看排序操作次数。前者统计全表扫描相关步数,后者不是耗时,也不是排序行数。

这两个计数都为零也不等于查询免费。大量索引访问仍可能发生;可补记 SQLITE_STMTSTATUS_VM_STEP,并结合返回条数判断。若封装未暴露语句句柄,先增加测试适配层,不要解析业务日志里的耗时冒充扫描统计。

预算应来自固定夹具的已审核基线,不照搬通用阈值。每次计数前重置,避免复用 prepared statement 后把上一次执行累加进来。

五、把分布变化放进交付检查

验收至少保留两套库:均匀分布库用于检查基础路径,倾斜分布库用于模拟大客户。两套都检查二十条上限、稳定排序、空结果,以及不同状态参数。

统计信息会影响计划。应分别记录新建库与执行 ANALYZE 后的结果;如果产品运行时不会生成统计信息,不能只验收有统计信息的理想状态。也不要为拿到好看的计划强制指定索引,掩盖真实选择。

最后归档 SQL、引擎版本、数据分布说明、索引定义、计划和计数。新增回归先比较这些证据,再看运行时间。工作室更换云端 Mac 或项目延期后,仍能依据同一份夹具解释:改动改变了访问路径,还是仅仅碰上了一次热缓存。

常见问题

查询计划里出现 SCAN 就必须阻断交付吗?

不必。小表或覆盖索引的顺序扫描可能合理。应结合被访问的对象、返回规模和固定夹具下的扫描预算判断,不能只搜索一个关键词。

云端 Mac 自带 sqlite3 的结果能代表 iOS 吗?

不能直接代表。命令行适合验证 SQL 与索引设计;最终必须在应用使用的 SQLite 引擎、连接配置和数据库状态下重新验收。

让租期跟着项目排期走

适合临时出包、交付周加机与多客户项目分机。若团队常年满负荷构建且已有自有设备,先比较自购与租用,再做决定。

VMSSD 提供独享物理 Mac mini,非虚拟机。按天、周、月、季选择周期,到期不续即停;机型与节点可用性以控制台实时返回为准。

把阅读变成下一步安排

先核对项目工期与构建负载,再决定机型和租期。首次使用,可以从下单清单开始。