☰
MySQL范围查询between and用法解析:边界规则、日期陷阱与索引优化
2026/10/3 9:23:53 网站建设 项目流程

经常有人问我:MySQL里查询某个区间,是不是直接写where 字段 between 小值 and 大值就完事了?这句话只对了一半。between and确实是范围查询里最直观的写法,但如果你不了解它的边界规则、数据类型陷阱、索引使用方式,写出来的SQL轻则结果不对,重则把慢查询拖到全表扫描,线上直接报警。

这篇文章我就用实际踩坑的经验,把between and的用法掰开揉碎讲清楚。包括它和>=、<=的等价关系,日期查询容易翻车的那些细节,还有怎么配合索引写出高效的区间查询。新手能照着抄,老手也能当个checklist自查。

1. 内容整体设计与思路拆解

1.1 从需求到SQL:范围查询到底在解决什么问题

业务里最常见的筛选场景无非就是:查某个时间段的数据、查某个价格区间、查某个ID范围。比如“统计上个月每天的订单量”“找出价格在100到200之间的商品”“拉取ID从1000到2000的用户列表”。这类需求背后有一个统一的逻辑:在一个有序的数值维度上,圈定一个连续的闭区间,然后把落在这个区间内的记录全部拿出来。

between and就是为这个场景设计的语法糖。它的核心语义是:expr between min and max等价于expr >= min AND expr <= max。注意是闭区间,两个边界值本身也会被包含在结果里。这一点和被很多人误记的(min, max)开区间完全不同。

1.2 为什么推荐用 between and,而不是一顿组合比较符

有些开发习惯写where a >= 100 and a <= 200,这当然没错,但可读性不如where a between 100 and 200。尤其在SQL里条件一多,比较符满天飞的时候,between and把区间语义直接标注出来,后来维护的人一眼就能看懂“哦,这里就是在圈一个范围”。

另外还有一个容易忽略的点:between and在MySQL优化器里会被拆成两个比较条件来做范围扫描(range access)。只要你字段上建有合适的索引,它就能走索引快速定位,不会因为写法变化而丧失优化机会。相比之下,如果写成where a in (100,101,102,...,200),不仅语句冗长,优化器还需要处理大量的等值条件,执行计划反而不好看。

所以它不是简单的语法替换,而是一种“语义更清晰、对优化器更友好”的区间表达方式。

2. 核心细节解析与实操要点

2.1 between and 的边界值规则:闭区间还是开区间

这是最容易出问题的地方。between 1 and 3查询结果包含1、2、3三个值,不包含0也不包含4。也就是说,它是一个左右都闭合的区间。举个例子:

select * from order_info where order_amount between 100.00 and 200.00;

这个查询会返回订单金额恰好等于100元和恰好等于200元的订单。如果需求是“100元以上、200元以下”,也就是不包含100和200本身,那就不能用between and,得改成:

select * from order_info where order_amount > 100.00 and order_amount < 200.00;

很多新手在这里翻车,就是因为没搞清“含边界”和“不含边界”的区别。建议在写SQL前先确认业务口径:到底要不要包含端点值?

2.2 日期范围查询:小心between '2024-01-01' and '2024-01-31'丢数据

日期范围是between and重灾区。因为日期字段如果不带时间部分,存的是2024-01-01这种格式,但如果你用datetime类型存了2024-01-01 08:30:00,那直接写between '2024-01-01' and '2024-01-31'会出问题吗?

先说结论:有问题,但不一定丢数据,取决于MySQL怎么比较。当datetime类型和字符串比较时,MySQL会把字符串转成日期时间再比。'2024-01-31'会被转成2024-01-31 00:00:00。那么1月31日当天所有非零点的记录,因为时间大于零点的关系,都会大于'2024-01-31 00:00:00',于是被排除在外。也就是说,你查“1月份数据”的时候,1月31日当天9点、12点、18点的数据全部没了。

正确的写法有两种。第一种,右边界加上23:59:59:

select * from order_info where create_time between '2024-01-01 00:00:00' and '2024-01-31 23:59:59';

第二种,把右边界改成“下个月的1号零点”,然后配合<:

select * from order_info where create_time >= '2024-01-01 00:00:00' and create_time < '2024-02-01 00:00:00';

我个人强烈推荐第二种。为什么?因为23:59:59写法有个隐患:如果时间精度到了秒以下,比如2024-01-31 23:59:59.500,依然会被漏掉。而>= '2024-02-01 00:00:00'这种左闭右开写法,天然包含了整个1月所有时刻,包括毫秒、微秒,一个都不会漏。

注意:如果你字段类型是date,那between '2024-01-01' and '2024-01-31'没问题,date类型没有时分秒。但为了统一编码习惯,我建议在用datetime字段时一律采用左闭右开。

