☰
MySQL字符串BETWEEN范围查询:揭开“左闭右开”误区的真相
2026/10/20 17:25:41 网站建设 项目流程

先抛个问题:查名字范围的时候,用过BETWEEN 'A' AND 'Z'这种写法吗?如果有,你可能见过一个很怪的现象——以 A、B 开头的记录都在,以 Z 开头的记录却只剩大写 Z 本身,像ZA、ZBC这种反而查不出来。于是网上很多教程都流传着一个说法:MySQL 的BETWEEN...AND...用在字符上时,范围是“左闭右开”。最近某培训课程里的讲师“老杜”也这么讲,评论区直接吵成一团。说实话不怪学员,这个知识点确实反直觉。我的观点很明确:这个说法是错的,或者说非常容易把人带沟里。今天这篇文章,就专门把BETWEEN...AND...在字符类型上的真实行为掰开揉碎讲清楚,顺便还原一下所谓“左闭右开”印象到底是怎么产生的,以及以后查字符串范围到底该怎么写才不出错。

1. 先从老杜的说法说起:为什么“左闭右开”听着像那么回事

1.1 老杜的原话和典型误解

老杜在课程里的表述大概是这样的:between...and...如果用在字符串上,实际效果是“左边包含,右边不包含”,也就是等价于>= 左边界 AND < 右边界。他给的例子通常也很“迷惑”,比如查BETWEEN 'a' AND 'c',结果发现'a'能查到,'aa'也能查到,'b'能查到,但'ca'、'cb'查不到。于是他就得出结论:右边是开的,所以'c'开头的一律查不到。

这个结论的迷惑性在于,他观察到的现象“部分属实”——确实有大量以'c'开头的字符串查不出来,比如'ca'、'cb'、'cabc'都不在BETWEEN 'a' AND 'c'范围内。但问题是,'c'本身呢?SELECT 'c' BETWEEN 'a' AND 'c';在 MySQL 里返回的是1,也就是'c'是在范围内的。如果真的是左闭右开,那'c'应该不在范围内才对。所以老杜只看到了“界面”,没看到“边界”,把边界上的特例漏掉了,得出了一个以偏概全的结论。

1.2 为什么会观察出“右边像开了”的现象

要理解老杜的错,得先明白字符串是怎么比较大小的。MySQL 里的字符串比较不是按人的直觉“先看长度,再看内容”,而是按排序规则(collation)逐字符比较字典序。字典序有一条很关键的前缀规则:如果'c'是'ca'的前缀,那么'c'小于'ca',因为短的字符串在字典里排前面。

于是当你写name BETWEEN 'a' AND 'c'时,等价条件其实是name >= 'a' AND name <= 'c'。任何一个以'c'开头但比'c'长的字符串,比如'ca',在字典序上都比'c'大,自然就不满足name <= 'c'。范围里唯一的“c 家族”成员就是'c'自己。这个现象太像“右边界开了”,以至于很多人不去验证'c'本身是否存在,就武断地得出“左闭右开”的结论。真相恰恰相反:右边界是闭的,'c'就在里面;只是比'c'更长的'c'前缀扩展字符串因为字典序规则被排除在外了。

2. 先搞清楚 BETWEEN...AND... 在 MySQL 中的真正语义

2.1 官方文档怎么说

MySQL 官方文档对BETWEEN...AND...的描述非常简洁:expr BETWEEN min AND max等价于(expr >= min AND expr <= max)。注意这里没有任何“字符串特殊处理”的字眼,也没有“左闭右开”。也就是说,无论你拿数值、字符串、日期还是字节类型,BETWEEN的语义永远都是闭区间,两端的边界值都在范围内。

网上有些说法会把BETWEEN和IN做类比,但IN是枚举匹配,BETWEEN是范围匹配,机制完全不一样。还有人拿某些编程语言里的切片语法硬套 MySQL,比如 Python 的list[1:3]是左闭右开,于是脑补BETWEEN也这样。这个类比完全站不住脚,BETWEEN的官方定义就是>=和<=的组合,没有任何一个数据库会把BETWEEN做成左闭右开,因为那样会破坏 ANSI SQL 的标准语义。

