
给 Text2SQL 加保护时,很多人会先做两件事:只让它执行读查询,再给结果加一个 LIMIT 100。界面最多展示一百行,模型拿到的上下文也短了,执行器看起来已经有了边界。
但项目面试再追问一句,边界就没那么清楚了:数据库为了交出这一百行,前面做了多少工作?
读取一百条明细,和算出所有分组的总额、排好名次再取前一百组,都可能交出一百行。返回行数一样,数据库里的计算却不一样。结果上限可以保留,但不能拿它证明运行成本已经受控。
DeepResearch 里的数据分析 Agent 同样会碰到这个问题。生成合法 SQL 之后,执行器还要替共享资源守住边界。本文沿一份本地 Text2SQL 教程里的执行片段,用标准库的小实验,把结果行数、锁等待和计算取消分开验证。
两份结果都只有一百行,后台工作却不同
实验只在临时目录建了一个 SQLite 小库,放入六千条人工整数记录,分为一百二十组,没有为分组列和金额列建立索引。运行环境是 Python 3.13.3、SQLite 3.49.1,数据生成规则、原 SQL 和结果都留了记录。
第一条查询是 SELECT id, amount FROM measurements LIMIT 100,直接取明细。第二条是 SELECT group_id, SUM(amount) AS total FROM measurements GROUP BY group_id ORDER BY total DESC, group_id LIMIT 100,按组求和,再按总额降序排列,同额时用组号打破并列,最后取一百组。
这两条查询回答的是不同问题,不能把第一条当成第二条的等价优化。把它们放在一起,是为了固定同一个返回行数上限,看看上限背后还有哪些工作。
实际的 EXPLAIN QUERY PLAN 里,第一条出现了表扫描路径;第二条除了扫描,还出现分组和排序各自的临时 B-tree。这里确实是先完成分组统计,再选出总额靠前的组,不能把先拿一百条明细求和当成相同答案。
尤其要分清输入行与结果行:六千条明细归成一百二十组,每组总额依赖属于它的明细。末尾舍弃二十组,并没有授权数据库遗漏那些参与总额计算的记录。结果少,不代表产生结果所需的信息也少。
不过,计划中的 SCAN 只是高层访问策略,不能单凭这个词断言本次一定读完了全表。临时 B-tree 也不等于必然落磁盘;换索引、数据或版本,计划可能改变。计划负责解释路径,实际工作仍要另找探针。
我给两条查询安装同样的进度回调,每隔约一百条 SQLite 虚拟机指令计一次数,包住执行到取完结果的过程。这次明细查询回调五次,聚合排序回调八百六十五次,两边都恰好返回一百行,内容也分别与人工数据计算结果一致。
次数乘以步长,只能叫这次记录口径下的近似指令采样。它不是扫描行数、CPU 时间,更不是线上耗时倍率;复杂语句的准备阶段也可能触发回调。这个小实验能说明的是:相同的输出上限,没有把两条查询的内部工作约束成同一个量。



