MySQL BETWEEN是闭区间,不是左闭右开——字符串字典序解析
发布时间:2026/10/20 8:07:29 锦皓数字建站

最近看到有讲师在讲 MySQL 的BETWEEN ... AND ...时抛出过一个观点大意是“这个范围操作符用在字符上是左闭右开”。评论区不少学员在问有的说自己试出来好像确实不包含右边的边界有的则说不对。我看完后只有一个感觉这种说法如果不纠正后面写 SQL 踩坑的绝对不止一个。为了避免更多人被绕晕我把这个问题从语义、字符比较规则、实际执行结果到常见的误区一次性讲清楚。1. 先给结论BETWEEN 到底是闭还是开先说答案没有任何花哨的例外MySQL 的BETWEEN ... AND ...是闭区间等价于 下限 AND 上限。这个结论对数字、日期、字符串全都适用。官方文档写得很明白expr BETWEEN min AND max -- 等价于 expr min AND expr max所以对于字符串只要表达式的值大于等于下界并且小于等于上界就会返回真。拿 SQL 直接验证是最直观的SELECT c BETWEEN a AND c AS r1; -- 结果是 1包含 c 本身 SELECT ca BETWEEN a AND c AS r2; -- 结果是 0不包含 ca第一条返回 1说明c是包含在区间内的。如果真是“左闭右开”c就不应该有结果。也正是这个简单的测试直接推翻了“字符上左闭右开”的说法。那为什么有人会得出“左闭右开”的错觉原因在于字符串的比较方式不是数值大小比较而是字典序比较。当你把上限设置为z时并不是“所有以 z 开头的字符串都小于等于 z”实际上任何以z开头并且后面还有内容的字符串比如za、zz它们都排在z的后面所以都不满足 z。于是查询结果中看不到这些字符串看起来“右边像开了个口子”但本质上这是闭区间只是你选的边界没有覆盖到你以为的内容。2. 字符比较的“字典序”到底怎么比要真正理解这个问题先得弄明白 MySQL 怎么比较两个字符串。字符串比较和数字比较最大的区别在于它是逐字符比较的。比较规则大致如下从第一个字符开始比较两个字符串在当前位上的字符顺序值。如果第一个字符已经分出大小整体比较就结束。如果第一个字符相同则继续比较第二个字符依此类推。如果其中一个字符串先结束另一个还有剩余字符那么较短的字符串被判定为小于较长的字符串。举个例子比较apple和banana第一个字符a小于b所以apple banana不需要再看后面的内容。再看一个容易踩坑的例子比较c和cherry。第一个字符都是c于是继续比较第二个字符c已经结束了而cherry还有第二个字符h。按照规则短字符串c小于长字符串cherry。所以c cherry成立反过来说cherry c就不成立。这就解释了为什么WHERE name BETWEEN a AND c不会返回name cherry。尽管cherry以c开头但它排在了c的后面不在闭区间内。同样z、za、zz之间的相对位置是z za zb ... zz所以BETWEEN a AND z能查到z但查不到za和zz。如果你把BETWEEN想象成一本字典中从第 1 页到第 300 页的区间那么第 300 页本身一定包含但第 301 页及之后就不在里面。z是第 300 页za是第 301 页。所以这不是“开区间”而是“你选的上界刚好卡在了一个位置上”。2.1 排序规则对字符比较的影响还有一个隐藏变量排序规则Collation。同一个字符在不同的排序规则下谁大谁小、是否相等结果可能完全不同。MySQL 中常见的排序规则有utf8_general_ci不区分大小写忽略尾部空格。utf8_bin按二进制字节比较区分大小写。utf8mb4_0900_ai_ciMySQL 8 默认不区分大小写不区分重音不忽略尾部空格NO PAD。如果你使用不区分大小写的排序规则BETWEEN a AND c也会匹配A、B、C因为a和A被认为是相等的。如果你希望做精确、大小写敏感的字符范围查询需要显式指定COLLATE utf8mb4_bin或类似规则。另外在旧的PAD SPACE排序规则下字符串末尾的空格会在比较时被忽略。也就是说表里如果有一条a a 加空格在utf8_general_ci下a a于是它也可能出现在BETWEEN a AND c的结果里。而在 NO PAD 规则下空格是参与比较的结果可能不同。这种细节平时不太会注意但一旦数据里有尾随空格就会成为排查问题的“大坑”。3. 老杜的说法错在哪把“前缀范围”当成“区间范围”老杜之所以说出“字符上左闭右开”这种话大概率是混淆了“前缀范围查询”和“区间查询”两个场景。举例来说他想查所有以字母a到m开头的用户名于是很自然地写了SELECT * FROM user WHERE name BETWEEN a AND m;执行后发现以m开头的mary没有被查出来。于是他想当然地认为BETWEEN a AND m对字符串不包含m本身所以是左闭右开。但真正的原因是什么呢我们分析一下m本身在区间内因为m a且m m所以BETWEEN返回 1。mary不在区间内因为mary与m比较时首字符相同但mary更长所以mary m不满足 m。也就是说m这个端点本身是被包含的但所有以m开头且后接其他字符的字符串都排在了m的后面。你真正想要的是“以 a 到 m 任意一个字母开头的所有字符串”这本质上是前缀范围而不是字典序区间。要实现这个需求应该用SELECT * FROM user WHERE name a AND name n;这里的逻辑是任何以a、b、...、m开头的字符串它们都大于等于a并且由于首字母小于n整体一定小于n。这就能覆盖所有以这些字母开头的名字。但注意这个写法是左闭右开它并不是BETWEEN的语义而是你自己用和构造出来的范围。老杜等于是把一个“自己想要的逻辑”强加到了BETWEEN的机制上然后说出了错误结论。3.1 为什么这个错误说法看起来还有点“道理”因为它确实能解决一部分问题如果查询BETWEEN a AND m至少能查到m本身也会查到所有以a到l开头的长字符串。结果表面上“好像右边缺了一块”于是有人干脆总结成“用在字符上就是左闭右开”。但实际上只要把区间上界换成ma来测试就能立刻发现矛盾SELECT m BETWEEN a AND ma; -- 结果 1包含 m SELECT ma BETWEEN a AND ma; -- 结果 1包含 ma说明右边界是关着的 SELECT mb BETWEEN a AND ma; -- 结果 0因为 mb mama本身能被包含进来这说明右边界是闭合的。所以用“开区间”去解释BETWEEN的底层语义是完全错误的。4. 什么情况下确实需要“左闭右开”虽然BETWEEN是闭区间但在实际业务中左闭右开是一种非常常用的区间表达方式。尤其是下面两类场景不要用BETWEEN硬凑。4.1 日期时间范围查询假设表里有一个DATETIME类型的字段create_time你想查询 2024 年 1 月 1 日全天产生的数据。如果写成SELECT * FROM t WHERE create_time BETWEEN 2024-01-01 AND 2024-01-01;你会漏掉绝大多数数据。因为2024-01-01作为字符串与DATETIME比较时等价于2024-01-01 00:00:00。只有当记录的时间恰好是零点零分零秒时才会命中这天其他时间的数据全部被排除。正确写法应该是SELECT * FROM t WHERE create_time 2024-01-01 00:00:00 AND create_time 2024-01-02 00:00:00;这里的下界包含上界不包含即标准左闭右开。但注意这是你手动用和实现的不是BETWEEN自身的行为。如果你非要用BETWEEN去包一天可以写WHERE create_time BETWEEN 2024-01-01 00:00:00 AND 2024-01-01 23:59:59但这样做会遗漏 23:59:59.5 等小数秒而且不同数据库的精度不同容易出事。所以强烈建议时间范围统一使用左闭右开。4.2 字符串前缀范围查询回到前缀范围。如果你想查询名字以 A 到 M 开头的所有用户用BETWEEN A AND M是错的因为Mary不满足小于等于M。正确方案是SELECT * FROM user WHERE name A AND name N;这里用N作为上界利用了字典序的特性任何以 M 开头的字符串首字母 MASCII 77小于 NASCII 78所以必然小于N。这也是一种左闭右开。但如果你把上界写成N查询结果不会包含N本身也不会包含以 N 开头的任何字符串这正是你想要的。注意这个写法依赖字符集和排序规则。在大小写不敏感的排序规则下a和A可能被视为相同会影响实际范围。所以在做精确前缀范围时显式指定COLLATE更保险。5. 别忽视隐式类型转换这个大坑除了边界问题使用BETWEEN时还有一个高频翻车点字符串和数字混用。假如有一列nickname是VARCHAR类型你写SELECT * FROM user WHERE nickname BETWEEN 10 AND 20;MySQL 不会把 10 和 20 转成字符串去比较而是会把nickname列的值隐式转换为数字再比较。于是nickname 15abc会转成 15命中。nickname abc会转成 0不命中。nickname 也会转成 0。如果你的本意是查找字符串10到20之间的值这种写法就是完全错误的。所以当字段是字符类型、而范围边界是数字字面量时最好显式把边界写成字符串或者使用CAST统一类型SELECT * FROM user WHERE nickname BETWEEN 10 AND 20;即便如此字符串按字典序比较20和200的关系又可能让你意外。总之类型不一致时先转成同一种类型再比较否则结果极其迷惑。5.1 一个能复现的坑非数字前缀全部变成 0我实际见过有人用字符列存手机号然后查询SELECT * FROM phone_table WHERE phone BETWEEN 13800000000 AND 13999999999;如果手机号列是VARCHAR且个别数据中有非数字字符比如空格、横线这些值会被转成 0 或部分数字导致结果完全不符合预期。后来排查到原因就是因为隐式转换。所以业务中手机号、身份证号这类长数字一定要用字符串存储查询时也一定要用字符串边界并且尽量用等值或前缀匹配而不是数值范围。6. 实际操作用 SQL 实测字符边界的包含关系为了让大家能直接复现我整理了一组测试 SQL可以跑在自己的环境里验证。-- 测试字符串 BETWEEN 的边界包含关系 SELECT a BETWEEN a AND c AS test_a, b BETWEEN a AND c AS test_b, c BETWEEN a AND c AS test_c, cb BETWEEN a AND c AS test_cb, ca BETWEEN a AND c AS test_ca, cc BETWEEN a AND c AS test_cc;我本地 MySQL 8.0 执行结果如下表达式结果aBETWEENaANDc1bBETWEENaANDc1cBETWEENaANDc1cbBETWEENaANDc0caBETWEENaANDc0ccBETWEENaANDc0可以看到端点c是包含的这足以说明闭区间。而ca、cb这些比c长的字符串因为排在c后面所以不包含。再测一下“看起来像开区间”的典型SELECT z BETWEEN a AND z AS z_in, za BETWEEN a AND z AS za_in, zz BETWEEN a AND z AS zz_in;结果为z_in 1za_in 0zz_in 0。这并不代表BETWEEN是右开而是字典序决定了za和zz已经超出了z这个端点。6.1 如果你仍然怀疑可以构造一个数字字符的对照其实数字字符0到9的字典序和数值顺序是一致的不容易看出问题。用多字符字符串来对比会更明显SELECT 10 BETWEEN 1 AND 9 AS r1, -- 0因为 10 9 9 BETWEEN 1 AND 9 AS r2, -- 1包含上界 11 BETWEEN 1 AND 9 AS r3; -- 0因为 11 9即使都是数字10作为字符串比较它大于9所以不在1到9这个字典序闭区间里。这正是字符串比较和数值比较的本质差异。7. 高频坑位速查BETWEEN 使用排查清单这里把实际工作中最容易出问题的几个点整理成一张表方便你排查 SQL 异常时对照现象可能原因正确做法时间类型查不出当天大部分数据用了BETWEEN 2024-01-01 AND 2024-01-01使用 2024-01-01 00:00:00 AND 2024-01-02 00:00:00字符串前缀范围查不到以边界字符开头的长字符串用了BETWEEN A AND M查询前缀使用 A AND N需要时指定COLLATE字符列和数字边界比较结果诡异MySQL 隐式将字符列转为数字明确统一类型或边界写成字符串大小写不敏感排序规则下结果超出预期默认_ci排序规则将大小写视为相等查询时显式指定utf8mb4_bin或对应二进制排序规则字符串尾部空格影响比较结果PAD SPACE 或 NO PAD 规则不同检查排序规则的 PAD 属性必要时TRIM字段或指定COLLATE这条表里的内容每一个我都实际踩过或者见别人踩过。尤其是第一条几乎每个月都能在技术群里看到有人问“为什么按天查数据少了”。很多人习惯性写BETWEEN 2024-01-01 AND 2024-12-31结果最后一天只有零点零分零秒的数据剩下的全部丢失。这不是BETWEEN的锅是你没有理解DATETIME的精度。8. 面对“左闭右开”的说法应该怎么记这类问题的本质是把编程语言中常见的切片语法比如 Python 的list[a:b]不包含 b带到了 SQL 里。但 SQL 不是一个统一的宿主语言BETWEEN的定义就是包含边界。你可以在项目规范里约定“范围查询统一使用左闭右开”那是业务层的约定不代表 SQL 语法本身如此。那么怎么记才准确我个人的建议是BETWEEN ... AND ...永远闭区间。字符列上看起来“右边的边界不生效”是因为字符串字典序里任何比上界本身长的字符串都排在上界之后。前缀范围查询不要用BETWEEN用和人为构造左闭右开。时间范围查询优先用和除非你的边界已经精确到秒/毫秒并且你清楚边界会包含。最后再分享一个小技巧当你怀疑BETWEEN行为异常时最快的定位方式就是把查询条件拆开直接写两个比较表达式看看结果差异在哪。-- 原写法 WHERE col BETWEEN a AND z; -- 拆开后的写法 WHERE col a AND col z;如果拆开后的结果和BETWEEN不一致说明你接触的 MySQL 版本/模式有特殊设定如果一致那就说明问题出在你的数据或对字符比较的理解上而不是BETWEEN本身。实测中绝大多数情况都是后者。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。