2.2 数值类型上的直觉:闭区间 [a, b] 刻进 DNA

数值类型能让人很清楚理解闭区间。SELECT * FROM t WHERE age BETWEEN 18 AND 25;会把 18 岁和 25 岁的人都查出来,这个没人抬杠。因为数值的大小关系是线性的,18 <= age <= 25非常直观。

但字符串的大小关系不是线性的,它是一棵字典树(Trie)上的前缀关系。同样是“闭区间”,映射到字符串的字典序上,表现为所有“落在'a'到'c'这个字典序区间内”的字符串,包括'a'、'aa'、'ab'、'b'、'c',但不包括'ca'、'cb'、'd'。所以问题不在BETWEEN本身,而是很多人把数值比较的直觉直接搬到了字符串上,忘了字符串还有前缀规则这个“隐藏机制”。

2.3 字符型真正的比较逻辑:排序规则决定一切

字符串比较在底层依赖的是排序规则。MySQL 里utf8mb4_general_ci、utf8mb4_unicode_ci、utf8mb4_bin都会对比较结果产生影响。_ci表示大小写不敏感,_bin表示二进制逐字节比较,大小写敏感。

在utf8mb4_general_ci下,'A'和'a'视为相等,所以'A' BETWEEN 'a' AND 'c'返回 1。在utf8mb4_bin下,'A'不等于'a',结果可能就变了。这进一步说明:字符范围查询的结果不是“左闭右开”一句话能概括的,它完全取决于排序规则。老杜那种笼统的说法,连排序规则都不提,遇到大小写敏感场景直接翻车。

3. 用一组可复现的实验证明:字符 BETWEEN 是闭区间而非左闭右开

3.1 实验环境与准备

我手头用的 MySQL 8.0,字符集utf8mb4,排序规则默认utf8mb4_general_ci。这些实验在 5.7 上结果一样,因为BETWEEN的语义从没变过。先创建一张临时表,插入一批有代表性的字符串。

CREATE TABLE demo_str_range ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; INSERT INTO demo_str_range (name) VALUES ('a'), ('A'), ('aa'), ('ab'), ('az'), ('b'), ('B'), ('bb'), ('c'), ('C'), ('ca'), ('cb'), ('cabc'), ('d'), ('z'), ('za'), ('zb');

注意utf8mb4_general_ci不分大小写,所以'A'和'a'在比较时算同一个值。后面的实验结果会体现这一点。

3.2 实验一:边界字符本身是否被包含

先做最直接的验证:'c'到底在不在BETWEEN 'a' AND 'c'内?执行下面这条 SQL:

SELECT 'a' BETWEEN 'a' AND 'c' AS a_in, 'b' BETWEEN 'a' AND 'c' AS b_in, 'c' BETWEEN 'a' AND 'c' AS c_in, 'ca' BETWEEN 'a' AND 'c' AS ca_in, 'cb' BETWEEN 'a' AND 'c' AS cb_in;

结果是1, 1, 1, 0, 0。也就是说'c'这个右边界本身稳稳地落在范围内。如果老杜的“左闭右开”成立,c_in应该是 0,可 MySQL 给了 1。这已经足够推翻“右开”的说法。

再回头看整个表里的数据,执行:

SELECT name, name BETWEEN 'a' AND 'c' AS in_range FROM demo_str_range ORDER BY name;

结果会看到'a'、'A'、'aa'、'ab'、'az'、'b'、'B'、'bb'、'c'、'C'都是 1,而'ca'、'cb'、'cabc'都是 0。这些结果恰好解释了两件事:右边界是包含的,同时边界的前缀扩展字符串被排除。看起来“怪异”,但完全符合字典序闭区间。

3.3 实验二:前缀扩展字符串的“错觉”

