【NL2SQL 实战 07】LIMIT 不是小事:默认 100 行背后的产品判断

上一篇讲「只允许 SELECT」做了三遍。评论区有人说:你这纵深做得够深了,LIMIT 不就是补一行吗,有什么好写的?

LIMIT 本身确实是一行。但围绕这一行,产品判断、安全兜底、方言差异、运维可改、前端承受力,五件事交织在一起。 写错一个,要么用户抱怨「只给我看 100 条」,要么业务库和前端一起喘。

这篇就用 STAR 的骨架讲:默认 100 是怎么来的(情境),LIMIT 在系统里承担什么(任务),我们怎么实现、怎么改、怎么防炸(行动),以及边界在哪(结果)。

仓库在这: github.com/yanqiuping1...


一、默认 100 不是拍脑袋(S:情境)

问数页面是对话式的。用户问「上个月各学校的跳绳人数」,期望看到一张能扫一眼的表格,不是一次导出三万行。

100 行是「够看、不炸」的平衡点:

  • 够看: 大部分 GROUP BY 查询,维度不超过几十个。100 行覆盖「按学校」「按月份」「按活动类型」等常见粒度;
  • 不炸: 前端表格组件一次渲染几百行还行,上万行开始卡。SSE 推流时数据量也要控制;
  • 不偏: 用户没写 LIMIT 时,不补等于放全量。全量查询打到业务库,DBA 会来找你。

关键产品判断: 100 这个数字存在系统参数表里,运维可以改。但默认值必须保守------新部署、没人配参数的时候,系统应该自我保护,而不是默默拖爆。

二、LIMIT 在系统里承担什么(T:任务)

LIMIT 不是一行 SQL 的事,它是数据库、应用层、前端渲染、网络推流四个环节的共同约束

环节 怕什么
业务库 全量查询拖垮数据库
应用层 拉回海量行处理不过来
前端渲染 表格组件一次渲染上万行卡顿、白屏
SSE 推流 行数多 = 事件体积大 = 推流时间长,用户等不及刷新页面

所以 LIMIT 的目标不是「限制用户」,是让四个环节都不出问题。想放大,四个环节都得确认能扛。

三、怎么实现:三层配合(A:行动)

第一层:模型写大了,压回去

模型不总是忘 LIMIT。有时候它会写 LIMIT 10000------因为用户说了「给我全部」。

闸门的逻辑:

模型写了什么 闸门做什么 结果
没写 LIMIT 补默认值 最多 100 行
写了 LIMIT 50 不动 50 行
写了 LIMIT 10000 压回上限 最多 100 行
写了 LIMIT 0 或负数 补默认值 最多 100 行

产品判断:模型的 LIMIT 不等于用户的需求。 用户说「全部」,真正想看的可能是「不只 10 条」。给 100 条够不够?大部分时候够。真要导全量,走下载 / 报表,别走对话------对话是给人看的,不是给导数的。

第二层:探查工具更狠------LIMIT 10

Agent 在问数链路里有个工具叫"探查 SQL",用来验证 JOIN 是否正确、字段取值分布长什么样。探查的目的是「看一眼结构」,不是「拉数据」。

探查 SQL 的 LIMIT 封顶到 10,原因有三:

  1. Agent loop 可能跑 3~6 轮,每轮可能探查一次。6 轮 × 大量行 = 上下文爆炸。10 行看取值分布够了;
  2. 探查是中间步骤,用户不会看到。拉多了浪费 token 和网络;
  3. 防诱导:探查如果不限,Agent 可能被注入问句诱导反复拉大量数据。10 行 + 步数上限双重封顶。

第三层:执行器兜底------多取一行检测溢出

LIMIT 改写是 sqlglot 层面的。B4 讲过:方言坑可能让 LIMIT 没生效(比如 SQL Server 的 TOP 和 LIMIT 映射不一致、Oracle 的 FETCH FIRST 被方言标签搞丢)。

执行器不信任 LIMIT 改写的结果。 它用「多取一行」的方式检测溢出------如果数据库返回超过上限的行数,说明 LIMIT 没生效:

