BETWEEN / IN / ANY / ALL
BETWEEN、IN、ANY、ALL 是四组用于「批量比较」的谓词与量词。它们把多个比较条件压缩成一行可读的 SQL,但各自的边界语义、NULL 行为、索引可用性差异很大。最典型的坑是:BETWEEN 是闭区间,处理日期时间时容易多算一天;NOT IN 遇 NULL 会返回空集;ALL 在空集上恒为 TRUE。本节把这些语义一次讲透。
前置知识
阅读本节前,建议先了解:IS NULL 与 NULL 判断
基础概念
| 概念 | 术语 | 说明 |
|---|---|---|
| 范围谓词 | BETWEEN | 判断值是否落在 [下界, 上界] 的闭区间内 |
| 对称范围 | BETWEEN SYMMETRIC | 自动交换上下界,不要求下界小于上界 |
| 成员谓词 | IN | 判断值是否等于列表或子查询结果中的任意一个 |
| 任意量词 | ANY / SOME | 与子查询或数组逐项比较,只要有一项满足即为真 |
| 全称量词 | ALL | 与子查询或数组逐项比较,必须全部满足才为真 |
| 量词比较 | quantified comparison | 形如 x > ANY(...)、x <> ALL(...) 的组合比较 |
| 闭区间 | closed interval | 包含两个端点,即 >= lower AND <= upper |
| 半开区间 | half-open interval | 只含左端点的 [lower, upper),日期范围常用 |
| 空集语义 | empty set semantics | = ANY(空集) 为 FALSE,= ALL(空集) 恒为 TRUE |
| 数组展开 | array expansion | ANY(数组) / ALL(数组) 直接对数组元素做比较 |
BETWEEN、IN 本质上都是语法糖:
a BETWEEN x AND y⟺a >= x AND a <= ya IN (x, y, z)⟺a = x OR a = y OR a = za > ANY (SELECT ...)⟺a > min(...)的语义(存在性)a > ALL (SELECT ...)⟺a > max(...)的语义(全称性)
理解这些等价关系,就能准确预判它们的 NULL 行为与索引表现。
-- 四组写法的最小示例
SELECT
50 BETWEEN 10 AND 100 AS between_demo, -- true
3 IN (1, 2, 3) AS in_demo, -- true
50 > ANY (ARRAY[10, 20, 30]) AS any_demo, -- true
50 > ALL (ARRAY[10, 20, 30]) AS all_demo; -- true语法与用法
| 形式 | 等价写法 | 说明 |
|---|---|---|
expr BETWEEN a AND b | expr >= a AND expr <= b | 闭区间 |
expr NOT BETWEEN a AND b | expr < a OR expr > b | 区间外 |
expr BETWEEN SYMMETRIC a AND b | 自动排序上下界 | 上下界顺序无关 |
expr IN (v1, v2, ...) | expr = v1 OR expr = v2 ... | 常量列表 |
expr IN (SELECT ...) | 等价于半连接 | 子查询 |
expr = ANY (array) | 数组元素逐个比较 | 数组专用 |
expr > ALL (SELECT ...) | 全称量词 | 子查询或数组 |
expr = SOME (array) | ANY 的同义词 | 兼容 SQL 标准 |
-- 最小可运行示例:无需建表即可执行
SELECT
5 BETWEEN 1 AND 10 AS a, -- true
5 NOT BETWEEN 1 AND 10 AS b, -- false
5 BETWEEN SYMMETRIC 10 AND 1 AS c, -- true(自动交换)
'b' IN ('a', 'b', 'c') AS d, -- true
5 = ANY (ARRAY[1, 5, 9]) AS e, -- true
5 < ALL (ARRAY[6, 7, 8]) AS f; -- true详细说明
BETWEEN 的闭区间语义
BETWEEN 同时包含两个端点。处理整数时通常没问题,但处理日期时间时要格外小心:date 与 timestamp 的「上界那一天」含义完全不同。
-- 对整数:包含 10 和 100
SELECT 10 BETWEEN 10 AND 100 AS lo_included, -- true
100 BETWEEN 10 AND 100 AS hi_included; -- true
-- 对 timestamp:上界是「那一刻」,不是「那一天」
-- 以下查询会漏掉 2025-03-31 当天 00:00:00 之后的记录
SELECT * FROM orders
WHERE created_at BETWEEN '2025-03-01' AND '2025-03-31'; -- 不推荐正确做法有两种:要么把上界写成次日零点并用 <,要么把类型收窄为 date。
-- 推荐:半开区间 [起, 止)
SELECT * FROM orders
WHERE created_at >= '2025-03-01'
AND created_at < '2025-04-01';
-- 或:先转成 date 再用 BETWEEN
SELECT * FROM orders
WHERE created_at::date BETWEEN '2025-03-01' AND '2025-03-31';BETWEEN SYMMETRIC
普通 BETWEEN 要求下界小于等于上界,否则结果恒为 FALSE。BETWEEN SYMMETRIC 会自动比较两个边界并把小的当上界、大的当下界,适合边界顺序不固定的场景。
SELECT 5 BETWEEN 10 AND 1 AS normal, -- false(下界大于上界)
5 BETWEEN SYMMETRIC 10 AND 1 AS symmetric; -- trueIN:常量列表与子查询
IN 支持两种右侧写法:常量列表与子查询。两者都可被优化器改写成 ANY 或半连接。
-- 常量列表
SELECT * FROM products WHERE category IN ('显示器', '键盘', '鼠标');
-- 子查询
SELECT * FROM products
WHERE id IN (SELECT product_id FROM orders WHERE qty > 1);IN 内部使用「等值比较」,因此 NULL 行为与 = 一致:x IN (1, NULL) 在 x 不等于任何非 NULL 值时结果为 NULL,会被 WHERE 过滤掉。
NOT IN 与 NULL 陷阱
NOT IN 是 IN 取反,但它遇 NULL 时的行为极具破坏性。
-- 若子查询返回任何 NULL,整个 NOT IN 结果为「无行」
SELECT * FROM products
WHERE id NOT IN (SELECT product_id FROM orders); -- 错误!可能返回空集
-- 原因:展开后为 id <> 1 AND id <> 2 AND ... AND id <> NULL
-- 最后一项恒为 NULL,AND 链结果只能是 NULL 或 FALSE,永远不为 TRUE修复方式有三种,推荐第一种:
-- 方案 A(推荐):改用 NOT EXISTS
SELECT p.* FROM products p
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.product_id = p.id);
-- 方案 B:在子查询中排除 NULL
SELECT * FROM products
WHERE id NOT IN (SELECT product_id FROM orders WHERE product_id IS NOT NULL);
-- 方案 C:用 ALL 显式表达(语义较绕,不推荐)
SELECT * FROM products p
WHERE p.id <> ALL (SELECT o.product_id FROM orders o WHERE o.product_id IS NOT NULL);ANY / SOME 与 ALL 量词
ANY(SOME 是其同义词,二者完全等价)与 ALL 必须配合比较运算符使用,右侧可以是子查询或数组。
语义规则:
x op ANY (集合):集合中存在一个元素使x op 元素为真,结果即真。x op ALL (集合):集合中所有元素都使x op 元素为真,结果才为真。
-- 找价格高于「任一」订单金额的商品(即高于最小金额)
SELECT * FROM products
WHERE price > ANY (SELECT amount FROM orders);
-- 找价格高于「所有」订单金额的商品(即高于最大金额,存在性要求高)
SELECT * FROM products
WHERE price > ALL (SELECT amount FROM orders);= ANY 与 IN 完全等价,<> ALL 与 NOT IN 完全等价:
-- 以下两组查询语义完全相同
SELECT * FROM products WHERE id = ANY (SELECT product_id FROM orders);
SELECT * FROM products WHERE id IN (SELECT product_id FROM orders);
SELECT * FROM products WHERE id <> ALL (SELECT product_id FROM orders);
SELECT * FROM products WHERE id NOT IN (SELECT product_id FROM orders);ANY / ALL 配合数组
当右侧是数组时,ANY 与 ALL 无需子查询,直接比较数组元素,非常简洁。
-- 判断值是否在数组中
SELECT * FROM products WHERE id = ANY (ARRAY[1, 3, 5]);
-- 判断值是否大于数组中所有元素(即大于最大值)
SELECT * FROM products WHERE price > ALL (ARRAY[100, 200, 300]);
-- 判断数组是否与目标有交集
SELECT * FROM products WHERE sku = ANY ('{SKU-1001,SKU-1002}'::text[]);空集语义:= ALL(空集) 恒为 TRUE
这是 ALL 最容易踩的坑。当右侧集合为空时:
x = ANY(空集)为FALSE(没有任何元素满足)。x = ALL(空集)为TRUE(「所有元素都满足」在空集上是空真,vacuous truth)。
-- 空集上的 ANY 与 ALL
SELECT 5 = ANY (SELECT 1 WHERE false) AS any_on_empty, -- false
5 = ALL (SELECT 1 WHERE false) AS all_on_empty; -- true这会导致一个反直觉的结果:x > ALL(子查询) 在子查询无行时返回 TRUE,从而把所有行都选出来。
-- 危险!orders 为空时,这条查询会返回 products 的全部行
SELECT * FROM products
WHERE price > ALL (SELECT amount FROM orders WHERE qty > 100); -- 需谨慎索引可用性与优化器改写
BETWEEN 与 IN 通常能走索引:
BETWEEN是范围条件,可走 B-tree 的范围扫描。IN (常量列表)会被改写成多个等值条件的OR,也可能走 Bitmap 扫描。IN (子查询)通常被改写为半连接(Hash Semi Join或Nested Loop Semi Join)。NOT IN/<> ALL通常是反连接,遇 NULL 无法用「反半连接」优化,需全表扫描。
-- BETWEEN 可走索引范围扫描
EXPLAIN (ANALYZE, COSTS OFF)
SELECT * FROM products WHERE price BETWEEN 100 AND 500;
-- NOT IN 含 NULL 时无法改写为高效反连接
EXPLAIN (ANALYZE, COSTS OFF)
SELECT * FROM products WHERE id NOT IN (SELECT product_id FROM orders);用 NOT EXISTS 版本可以让优化器生成「反半连接」,性能通常更好:
EXPLAIN (ANALYZE, COSTS OFF)
SELECT * FROM products p
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.product_id = p.id);量词与 NULL 的组合规则
ANY / ALL 遇 NULL 时遵循三值逻辑:
| 情形 | x = ANY(含 NULL) | x = ALL(含 NULL) |
|---|---|---|
| 存在匹配的非 NULL 元素 | TRUE | 取决于是否全部匹配 |
| 无匹配但含 NULL | NULL | NULL 或 FALSE |
| 全为非 NULL 且全部不匹配 | FALSE | FALSE |
-- ANY 遇 NULL:非 NULL 匹配即返回 true
SELECT 1 = ANY (ARRAY[1, NULL]) AS a; -- true
-- ALL 遇 NULL:有非 NULL 不匹配即 false;否则可能为 NULL
SELECT 1 = ALL (ARRAY[1, NULL]) AS b, -- NULL
1 = ALL (ARRAY[2, NULL]) AS c; -- false实战示例
步骤 1:建表并造数据
-- 统一的示例库
CREATE DATABASE demo;
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS products;
CREATE TABLE products (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sku text NOT NULL UNIQUE,
name text NOT NULL,
category text,
price numeric(10, 2),
stock integer NOT NULL DEFAULT 0,
added_on date
);
CREATE TABLE orders (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_no text NOT NULL,
product_id integer,
qty integer NOT NULL,
amount numeric(10, 2),
created_at timestamp NOT NULL DEFAULT now()
);
INSERT INTO products (sku, name, category, price, stock, added_on) VALUES
('SKU-1001', '27 英寸显示器', '显示器', 1299.00, 12, '2025-01-10'),
('SKU-1002', '机械键盘', '键盘', 399.00, 30, '2025-02-01'),
('SKU-1003', '无线鼠标', '鼠标', 159.00, 0, '2025-02-15'),
('SKU-1004', 'HDMI 线缆', '线缆', 49.00, 200, '2025-03-01'),
('SKU-1005', 'USB 集线器', '配件', 89.00, 50, '2025-03-20'),
('SKU-1006', '显卡支架', '配件', 129.00, 8, '2025-04-05');
INSERT INTO orders (order_no, product_id, qty, amount, created_at) VALUES
('SO-2001', 1, 2, 2598.00, '2025-03-05 10:12:00'),
('SO-2002', 2, 1, 399.00, '2025-03-18 15:30:00'),
('SO-2003', NULL, 1, 159.00, '2025-03-31 23:59:00'), -- product_id 为 NULL
('SO-2004', 3, 3, 477.00, '2025-04-01 08:00:00'),
('SO-2005', NULL, 2, 178.00, '2025-04-20 12:00:00');步骤 2:BETWEEN 范围查询与日期陷阱
-- 价格区间查询(闭区间,含 100 与 500)
SELECT sku, name, price
FROM products
WHERE price BETWEEN 100 AND 500
ORDER BY price; sku | name | price
----------+------------+---------
SKU-1006 | 显卡支架 | 129.00
SKU-1003 | 无线鼠标 | 159.00
SKU-1002 | 机械键盘 | 399.00
(3 rows)-- 日期陷阱演示:BETWEEN 上界是「那一刻」,会漏掉 3 月 31 日全天数据
SELECT count(*) AS between_rows
FROM orders
WHERE created_at BETWEEN '2025-03-01' AND '2025-03-31'; between_rows
--------------
2
(1 row)-- 推荐写法:半开区间,包含 3 月 31 日全天
SELECT count(*) AS half_open_rows
FROM orders
WHERE created_at >= '2025-03-01'
AND created_at < '2025-04-01'; half_open_rows
----------------
3
(1 row)步骤 3:IN 与 NOT IN 陷阱修复
-- IN 常量列表
SELECT sku, name, category
FROM products
WHERE category IN ('键盘', '鼠标')
ORDER BY sku; sku | name | category
----------+----------+----------
SKU-1002 | 机械键盘 | 键盘
SKU-1003 | 无线鼠标 | 鼠标
(2 rows)-- 陷阱:orders.product_id 含 NULL,NOT IN 返回空集
SELECT sku, name
FROM products
WHERE id NOT IN (SELECT product_id FROM orders); sku | name
-----+------
(0 rows)-- 修复:NOT EXISTS
SELECT p.sku, p.name
FROM products p
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.product_id = p.id)
ORDER BY p.sku; sku | name
----------+------------
SKU-1004 | HDMI 线缆
SKU-1005 | USB 集线器
SKU-1006 | 显卡支架
(3 rows)步骤 4:ANY / ALL 量词查询
-- 价格高于任意一笔订单金额(即 > min)
SELECT sku, name, price
FROM products
WHERE price > ANY (SELECT amount FROM orders)
ORDER BY price; sku | name | price
----------+------------+---------
SKU-1003 | 无线鼠标 | 159.00
SKU-1002 | 机械键盘 | 399.00
SKU-1001 | 27 英寸显示器 | 1299.00
(3 rows)-- 价格高于所有订单金额(即 > max)
SELECT sku, name, price
FROM products
WHERE price > ALL (SELECT amount FROM orders)
ORDER BY price; sku | name | price
----------+------------+---------
SKU-1001 | 27 英寸显示器 | 1299.00
(1 row)-- 删除 NULL 后,NOT IN 与 <> ALL 可正常工作
SELECT sku, name
FROM products
WHERE id <> ALL (SELECT product_id FROM orders WHERE product_id IS NOT NULL)
ORDER BY sku; sku | name
----------+------------
SKU-1004 | HDMI 线缆
SKU-1005 | USB 集线器
SKU-1006 | 显卡支架
(3 rows)步骤 5:空集语义与安全写法
-- 危险演示:子查询为空时 > ALL 返回所有行
SELECT count(*) AS all_rows_when_empty
FROM products
WHERE price > ALL (SELECT amount FROM orders WHERE qty > 100); all_rows_when_empty
---------------------
6
(1 row)-- 安全写法:显式判断子查询非空
SELECT count(*) AS safe_rows
FROM products
WHERE EXISTS (SELECT 1 FROM orders WHERE qty > 100)
AND price > ALL (SELECT amount FROM orders WHERE qty > 100); safe_rows
-----------
0
(1 row)步骤 6:ANY 配合数组做批量筛选
-- 用数组代替一长串 OR
SELECT sku, name, price
FROM products
WHERE sku = ANY (ARRAY['SKU-1001', 'SKU-1003', 'SKU-1006'])
ORDER BY sku; sku | name | price
----------+-------------+---------
SKU-1001 | 27 英寸显示器 | 1299.00
SKU-1003 | 无线鼠标 | 159.00
SKU-1006 | 显卡支架 | 129.00
(3 rows)-- 数组作为参数传入(应用层常用)
-- psql 中可直接用字面量
SELECT sku, name FROM products WHERE id = ANY ('{1,2,4}'::int[]) ORDER BY id; sku | name
----------+------------
SKU-1001 | 27 英寸显示器
SKU-1002 | 机械键盘
SKU-1004 | HDMI 线缆
(3 rows)注意事项
BETWEEN是闭区间,且上界是「那一刻」而非「那一天」。 处理timestamp时优先用半开区间>= 起 AND < 止。
-- 不推荐:会漏掉上界当天之后的数据
WHERE created_at BETWEEN '2025-03-01' AND '2025-03-31'
-- 推荐
WHERE created_at >= '2025-03-01' AND created_at < '2025-04-01'NOT IN遇 NULL 返回空集。 只要有 NULL 参与,结果就不可预期,一律改用NOT EXISTS。= ALL (空集)恒为TRUE。 使用ALL前要确认子查询不会返回空集,否则会意外选中所有行。BETWEEN的下界若大于上界,普通写法恒为FALSE。 需要容忍乱序边界时才用BETWEEN SYMMETRIC。
SELECT 5 BETWEEN 10 AND 1; -- false,不是「报错」
SELECT 5 BETWEEN SYMMETRIC 10 AND 1; -- trueIN的列表长度不宜过大。 几千个常量值会让解析与计划生成变慢,此时应改用临时表或= ANY(数组)。ANY与SOME完全等价,不是「部分匹配」的意思,不要与LIKE混淆。NOT IN无法使用反半连接优化,性能通常劣于NOT EXISTS,在大表上差距明显。IN子查询的列必须非 NULL 才安全,即使是IN(非NOT IN),也建议在子查询中排除 NULL 以避免三值逻辑歧义。
最佳实践
- 日期区间统一用半开区间
[起, 止),这是跨越时区、闰秒与精度问题最稳妥的写法。
SELECT * FROM orders
WHERE created_at >= :start AND created_at < :start + interval '1 day';- 反连接一律用
NOT EXISTS,彻底规避NOT IN的 NULL 陷阱。
SELECT p.* FROM products p
WHERE NOT EXISTS (SELECT 1 FROM order_items i WHERE i.product_id = p.id);IN列表超过几百项时改成= ANY(数组)或临时表连接,避免生成超长的OR链。
SELECT * FROM products WHERE id = ANY ($1::int[]);- 参数化的多值查询用数组传入,既安全(无 SQL 注入)又高效。
-- 应用层传 int[] 绑定参数
SELECT * FROM products WHERE category = ANY (:categories);- 使用
ALL前先判断子查询非空,或改用> (SELECT max(...))这种聚合写法,语义更明确。
-- 更清晰的等价写法
SELECT * FROM products WHERE price > (SELECT max(amount) FROM orders);- 整数/数值区间直接用
BETWEEN,它比>= AND <=更简洁且优化器识别一致。
SELECT * FROM products WHERE price BETWEEN 100 AND 500;边界可能乱序时用
BETWEEN SYMMETRIC,让 SQL 自己处理顺序,避免应用层额外判断。用
EXPLAIN确认IN/BETWEEN走了索引,特别注意子查询是否被改写为半连接。
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM products WHERE price BETWEEN 100 AND 500;下一节
继续学习:CASE WHEN 表达式