为了把“错觉”展示得更明显,我建议用一个更贴近实际业务的例子。比如用户表里有一批以c开头的订单编号,'c0001'、'c0002'、'c1000'等等。你执行WHERE order_no BETWEEN 'c' AND 'd',结果可能只返回'c'本身以及以'c'开头但字典序不超过'd'的编号?不对,实际上'c0001'比'c'长,字典序上'c' < 'c0001',所以'c0001'不满足<= 'c',全都被排除。而'c0'这种字符串也可能被排除,除非它们的字典序刚好落在(c, d]?不,'c0'与'c'比较:第一个字符相等,然后'c0'还有'0','c'结束,所以'c' < 'c0',同样不在。

这时如果你只看到“c 开头的编号都查不出来”,当然会说“右边是开的”。但注意,'c'这个精确值本身能查出来。所以正确的认知是:字符串范围查询包含边界精确值,但不包含任何比边界更长且以边界为前缀的值。这不是“右开”,而是字典序的前缀规则在起作用。

3.4 实验三:大小写与排序规则的影响

把排序规则改成utf8mb4_bin,重新验证:

SELECT 'A' BETWEEN 'a' AND 'c' AS A_in_bin, 'a' BETWEEN 'a' AND 'c' AS a_in_bin, 'C' BETWEEN 'a' AND 'c' AS C_in_bin, 'c' BETWEEN 'a' AND 'c' AS c_in_bin;

在utf8mb4_bin下,'A'的字节值比'a'小,所以'A' BETWEEN 'a' AND 'c'返回 0,而'a'返回 1。'C'同理返回 0,'c'返回 1。大小写敏感环境下,连边界本身都可能被大小写规则推出范围。这就更说明问题:所谓“左闭右开”完全忽略了排序规则这个关键变量,用它作为通用结论去教人,遇到bin排序就会产生灾难性误判。

4. 之所以“看起来像左闭右开”的三个深层原因

4.1 字典序比较里的“前缀规则”是罪魁祸首

字符串比较的本质是逐字符按排序规则比较。一旦两个字符串在某个位置分出了大小,比较就结束;如果一直相等直到一个字符串结束,那么短的字符串排在前面。举个例子,'c'和'ca'比较:第一个字符'c'等于'c',然后'c'已经结束,'ca'还有'a',所以'c' < 'ca'。这个规则导致所有“以边界字符串为前缀且更长的字符串”都比边界大。范围上限一旦被这种值碰到,看起来就像被切开了一道口子,但这不是开区间,只是前缀规则造成的排序结果。

这个前缀规则在字典里很好理解:整本字典里,"c"这个单词所在的位置后面紧接着"ca"、"cab"、"cabbage"。如果你查“c 到 c”的范围,只能查出"c"本身,查不出"ca",因为"ca"严格大于"c"。这不叫“右开”,这叫字典序的自然排序。

4.2 上限为前缀字符串时的边界效应

如果范围上限不是一个短字符串,而是一个完整的长字符串,比如BETWEEN 'a' AND 'ca',那结果会怎样?它会包含'ca'、'cab'、'cabbage',但不会包含'cb'。此时你会发现右侧边界又像“闭”的了。所以所谓“左闭右开”并不是稳定特征,它只在右边界恰好是一个短字符串且存在大量以它为前缀的数据时才显现。你拿BETWEEN 'a' AND 'cb'去查,'cabc'也在范围内,右边看起来又“开了”?其实不是开了,是因为'cabc'比较时第一个字符相等,第二个字符'a' < 'b',所以'cabc' < 'cb',于是它又被包含进来了。这些行为用“前缀规则 + 字典序”都能解释,用“左闭右开”反而越解释越乱。

换个更直白的说法:字符串的范围不是一条直线,而是一棵按前缀组织的树。你把BETWEEN理解成“在这棵树的某个子树区间内”,就完全不会产生“左闭右开”的错觉。哪个节点在边界上、哪个节点是边界的子孙,决定了它在不在范围内。

4.3 排序规则让字符顺序“非线性”

