1. 项目概述BETWEEN 不是“范围查询”的快捷键而是带语义边界的精确过滤器SQL 中的BETWEEN看似简单——不就是查两个值之间的数据吗但我在给金融系统做审计报表、给电商后台写库存预警脚本、给教育平台导出学生成绩区间时反复踩过坑明明写了BETWEEN 100 AND 200结果漏掉了 200 分的学生用BETWEEN 2023-01-01 AND 2023-12-31统计全年订单却少算了 12 月 31 日晚 23:59:59 的那笔支付更离谱的是在 PostgreSQL 里对TEXT字段用BETWEEN A AND Z居然把apple和zebra都筛进来了而Zebra却被排除在外。这些都不是 Bug而是BETWEEN本身携带的隐式语义边界在起作用。它不是数学意义上的开闭区间符号而是一条严格遵循 SQL 标准、受数据类型、排序规则、时区精度和 NULL 处理逻辑共同约束的过滤指令。本文聚焦How to Use the SQL BETWEEN Operator这一标题不讲泛泛而谈的语法定义而是从一线实战出发拆解它在真实业务场景中如何被误用、为何失效、怎样精准控制边界、以及如何与 AND 形成互补而非替代。适合所有已会写SELECT * FROM users WHERE age BETWEEN 18 AND 35但还没在凌晨三点被生产环境数据偏差叫醒过的 SQL 使用者。你不需要是 DBA但必须清楚BETWEEN的“之间”从来就不是生活语言里的模糊地带而是数据库引擎逐字节比对后给出的确定性判决。2. 核心设计逻辑与方案选型解析为什么 BETWEEN 存在它解决的到底是什么问题2.1 BETWEEN 的原始设计动机对抗冗余书写与可读性陷阱我们先看一个最基础的对比。假设要查年龄在 25 到 40 岁含之间的用户-- 方案 A显式写法 AND SELECT * FROM users WHERE age 25 AND age 40; -- 方案 BBETWEEN 写法 SELECT * FROM users WHERE age BETWEEN 25 AND 40;表面看B 方案字符数更少看起来更“简洁”。但这绝非 SQL 标准委员会引入BETWEEN的核心原因。真正驱动它诞生的是可读性陷阱与维护性风险。我曾接手一个遗留报表系统其核心查询里有这样一段WHERE order_date 2022-01-01 AND order_date 2022-12-31 AND status completed AND amount 100.00 AND amount 5000.00 AND region_id IN (1,2,3,4)这段代码的问题不在于功能而在于视觉疲劳导致的逻辑错位。当开发人员快速扫视时“”和“”这两个符号在长列表中极易被忽略或混淆。某次紧急修复中同事把amount 100.00错打成了amount 100.00测试没覆盖到这个边界上线后导致大量小额订单被错误剔除财务对账直接崩盘。BETWEEN的价值正在于它把一对“下界-上界”的逻辑绑定为一个不可分割的语义单元。amount BETWEEN 100.00 AND 5000.00这个表达式一眼就能看出这是一个完整的数值区间约束上下界天然关联无法被单独修改。它强制开发者以“区间”为思考单位而不是以“单个比较符”为操作单位。这就像编程语言中的for (int i 0; i n; i)i和i n是耦合的不能随意拆开。BETWEEN就是 SQL 里的“区间 for 循环”。2.2 BETWEEN 与 AND 的本质等价性与关键差异标准 SQL 明确规定expr BETWEEN low_expr AND high_expr在逻辑上完全等价于expr low_expr AND expr high_expr。这意味着执行计划层面现代主流数据库PostgreSQL, MySQL 8.0, SQL Server, Oracle在优化器阶段会将BETWEEN自动重写为 AND 形式因此性能上没有任何差异。你不会因为用了BETWEEN而获得索引扫描加速也不会因为它而变慢。语义层面这是最关键的差异点。BETWEEN是一个原子性谓词atomic predicate它的真假判断是一次性完成的。而 AND 是两个独立谓词的逻辑与AND。这个区别在处理NULL时暴露无遗。我们来看一个经典案例-- 假设表中有以下数据 -- id | score -- 1 | 85 -- 2 | NULL -- 3 | 92 -- 查询score BETWEEN 80 AND 90 -- 结果只返回 id1 的记录85 -- 查询score 80 AND score 90 -- 结果同样只返回 id1 的记录85看起来一样再加一行-- 查询score BETWEEN NULL AND 90 -- 结果**空集**。因为 BETWEEN 的任意一个边界为 NULL整个表达式结果为 UNKNOWN三值逻辑而 WHERE 子句只接受 TRUE 的行。 -- 查询score NULL AND score 90 -- 结果同样是**空集**。因为 score NULL 永远为 UNKNOWN。 -- 但关键来了 -- 查询score BETWEEN 80 AND NULL -- 结果**空集**同上 -- 查询score 80 AND score NULL -- 结果**空集**同上所以NULL边界的影响是等价的。真正的差异在于边界值的顺序要求。BETWEEN要求low_expr high_expr必须成立否则结果恒为 FALSE。而 AND 没有这个隐式检查-- 在 PostgreSQL 和 SQL Server 中 SELECT 5 BETWEEN 10 AND 1; -- 返回 FALSE -- 但等价写法 SELECT 5 10 AND 5 1; -- 同样返回 FALSE -- 看似没区别错。问题出在动态计算的边界上 SELECT * FROM products WHERE price BETWEEN min_price * 0.9 AND max_price * 1.1; -- 如果某条记录的 min_price 是 100max_price 是 50数据异常那么 -- min_price * 0.9 90, max_price * 1.1 55 -- 90 55所以 BETWEEN 90 AND 55 恒为 FALSE该记录必然被排除。 -- 而如果写成 SELECT * FROM products WHERE price min_price * 0.9 AND price max_price * 1.1; -- 同样price 90 AND price 55 永远为 FALSE结果一致。 -- 所以BETWEEN 并没有引入新的逻辑它只是把“低界必须小于等于高界”这个业务规则从应用层逻辑你需要自己写 CASE WHEN min_price max_price THEN ... END下沉到了 SQL 语法层由数据库引擎自动保证。这是一种**防御性编程Defensive Programming** 的体现。当你看到 BETWEEN你就知道这条语句的作者已经默认了“低界 高界”这个前提如果数据不满足结果为空是预期行为而非 bug。2.3 为什么不能用 BETWEEN 替代 IN 或 LIKE边界语义的不可扩展性很多新手会尝试用BETWEEN去替代其他操作符这是典型的“工具滥用”。BETWEEN的核心能力是连续有序域上的闭区间过滤。它依赖于数据类型的内在排序规则collation。不能替代ININ是离散值集合匹配。status IN (active, pending, archived)表达的是三个互不相关的状态。而status BETWEEN active AND archived会利用字符串的字典序把active,archived,pending甚至banned因为b a and b c都包含进来只要它们的字典序落在active和archived之间。这完全违背业务意图。不能替代LIKELIKE是模式匹配处理的是字符串的结构特征如%gmail.com。BETWEEN只能比较完整字符串的大小无法识别通配符或子串位置。试图用email BETWEEN agmail.com AND zgmail.com来筛选 gmail 用户会漏掉aagmail.com因为aa z也会包含ygmail.com符合但完全无法处理usertaggmail.com这类合法邮箱。不能用于无序类型某些数据库如旧版 MySQL允许对ENUM类型使用BETWEEN但这极其危险。ENUM(small, medium, large)的内部序号是 1,2,3medium BETWEEN small AND large会按序号比较看似正确。但一旦业务变更新增x-small插入到small前序号全乱BETWEEN的结果就不可预测了。正确的做法永远是显式枚举或用CASE。提示BETWEEN的适用性铁律——仅当你的数据类型具有明确定义、稳定可靠、业务认可的全序关系Total Order时才可使用。数字、日期时间、规范化的字符串如 ISO 8601 格式日期是安全的状态码、分类标签、用户自定义文本通常是不安全的。3. 核心细节解析与实操要点数据类型、边界精度与排序规则的深度博弈3.1 数值类型整数与浮点数的“相等”陷阱数值类型看似最安全但浮点数FLOAT,REAL,DOUBLE PRECISION是BETWEEN的第一个雷区。根本原因在于二进制浮点数无法精确表示十进制小数。-- 假设有一个价格表price 是 FLOAT 类型 -- 我们想查价格在 9.99 到 19.99 之间的商品 -- 错误示范可能漏数据 SELECT * FROM products WHERE price BETWEEN 9.99 AND 19.99; -- 为什么因为 9.99 在二进制中是无限循环小数实际存储可能是 9.990000000000001 或 9.98999999999999。 -- 当数据库从磁盘读取这个近似值并与你输入的字面量 9.99它也被转换为近似值比较时微小的舍入误差可能导致 price 9.99 为 FALSE即使业务上它就是 9.99。 -- 正确做法使用精确数值类型DECIMAL/NUMERIC并配合 ROUND 函数如果必须用 FLOAT -- 方案1推荐建表时就用 DECIMAL(10,2) ALTER TABLE products MODIFY COLUMN price DECIMAL(10,2); -- 方案2补救在查询时进行标准化 SELECT * FROM products WHERE ROUND(price, 2) BETWEEN 9.99 AND 19.99;但ROUND也有代价它会使索引失效因为对列进行了函数运算。所以最佳实践是源头治理——在数据录入和存储阶段就确保精度。BETWEEN本身不创造精度它只是忠实地执行比较。它放大了底层数据类型的缺陷。对于整数BETWEEN是绝对可靠的。id BETWEEN 100 AND 200就是id IN (100,101,...,200)的高效写法且能充分利用主键索引。3.2 日期时间类型精度战争与时区迷雾日期时间是BETWEEN最常用也最容易出错的领域。核心矛盾在于人类的“一天”与数据库的“时间戳”在精度上存在天然鸿沟。-- 场景统计 2023 年全年的订单量 -- 错误写法几乎所有人都这么写过 SELECT COUNT(*) FROM orders WHERE order_date BETWEEN 2023-01-01 AND 2023-12-31; -- 问题在哪2023-12-31 这个字符串在大多数数据库中会被隐式转换为 2023-12-31 00:00:00。 -- 所以BETWEEN 2023-01-01 00:00:00 AND 2023-12-31 00:00:00 实际上只包含了从 2023-01-01 00:00:00 到 2023-12-31 00:00:00不含的所有时间点。 -- 2023-12-31 00:00:01 及之后的订单全部被排除整整漏掉了 23 小时 59 分 59 秒的数据。 -- 正确写法两种主流方案 -- 方案A使用开闭区间推荐清晰且索引友好 SELECT COUNT(*) FROM orders WHERE order_date 2023-01-01 AND order_date 2024-01-01; -- 方案B显式指定时间边界兼容性好 SELECT COUNT(*) FROM orders WHERE order_date BETWEEN 2023-01-01 00:00:00 AND 2023-12-31 23:59:59.999; -- 方案A 的优势2024-01-01 是一个明确的、无歧义的“下一年第一天”且 order_date 2024-01-01 可以完美利用 order_date 列上的索引。而方案B 的 23:59:59.999 在不同数据库中精度支持不同MySQL 5.6 只支持秒级PostgreSQL 支持微秒容易出错。时区是另一个维度的复杂性。如果你的order_date是TIMESTAMP WITH TIME ZONE如 PostgreSQL或者DATETIME存储的是 UTC 时间而你的业务逻辑是基于本地时区如北京时间 UTC8那么BETWEEN的边界就必须进行时区转换-- 假设 order_date 是 UTC 时间戳要查北京时间 2023-01-01 00:00:00 到 2023-01-01 23:59:59 的订单 -- 北京时间 2023-01-01 00:00:00 UTC 时间 2022-12-31 16:00:00 -- 北京时间 2023-01-01 23:59:59 UTC 时间 2023-01-01 15:59:59 SELECT COUNT(*) FROM orders WHERE order_date 2022-12-31 16:00:0000 AND order_date 2023-01-01 16:00:0000;注意永远不要在BETWEEN的边界里写2023-01-01这样的裸字符串来处理带时区的时间戳。必须明确写出带时区偏移的完整时间字面量或使用AT TIME ZONE函数进行转换。BETWEEN不会帮你做时区推算它只做字面量比较。3.3 字符串类型排序规则Collation是隐形的指挥官字符串的BETWEEN是最易被忽视的“玄学”区域。它的行为完全由数据库的排序规则Collation决定。同一个BETWEEN A AND Z在不同的 collation 下结果天差地别。-- 场景筛选用户名以字母 A-Z 开头的用户不区分大小写 -- 在 MySQL 默认的 utf8mb4_0900_as_cs区分大小写下 SELECT * FROM users WHERE username BETWEEN A AND Z; -- 结果只返回 username 以大写 A-Z 开头的用户alice小写被排除。 -- 在 utf8mb4_0900_as_cs不区分大小写下 SELECT * FROM users WHERE username BETWEEN A AND Z; -- 结果返回 Alice, bob, Charlie... 因为 bob 的字典序在 A 和 Z 之间b A and b Z。 -- 更诡异的是在某些 collation 下Å带圈A可能排在 Z 之后也可能排在 A 和 B 之间取决于它是按 Unicode 码点还是按语言习惯排序。 -- 安全实践 -- 1. 永远显式指定 collation如果数据库支持 SELECT * FROM users WHERE username COLLATE utf8mb4_0900_as_cs BETWEEN A AND Z; -- 2. 对于首字母筛选用 SUBSTRING UPPER/Lower 更可靠 SELECT * FROM users WHERE UPPER(LEFT(username, 1)) BETWEEN A AND Z; -- 3. 对于前缀匹配LIKE A% 比 BETWEEN A AND A\ufffdUnicode 最大字符更直观、更易读、索引效率也更高。BETWEEN在字符串上的威力只应发挥在高度结构化、格式统一、排序规则明确的场景例如ISO 8601 日期字符串2023-01-01 BETWEEN 2022-01-01 AND 2023-12-31安全因为字典序与时间序一致订单编号前缀order_id BETWEEN ORD-2023-000001 AND ORD-2023-999999安全前提是编号严格按此规则生成4. 实操过程与核心环节实现从需求分析到健壮 SQL 的完整链路4.1 需求分析画出你的“业务区间”草图在写任何BETWEEN之前拿出一张纸画出你的业务需求区间。这不是写代码而是做需求澄清。我坚持用这个模板业务描述期望包含的最小值期望包含的最大值数据库中该值的精确表示是否包含端点为什么查找2023年创建的用户2023-01-01 00:00:002023-12-31 23:59:59.999created_at(TIMESTAMP)是“2023年”在业务上指全年每一天的每一秒查找价格在100元到500元之间的商品100.00500.00price(DECIMAL(10,2))是价格是离散的货币单位100元和500元都是有效价格查找ID在1000到2000之间的订单10002000id(BIGINT)是ID是整数序列1000和2000都是合法ID这个表格强迫你回答三个关键问题端点是否业务有效如果“2023-12-31 23:59:59.999”不是一个真实的业务概念比如你的系统只记录到秒那就不要硬凑。数据库字段的精度是否匹配如果created_at只存到秒写.999就是自欺欺人。“包含”是业务要求还是技术妥协有时业务上“大于等于100”就够了非要写BETWEEN 100 AND 500反而增加了维护负担。4.2 SQL 编写四步构建法与边界验证我总结了一套BETWEEN编写四步法每一步都有对应的验证动作第一步选择基准类型确定你要比较的字段类型INT, DATE, TIMESTAMP, DECIMAL。验证DESCRIBE table_name;或SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE ...确认字段的实际类型和精度。第二步构造边界字面量根据上一步的类型写出数据库能精确理解的字面量。INT:100,200DATE:2023-01-01TIMESTAMP:2023-01-01 00:00:00如果字段是DATETIME或2023-01-01 00:00:0000如果字段是TIMESTAMPTZDECIMAL:100.00,500.00验证在SELECT子句中单独测试字面量看是否被正确解析SELECT 2023-01-01::DATE, 2023-01-01 00:00:00::TIMESTAMP;第三步编写 BETWEEN 表达式严格按照field BETWEEN low_literal AND high_literal的语法。关键检查low_literal必须在数据库的排序规则下high_literal。可以用一个简单的SELECT验证SELECT 2023-01-01::DATE 2023-12-31::DATE; -- 应返回 true SELECT 100 500; -- 应返回 true第四步边界压力测试这是最容易被跳过的一步却是避免线上事故的关键。写一个测试查询专门检查端点值是否被正确包含-- 测试下界 SELECT * FROM your_table WHERE your_field low_literal; -- 测试上界 SELECT * FROM your_table WHERE your_field high_literal; -- 运行你的 BETWEEN 查询 SELECT * FROM your_table WHERE your_field BETWEEN low_literal AND high_literal; -- 对比三个结果集的行数。理想情况下BETWEEN 的结果数 下界行数 上界行数 中间行数。 -- 如果下界或上界行数为0说明你的字面量写错了或者数据里根本没有这个精确值。4.3 性能调优索引友好型 BETWEEN 写法BETWEEN本身不决定性能但它写的是否“索引友好”决定了性能上限。最佳实践让 BETWEEN 的左右操作数都是常量或确定性表达式-- ✅ 索引友好left 和 right 都是常量 WHERE created_at BETWEEN 2023-01-01 AND 2023-12-31 -- ⚠️ 索引可能失效right 是函数且函数作用于列 WHERE created_at BETWEEN 2023-01-01 AND DATE_ADD(NOW(), INTERVAL 1 DAY) -- ❌ 索引失效left 是函数且函数作用于列 WHERE created_at BETWEEN DATE_SUB(NOW(), INTERVAL 7 DAY) AND 2023-12-31复合索引的顺序至关重要假设你有一个查询SELECT * FROM orders WHERE status shipped AND created_at BETWEEN 2023-01-01 AND 2023-01-31 AND amount 100;最佳复合索引应该是(status, created_at, amount)。为什么status shipped是等值查询应该放在索引最左。created_at BETWEEN ...是范围查询应该紧跟在等值列之后。amount 100也是范围查询但BETWEEN已经消耗了索引的“范围能力”amount列的范围查询无法再利用索引进行高效过滤它只能作为最后的过滤条件Filter。所以把它放在索引末尾对前两个条件的扫描效率影响最小。警惕“虚假的范围查询”-- 如果 status 是一个低基数列只有 pending, shipped, cancelled 三种值 -- 那么 status shipped 可能只过滤掉 1/3 的数据此时索引 (status, created_at) 的效果可能不如单纯的 (created_at) 索引因为后者能直接定位到时间范围内的所有行再在内存中过滤 status。 -- 这需要通过 EXPLAIN 查看实际执行计划而不是凭经验猜测。5. 常见问题与排查技巧实录那些让你抓狂的 BETWEEN 异常现场5.1 问题速查表症状、根因与解决方案症状根因分析解决方案我的实操心得BETWEEN查询结果为空但用 AND 却有数据边界值low_expr high_expr导致BETWEEN恒为 FALSE1. 用SELECT low_expr, high_expr检查边界值。2. 在BETWEEN前加CASE WHEN low_expr high_expr THEN ... ELSE NULL END包裹。这个坑我踩过三次。第一次是动态 SQL 拼接错误第二次是前端传参时start_date和end_date顺序颠倒第三次是 ETL 脚本里min_value和max_value字段名写反了。现在我的所有动态BETWEEN查询第一行必加WHERE low_expr high_expr的守卫。查询结果比预期多包含了不该有的记录字符串BETWEEN受排序规则影响字典序与业务序不一致1.SHOW COLLATION LIKE your_collation;查看当前排序规则。2. 用SELECT value COLLATE your_collation测试单个值的排序权重。3. 改用LIKE或正则表达式。在一个跨国 SaaS 项目里客户要求按国家名称首字母分组。我用country_name BETWEEN A AND M结果德国Germany被分到了 M 组因为德语排序规则里G排在M后面。最后改用SUBSTR(country_name, 1, 1) IN (A,B,..., M)虽然啰嗦但绝对可控。BETWEEN在某个数据库版本工作正常升级后失效数据库版本升级改变了默认排序规则或隐式类型转换规则1. 查阅新版本的 Release Notes重点关注 Collation 和 Type Conversion 部分。2. 在 SQL 中显式指定COLLATE和类型转换如CAST(column AS VARCHAR) COLLATE utf8mb4_0900_as_cs。MySQL 8.0 升级到 8.0.30 后utf8mb4_0900_as_cs成为默认 collation而我们旧代码里大量BETWEEN依赖旧的utf8mb4_general_ci。上线前我用pt-query-digest抓取了所有慢查询然后用mysqlpump导出 schema用grep BETWEEN找出所有相关 SQL逐个加上COLLATE。花了两天但避免了上线后的大面积故障。BETWEEN查询很慢EXPLAIN显示走了全表扫描BETWEEN的边界是不确定的表达式如NOW(),CURDATE()导致优化器无法估算选择率1. 将不确定表达式提取为变量MySQL或 CTEPostgreSQL。2. 为范围查询列建立单独索引或调整复合索引顺序。在一个实时监控大屏项目中WHERE event_time BETWEEN DATE_SUB(NOW(), INTERVAL 1 HOUR) AND NOW()每秒执行一次把数据库 CPU 拉到 90%。我把NOW()提取出来SET now : NOW(); SET one_hour_ago : DATE_SUB(now, INTERVAL 1 HOUR);然后WHERE event_time BETWEEN one_hour_ago AND now。QPS 瞬间提升 5 倍CPU 回落至 15%。5.2 独家避坑技巧来自生产环境的血泪经验技巧1用EXPLAIN FORMATJSON深挖BETWEEN的执行细节EXPLAIN的文本输出太简略。FORMATJSON会告诉你优化器对BETWEEN的真实解读EXPLAIN FORMATJSON SELECT * FROM orders WHERE order_date BETWEEN 2023-01-01 AND 2023-01-31;在 JSON 输出中重点看range_analysis部分chosen_range_access_summary会显示优化器是否选择了 range 访问。rows_for_plan会显示优化器预估的扫描行数。如果这个数字远大于你表的总行数说明优化器对BETWEEN的选择率估算严重失真需要检查统计信息是否过期ANALYZE TABLE orders;。技巧2“BETWEEN 伪装术”——当业务需要开区间时业务上常说“价格在100到500之间”潜台词往往是“大于100小于500”即开区间(100, 500)。BETWEEN只能做闭区间[100, 500]。强行用BETWEEN 101 AND 499是错的因为 100.5 这样的价格会被排除。正确解法是-- 方法1用 AND 推荐 WHERE price 100 AND price 500 -- 方法2用 BETWEEN 配合边界微调仅适用于整数 WHERE price BETWEEN 100.0000001 AND 499.9999999 -- 但这种方法在 DECIMAL 上有精度风险且可读性差不推荐。技巧3BETWEEN与NULL的终极共存协议BETWEEN遇到NULL边界就是UNKNOWN这是 SQL 标准。但业务上我们经常需要“如果下界是 NULL就认为没有下界”。这时BETWEEN就无能为力了必须回归 AND 并用OR逻辑-- 业务需求动态查询lower_bound 和 upper_bound 可能为 NULL -- 期望lower_bound 为 NULL 时不限制下界upper_bound 为 NULL 时不限制上界 -- ✅ 正确写法清晰、标准、索引友好 WHERE (lower_bound IS NULL OR price lower_bound) AND (upper_bound IS NULL OR price upper_bound) -- ❌ 错误写法BETWEEN 无法处理 NULL 边界 WHERE price BETWEEN COALESCE(lower_bound, ???) AND COALESCE(upper_bound, ???) -- 你无法为 COALESCE 找到一个“无穷小”或“无穷大”的值因为所有数据类型都有其最大/最小值限制。5.3 实战复盘一个电商库存预警脚本的BETWEEN重构最后分享一个
SQL BETWEEN 操作符深度解析:边界语义、精度陷阱与实战避坑指南
发布时间:2026/7/7 21:38:40
1. 项目概述BETWEEN 不是“范围查询”的快捷键而是带语义边界的精确过滤器SQL 中的BETWEEN看似简单——不就是查两个值之间的数据吗但我在给金融系统做审计报表、给电商后台写库存预警脚本、给教育平台导出学生成绩区间时反复踩过坑明明写了BETWEEN 100 AND 200结果漏掉了 200 分的学生用BETWEEN 2023-01-01 AND 2023-12-31统计全年订单却少算了 12 月 31 日晚 23:59:59 的那笔支付更离谱的是在 PostgreSQL 里对TEXT字段用BETWEEN A AND Z居然把apple和zebra都筛进来了而Zebra却被排除在外。这些都不是 Bug而是BETWEEN本身携带的隐式语义边界在起作用。它不是数学意义上的开闭区间符号而是一条严格遵循 SQL 标准、受数据类型、排序规则、时区精度和 NULL 处理逻辑共同约束的过滤指令。本文聚焦How to Use the SQL BETWEEN Operator这一标题不讲泛泛而谈的语法定义而是从一线实战出发拆解它在真实业务场景中如何被误用、为何失效、怎样精准控制边界、以及如何与 AND 形成互补而非替代。适合所有已会写SELECT * FROM users WHERE age BETWEEN 18 AND 35但还没在凌晨三点被生产环境数据偏差叫醒过的 SQL 使用者。你不需要是 DBA但必须清楚BETWEEN的“之间”从来就不是生活语言里的模糊地带而是数据库引擎逐字节比对后给出的确定性判决。2. 核心设计逻辑与方案选型解析为什么 BETWEEN 存在它解决的到底是什么问题2.1 BETWEEN 的原始设计动机对抗冗余书写与可读性陷阱我们先看一个最基础的对比。假设要查年龄在 25 到 40 岁含之间的用户-- 方案 A显式写法 AND SELECT * FROM users WHERE age 25 AND age 40; -- 方案 BBETWEEN 写法 SELECT * FROM users WHERE age BETWEEN 25 AND 40;表面看B 方案字符数更少看起来更“简洁”。但这绝非 SQL 标准委员会引入BETWEEN的核心原因。真正驱动它诞生的是可读性陷阱与维护性风险。我曾接手一个遗留报表系统其核心查询里有这样一段WHERE order_date 2022-01-01 AND order_date 2022-12-31 AND status completed AND amount 100.00 AND amount 5000.00 AND region_id IN (1,2,3,4)这段代码的问题不在于功能而在于视觉疲劳导致的逻辑错位。当开发人员快速扫视时“”和“”这两个符号在长列表中极易被忽略或混淆。某次紧急修复中同事把amount 100.00错打成了amount 100.00测试没覆盖到这个边界上线后导致大量小额订单被错误剔除财务对账直接崩盘。BETWEEN的价值正在于它把一对“下界-上界”的逻辑绑定为一个不可分割的语义单元。amount BETWEEN 100.00 AND 5000.00这个表达式一眼就能看出这是一个完整的数值区间约束上下界天然关联无法被单独修改。它强制开发者以“区间”为思考单位而不是以“单个比较符”为操作单位。这就像编程语言中的for (int i 0; i n; i)i和i n是耦合的不能随意拆开。BETWEEN就是 SQL 里的“区间 for 循环”。2.2 BETWEEN 与 AND 的本质等价性与关键差异标准 SQL 明确规定expr BETWEEN low_expr AND high_expr在逻辑上完全等价于expr low_expr AND expr high_expr。这意味着执行计划层面现代主流数据库PostgreSQL, MySQL 8.0, SQL Server, Oracle在优化器阶段会将BETWEEN自动重写为 AND 形式因此性能上没有任何差异。你不会因为用了BETWEEN而获得索引扫描加速也不会因为它而变慢。语义层面这是最关键的差异点。BETWEEN是一个原子性谓词atomic predicate它的真假判断是一次性完成的。而 AND 是两个独立谓词的逻辑与AND。这个区别在处理NULL时暴露无遗。我们来看一个经典案例-- 假设表中有以下数据 -- id | score -- 1 | 85 -- 2 | NULL -- 3 | 92 -- 查询score BETWEEN 80 AND 90 -- 结果只返回 id1 的记录85 -- 查询score 80 AND score 90 -- 结果同样只返回 id1 的记录85看起来一样再加一行-- 查询score BETWEEN NULL AND 90 -- 结果**空集**。因为 BETWEEN 的任意一个边界为 NULL整个表达式结果为 UNKNOWN三值逻辑而 WHERE 子句只接受 TRUE 的行。 -- 查询score NULL AND score 90 -- 结果同样是**空集**。因为 score NULL 永远为 UNKNOWN。 -- 但关键来了 -- 查询score BETWEEN 80 AND NULL -- 结果**空集**同上 -- 查询score 80 AND score NULL -- 结果**空集**同上所以NULL边界的影响是等价的。真正的差异在于边界值的顺序要求。BETWEEN要求low_expr high_expr必须成立否则结果恒为 FALSE。而 AND 没有这个隐式检查-- 在 PostgreSQL 和 SQL Server 中 SELECT 5 BETWEEN 10 AND 1; -- 返回 FALSE -- 但等价写法 SELECT 5 10 AND 5 1; -- 同样返回 FALSE -- 看似没区别错。问题出在动态计算的边界上 SELECT * FROM products WHERE price BETWEEN min_price * 0.9 AND max_price * 1.1; -- 如果某条记录的 min_price 是 100max_price 是 50数据异常那么 -- min_price * 0.9 90, max_price * 1.1 55 -- 90 55所以 BETWEEN 90 AND 55 恒为 FALSE该记录必然被排除。 -- 而如果写成 SELECT * FROM products WHERE price min_price * 0.9 AND price max_price * 1.1; -- 同样price 90 AND price 55 永远为 FALSE结果一致。 -- 所以BETWEEN 并没有引入新的逻辑它只是把“低界必须小于等于高界”这个业务规则从应用层逻辑你需要自己写 CASE WHEN min_price max_price THEN ... END下沉到了 SQL 语法层由数据库引擎自动保证。这是一种**防御性编程Defensive Programming** 的体现。当你看到 BETWEEN你就知道这条语句的作者已经默认了“低界 高界”这个前提如果数据不满足结果为空是预期行为而非 bug。2.3 为什么不能用 BETWEEN 替代 IN 或 LIKE边界语义的不可扩展性很多新手会尝试用BETWEEN去替代其他操作符这是典型的“工具滥用”。BETWEEN的核心能力是连续有序域上的闭区间过滤。它依赖于数据类型的内在排序规则collation。不能替代ININ是离散值集合匹配。status IN (active, pending, archived)表达的是三个互不相关的状态。而status BETWEEN active AND archived会利用字符串的字典序把active,archived,pending甚至banned因为b a and b c都包含进来只要它们的字典序落在active和archived之间。这完全违背业务意图。不能替代LIKELIKE是模式匹配处理的是字符串的结构特征如%gmail.com。BETWEEN只能比较完整字符串的大小无法识别通配符或子串位置。试图用email BETWEEN agmail.com AND zgmail.com来筛选 gmail 用户会漏掉aagmail.com因为aa z也会包含ygmail.com符合但完全无法处理usertaggmail.com这类合法邮箱。不能用于无序类型某些数据库如旧版 MySQL允许对ENUM类型使用BETWEEN但这极其危险。ENUM(small, medium, large)的内部序号是 1,2,3medium BETWEEN small AND large会按序号比较看似正确。但一旦业务变更新增x-small插入到small前序号全乱BETWEEN的结果就不可预测了。正确的做法永远是显式枚举或用CASE。提示BETWEEN的适用性铁律——仅当你的数据类型具有明确定义、稳定可靠、业务认可的全序关系Total Order时才可使用。数字、日期时间、规范化的字符串如 ISO 8601 格式日期是安全的状态码、分类标签、用户自定义文本通常是不安全的。3. 核心细节解析与实操要点数据类型、边界精度与排序规则的深度博弈3.1 数值类型整数与浮点数的“相等”陷阱数值类型看似最安全但浮点数FLOAT,REAL,DOUBLE PRECISION是BETWEEN的第一个雷区。根本原因在于二进制浮点数无法精确表示十进制小数。-- 假设有一个价格表price 是 FLOAT 类型 -- 我们想查价格在 9.99 到 19.99 之间的商品 -- 错误示范可能漏数据 SELECT * FROM products WHERE price BETWEEN 9.99 AND 19.99; -- 为什么因为 9.99 在二进制中是无限循环小数实际存储可能是 9.990000000000001 或 9.98999999999999。 -- 当数据库从磁盘读取这个近似值并与你输入的字面量 9.99它也被转换为近似值比较时微小的舍入误差可能导致 price 9.99 为 FALSE即使业务上它就是 9.99。 -- 正确做法使用精确数值类型DECIMAL/NUMERIC并配合 ROUND 函数如果必须用 FLOAT -- 方案1推荐建表时就用 DECIMAL(10,2) ALTER TABLE products MODIFY COLUMN price DECIMAL(10,2); -- 方案2补救在查询时进行标准化 SELECT * FROM products WHERE ROUND(price, 2) BETWEEN 9.99 AND 19.99;但ROUND也有代价它会使索引失效因为对列进行了函数运算。所以最佳实践是源头治理——在数据录入和存储阶段就确保精度。BETWEEN本身不创造精度它只是忠实地执行比较。它放大了底层数据类型的缺陷。对于整数BETWEEN是绝对可靠的。id BETWEEN 100 AND 200就是id IN (100,101,...,200)的高效写法且能充分利用主键索引。3.2 日期时间类型精度战争与时区迷雾日期时间是BETWEEN最常用也最容易出错的领域。核心矛盾在于人类的“一天”与数据库的“时间戳”在精度上存在天然鸿沟。-- 场景统计 2023 年全年的订单量 -- 错误写法几乎所有人都这么写过 SELECT COUNT(*) FROM orders WHERE order_date BETWEEN 2023-01-01 AND 2023-12-31; -- 问题在哪2023-12-31 这个字符串在大多数数据库中会被隐式转换为 2023-12-31 00:00:00。 -- 所以BETWEEN 2023-01-01 00:00:00 AND 2023-12-31 00:00:00 实际上只包含了从 2023-01-01 00:00:00 到 2023-12-31 00:00:00不含的所有时间点。 -- 2023-12-31 00:00:01 及之后的订单全部被排除整整漏掉了 23 小时 59 分 59 秒的数据。 -- 正确写法两种主流方案 -- 方案A使用开闭区间推荐清晰且索引友好 SELECT COUNT(*) FROM orders WHERE order_date 2023-01-01 AND order_date 2024-01-01; -- 方案B显式指定时间边界兼容性好 SELECT COUNT(*) FROM orders WHERE order_date BETWEEN 2023-01-01 00:00:00 AND 2023-12-31 23:59:59.999; -- 方案A 的优势2024-01-01 是一个明确的、无歧义的“下一年第一天”且 order_date 2024-01-01 可以完美利用 order_date 列上的索引。而方案B 的 23:59:59.999 在不同数据库中精度支持不同MySQL 5.6 只支持秒级PostgreSQL 支持微秒容易出错。时区是另一个维度的复杂性。如果你的order_date是TIMESTAMP WITH TIME ZONE如 PostgreSQL或者DATETIME存储的是 UTC 时间而你的业务逻辑是基于本地时区如北京时间 UTC8那么BETWEEN的边界就必须进行时区转换-- 假设 order_date 是 UTC 时间戳要查北京时间 2023-01-01 00:00:00 到 2023-01-01 23:59:59 的订单 -- 北京时间 2023-01-01 00:00:00 UTC 时间 2022-12-31 16:00:00 -- 北京时间 2023-01-01 23:59:59 UTC 时间 2023-01-01 15:59:59 SELECT COUNT(*) FROM orders WHERE order_date 2022-12-31 16:00:0000 AND order_date 2023-01-01 16:00:0000;注意永远不要在BETWEEN的边界里写2023-01-01这样的裸字符串来处理带时区的时间戳。必须明确写出带时区偏移的完整时间字面量或使用AT TIME ZONE函数进行转换。BETWEEN不会帮你做时区推算它只做字面量比较。3.3 字符串类型排序规则Collation是隐形的指挥官字符串的BETWEEN是最易被忽视的“玄学”区域。它的行为完全由数据库的排序规则Collation决定。同一个BETWEEN A AND Z在不同的 collation 下结果天差地别。-- 场景筛选用户名以字母 A-Z 开头的用户不区分大小写 -- 在 MySQL 默认的 utf8mb4_0900_as_cs区分大小写下 SELECT * FROM users WHERE username BETWEEN A AND Z; -- 结果只返回 username 以大写 A-Z 开头的用户alice小写被排除。 -- 在 utf8mb4_0900_as_cs不区分大小写下 SELECT * FROM users WHERE username BETWEEN A AND Z; -- 结果返回 Alice, bob, Charlie... 因为 bob 的字典序在 A 和 Z 之间b A and b Z。 -- 更诡异的是在某些 collation 下Å带圈A可能排在 Z 之后也可能排在 A 和 B 之间取决于它是按 Unicode 码点还是按语言习惯排序。 -- 安全实践 -- 1. 永远显式指定 collation如果数据库支持 SELECT * FROM users WHERE username COLLATE utf8mb4_0900_as_cs BETWEEN A AND Z; -- 2. 对于首字母筛选用 SUBSTRING UPPER/Lower 更可靠 SELECT * FROM users WHERE UPPER(LEFT(username, 1)) BETWEEN A AND Z; -- 3. 对于前缀匹配LIKE A% 比 BETWEEN A AND A\ufffdUnicode 最大字符更直观、更易读、索引效率也更高。BETWEEN在字符串上的威力只应发挥在高度结构化、格式统一、排序规则明确的场景例如ISO 8601 日期字符串2023-01-01 BETWEEN 2022-01-01 AND 2023-12-31安全因为字典序与时间序一致订单编号前缀order_id BETWEEN ORD-2023-000001 AND ORD-2023-999999安全前提是编号严格按此规则生成4. 实操过程与核心环节实现从需求分析到健壮 SQL 的完整链路4.1 需求分析画出你的“业务区间”草图在写任何BETWEEN之前拿出一张纸画出你的业务需求区间。这不是写代码而是做需求澄清。我坚持用这个模板业务描述期望包含的最小值期望包含的最大值数据库中该值的精确表示是否包含端点为什么查找2023年创建的用户2023-01-01 00:00:002023-12-31 23:59:59.999created_at(TIMESTAMP)是“2023年”在业务上指全年每一天的每一秒查找价格在100元到500元之间的商品100.00500.00price(DECIMAL(10,2))是价格是离散的货币单位100元和500元都是有效价格查找ID在1000到2000之间的订单10002000id(BIGINT)是ID是整数序列1000和2000都是合法ID这个表格强迫你回答三个关键问题端点是否业务有效如果“2023-12-31 23:59:59.999”不是一个真实的业务概念比如你的系统只记录到秒那就不要硬凑。数据库字段的精度是否匹配如果created_at只存到秒写.999就是自欺欺人。“包含”是业务要求还是技术妥协有时业务上“大于等于100”就够了非要写BETWEEN 100 AND 500反而增加了维护负担。4.2 SQL 编写四步构建法与边界验证我总结了一套BETWEEN编写四步法每一步都有对应的验证动作第一步选择基准类型确定你要比较的字段类型INT, DATE, TIMESTAMP, DECIMAL。验证DESCRIBE table_name;或SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE ...确认字段的实际类型和精度。第二步构造边界字面量根据上一步的类型写出数据库能精确理解的字面量。INT:100,200DATE:2023-01-01TIMESTAMP:2023-01-01 00:00:00如果字段是DATETIME或2023-01-01 00:00:0000如果字段是TIMESTAMPTZDECIMAL:100.00,500.00验证在SELECT子句中单独测试字面量看是否被正确解析SELECT 2023-01-01::DATE, 2023-01-01 00:00:00::TIMESTAMP;第三步编写 BETWEEN 表达式严格按照field BETWEEN low_literal AND high_literal的语法。关键检查low_literal必须在数据库的排序规则下high_literal。可以用一个简单的SELECT验证SELECT 2023-01-01::DATE 2023-12-31::DATE; -- 应返回 true SELECT 100 500; -- 应返回 true第四步边界压力测试这是最容易被跳过的一步却是避免线上事故的关键。写一个测试查询专门检查端点值是否被正确包含-- 测试下界 SELECT * FROM your_table WHERE your_field low_literal; -- 测试上界 SELECT * FROM your_table WHERE your_field high_literal; -- 运行你的 BETWEEN 查询 SELECT * FROM your_table WHERE your_field BETWEEN low_literal AND high_literal; -- 对比三个结果集的行数。理想情况下BETWEEN 的结果数 下界行数 上界行数 中间行数。 -- 如果下界或上界行数为0说明你的字面量写错了或者数据里根本没有这个精确值。4.3 性能调优索引友好型 BETWEEN 写法BETWEEN本身不决定性能但它写的是否“索引友好”决定了性能上限。最佳实践让 BETWEEN 的左右操作数都是常量或确定性表达式-- ✅ 索引友好left 和 right 都是常量 WHERE created_at BETWEEN 2023-01-01 AND 2023-12-31 -- ⚠️ 索引可能失效right 是函数且函数作用于列 WHERE created_at BETWEEN 2023-01-01 AND DATE_ADD(NOW(), INTERVAL 1 DAY) -- ❌ 索引失效left 是函数且函数作用于列 WHERE created_at BETWEEN DATE_SUB(NOW(), INTERVAL 7 DAY) AND 2023-12-31复合索引的顺序至关重要假设你有一个查询SELECT * FROM orders WHERE status shipped AND created_at BETWEEN 2023-01-01 AND 2023-01-31 AND amount 100;最佳复合索引应该是(status, created_at, amount)。为什么status shipped是等值查询应该放在索引最左。created_at BETWEEN ...是范围查询应该紧跟在等值列之后。amount 100也是范围查询但BETWEEN已经消耗了索引的“范围能力”amount列的范围查询无法再利用索引进行高效过滤它只能作为最后的过滤条件Filter。所以把它放在索引末尾对前两个条件的扫描效率影响最小。警惕“虚假的范围查询”-- 如果 status 是一个低基数列只有 pending, shipped, cancelled 三种值 -- 那么 status shipped 可能只过滤掉 1/3 的数据此时索引 (status, created_at) 的效果可能不如单纯的 (created_at) 索引因为后者能直接定位到时间范围内的所有行再在内存中过滤 status。 -- 这需要通过 EXPLAIN 查看实际执行计划而不是凭经验猜测。5. 常见问题与排查技巧实录那些让你抓狂的 BETWEEN 异常现场5.1 问题速查表症状、根因与解决方案症状根因分析解决方案我的实操心得BETWEEN查询结果为空但用 AND 却有数据边界值low_expr high_expr导致BETWEEN恒为 FALSE1. 用SELECT low_expr, high_expr检查边界值。2. 在BETWEEN前加CASE WHEN low_expr high_expr THEN ... ELSE NULL END包裹。这个坑我踩过三次。第一次是动态 SQL 拼接错误第二次是前端传参时start_date和end_date顺序颠倒第三次是 ETL 脚本里min_value和max_value字段名写反了。现在我的所有动态BETWEEN查询第一行必加WHERE low_expr high_expr的守卫。查询结果比预期多包含了不该有的记录字符串BETWEEN受排序规则影响字典序与业务序不一致1.SHOW COLLATION LIKE your_collation;查看当前排序规则。2. 用SELECT value COLLATE your_collation测试单个值的排序权重。3. 改用LIKE或正则表达式。在一个跨国 SaaS 项目里客户要求按国家名称首字母分组。我用country_name BETWEEN A AND M结果德国Germany被分到了 M 组因为德语排序规则里G排在M后面。最后改用SUBSTR(country_name, 1, 1) IN (A,B,..., M)虽然啰嗦但绝对可控。BETWEEN在某个数据库版本工作正常升级后失效数据库版本升级改变了默认排序规则或隐式类型转换规则1. 查阅新版本的 Release Notes重点关注 Collation 和 Type Conversion 部分。2. 在 SQL 中显式指定COLLATE和类型转换如CAST(column AS VARCHAR) COLLATE utf8mb4_0900_as_cs。MySQL 8.0 升级到 8.0.30 后utf8mb4_0900_as_cs成为默认 collation而我们旧代码里大量BETWEEN依赖旧的utf8mb4_general_ci。上线前我用pt-query-digest抓取了所有慢查询然后用mysqlpump导出 schema用grep BETWEEN找出所有相关 SQL逐个加上COLLATE。花了两天但避免了上线后的大面积故障。BETWEEN查询很慢EXPLAIN显示走了全表扫描BETWEEN的边界是不确定的表达式如NOW(),CURDATE()导致优化器无法估算选择率1. 将不确定表达式提取为变量MySQL或 CTEPostgreSQL。2. 为范围查询列建立单独索引或调整复合索引顺序。在一个实时监控大屏项目中WHERE event_time BETWEEN DATE_SUB(NOW(), INTERVAL 1 HOUR) AND NOW()每秒执行一次把数据库 CPU 拉到 90%。我把NOW()提取出来SET now : NOW(); SET one_hour_ago : DATE_SUB(now, INTERVAL 1 HOUR);然后WHERE event_time BETWEEN one_hour_ago AND now。QPS 瞬间提升 5 倍CPU 回落至 15%。5.2 独家避坑技巧来自生产环境的血泪经验技巧1用EXPLAIN FORMATJSON深挖BETWEEN的执行细节EXPLAIN的文本输出太简略。FORMATJSON会告诉你优化器对BETWEEN的真实解读EXPLAIN FORMATJSON SELECT * FROM orders WHERE order_date BETWEEN 2023-01-01 AND 2023-01-31;在 JSON 输出中重点看range_analysis部分chosen_range_access_summary会显示优化器是否选择了 range 访问。rows_for_plan会显示优化器预估的扫描行数。如果这个数字远大于你表的总行数说明优化器对BETWEEN的选择率估算严重失真需要检查统计信息是否过期ANALYZE TABLE orders;。技巧2“BETWEEN 伪装术”——当业务需要开区间时业务上常说“价格在100到500之间”潜台词往往是“大于100小于500”即开区间(100, 500)。BETWEEN只能做闭区间[100, 500]。强行用BETWEEN 101 AND 499是错的因为 100.5 这样的价格会被排除。正确解法是-- 方法1用 AND 推荐 WHERE price 100 AND price 500 -- 方法2用 BETWEEN 配合边界微调仅适用于整数 WHERE price BETWEEN 100.0000001 AND 499.9999999 -- 但这种方法在 DECIMAL 上有精度风险且可读性差不推荐。技巧3BETWEEN与NULL的终极共存协议BETWEEN遇到NULL边界就是UNKNOWN这是 SQL 标准。但业务上我们经常需要“如果下界是 NULL就认为没有下界”。这时BETWEEN就无能为力了必须回归 AND 并用OR逻辑-- 业务需求动态查询lower_bound 和 upper_bound 可能为 NULL -- 期望lower_bound 为 NULL 时不限制下界upper_bound 为 NULL 时不限制上界 -- ✅ 正确写法清晰、标准、索引友好 WHERE (lower_bound IS NULL OR price lower_bound) AND (upper_bound IS NULL OR price upper_bound) -- ❌ 错误写法BETWEEN 无法处理 NULL 边界 WHERE price BETWEEN COALESCE(lower_bound, ???) AND COALESCE(upper_bound, ???) -- 你无法为 COALESCE 找到一个“无穷小”或“无穷大”的值因为所有数据类型都有其最大/最小值限制。5.3 实战复盘一个电商库存预警脚本的BETWEEN重构最后分享一个