MySQL BETWEEN AND边界陷阱:闭区间语义与索引失效全面解析
发布时间:2026/10/22 16:14:54 锦皓数字建站

大多数刚接触 SQL 的人都会把 MySQL 里BETWEEN AND当成一个闭着眼睛就能写对的小语法不就是某个字段在区间内吗可真到了生产环境我见过因为这五个单词写错导致报表数据漏掉的案例不止一次。尤其当字段类型是datetime、或者业务口径要求含头不含尾的时候BETWEEN AND的边界行为简直就是一个埋好的雷。这篇文章不打算只讲语法我会从边界值、索引行为、NULL 处理、实际踩坑这几个角度把这一个操作符彻底讲透。适合刚学 MySQL 的初学者也适合写过几年 SQL、但没仔细研究过边界语义的开发同学。1. 先确认一下BETWEEN AND 到底是哪种区间判断1.1 语法与语义它只是 AND 的语法糖BETWEEN AND的标准语法是expr BETWEEN min AND max这句话的语义等价于expr min AND expr max注意这里的AND不是逻辑运算符它只是BETWEEN这个关键字的一部分用来连接下限和上限。换句话说BETWEEN AND表达的是一个闭区间两个端点都包含在内。很多初学者会把它误当成取值范围模型里的(min, max)开区间这是第一层误解。举个最简单的例子SELECT 10 BETWEEN 1 AND 10; -- 结果为 1 SELECT 11 BETWEEN 1 AND 10; -- 结果为 0第一个查询返回 1说明 10 被包含进来了。BETWEEN AND不是大于等于左边且小于右边而是大于等于左边且小于等于右边。这个语义本身不难难的是它落在不同类型字段上之后表现会不一样。后面我会专门用一节来验证数字、日期、字符串三种数据类型。1.2 从一个订单场景开始最朴素的使用方式为了把后面的例子串起来我先建一张虚构的订单表内容完全是模拟数据用来演示问题就够了CREATE TABLE order_info ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, order_amount DECIMAL(10,2) NOT NULL, created_at DATETIME NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;插入几条测试数据INSERT INTO order_info (user_id, order_amount, created_at) VALUES (101, 88.50, 2024-01-01 08:30:00), (102, 199.00, 2024-01-15 14:00:00), (103, 300.00, 2024-01-20 23:59:59), (104, 500.00, 2024-01-31 09:00:00), (105, 750.00, 2024-02-01 00:00:00);现在要查订单金额在 100 到 500 之间的所有订单最直白的写法就是SELECT id, user_id, order_amount FROM order_info WHERE order_amount BETWEEN 100 AND 500 ORDER BY order_amount;查询结果如下iduser_idorder_amount2102199.003103300.004104500.00注意 id4 的这张订单金额刚好是 500它被查出来了。这就验证了闭区间的行为边界值 500 属于结果集。如果需求是100 到 500但不包含 500那BETWEEN AND就不合适得写成order_amount 100 AND order_amount 500。1.3 为什么基本用法背后要较真需求口径往往比语法更容易出事我见过不少排查了很久的线上问题最后发现根本不是 SQL 语法错误而是业务需求里的区间没有定义清楚。比如产品说查 1 月到 2 月的订单开发随手写了BETWEEN 2024-01-01 AND 2024-02-29。看起来没问题但如果created_at是datetime类型这个写法很可能把 2 月 29 日 0 点之后的所有订单漏掉。原因后面会详细说。所以拿到这种区间需求时第一步不是写 SQL而是先确认三件事边界是闭区间还是半开半闭字段类型是date、datetime还是字符串边界上的数据是什么精度有没有带时间、带毫秒把这三个问题问清楚再落 SQL基本能避开 80% 的坑。2. 边界值实测数字、日期、字符串三类数据的真相2.1 数字边界端点会被包含但也有个精度暗坑数字类型的BETWEEN AND行为最直观。验证一下SELECT 1 BETWEEN 1 AND 10, 10 BETWEEN 1 AND 10;结果都是 1两个端点都包含。这在整数类型上毫无争议。但如果你用的字段是FLOAT或DOUBLE并且比较的小数位比较深BETWEEN AND可能会因为浮点精度问题出现诡异结果。比如一个FLOAT字段存了 10.35查询BETWEEN 10.35 AND 10.45结果可能不包含 10.35因为 10.35 在FLOAT里的实际存储值可能比字面量小一点点。生产环境里存储金额、分数这类数据强烈建议用DECIMAL而不是FLOAT或DOUBLE。DECIMAL是定点数能精确表达小数BETWEEN AND的边界判断才可靠。2.2 日期边界最经典的翻车案例这是BETWEEN AND最容易出问题的地方必须单独拿出来讲。假设我有一张订单表的created_at是DATETIME类型要查 2024 年 1 月的所有订单。很多人的第一反应是SELECT id, created_at FROM order_info WHERE created_at BETWEEN 2024-01-01 AND 2024-01-31;这条 SQL 的问题在于2024-01-31会被 MySQL 隐式转换成2024-01-31 00:00:00。那么created_at 2024-01-01 00:00:00 AND created_at 2024-01-31 00:00:00能查到的数据最多只到 1 月 31 日 0 点整。回到上面那张模拟表id4 的订单是2024-01-31 09:00:00它会被这条 SQL 漏掉。尽管这单确实发生在 1 月 31 日。正确的做法是使用左闭右开区间SELECT id, created_at FROM order_info WHERE created_at 2024-01-01 AND created_at 2024-02-01;这样写法能覆盖2024-01-31 23:59:59.999...之前的所有时刻不会因为时间精度漏数据。有人喜欢写成BETWEEN 2024-01-01 AND 2024-01-31 23:59:59这比原来好一点但如果数据里有毫秒比如23:59:59.500还是可能漏。左闭右开是最稳妥的。注意不要为了省事在查询条件里写DATE(created_at) BETWEEN 2024-01-01 AND 2024-01-31。虽然结果对了但对created_at字段套了函数索引会失效数据量一大查询会明显变慢。细节放在后面性能部分讲。2.3 字符串边界按字典序比较不是你以为的数字大小字符串类型的BETWEEN AND用的是字典序也就是逐字符比较的排序规则不是数值大小。这一点经常被忽略。比如SELECT 10 BETWEEN 1 AND 9;这个查询返回 1true。因为按字符串比较10 的第一个字符是 1跟 1 相等接着比较第二个字符0 小于 9所以 10 排在 9 前面。于是 10 落在 1 和 9 之间。这在业务上很容易造成错觉。如果你想按照数字大小去筛选一个VARCHAR类型的编号字段比如查编号 1 到 9 的记录BETWEEN 1 AND 9会把 10、11、100 这种也拉进来。再比如大小写问题。MySQL 默认的排序规则下A 到 Z 之间并不包含小写 a 开头的字符串因为按 ASCII 码a 排在 Z 后面SELECT apple BETWEEN A AND Z; -- 返回 0 SELECT Zebra BETWEEN A AND Z; -- 返回 0这个你可能没想到原因是Zebra以 Z 开头和上限Z比较时Zebra 比 Z 长Zebra Z所以超出了范围。如果业务上确实需要字符串区间筛选建议先ORDER BY看一遍实际排序确认范围是否符合预期再写BETWEEN AND。3. 性能表现BETWEEN AND 与索引的配合及失效场景3.1 走索引的原理优化器把它当范围扫描BETWEEN AND在执行计划里通常对应typerange的范围扫描前提是查询字段上有合适的索引。拿上一节的order_info表举例给order_amount建个索引ALTER TABLE order_info ADD INDEX idx_amount (order_amount);然后看执行计划EXPLAIN SELECT id, order_amount FROM order_info WHERE order_amount BETWEEN 100 AND 500;执行计划里type列是rangekey列是idx_amount说明用上了索引。这是因为BETWEEN AND被优化器转换成两个比较条件索引可以快速定位到区间的起点和终点然后做顺序扫描。这里有个常见的疑问BETWEEN AND和 AND 在性能上有区别吗答案是没有优化器最终生成的条件基本相同。差别只在于写代码时的可读性。3.2 让索引失效的三种写法同样一个BETWEEN AND换几种写法索引可能就废了。列三个最常见的坑第一种对索引字段套函数SELECT * FROM order_info WHERE DATE(created_at) BETWEEN 2024-01-01 AND 2024-01-31;DATE()包裹了created_at索引树里的值是原始时间戳无法直接和DATE()的计算结果做范围匹配优化器只能全表扫描。第二种隐式类型转换。比如mobile字段是VARCHAR写SELECT * FROM user_info WHERE mobile BETWEEN 13800000000 AND 13999999999;字段类型是字符串拿数字去比MySQL 会把字符串转换成数字再比较索引失效。正确写法是把数字当字符串WHERE mobile BETWEEN 13800000000 AND 13999999999;第三种虽然不是BETWEEN AND直接导致但组合条件时也常见OR连接的多个条件中只要有一个没用到索引整个条件可能都走不了。3.3 与其他写法的性能对比IN 列表、范围查询怎么选日常开发里还有两个容易拿来比较的写法IN和连续多个条件。整理成一张表写法适用场景注意点BETWEEN AND连续数值、日期区间边界必须确认 AND 同样的闭区间与 BETWEEN 性能一致IN (x, y, z)离散的几个值列表太长时性能下降多个OR条件同 IN但更啰嗦可能影响索引选择如果查的是一段连续区间比如 1000 到 2000 的整数IN (1000, 1001, ...)是很蠢的写法列表长且没意义。BETWEEN AND或者 AND 更合适。还有一点容易被忽略即使有索引如果区间范围特别大比如一个字段的值只有 0 到 100你查BETWEEN 1 AND 99覆盖了表里 99% 的行优化器也可能放弃索引直接全表扫描因为扫索引再回表的成本比全表更高。这不是语法问题是优化器的选择逻辑。所以排查性能问题时别只看有没有索引还要看查询范围的比例。4. 生产环境里最容易翻车的四个场景4.1 NULL 值参与时结果集莫名变少SQL 里的三值逻辑TRUE、FALSE、NULL影响了很多人的直觉。比如员工表里有些人的奖金bonus是 NULL查询SELECT name, bonus FROM employee_info WHERE bonus BETWEEN 1000 AND 5000;结果是奖金为 NULL 的员工不会出现在结果里。因为NULL 1000 AND NULL 5000的结果是 NULL不是 TRUEWHERE 条件只有结果为 TRUE 才放行。如果业务上希望没有奖金记录也展示出来就得显式处理 NULLSELECT name, bonus FROM employee_info WHERE (bonus BETWEEN 1000 AND 5000) OR bonus IS NULL;但这样做之前先想清楚业务口径NULL 代表没有奖金还是奖金为 0如果是 0应该先COALESCE(bonus, 0)再比较否则你的报表可能会把没有记录和0 元混在一起。4.2 小数精度问题导致边界判断失灵用FLOAT存价格、分数这类数据再用BETWEEN AND做边界查询是生产环境另一个隐蔽的坑。我举个例子假设一张评分表score FLOAT某一行实际存的是 3.6但插入时因为浮点表示问题内部存储值可能是3.5999999。执行SELECT * FROM score_info WHERE score BETWEEN 3.6 AND 4.0;这行就可能被漏掉。排查起来非常难受因为SELECT * FROM score_info WHERE score 3.6在某些巧合下能查出来但BETWEEN不行。最佳实践是金额、评分、精确百分比这类数据用DECIMAL类型。如果历史表已经用了FLOAT可以先用ROUND()处理但要注意这样可能牺牲索引。数据量小无所谓数据量大只能做表结构改造。4.3 半开半闭的业务需求BETWEEN AND 只能做闭区间业务上经常出现大于等于 A 且小于 B这样的需求比如年龄 18 到 25 之间25 岁不算优惠满 100 减 20正好 100 算但 200 的下一档另算这时候BETWEEN AND语义上就撑不住了因为它天生包含两边端点。强行用BETWEEN 100 AND 199.99这种写法属于凑数一旦字段精度变化比如从两位小数变成三位小数就出问题。正确的解法是回到最基础的条件写法SELECT * FROM activity_user WHERE order_amount 100 AND order_amount 200;写这种语句时我一般会在注释里标明区间口径左闭右开避免后续维护的人理解偏差。4.4 NOT BETWEEN 和 NULL、括号的连锁反应NOT BETWEEN是BETWEEN AND的取反但它也遵循三值逻辑。举例SELECT * FROM employee_info WHERE id NOT BETWEEN 2 AND 4;没有 id 的 NULL 行依然不会出现。这本身逻辑没错但如果你在 NOT 条件后面又叠加了其他条件比如SELECT * FROM employee_info WHERE id NOT BETWEEN 2 AND 4 AND status 1;可读性就开始下降了。代码评审时别人很难一眼判断这个条件的范围容易在后面维护时加错括号。我建议遇到多个条件组合时给区间条件单独加括号比如WHERE (id NOT BETWEEN 2 AND 4) AND status 1这样语义更清晰也不会因为NOT和AND的优先级产生误读。5. 我的使用原则什么情况下放心用什么情况下建议绕开5.1 放心用闭区间需求 连续范围 明确的数据类型说了这么多坑不代表BETWEEN AND一无是处。恰恰相反在很多场景它都是最合适的写法。比如按主键拉取一段连续 IDSELECT * FROM order_info WHERE id BETWEEN 10001 AND 20000 AND tenant_id 8;比如数值型字段的闭区间筛选SELECT * FROM product_info WHERE price BETWEEN 50 AND 100;只要字段是DECIMAL、INT这类精确类型需求明确是闭区间BETWEEN AND就是清晰高效的表达。写起来比两个AND条件简短语义也更接近自然语言。5.2 绕开日期当天、字符串范围、半开区间、NULL 特殊语义以下情况我会主动绕开BETWEEN AND查询DATETIME字段的某一天或某个月用和组合避免边界漏数据。字段是VARCHAR但存的是数字不要用字符串区间。需要含头不含尾的半开区间别凑BETWEEN。字段允许为空且 NULL 有特殊业务含义时先想清楚要不要单独处理。另外一个经验在数据库客户端里跑EXPLAIN已经成习惯了特别是遇到BETWEEN AND花 10 秒看一下执行计划的type和key能提前发现一半的性能隐患。5.3 最后分享一个小技巧日期区间统一用左闭右开我现在写业务 SQL凡是日期时间字段的区间筛选基本统一写成左闭右开WHERE created_at 2024-01-01 AND created_at 2024-02-01;好处有两个第一不管created_at有没有带时间、带毫秒都能覆盖到月末最后一刻第二代码里写清楚了边界后续同事接手不会因为BETWEEN 2024-01-31这种写法产生疑问。这个习惯是我在一次需求上线后改过来的。当时报表统计1 月订单量SQL 用了created_at BETWEEN 2024-01-01 AND 2024-01-31结果2024-01-31 09:00:00以后的订单全部没统计进去。排查了两个小时才发现是边界问题。如果你以前也写过类似的 SQL不妨现在回过去看一眼那个每月统计的查询条件说不定就找到了数据对不上的原因。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。