除了前缀规则,排序规则还会给字符串顺序引入更加反直觉的“非线性”。utf8mb4_general_ci会做大小写折叠,utf8mb4_unicode_ci会按 Unicode 排序权重处理重音、变体字符,中文在不少排序规则下按编码或拼音排序。同一个字符在不同排序规则下可能处在完全不同的位置。

举一个很容易踩坑的例子:utf8mb4_general_ci下,'A'、'a'的排序权重相同,但'a' < 'aa',同时'A' < 'aa'是否成立?成立,因为大小写折叠后'a' < 'aa'。如果你建表用了utf8mb4_bin,'A'会排在'a'前面,范围边界完全不同。这些差异根本不是“左闭右开”能概括的。要正确判断字符串范围,必须知道表或字段的排序规则,否则你写的BETWEEN就是一个“薛定谔的范围”。

5. 如果真要实现左闭右开(或精确的区间查询),该怎么写 SQL

5.1 先牢记 BETWEEN 的等价写法

任何BETWEEN都可以安全地改写成两个显式条件,这也是排查问题时的第一反应:

-- 原始写法 SELECT * FROM demo_str_range WHERE name BETWEEN 'a' AND 'c'; -- 等价写法 SELECT * FROM demo_str_range WHERE name >= 'a' AND name <= 'c';

改写之后,字符串比较规则不变,但“右边界闭”这个语义更明确。当你怀疑某个边界值为什么查不出来时,直接改成>=和<=形式,逐个边界试,立刻就能看出问题在哪。我在日常开发中一般直接写>= AND <=,不是为了绕开BETWEEN,而是因为显式的比较运算符可以让其他开发者一眼看到边界条件,减少“BETWEEN到底含不含边界”的争论。

5.2 实现真正的 [left, right):用 < 替换 <=

如果你确实需要“左边包含、右边不包含”的语义,那就不要用BETWEEN,直接自己控制比较运算符:

-- 左闭右开:包含 a,但不包含 c SELECT * FROM demo_str_range WHERE name >= 'a' AND name < 'c';

这个写法会查出来'a'、'A'、'aa'、'ab'、'az'、'b'、'B'、'bb',但查不出来'c'、'C'、'ca'、'cb'、'cabc'。如果你希望把'c'也排除掉,同时还想查所有以'a'和'b'开头的字符串,这个写法是正确的。反过来,如果你想查“以 a 开头、以 c 开头也包含,但 d 开头不含”,就用>= 'a' AND < 'd'。

这里要特别提醒:如果你真的要“按首字母分区”,千万别用BETWEEN 'a' AND 'c',因为它会把'c'单独算进去,造成统计口径混乱。分段统计时统一用[起始, 下一个起始)的写法,比如:

SELECT CASE WHEN name >= 'a' AND name < 'b' THEN 'a组' WHEN name >= 'b' AND name < 'c' THEN 'b组' WHEN name >= 'c' AND name < 'd' THEN 'c组' ELSE '其他' END AS name_group, COUNT(*) AS cnt FROM demo_str_range GROUP BY name_group;

这样每个组的边界清晰,不会把'c'和'ca'分到两个组里去。

5.3 利用前缀规则实现“按前缀模糊范围”

在 MySQL 中,字符串范围查询经常被用来替代LIKE 'prefix%'。比如查所有姓“张”的用户,你可以写WHERE name >= '张' AND name < '仝'(用下一个汉字作为上界),但前提是排序规则对汉字排序符合预期。更稳妥的做法是直接:

SELECT * FROM user WHERE name LIKE '张%';

不过LIKE '张%'无法使用普通 B-Tree 索引?实际上在 MySQL 中,只要字符集和排序规则一致,LIKE '张%'通常会走索引范围扫描,而BETWEEN '张' AND '仝'也会走索引,两者性能差别不大。区别在于BETWEEN写法可能因为“上界猜错”而漏数据或含错数据。比如你猜“下一个字是仝”,但排序规则下可能不是这样。所以我的经验是:明确需要闭区间就BETWEEN,需要前缀匹配就LIKE 'prefix%',不要硬拿范围模拟模糊匹配。