2.3 数据类型不一致时的隐式转换陷阱

between and的两侧,min和max的类型如果和字段类型不一致,MySQL可能做隐式类型转换。一旦字段上建了索引,隐式转换可能导致索引失效。

举个例子,字段user_id是varchar类型,但你查询时写:

select * from user where user_id between 1000 and 2000;

这里1000和2000是数值,MySQL会把user_id的字符串值转成数值来比较,结果就是:如果某个user_id是'1000abc',它也有可能被匹配上,因为转换后是1000,落在了区间内。而且,对字符串字段做数值转换,通常意味着没法直接走索引扫描,导致性能下降。

反过来也一样:字段是int,你写between '1000' and '2000',MySQL会把字符串转成数字,这时一般不影响索引使用,因为转换发生在常量一侧,但我依然建议保持类型一致,省得执行计划跑偏。

还有一种坑是浮点数。between 0.1 and 0.3这种浮点比较在计算机里存在精度误差,有时候你以为包含0.3,实际可能因为浮点表示问题把0.3排除。所以涉及金额、单价等敏感字段,能用小数定点类型(decimal)就别用float/double,比较时也更稳。

2.4 between and 与 NULL 的处理

between and对NULL的处理很直接:如果字段值是NULL,它不满足任何区间条件,因为NULL >= min的结果不是TRUE而是NULL,NULL AND NULL最终是NULL,被当作不成立过滤。

这个行为在大多数业务场景下是符合预期的——空值不属于任何区间。但如果你统计分组时用between and去圈范围,容易漏掉空值,导致统计口径不对。比如按年龄段统计用户数,年龄段SQL里没有覆盖age is null的用户,那这波用户要么被丢弃,要么需要在应用层单独处理。

2.5 字符串范围查询的“字母序”规则

between and不只是用在数字和日期上,字符串也能用。此时比较规则是按字符的字典序(collation)。比如:

select * from product where product_name between 'apple' and 'banana';

这个查询会返回所有产品名按字典序排在apple和banana之间的记录,包括apple1、applez、banana本身。但注意大小写和排序规则:如果表用的是utf8mb4_general_ci(不区分大小写),那么Apple也会被算进去;如果是utf8mb4_bin(区分大小写),则Apple在字典序中因为A(ASCII 65)小于a(ASCII 97),可能不会出现在apple到banana的区间里。

所以在字符串范围查询前,先确认排序规则,再确认业务预期。我见过有人用between查姓名字段,结果因为排序规则问题,把大写开头的名字全漏了,查了半天才发现是collation的锅。

3. 实操过程与核心环节实现

3.1 基础语法:一张表看清 between and 的写法

我们先建一个简单的用户表做演示:

create table user_info ( id int primary key auto_increment, user_name varchar(50), age int, salary decimal(10, 2), create_time datetime, key idx_age (age), key idx_create_time (create_time) ) engine=InnoDB default charset=utf8mb4;

插入几条测试数据:

insert into user_info(user_name, age, salary, create_time) values ('张三', 18, 5000.00, '2024-01-01 09:00:00'), ('李四', 25, 8000.00, '2024-01-15 14:30:00'), ('王五', 30, 12000.00, '2024-01-31 23:59:59'), ('赵六', 35, 15000.00, '2024-02-01 00:00:00'), ('孙七', 40, 20000.00, '2023-12-31 10:00:00');

年龄范围查询,包含18和35:

select * from user_info where age between 18 and 35;

这条SQL会返回张三、李四、王五、赵六。因为18、35都包含在内。如果要把赵六(35岁)排除,就改成age > 18 and age < 35,此时只返回李四和王五。

工资范围查询:

select * from user_info where salary between 6000 and 15000;

返回李四、王五、赵六。注意salary是decimal,比较的时候不会出现浮点误差,钱相关的字段最好都用decimal。

3.2 日期范围查询的正确姿势

回到前面说的日期坑,按1月份创建时间查:

错误写法:

select * from user_info where create_time between '2024-01-01' and '2024-01-31';

这条SQL会返回张三、李四,但王五的2024-01-31 23:59:59因为大于2024-01-31 00:00:00而被排除,赵六的是2月1日零点,本来就不在范围内。所以结果少了王五,数据就错了。

正确写法:

select * from user_info where create_time >= '2024-01-01 00:00:00' and create_time < '2024-02-01 00:00:00';

这条SQL返回张三、李四、王五,一个不落。注意我用了>=和<而不是between and,因为这里需要的是左闭右开区间。

如果你执意要用between and,可以这么写:

select * from user_info where create_time between '2024-01-01 00:00:00' and '2024-01-31 23:59:59';

不过正如前面说的,这个写法在存在毫秒级数据时仍有漏数据风险。所以我的习惯是:日期时间范围查询,统一用>=和<。

