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重点看key、type、rows和Extra,别光盯着有没有建索引判断走不走索引。- 结束日期要先转成下一天的零点,避免漏掉当天带微秒精度的订单记录。
先把订单筛选的时间边界说清楚
假设表结构里有一列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已经建了普通索引,条件也不是直接作用在原始的索引列上做连续范围比较。数据量小的时候你根本感知不到差别,等订单量涨上去之后,接口延迟会跟着扫描的行数一起往上涨,最终拖垮整个服务。

更稳妥的做法是在应用层先把前后两个日期转换成精确的起止时间,把原始时间列直接放在比较符号的左侧。数据库不需要逐行修改列值做运算,优化器可以直接顺着时间索引找对应的连续范围,性能会好很多。
三种写法放在同一张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之后、当天结束之前的所有记录。把结束时间转成下一天零点,再用小于号做判断,边界规则不管是写代码还是做测试都更容易统一,不容易出漏子。

列表接口还要处理排序和空值边界
日期条件改对之后,订单列表接口还是慢,问题大概率会转移到排序环节。如果接口固定要按最新生成的订单倒序展示,你可以先确认下查询是不是真的需要返回全部字段;比如订单详情里的大段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核对实际选中的索引、扫描行数和排序的额外开销。这样既能减少统计漏数的问题,也能让订单列表的响应时间有明确的优化效果。
-
374 收藏
-
499 收藏
-
384 收藏
-
184 收藏
-
265 收藏
-
421 收藏
-
419 收藏
-
238 收藏
-
数据库 · MySQL | 9小时前 | MySQL · 回滚 · 数据库运维 · 配置变更 · 系统变量 · MySQL 8.4 SET PERSIST SET PERSIST_ONLY RESET PERSIST mysqld-auto.cnf persisted_variables296 收藏
-
数据库 · MySQL | 9小时前 | MySQL · 回滚 · 数据库运维 · 配置变更 · 系统变量 · MySQL 8.4 SET PERSIST SET PERSIST_ONLY RESET PERSIST mysqld-auto.cnf persisted_variables244 收藏
-
数据库 · MySQL | 9小时前 | MySQL · sql优化 · 数据库运维 · 性能排查 · 优化器提示 · MySQL 8.4 SET_VAR optimizer hint sort_buffer_size SQL 性能隔离497 收藏
-
数据库 · MySQL | 1天前 | MySQL · DDL · 元数据锁 · 性能排查 · performance_schema · MySQL 元数据锁 metadata_locks performance_schema DDL阻塞 Waiting for table metadata lock297 收藏
-
数据库 · MySQL | 2天前 | MySQL · 索引 · 执行计划 · sql优化 · 性能排查 · 执行计划 MySQL 8.4 EXPLAIN FORMAT=JSON cost_info query_cost239 收藏
-
数据库 · MySQL | 2天前 | MySQL · 索引 · 执行计划 · sql优化 · 性能排查 · 执行计划 MySQL 8.4 EXPLAIN FORMAT=JSON cost_info query_cost284 收藏
-
数据库 · MySQL | 2天前 | MySQL · 复制 · 主键 · InnoDB · 数据库迁移 · 数据库迁移 MySQL 8.4 sql_generate_invisible_primary_key 生成不可见主键 my_row_id419 收藏
-
数据库 · MySQL | 2天前 | MySQL · 复制 · 主键 · InnoDB · 数据库迁移 · 数据库迁移 MySQL 8.4 sql_generate_invisible_primary_key 生成不可见主键 my_row_id208 收藏
-
数据库 · MySQL | 2天前 | MySQL · InnoDB · Online DDL · 数据库变更 · 表重建 · MySQL 8.4 ALGORITHM=INSTANT TOTAL_ROW_VERSIONS ERROR 4092 Online DDL234 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习