python 复制代码
# backend/app/sql/executor.py(节选)
raw_rows = result.fetchmany(max_rows + 1)
if len(raw_rows) > max_rows:
    raise SqlGuardError("TOO_MANY_ROWS", f"结果超过 {max_rows} 行上限")

为什么是 max_rows + 1?不是为了多给一行,是为了检测溢出 。拿到 101 行说明数据库返回了超过 100 行------LIMIT 没生效。这时候不是默默截断,而是报错

为什么报错比截断好?因为截断比报错危险------用户以为看到了全部,其实不是。报错至少让人知道「还有」。

两层 LIMIT 的关系: sqlglot 改写是让数据库少干活(SQL 层面),fetchmany 兜底是让应用层不崩(应用层面)。两层抓的是同一个风险(拖爆),但在不同层面兜。

四、谁能改、怎么改、改完怎么不炸(A:行动)

sql_max_rows 有三个来源,优先级从高到低:

来源 谁改 什么时候生效
系统参数表 运维 / 管理员通过管理端 热生效
环境变量 部署时配 .env 重启生效
代码默认值 开发者 兜底

运维改参数表,不用重启。而且参数本身被夹在安全范围内(比如 1~10000)------运维改成 0 或 99999,也会被夹回安全范围。

改完前端会不会炸? 会,如果改太大。前端表格组件 100 行没压力,1000 行开始卡顿,5000 行以上可能白屏。更隐蔽的是 SSE 推流:用户等 10 秒看到 100 行能接受,等 30 秒看到 5000 行大概率会刷新页面。

所以改大之前,先确认四个环节都扛得住。 默认值保守就是为了让没配参数的新部署不至于出事。

五、几笔取舍,我现在还认(R:结果)

默认 100,不是 1000。 保守的默认值保护不懂配参数的部署者。想要更多行,显式改参数,别让默认放行。

压回去,不是警告。 模型写 LIMIT 10000,不是提示用户「建议减少」,是直接压到 100。对话场景不需要万行结果------这不是限制用户,是保护用户(和业务库)。

报错,不截断。 截断意味着用户看到不完整的数据但以为是完整的。报错至少让人知道「还有」。

探查 10 行,不可商量。 Agent 的探查是中间步骤。10 行看取值分布够了。如果模型需要看更多,那是 Plan 层的问题------该拆查询,不该拉全量。

上限 10000,不是无限。 即使运维想放开,也夹在 10000。这是「对话问数」的场景约束。真要超过 10000 行的导数需求,应该走报表 / 下载 / ETL,不走对话。

带走三句

  • LIMIT 默认 100 不是拍脑袋,是「够看、不炸」的平衡------数据库、应用层、前端、推流四个环节的共同约束
  • 模型写大了压回去,没写就补,执行器多取一行检测溢出------LIMIT 在两层做,抓的是同一个风险
  • 想改可以改(系统参数表,热生效),但改完要确认前端扛得住

开源仓库在下面,本地可以跑通(Compose + Fixture,不必先配云钥匙)。说明看 docs/DEMO.md / AGENTS.md

GitHub: github.com/yanqiuping1...

相关推荐
掘金者阿豪14 分钟前
Let‘s Encrypt 证书到底会不会自动续期?从一次服务器迁移后的证书排查说起
后端
joinwell5221 分钟前
Agent 中断后,原任务如何安全接管?从恢复标记到效果事实
人工智能·后端·架构
sp4233 分钟前
羽量级的 Java Bean 实体校验器
后端
qq_3391911443 分钟前
go cpu占比高排查,cpu100%排查,go pprof cpu命令
开发语言·后端·golang
笃行3501 小时前
异构数据同步如何真正做到“数据无忧“?——解读 KFS 的全周期一致性校验与修复能力
后端
云边有个稻草人1 小时前
异构数据同步如何保证数据不出错:KFS全周期校验与修复实践
后端
JaguarJack1 小时前
现在就值得尝试的 5 个 Laravel 新包
后端·php·laravel·服务端
BingoGo1 小时前
现在就值得尝试的 5 个 Laravel 新包
后端·php
PBitW1 小时前
Travel Planner — 智能旅行行程规划助手
前端·后端