3.3 配合 ORDER BY 和 LIMIT 实现分段查询

范围查询经常会搭配排序和分页。比如查某个年龄段里工资最高的前3个人:

select * from user_info where age between 20 and 40 order by salary desc limit 3;

这里between and负责圈定范围,order by和limit负责排序和截断。需要注意一点:当数据量很大时,order by可能需要在范围扫描的基础上做额外排序。如果排序字段也有索引,MySQL可能直接走索引避免filesort,所以建索引的时候可以把排序字段加进联合索引。

比如经常查age between ? and ? order by salary desc,可以建一个(age, salary)的联合索引,这样既能用age做范围筛选,又能用salary做排序,效率高很多。

3.4 在存储过程中使用 between and

有网友问到MySQL存储过程里能不能用between and,答案当然可以。比如写一个根据年龄区间统计人数的存储过程:

delimiter // create procedure count_by_age_range(in min_age int, in max_age int, out user_count int) begin select count(*) into user_count from user_info where age between min_age and max_age; end // delimiter ;

调用方式:

call count_by_age_range(18, 35, @cnt); select @cnt;

存储过程里使用between and和普通SQL没区别,唯一要注意的是变量类型要和字段类型匹配,否则就有可能触发隐式转换。

3.5 动态拼SQL时注意参数化

在实际项目里,范围查询常由前端传参,比如两个输入框“最低价”“最高价”。我在很多代码里见过这种拼法:

String sql = "select * from product where price between " + minPrice + " and " + maxPrice;

这种做法必须禁止。它存在两个问题:一是SQL注入风险,minPrice或maxPrice里夹带"1 and 1=1 union select ..."就直接被打穿;二是参数类型不可控,用户传了字符串可能导致隐式转换。

正确做法是用预编译语句的占位符:

String sql = "select * from product where price between ? and ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setBigDecimal(1, minPrice); ps.setBigDecimal(2, maxPrice);

这样既安全又保持了执行计划的高效性。建议所有涉及范围查询的SQL都走参数化。

3.6 业务场景扩展:NOT BETWEEN 怎么用

between and还有个反义写法not between and,用于排除某个区间。比如查不属于18到35年龄段的用户:

select * from user_info where age not between 18 and 35;

这条SQL返回年龄小于18或大于35的记录,即孙七。注意not between and并不等价于age < 18 or age > 35的反面,它本质上等价于not (age >= 18 and age <= 35),逻辑上就是age < 18 or age > 35。但有个细节:NULL值在这种查询下依然会被过滤掉,因为not (NULL)还是NULL。

not between and有个性能上的坑:它通常会被优化成两个范围条件,但因为是OR关系,索引使用上可能不如正向between and那么友好。所以如果你能用正向条件表达的,尽量别用反向。

4. 常见问题与排查技巧实录

4.1 为什么 between and 没有走索引

很多人会问:我明明在字段上建了索引,为什么执行计划还是全表扫描?这里有几个常见原因。

第一,字段类型发生隐式转换。上面说过,varchar字段和数值比较时,MySQL无法直接使用索引。检查方法很简单,看执行计划里key是不是NULL,以及Extra里有没有Using where。如果字段是varchar,查询参数是数字,先改成字符串再查:

select * from user where user_id between '1000' and '2000';

第二,查询返回的数据量太大。优化器觉得走索引回表的代价比全表扫描还高,就自动放弃索引。比如区间覆盖了表里80%的数据,这时候全表扫描反而更快。这种情况不是SQL写错了,而是业务和数据分布决定的,可考虑缩小范围或改用覆盖索引。

第三,联合索引的最左前缀原则。如果你建的是(a, b)联合索引,但查询里只有b between ? and ?,没有用到a,那这个联合索引对b的筛选是不起作用的。

排查SQL是否走索引,老规矩:

explain select * from user_info where age between 18 and 35;

看type字段,如果是range,说明走了范围扫描,正常;如果是ALL,说明全表扫描,需要按上面的原因逐一排查。

4.2 查询结果比预期少了数据,先查边界和时区

有次我在线上排查一个数据对不上问题:统计“当日订单量”,SQL写的是:

select count(*) from orders where create_time between '2024-03-01 00:00:00' and '2024-03-01 23:59:59';

结果比业务侧统计少了十几单。查下来发现,问题出在时区。MySQL的datetime不携带时区信息,但应用写入时用的是Asia/Shanghai时间,而查询时数据库连接的时区被设成了UTC,导致写入的“北京时间 2024-03-01 08:00:00”存进去后,用UTC时间边界去查,自然就漏了。

处理方式:一是业务统一,写入和查询都使用同一时区;二是datetime或timestamp字段与时区关系要理清。这里不展开,但提醒一句:范围查询的边界值,一定要和应用写入时的时区保持一致。

