登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  MySQL

MySQL 日期范围筛选为什么不走索引:半开区间、函数包裹与 EXPLAIN 核对

来源:17golang原创

时间:2026-07-27 11:59:33 334浏览 收藏

订单后台最常见的按日期筛选功能,上线后经常莫名其妙变成慢查询高发点:用户选了2026-07-01到2026-07-07的区间,SQL给order_created_at明明建了索引,MySQL还是扫了大半张表。先别急着删了索引重建,最容易踩的两个坑,要么是给时间列套了DATE()做运算,要么是偷懒把结束日的时间直接写成含糊的23:59:59。

明明建了对应索引的日期范围查询没走索引,大部分情况都不是索引本身出问题,要么是边界写法不符合索引的有序匹配规则,要么是你提前用运算破坏了索引列的原始值,调整写法再核对执行计划就能快速定位问题。
要点速览
  • 对已经建好索引的DATETIME列,优先用「包含开始、不包含结束」的半开区间写法。
  • DATE(order_created_at)会破坏索引列的有序性,让过滤条件没法直接利用B+Tree的有序范围快速扫描。
  • EXPLAIN重点看keytyperowsExtra,别光盯着有没有建索引判断走不走索引。
  • 结束日期要先转成下一天的零点,避免漏掉当天带微秒精度的订单记录。

先把订单筛选的时间边界说清楚

假设表结构里有一列order_created_at DATETIME(6) NOT NULL,对应的索引名叫idx_orders_created_at。前端传过来的是两个用户选中的自然日,我们需要在程序里把它们转换成数据库能直接识别的精确时间点:

-- 2026-07-01 00:00:00 = '2026-07-01 00:00:00'
  AND order_created_at 

这个写法能把7月1日的所有记录都纳入筛选范围,也能把7月7日23:59:59.999999的订单也完整覆盖到。下次你再从7月8日零点开始筛选,两段区间的边界完全不会重叠,不会出现同一条记录被两个相邻日期同时统计到的问题。

为什么DATE()会让范围筛选变慢

不少列表接口开发图省事,会直接写出下面这类SQL:

WHERE DATE(order_created_at) BETWEEN '2026-07-01' AND '2026-07-07'

这种写法下MySQL需要先逐行对全表的记录计算DATE(order_created_at),再判断计算后的结果有没有落在你指定的区间里。哪怕order_created_at已经建了普通索引,条件也不是直接作用在原始的索引列上做连续范围比较。数据量小的时候你根本感知不到差别,等订单量涨上去之后,接口延迟会跟着扫描的行数一起往上涨,最终拖垮整个服务。

MySQL 订单日期筛选中 DATE 函数包裹时间列导致全表扫描的证据对比图

更稳妥的做法是在应用层先把前后两个日期转换成精确的起止时间,把原始时间列直接放在比较符号的左侧。数据库不需要逐行修改列值做运算,优化器可以直接顺着时间索引找对应的连续范围,性能会好很多。

三种写法放在同一张EXPLAIN里核对

为了直观对比不同写法的差别,你可以提前准备一张测试表,用完全相同的日期条件分别跑。别光靠经验判断索引走没走,一定要拉出来执行计划看实际效果:

EXPLAIN
SELECT id, user_id, total_amount
FROM orders
WHERE order_created_at >= '2026-07-01 00:00:00'
  AND order_created_at 

看执行计划的时候重点关注四个字段就行:

  • key:实际被优化器选中的索引,不能只看possible_keys里列出来的可选索引。
  • type:范围查询正常情况下常见值是range,如果退化成了ALL,就要接着往下排查原因。
  • rows:优化器预估需要检查的行数,这个数和最终返回的结果行数不是同一个概念。
  • Extra:出现Using where并不直接代表异常;如果排序过程还触发了额外的临时表或者文件排序,要结合排序列和索引的顺序一起判断。

BETWEEN也能表达范围,但它是两端都包含的闭区间:

WHERE order_created_at BETWEEN '2026-07-01 00:00:00'
                              AND '2026-07-07 23:59:59'

如果你的时间列支持微秒精度,这种写法就会漏掉23:59:59之后、当天结束之前的所有记录。把结束时间转成下一天零点,再用小于号做判断,边界规则不管是写代码还是做测试都更容易统一,不容易出漏子。

MySQL EXPLAIN 对比半开区间使用时间索引并减少扫描行数的工程核对图

列表接口还要处理排序和空值边界

日期条件改对之后,订单列表接口还是慢,问题大概率会转移到排序环节。如果接口固定要按最新生成的订单倒序展示,你可以先确认下查询是不是真的需要返回全部字段;比如订单详情里的大段JSON文本,完全没必要在列表查询阶段就读出来拖慢速度。

SELECT id, user_id, status, total_amount, order_created_at
FROM orders
WHERE order_created_at >= ?
  AND order_created_at 

加上id DESC是为了让同一时间戳下生成的订单也能有稳定的排序顺序。如果业务允许order_created_at为空,就要先明确空值的记录要不要出现在日期筛选的结果里;别为了兼容旧数据随手加OR order_created_at IS NULL,这会改变范围条件的选择性,要单独核对执行计划确认性能没有问题。

上线前用结果边界做一次复查

提前在测试库造四条边界测试数据:开始时刻、结束前一微秒、下一天零点,再加一条更早的历史订单。筛选7月1日到7月7日的区间时,前两条边界数据应该命中,下一天零点的记录和更早的历史订单都不应该出现在结果里。

-- 应该命中:2026-07-01 00:00:00
-- 应该命中:2026-07-07 23:59:59.999999
-- 不应命中:2026-07-08 00:00:00
-- 不应命中:2026-06-30 23:59:59.999999

再把实际业务参数放回接口跑一遍,记录一次EXPLAIN ANALYZE的耗时和实际扫描行数。随着表里数据分布变化,MySQL的索引选择可能会跟着变;更稳妥的做法是把这条查询加入慢查询日志长期观察,不要把某次测出来的执行计划当成永远生效的结论。

常见问题

日期列必须改成DATE类型吗?

完全没必要。订单场景大多需要保留时分秒的精度,直接继续用DATETIME或者其他合适的时间类型就好,只要用半开区间来筛选自然日,效果完全没问题。

明明有索引执行计划却显示type=ALL?

常见原因包括列被函数包裹、过滤范围的选择性太差、存在隐式类型转换,或者优化器判断直接全表扫描的成本比走索引更低。先核对实际执行的SQL、传入的参数类型和key,再用测试数据重新复查一遍就能定位问题。

结束日期能不能直接写当天23:59:59?

不建议把它当成通用规则。如果时间列支持微秒精度就会出现漏数据的情况,而且结束日的时间还可能受用户时区偏移的影响。转换成下一天零点再用小于号判断要稳得多。

索引里只放order_created_at就够了吗?

要看你固定的过滤条件、排序规则和表里的数据分布。先用真实业务SQL配合执行计划确认性能瓶颈,再决定要不要建联合索引,别为了一个列表页面堆上一堆不好维护的冗余索引。

总结:让数据库直接比较原始时间列

日期筛选优化的第一步绝对不是上来就重建索引,而是先把用户选中的自然日转换成明确的时间边界。去掉DATE()对原始列的包裹逻辑,采用「开始小于等于、结束小于」的半开区间写法,再用EXPLAIN核对实际选中的索引、扫描行数和排序的额外开销。这样既能减少统计漏数的问题,也能让订单列表的响应时间有明确的优化效果。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>