6. 实战中的常见问题与避坑指南

6.1 中文和大小写引发的边界错位

中文场景是BETWEEN的重灾区。我见过一个业务需求:按汉字首字母分组统计城市名,开发直接写了WHERE city_name BETWEEN 'A' AND 'Z',结果把一堆中文城市也查进去了。原因很简单:MySQL 的中文排序不是按拼音首字母来的,utf8mb4_general_ci下是按 Unicode 码点排序,中文的码点普遍远大于'Z',所以'北京' BETWEEN 'A' AND 'Z'返回 1。如果你想让表达式结果是 0,得用拼音首字母字段,或者用LEFT(city_name,1)转成拼音再比。

另一个问题是大小写。默认utf8mb4_general_ci不区分大小写,'a'和'A'会被视为相等。如果你查WHERE code BETWEEN 'a' AND 'z',结果会发现'Z'也在里面。很多人把这种现象也归咎于“左闭右开”,实际上又是排序规则在捣鬼。要严格区分大小写,必须让字段排序规则是utf8mb4_bin,或者显式加COLLATE utf8mb4_bin:

SELECT * FROM demo_str_range WHERE name COLLATE utf8mb4_bin BETWEEN 'a' AND 'c';

这样'C'就不会被查出来了。

6.2 索引与字符范围查询

字符串范围能不能走索引,取决于排序规则是否一致。如果字段是utf8mb4_general_ci,你查询时写name COLLATE utf8mb4_bin BETWEEN 'a' AND 'c',MySQL 没法直接用索引,因为比较规则被临时改掉了。上面的实验我加COLLATE只是为了演示,生产环境这么写会让索引失效,全表扫描跑哭你。

要想高效执行字符范围查询,最稳的方案是让字段的排序规则和查询条件保持一致,并且把范围条件写成>=和<=。比如订单编号字段排序规则是utf8mb4_general_ci,查询直接写WHERE order_no >= 'a' AND order_no <= 'z',优化器大概率能走索引范围扫描。如果你发现EXPLAIN显示type=ALL,先检查有没有在字段上施加COLLATE或函数,比如LEFT(name,1),那基本就废了。

6.3 和日期时间范围混用时的注意点

日期时间不是字符,但很多人会把它转成字符串再查。比如WHERE date_str BETWEEN '2024-01-01' AND '2024-12-31',如果date_str是VARCHAR,这个范围是左闭右闭,'2024-12-31'会被包含;但如果你把上界改成'2025-01-01',那么'2024-12-31 23:59:59'这种带时间的字符串就比'2025-01-01'小,也会被包含。这又回到字符串字典序:'2024-12-31 23:59:59' < '2025-01-01'成立,因为前 10 位固定,第 11 位分别是空格和'0',空格的排序位置几乎总是小于数字字符,所以整个字符串被算在范围内。

所以处理日期时间区间,我强烈建议不要用字符串VARCHAR存。用DATETIME类型,然后WHERE created_at >= '2024-01-01 00:00:00' AND created_at < '2025-01-01 00:00:00',才能干净利落。如果因为历史原因只能用字符串,就用DATE()函数转成日期比较,或者统一存储成固定格式再范围查询,否则边界问题会层出不穷。

6.4 一句话结论背下来

字符串上BETWEEN...AND...就是左闭右闭,等价于>=和<=,比较规则是排序规则下的字典序。它不包含“比右边界更长且以右边界为前缀”的字符串,这让很多人误以为右开。解决这个问题的关键不是死记“左闭右开”,而是理解字典序前缀规则。真要用左闭右开,就用>= 左 AND < 右。

最后分享一个小习惯:我在写任何范围查询前,都会先跑一条不带WHERE的边界探查 SQL,把要比较的边界值直接放在SELECT里验证,比如SELECT 'c' BETWEEN 'a' AND 'c';,十秒钟就能确认语义,比争论一小时“到底闭不闭”高效得多。这个习惯帮我少踩了好几次线上数据漏查的坑,你可以直接拿去用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询