三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

库存 SQL 忽快忽慢,别先怪索引:把 WHERE 函数冲突拆成可复现实验

库存 SQL 忽快忽慢,别先怪索引:把 WHERE 函数冲突拆成可复现实验

同一条库存查询有时走索引、有时全表扫描,常被归因于“优化器不稳定”。更常见的原因是WHERE中的类型转换、日期函数或字符串函数改变了谓词形态,再叠加参数类型、数据分布和统计信息,最终产生不同计划。本文用可搜索条件、隐式转换和区间边界三条线,给出从采样、复现、改写到回归的完整诊断方法。

先冻结现场,不要立刻加索引

慢查询出现时,先保存 SQL 文本、绑定参数和值的类型、执行时间、返回行数、数据库版本、表结构、索引、统计信息时间和执行计划。只有 SQL 文本没有参数,通常无法复现;库存编码传字符串还是数字,就可能让转换发生在完全不同的一侧。

同时区分“数据库执行慢”和“应用看起来慢”。连接池等待、网络传输、锁等待、结果集过大和应用反序列化都可能占用时间。使用数据库侧的实际执行统计确认扫描行数、过滤行数、等待事件和耗时,再决定是否进入 SQL 优化。

不要在生产上立即创建多个试验索引。新索引会增加写入成本、锁风险和统计变化,还会改变现场。先在具有相近数据分布的环境中复现,或使用数据库支持的不可见索引、计划分析能力进行低风险验证。

可搜索条件为什么重要

B-Tree 索引按列值的有序形式组织。谓词若直接约束索引列,优化器容易把它转成一个连续范围,例如sku = ?created_at >= ? AND created_at < ?。若对列先执行函数,数据库往往需要逐行计算后才能判断,普通索引中的原始顺序就难以利用。

典型写法是:

WHEREDATE(created_at)='2026-08-05'

更可搜索的改写是半开区间:

WHEREcreated_at>='2026-08-05 00:00:00'ANDcreated_at<'2026-08-06 00:00:00'

半开区间避免猜测时间精度,也能覆盖带小数秒的值。但应用必须明确数据库时区和业务时区。若库存日按上海时间计算、字段却保存 UTC,需要先在应用侧换算边界,而不是在列上逐行做时区转换。

两个函数“打架”的本质可能是类型方向

假设sku_code是字符列,查询却把参数绑定成整数。数据库可能把列转换为数字再比较,导致索引无法按原字符顺序定位;'00123''123'和包含字母的编码还可能产生意外等价或转换警告。

正确方向通常是让参数匹配列类型,而不是给列套CAST

WHEREsku_code=CAST(?ASCHAR)

更稳妥的是驱动层一开始就按字符串绑定,并让字符集与排序规则匹配。SQL 中显式转换参数可以用于排查,但长期方案应修正接口契约。列、参数、临时表和关联键的类型要一起检查,只改一处可能让转换移动到 JOIN 的另一侧。

字符串清洗函数也常见,例如TRIM(sku_code)LOWER(warehouse_code)。如果数据应当规范,优先在写入时校验并回填历史脏数据;如果业务确实按表达式查询,再评估数据库版本支持的函数索引或生成列。不要用表达式索引掩盖不清晰的数据定义。

用最小数据集证明是哪一个条件

把复杂查询缩成目标表和两个可疑谓词,准备四组数据:常见值、稀有值、边界时间、类型异常值。分别执行原查询、只保留条件 A、只保留条件 B、两个条件改写后的版本,并保存实际执行计划。

每组比较访问方式、使用索引、估算行数、实际行数、扫描行数和耗时。若估算与实际差距巨大,问题可能还包括统计信息或列相关性;若单个条件都能走索引,组合后退化,则检查复合索引顺序、选择性和返回比例。

缓存会影响“忽快忽慢”。第一次读取要从存储加载页,后续可能命中缓冲池。比较计划时不要只跑一次,也不要通过清空生产缓存制造公平。记录冷暖状态,多次交替执行候选 SQL,关注扫描量和计划稳定性,而非只看某一次毫秒数。

锁等待同样会伪装成查询退化。库存表通常写入频繁,慢样本若等待事务锁,即使执行计划完全正常,也不应通过加索引解决。把等待时间与 CPU 执行时间分开保存。

计划变化不一定是优化器“随机”

参数值不同,返回比例可能相差几个数量级。某个仓库占全表一半,另一个仓库只有几十行,优化器选择不同计划可能合理。准备语义相同但选择性不同的参数集,检查系统是否使用参数化计划、是否复用首次编译结果,以及当前统计直方图能否表达倾斜。

统计信息过旧会让估算失真,但更新统计并非万能。更新前记录旧计划与数据变化,更新后验证写入压力和其他关键查询。不要因为一条 SQL 变快,就忽略共享索引与统计变化对其他查询的影响。

如果需要借助模型整理计划或生成改写候选,应先删除业务数据、账号和真实 SQL 常量,通过本地适配层限制上传范围。haerapi.com可以作为待评估的 API 中转候选之一,但模型建议只能进入实验清单,不能直接在生产执行建索引、改类型或更新统计。

改写之后必须做语义回归

SQL 更快但结果变化,是最昂贵的“优化”。日期改为半开区间后,验证零点、月底、闰日、夏令时和小数秒;字符串类型修正后,验证前导零、空值、空串、大小写和尾部空格;库存条件还要验证负库存、冻结量和多仓合并规则。

建立一份固定回归集,保存输入参数、预期主键集合和聚合结果。性能测试记录 P50、P95、扫描行数和计划摘要,正确性测试比较结果集合。两类测试都通过,才允许进入灰度。

上线采用单一可回退变更。先改参数类型或谓词,再观察;不要同时改 SQL、加索引、升级驱动和更新统计,否则发生回归时无法定位。灰度期间监控该查询的调用量、错误率、延迟分位数、扫描行数与数据库负载。

一张排查清单收尾

遇到 WHERE 中函数导致的疑似索引问题,按顺序问:函数作用在列还是参数;两边类型和字符集是否一致;日期边界和时区是否明确;谓词能否改成等值或半开范围;返回比例是否适合索引;估算与实际是否接近;慢样本是否包含锁等待;不同参数是否需要不同计划;改写是否通过边界语义回归。

这些问题比“有没有索引”更接近根因。索引是数据访问结构,不是让任意表达式自动变快的开关。

结语

查询忽快忽慢时,最可靠的方法是保存参数化现场,把两个函数拆成独立变量,用实际计划证明扫描发生在哪里。先让列保持可搜索、让参数匹配类型、让时间使用明确区间,再处理统计与索引。这样得到的不只是一次提速,而是一条能够解释、复现和回退的优化结论。

← 返回列表