4.3 高并发大表下的 between and 慢查询优化

如果你的表已经上亿行,用between and做范围查询,即使走了索引,也可能因为返回行数太多而导致延迟高。这里有几个优化思路。

第一,使用覆盖索引。把查询需要用到的字段都塞进索引里,让索引覆盖查询,避免回表。比如:

select id, age from user_info where age between 18 and 35;

如果建一个(age, id)的联合索引,那么上面这个查询直接遍历索引就能拿到id和age,完全不需要回表,速度会快很多。

第二,分页查询用“延迟关联”或“书签”。传统分页limit 2000000, 20会把前面200万条记录都扫一遍,非常慢。可以先把主键取出来:

select id from user_info where age between 18 and 35 order by id limit 2000000, 20;

然后再用主键关联原表,或者记录上一页最后一条记录的ID,作为下一页的起点:

select * from user_info where age between 18 and 35 and id > 上一页最大id order by id limit 20;

这个技巧本质是把范围查询和分页结合起来,用主键的有序性跳过无关数据。

4.4 慢查询日志里发现大量的 between and 低效SQL

如果你开启了慢查询日志,比如:

set global slow_query_log = on; set global long_query_time = 1;

那么超过1秒的SQL都会被记录下来。分析慢日志时,如果发现大量between and查询,先别急着改SQL,可能问题不在写法,而在索引缺失。用explain看执行计划,如果type是ALL,先改造索引;如果type是range但rows扫描行数很大,再考虑覆盖索引、分区表、查询条件细化等手段。

另外,检查一下between的边界值是不是“恒定”的。比如between now() - interval 7 day and now()这种查询,每次执行边界都不同,MySQL的缓存机制很难命中,执行计划也没法复用。这种情况可以考虑把时间窗口固定,或者用分区表按时间裁剪。

4.5 常见错误速查表

错误场景错误写法正确写法说明
日期查1月数据between '2024-01-01' and '2024-01-31'create_time >= '2024-01-01' and create_time < '2024-02-01'datetime会漏掉1月31日非零点数据
字符串字段数值比较varchar_col between 100 and 200varchar_col between '100' and '200'避免隐式转换导致索引失效
不包含边界值between 1 and 3> 1 and < 3between and是闭区间
时间精度到毫秒between '...23:59:59' and '...23:59:59'>= '...' and < '次日00:00:00'避免23:59:59.500这类数据漏掉
反向范围查询not between 18 and 35尽量改为正向age < 18 or age > 35反向条件索引利用不够好

4.6 关于 between and 与 IN 的选择

有些场景下,between and能表达的范围也可以拆成in列表,比如between 1 and 3等价于in (1,2,3)。但两者有本质区别:in是离散值集合,between是连续区间。如果区间很大,in列表会非常长,SQL解析和优化都费劲;如果区间很窄且都是整数,in可能更快,因为优化器可以针对每个值做等值匹配。但从可维护性角度,between更简洁。

我个人的选择标准是:能确定枚举值用in,比如状态字段的in (1,2,3);连续数值、日期区间,一律用between或>=/<。别把between用到枚举上,也别把in硬掰成连续区间,那是给自己找麻烦。

5. 一些经验补充

最后分享几个我在实际工作中总结的小习惯,不一定写在官方文档里,但对排查问题很有帮助。

第一,写范围查询之前,先问自己三个问题:边界值要不要包含?字段类型是什么?会不会触发隐式转换?这三点想清楚,80%的坑都能避开。

第二,用explain养成肌肉记忆。任何一条范围查询上线前,至少跑一次explain,确认type不是ALL。尤其是日期范围查询,表数据一多,忘了索引就是灾难。

第三,如果你维护的老表里既有date类型又有datetime类型,查询条件统一用字符串边界,并且显式带上时间部分。比如查某一天,写成>= '2024-01-01 00:00:00' and < '2024-01-02 00:00:00',这样即使字段类型是datetime也没问题,是date也没问题,通用性最好。

第四,SQL审查时如果看到同事写了between '2024-01-01' and '2024-01-31'这种日期查询,直接提醒他改成左闭右开写法。因为这个错误太隐蔽了,普通测试数据很难覆盖到月末最后几秒,经常要等线上数据出问题才暴露。

第五,线上出现范围查询数据不准,不要急着骂数据写入端。先用一条不带between的裸查询,比如select * from table where create_time >= '2024-01-01',把结果导出来对比,再把边界逐步收紧,很快就能定位是SQL边界问题还是数据本身问题。这种排查方式比对着SQL死磕快十倍。

between and虽然只是一个看似简单的小语法,但真正用对、用好、用出性能,需要考虑边界、类型、索引、时区、业务口径等多个维度。希望这篇文章能帮你把范围查询这块彻底理清。

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

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

立即咨询