Skip to content

driver-turso: remote mode never materializes object-level indexes — every declared secondary index is absent on production Turso tenant databases, so hot polling queries full-scan #17609

Description

@hotlong

现象(生产实测;租户信息已匿名)

两个生产租户库(driver-turso remote 模式,libsql://)的 sqlite_master

索引总数 其中主键自动索引 其余
库 A 100 106 100 6 = 4 个字段级 unique 生成的 uniq_* + 2 个 metadata-protocol 原生 SQL 建的 idx_sys_metadata_overlay_*
库 B 100 105 100 5 个 uniq_*
  • sys_notification_delivery 声明了 5 个索引(含 1 个对象级 unique)→ 两个库里一个都没有,只有 sqlite_autoindex_sys_notification_delivery_1
  • sys_job_queue 声明了 3 个 → 两个库里一个都没有
  • 同一批对象定义在本地 SqlDriver(better-sqlite3)落地后:sys_notification_delivery 6 个、sys_job_queue 4 个,齐全

同一条领取查询的执行计划:

EXPLAIN QUERY PLAN SELECT id FROM sys_notification_delivery
 WHERE status = 'pending' AND partition_key = ? AND next_attempt_at <= ?
 ORDER BY next_attempt_at LIMIT 50
远程租户库 → SCAN sys_notification_delivery
            USE TEMP B-TREE FOR ORDER BY
本地       → SEARCH sys_notification_delivery USING INDEX idx_sys_notification_delivery_b3081700
            (status=? AND partition_key=? AND next_attempt_at<?)

原因(读码;origin/main 3ef96b471 仍如此,无在途 PR)

remote 模式下 TursoDriver.initObjectsRemoteTransport.syncSchemasBatchremote-transport.ts所有索引 DDL 都来自 buildUniqueIndexDDL:2160,由 uniqueIndexesFromFields 生成——只看字段级 unique),发射点 :1841 / :1868 / :1941 / :1981,全文件唯一一条 DDL 模板是 :2196CREATE UNIQUE INDEX IF NOT EXISTS

对象级 indexes: [...]——无论是否 unique——在 remote 路径上没有任何消费者。本地的对应物是 SqlDriver.syncDeclaredIndexessql-driver.ts:9812)。

#16570 修的是 indexesinitObjects 参数的类型面turso-driver.ts 那段注释自己也写了「the runtime carried both keys the whole time」——但带着键不等于把它渲染成 DDL。这是 ADR-0049「声明无执行点」在 DDL 层的一例:从外面看什么都没坏,代价只出现在账单上。

后果(生产实测)

一个租户库 9 天 1,011.6 亿次行读,占其所在 Turso 组织的 99.9%,已进入超额计费;该库数据总量 12.7 MB。

机制是三件事叠乘(另两件分卡):

  1. 本卡:热表没有索引,每条查找都是全表扫描
  2. 通知调度器空闲时每跳仍发 32 条语句(service-messaging 另卡)
  3. 投递表持续增长——未配置邮件传输的租户,每条通知写一条必死的 email 投递,保留 90 天(service-messaging 另卡)

实测稳态约每秒 28 次全表扫描 sys_notification_delivery;表每天 +316 行,日读量每天线性 +8 亿。

验收

  1. remote 模式为每个对象级声明索引发射 DDL——非 unique 与 unique、含 nullSafe / 租户键部分——与 SqlDriver.syncDeclaredIndexes 同名同键buildIndexName)。新表随建表批次一起落;已有表走 retrofitIF NOT EXISTS,零改数据)
  2. 失败不静默:建不出的索引按 AGENTS.md 降级规则以 error 级报出原因(与 drivers(turso): the remote face drops unique entirely — a declared-unique column takes duplicates, and an upsert on conflictKeys dies with a raw SqliteError #8413 的 unique retrofit 同等待遇)
  3. 对等测试:同一组对象定义分别经 local SqlDriver 与 remote transport 落地,比较 sqlite_master 的索引名集合全等;至少覆盖 sys_notification_deliverysys_job_queue
  4. 开机成本受控:稳态(索引都已存在)不得每次 kernel 构建为每张表各发一轮语句——沿用 unique retrofit 的批量 / 一次性做法
  5. 回归证据:remote 面上对上面那条领取查询 EXPLAIN QUERY PLANSEARCH … USING INDEX

影响面

所有 remote 模式租户库(托管云的每个环境)。修复发布并被消费方 pin 后,已有库靠 retrofit 在下一次 kernel 构建时补齐。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions