做开发这些年,我见过太多同事在 BETWEEN 上翻车了。SQL 基础语法里,BETWEEN 看起来人畜无害,一眼就能读懂,可真到了线上数据面前,它往往是漏数据、慢查询、甚至面试挂掉的头号元凶。今天这篇文章就专门把 BETWEEN 的常见用法掰开揉碎聊一遍,从数字、日期、字符串三个核心场景入手,把底层逻辑、边界条件、索引问题一次讲透。无论你是刚入门的新手,还是经常写 SQL 的业务开发、数据分析师,这篇文章都能帮你少踩几个坑。
1. BETWEEN 的基础语法与底层语义
1.1 一条 SQL 背后的区间逻辑
BETWEEN 的标准语法特别简单:
sql复制SELECT 列名
FROM 表名
WHERE 列名 BETWEEN 下限值 AND 上限值;
它表达的意思是:某个字段的值落在下限和上限之间。但这里有个关键点,无数新手第一次都栽在这里:BETWEEN 在 SQL 里是闭区间,也就是说,下限值和上限值本身也会被包含在结果里。
它和你下面这种写法是完全等价的:
sql复制WHERE 列名 >= 下限值 AND 列名 <= 上限值;
我遇到很多同学写 BETWEEN 时以为它只包含中间的值,不包含边界。这个认知会造成非常隐蔽的漏数问题。比如你统计价格在 100 到 200 之间的商品,刚好有一个商品价格就是 200 元,用 BETWEEN 100 AND 200 会把它查出来,但如果你心里想的是“超过 100 且不到 200”,那结果就和你预期不一致了。
理解闭区间是理解 BETWEEN 的第一步。只要记住一个等价关系,后面所有问题都能推导出来:
col BETWEEN a AND b等价于col >= a AND col <= b,两边都取等号。
这句话值得写在你的 SQL 笔记第一页。因为后面所有日期边界、索引优化的问题,本质上都是在和这个闭区间语义作斗争。
1.2 BETWEEN 和 IN:离散集合与连续范围的区别
很多人在筛选条件时会在 BETWEEN 和 IN 之间犹豫。这两个语法看着像,其实是完全不同的两种逻辑。
IN 表达的是离散集合的匹配,比如 WHERE status IN (1, 2, 3),它只会匹配等于 1、2、3 这三条状态记录,中间值 1.5 不会被匹配。BETWEEN 表达的是连续区间的匹配,WHERE price BETWEEN 100 AND 200 会匹配从 100 到 200 之间所有的数,包括 100.5、150、199.99 这些中间值。
用生活里的话说就是:IN 像是点名,喊到谁谁举手;BETWEEN 像是划了一条分数线,过线的人全算。所以如果你要筛选的是“状态枚举值”,用 IN 更合适;如果你要筛选的是“数值/日期区间”,用 BETWEEN 更贴切。
1.3 与 AND、OR 混用时的优先级坑
BETWEEN 自带一个 AND 关键字,这就导致它和普通 AND、OR 混在一起时,很容易产生优先级歧义。SQL 中 AND 的优先级高于 OR,这是数据库的老规矩,但组合上 BETWEEN 后很多人会疏忽。
看这个例子:
sql复制WHERE status = 1 OR status = 2 AND price BETWEEN 100 AND 200;
这条 SQL 实际上会被解析成:
sql复制WHERE status = 1 OR (status = 2 AND price BETWEEN 100 AND 200);
因为 AND 优先于 OR,所以状态为 1 的记录无论价格多少都会被查出来,而状态为 2 的记录必须价格落在 100 到 200 之间才行。如果你本意是“状态为 1 或 2,同时价格在 100 到 200 之间”,就必须加括号:
sql复制WHERE (status = 1 OR status = 2) AND price BETWEEN 100 AND 200;
这个坑在线上代码 review 时特别常见。我的习惯是:只要条件和 BETWEEN 混用,一律用括号把逻辑分组写明确,绝不靠数据库的默认优先级兜底。因为过三个月回来看代码的人(很可能就是你自己)不一定还记得这些优先级规则,但括号谁都看得懂。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数字范围查询:最常见的落地场景
2.1 商品价格区间完整示例
数字范围是 BETWEEN 最直观的使用场景,比如查价格、数量、年龄、分数等等。我先造一张简单的商品表:
sql复制CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(50),
price DECIMAL(10, 2),
stock INT
);
INSERT INTO products (id, name, price, stock) VALUES
(1, '键盘', 129.00, 50),
(2, '鼠标', 89.00, 120),
(3, '显示器', 999.00, 30),
(4, 'USB扩展坞', 189.00, 80),
(5, '摄像头', 329.00, 15);
现在要查价格在 100 到 300 元之间的商品,SQL 写起来很直白:
sql复制SELECT id, name, price
FROM products
WHERE price BETWEEN 100 AND 300;
执行结果会返回键盘(129)、USB扩展坞(189)、摄像头(329)这三条吗?不对,摄像头 329 已经超过 300 了,所以不会被返回。结果应该是键盘、USB扩展坞两条。这就是闭区间和直觉最容易冲突的地方:如果你把“100 到 300”理解成数学上的开区间,你会把 100 和 300 去掉,但 SQL 里这两个边界值都包含,所以价格刚好等于 100 或 300 的商品也会被查出来。
2.2 开区间与闭区间的选择
业务上有些场景确实需要“大于等于下限,但小于上限”的开区间逻辑。比如电商平台做一个“满 100 减 20”的活动,规则是订单金额满 100 元才参与,但 200 元以上走另一档满减。这时候如果你用 BETWEEN 100 AND 200,那订单金额刚好 200 元的那单会被算进第一档,这就不符合业务预期了。
开区间的写法很简单,直接用比较运算符替代 BETWEEN:
sql复制WHERE price >= 100 AND price < 200;
我建议你在需求里先想清楚“边界是否包含”。如果不确定,默认用 >= 和 < 的组合会安全得多,因为半开区间的语义比全闭区间更容易控制,特别是后续要接分页、统计、聚合的时候。
2.3 NULL 与类型隐式转换的干扰
数字范围还有一个隐藏的坑:NULL 值永远不会被 BETWEEN 选中。这不是 BETWEEN 的问题,而是 SQL 三值逻辑(TRUE / FALSE / UNKNOWN)决定的。NULL 参与任何比较运算,结果都是 UNKNOWN,最终会被 WHERE 过滤掉。
所以如果你的价格列允许为 NULL,写 WHERE price NOT BETWEEN 100 AND 200 时,那些价格为 NULL 的行并不会出现在结果里。很多人以为 NOT BETWEEN 会把“所有不在范围内的行”都筛出来,包括空值,但实际上空值在比较时被单独处理了。要单独处理空值,需要显式加 OR price IS NULL。
另一个容易踩的是隐式类型转换。比如价格列是字符串类型(历史表设计遗留),你写 WHERE price BETWEEN 100 AND 300,数据库通常会尝试把字符串转成数字再比较。在小数据量下看不出问题,但大表上这个转换会让索引失效,直接变成全表扫描。所以条件列是什么类型,边界值就写什么类型,尽量不要让数据库去猜。
3. 日期时间范围:坑最多也最值得细抠的地方
3.1 边界问题:为什么 31 号的数据查不全
日期时间范围是 BETWEEN 用得最多、也是翻车率最高的场景。先看一段最常见的错误写法:
sql复制SELECT order_id, order_time
FROM orders
WHERE order_time BETWEEN '2024-01-01' AND '2024-01-31';
这段 SQL 看起来是在查一月份的所有订单,但如果 order_time 列是 DATETIME 类型,它查出来的结果通常会漏掉 1 月 31 日当天的绝大部分数据。
原因还是闭区间的边界问题:'2024-01-31' 在数据库里会被解析成 '2024-01-31 00:00:00',而不是 '2024-01-31 23:59:59'。所以这个条件实际等价于:
sql复制WHERE order_time >= '2024-01-01 00:00:00'
AND order_time <= '2024-01-31 00:00:00';
也就是说,1 月 31 日零点之后的订单,全部被排除在外了。这是一种静默的、不会报错的漏数据,比报错更可怕——报表不汇总 31 号的数据,你还不容易发现。
3.2 推荐写法:半开区间替代 BETWEEN
处理日期时间范围,我最推荐的写法是半开区间,用一个范围的下界和下一个范围的上界:
sql复制SELECT order_id, order_time
FROM orders
WHERE order_time >= '2024-01-01'
AND order_time < '2024-02-01';
这样写能完整覆盖 1 月 1 日零点到 1 月 31 日 23:59:59.999 之间的所有数据。使用这种写法的好处有三个:
第一,语义清晰,不用去算“月底最后一天是 28 还是 31”;第二,不会漏掉边界时刻的数据,特别是毫秒级别的数据;第三,更容易配合索引优化,因为上下界都是裸列比较,不会在列上做函数运算。
如果 order_time 列是 DATE 类型(只有日期没有时间),那 BETWEEN '2024-01-01' AND '2024-01-31' 是没问题的,因为 DATE 类型没有时间部分,边界就是当天零点。但我还是建议统一写成半开区间,防止某天字段类型从 DATE 改成了 DATETIME,SQL 就悄悄出问题。
3.3 不同数据库的细节差异
我实际用过的几个数据库在日期边界处理上大同小异,但细节值得单独说。
| 数据库 | 日期时间类型 | BETWEEN 边界行为 | 推荐写法 |
|---|---|---|---|
| MySQL | DATETIME / TIMESTAMP | 字符串日期被解析为当天 00:00:00,包含下边界、排除当天后续时间 | col >= '2024-01-01' AND col < '2024-02-01' |
| SQL Server | DATETIME / DATETIME2 | 同样会解析为 00:00:00,且 DATETIME 的精度到 3.33 毫秒 | col >= '2024-01-01' AND col < '2024-02-01' |
| PostgreSQL | TIMESTAMP / DATE | 字符串日期转 TIMESTAMP 时也是当天零点 | col >= '2024-01-01' AND col < '2024-02-01' |
| Oracle | DATE | DATE 本身就带时间部分,TO_DATE 不带时间是当天零点 | col >= DATE '2024-01-01' AND col < DATE '2024-02-01' |
| 达梦 / 人大金仓 | 兼容上述常见类型 | 语法与 Oracle/MySQL 兼容,边界逻辑一致 | 同上 |
另外提一句,SQL Server 2008 R2 这类老版本在日期计算上有个经典坑:用 BETWEEN 查日期时如果你写 '2024-01-31',它会转成 2024-01-31 00:00:00.000,而 DATETIME 类型的最小精度是 3.33 毫秒,一些极端边界值可能出现意料之外的舍入。不要在这种老库上写那种“刚好等于某个时间点”的精确匹配,范围查询更稳妥。
3.4 日期字段上的函数包裹会让索引失效
日期范围还有一个更隐蔽的坑,就是有人在 BETWEEN 条件里对字段套了函数,比如:
sql复制WHERE DATE(order_time) BETWEEN '2024-01-01' AND '2024-01-31';
这种写法在逻辑上没毛病,但它对 order_time 列调用了 DATE() 函数。只要列被函数包裹,数据库就很难正常走该列上的索引,优化器只能老老实实全表扫描。数据量小无所谓,几百万行以上性能差距就是几秒和几十毫秒的区别。
我在实际优化慢 SQL 时,发现这个错误的出现频率高得离谱。优化方法就是去掉函数,改成裸列范围比较:
sql复制WHERE order_time >= '2024-01-01'
AND order_time < '2024-02-01';
这样既能走索引,又能覆盖完整时间段,一举两得。
4. 字符串范围与 NOT BETWEEN 的实战细节
4.1 字典序范围:按名字首字母、前缀筛选
很多人以为 BETWEEN 只能用于数字和日期,其实它也可以用于字符串。字符串比较走的是字典序规则。比如查姓氏首字母在 A 到 M 之间的用户:
sql复制SELECT user_name
FROM users
WHERE user_name BETWEEN 'A' AND 'M';
这条 SQL 会返回所有以 A 到 M 开头的用户名。但这里有个容易忽略的点:字典序里 'M' 开头的字符串都算,但 'M' 之后如果还有更多字符,比如 'Mango',它也落在 'A' 到 'M' 之间吗?答案是会。因为字符串比较是按字符逐个比较的,'Mango' 以 'M' 开头,'Mango' <= 'M' 则是比较 'Mango' 和 'M',数据库比较时会发现第一个字符 M 相等,但 'Mango' 更长,在常见排序规则下较长的字符串被视为更大,所以 'Mango' 实际上不会被 BETWEEN 'A' AND 'M' 包含。这就让很多查询结果看起来“缺了一条”。
如果业务需求是“按首字母区间筛选”,我建议用 LEFT() 函数更直观:
sql复制WHERE LEFT(user_name, 1) BETWEEN 'A' AND 'M';
但这种写法又不走索引。更合适的方案是做成前缀范围:如果只想匹配以 A 到 M 开头的人,可以写 WHERE user_name >= 'A' AND user_name < 'N',这样能覆盖 A 到 M 开头的所有字符串,不管后面跟多长。本质上又回到了半开区间的思路。
4.2 排序规则对字符串范围的影响
字符串范围查询的结果受数据库排序规则(collation)影响非常大。比如 MySQL 默认的 utf8mb4_general_ci 或 utf8mb4_unicode_ci 通常不区分大小写,BETWEEN 'a' AND 'c' 会把 'Apple' 这种大写开头的字符串也包含进来;而 PostgreSQL 默认的排序规则是区分大小写的,'Apple' 的首字母 'A' 的编码在 'a' 之前,所以它不会出现在结果里。
这种差异在跨库迁移时特别坑。我从 MySQL 迁到 PostgreSQL 时,就遇到过字符串范围查询结果变少的诡异问题。排查了半天才发现是排序规则在捣鬼。所以写字符串 BETWEEN 之前,先搞清楚当前库的 collation,尤其是项目涉及多个数据库时,别假设行为一致。
4.3 NOT BETWEEN 和 NULL 的三值逻辑
NOT BETWEEN 同样是闭区间的取反。比如:
sql复制SELECT name, price
FROM products
WHERE price NOT BETWEEN 100 AND 200;
这会查出价格小于 100 或大于 200 的商品。但如果价格列里有 NULL,那这些行不会出现。原因前面说过,NULL 参与比较结果是 UNKNOWN,WHERE 只保留 TRUE 的记录。
在实际业务里,NOT BETWEEN 配合 NULL 的问题很容易造成“看起来查出来了,但又没完全查出来”的困惑。如果你需要把空值也纳入“不在区间内”的语义,可以这样写:
sql复制WHERE price NOT BETWEEN 100 AND 200
OR price IS NULL;
或者用 COALESCE 给个默认值再比较:
sql复制WHERE COALESCE(price, 0) NOT BETWEEN 100 AND 200;
我个人更推荐显式的 OR price IS NULL,因为 COALESCE 会让 SQL 阅读者去猜你为什么要兜这个底,而 IS NULL 一眼就能看懂意图。
5. 从慢 SQL 优化看 BETWEEN 的索引问题
5.1 BETWEEN 通常能走索引的原因
先给结论:BETWEEN 本身并不慢,它转换为范围条件后,完全可以利用 B+ 树索引进行范围扫描。因为 col BETWEEN a AND b 等价于 col >= a AND col <= b,这两个都是典型的可索引条件。
在 MySQL InnoDB 引擎下,这条 SQL:
sql复制SELECT * FROM orders
WHERE order_time BETWEEN '2024-06-01' AND '2024-06-30';
在 order_time 建有索引的情况下,执行计划会显示 type: range,意思就是索引范围扫描。这个效率比全表扫描高很多,几百万行的表也能在毫秒级返回。
所以不要一看到 BETWEEN 就担心性能,它不是慢查询的原罪,真正的原罪是让索引无法生效的写法。
5.2 三种让 BETWEEN 变成慢查询的写法
根据我处理慢 SQL 的实际经验,BETWEEN 导致慢查询的典型原因就三种:
第一种,在列上套函数。比如 WHERE DATE(order_time) BETWEEN ...,列被函数包裹后,索引失效。这种问题在日期统计类 SQL 里极其常见,因为写的人想当然地认为“把日期截断到天再比较更精确”。
第二种,隐式类型转换。比如字符串字段和数字边界值比较,或者日期字段和字符串边界值直接比较。MySQL 遇到字段类型和右侧值类型不一致时,会尝试把字段转换成右侧值的类型,一旦发生转换,索引大概率失效。安全做法是让字段类型和边界值类型保持一致,必要时显式 CAST。
第三种,范围过大导致优化器放弃索引。就算有索引,如果 BETWEEN 的范围覆盖了表中绝大部分数据,优化器一算账发现“回表成本还不如全表扫描”,就会放弃索引。这是优化器的正常策略,不是 bug。这种情况下业务上如果真需要扫那么多数据,可以考虑加其他过滤条件缩小范围,或者用覆盖索引减少回表。
5.3 一个真实的慢 SQL 优化小案例
有一回我排查一条线上订单报表查询,SQL 大概是这样的:
sql复制SELECT order_id, order_time, amount
FROM orders
WHERE DATE(order_time) BETWEEN '2024-05-01' AND '2024-05-01';
订单表有 500 万行,order_time 上有索引,但这条 SQL 每次跑都要 800 多毫秒,报表页面转半天。用 EXPLAIN 一看,type 是 ALL,rows 估算 500 万,典型的全表扫描。问题就出在 DATE(order_time) 这个函数包列上。
改成半开区间写法后:
sql复制SELECT order_id, order_time, amount
FROM orders
WHERE order_time >= '2024-05-01'
AND order_time < '2024-05-02';
同样的数据,执行时间直接降到 15 毫秒以内,type 变成了 range,扫描行数只有 7000 多行。这个案例我每次讲索引优化都会拿出来说,因为它太典型了:逻辑结果完全一样,性能差了几十倍,原因仅仅是函数包裹和边界写法。
6. 面试高频题与避坑速查清单
6.1 面试官常问的 5 个点
BETWEEN 看起来简单,但面试官很喜欢拿它考基础扎不扎实。我总结几个高频问题,你可以自测一下。
第一个问题:BETWEEN 是闭区间还是开区间?答案是闭区间,包含上下界。说不清楚的,基本等于没写过 SQL。
第二个问题:查某一天的数据,用 BETWEEN '2024-06-01' AND '2024-06-01' 能查到当天全部数据吗?如果字段是 DATETIME 类型,查不到,因为右边等同 '2024-06-01 00:00:00',当天零点之后的数据全被排除。正确做法是 >= '2024-06-01' AND < '2024-06-02'。
第三个问题:WHERE price NOT BETWEEN 100 AND 200 会包含 price IS NULL 的行吗?不会,NULL 参与比较结果是 UNKNOWN,需要单独处理。
第四个问题:BETWEEN 和 IN 有什么区别?BETWEEN 是连续区间,IN 是离散集合匹配。连续范围用 BETWEEN,少量枚举值用 IN,大量枚举值考虑用临时表关联。
第五个问题:一条带 BETWEEN 的查询很慢,你会怎么排查?优先看执行计划,确认是否走了索引范围扫描;检查列上有没有函数包裹、隐式类型转换、范围是否过大;如果索引正常但依然慢,关注是否回表过多,考虑覆盖索引。
这几个问题其实是同一个知识体系的几种考法,核心就四个字:闭区间、边界、NULL、索引。把这四件事想明白,面试题随便怎么变都能应付。
6.2 实操避坑清单
最后整理一份 BETWEEN 避坑清单,都是我在实际写 SQL 和优化慢查询时总结出来的,可以直接收藏。
| 场景 | 正确做法 | 原因 |
|---|---|---|
| 日期时间列查某月数据 | 用 col >= '2024-01-01' AND col < '2024-02-01' |
避免月底最后一天的数据被边界值截断 |
| 列上写了函数 | 去掉函数,改成裸列范围比较 | 函数包裹会导致索引失效 |
| 字段是字符串但值是数字 | 用 CAST 显式转换或改字段类型 |
隐式类型转换可能引发全表扫描 |
| 需要排除区间但保留空值 | 加 OR col IS NULL |
NULL 不会参与任何比较运算 |
| 和 OR 混用 | 加括号明确逻辑分组 | AND 优先级高于 OR,不写括号容易歧义 |
| 大范围查询 | 增加业务过滤条件或使用覆盖索引 | 范围过大会让优化器放弃索引 |
| 动态拼接 SQL | 用参数化查询绑定边界值 | 防止拼接破坏边界语义,也防注入风险 |
这里再强调一下最后一条。不管是 BETWEEN 还是其他的条件值,只要是动态拼接生成的 SQL,边界值必须走参数绑定。一方面是为了防止 SQL 注入,另一方面是避免把 '2024-01-31' 这种字符串拼成奇怪的形式,导致数据库解析出错误语义。安全性和正确性,在写条件查询时永远是同一件事。
最后说一个我自己的使用习惯:只要条件涉及日期时间,我现在几乎不写 BETWEEN,一律用 col >= '2024-01-01' AND col < '2024-02-01' 这种半开区间。从维护角度看,这种写法语义最清晰,也最不容易被后续接手的人误解。哪怕哪天字段从 DATE 改成 DATETIME,SQL 都不用动。希望这篇记录能帮你把 BETWEEN 用得明明白白,在 SQL 基础上少踩几个暗坑。
