条件查询与排序
本节讲解如何用 WHERE 精确筛选数据、用 ORDER BY 控制结果顺序,以及 LIMIT、OFFSET、DISTINCT、IN、BETWEEN、LIKE 等高频配套子句。学完本节,你将能写出精确、高效、可读的查询条件,并理解 NULL 在条件判断中的特殊行为。
前置知识
阅读本节前,建议先了解:CRUD 增删改查
基础概念
WHERE 子句接收一个布尔表达式(Boolean Expression),对 FROM 产生的每一行求值,只有结果为 true 的行才会进入后续处理。
| 概念 | 说明 |
|---|---|
| 谓词 | Predicate,即返回真/假/未知的表达式,如 price > 100 |
| 三值逻辑 | Three-Valued Logic,SQL 的布尔结果有三种:true、false、NULL(未知) |
| 过滤 | Filter,在 WHERE 中筛掉不符合条件的行 |
| 排序键 | Sort Key,ORDER BY 中用于比较的列或表达式 |
| 稳定性 | 排序的确定性,即在排序键取值相同时结果是否稳定 |
| 空值排序 | NULLS FIRST / NULLS LAST,指定 NULL 排在最前还是最后 |
三值逻辑(Three-Valued Logic) 是理解 WHERE 的关键。SQL 的布尔运算不是二值而是三值:
| 表达式 | 结果 | 说明 |
|---|---|---|
NULL = NULL | NULL | 未知与未知比较仍是未知 |
NULL <> 1 | NULL | 任何与 NULL 的比较都是未知 |
NULL IS NULL | true | IS 系列专门用于判空,返回确定的布尔值 |
true AND NULL | NULL | 与未知做 AND 结果未知 |
false AND NULL | false | false 已经决定整体结果 |
true OR NULL | true | true 已经决定整体结果 |
关键结论:WHERE 只保留结果为 true 的行,结果为 false 与 NULL 的行都会被丢弃。因此 WHERE price <> 100 会把 price 为 NULL 的行也一并排除——这通常是符合预期但容易被忽略的行为。
PostgreSQL 的布尔值在表中可以存储为 boolean 类型(取值 true/false/NULL),三值逻辑同样适用于列值为 NULL 的场景。
语法与用法
WHERE 与 ORDER BY 的基本形态:
SELECT 列列表
FROM 表名
WHERE 条件表达式
ORDER BY 排序键 [ASC | DESC] [NULLS FIRST | NULLS LAST]
LIMIT 行数 OFFSET 偏移量;准备本节贯穿使用的示例表:
-- 商品表:包含价格、库存、分类、标签、上架时间
CREATE TABLE products (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sku TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
category TEXT NOT NULL,
price NUMERIC(10, 2) NOT NULL CHECK (price >= 0),
stock INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0),
tags TEXT[] NOT NULL DEFAULT '{}',
launched DATE, -- 可能为 NULL(未上架)
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO products (sku, name, category, price, stock, tags, launched, is_active) VALUES
('SKU-1001', '机械键盘', 'peripherals', 349.00, 120, '{keyboard, wired}', DATE '2024-03-01', true),
('SKU-1002', '无线鼠标', 'peripherals', 129.00, 300, '{mouse, wireless}', DATE '2024-05-12', true),
('SKU-1003', 'USB-C 扩展坞', 'accessories', 199.00, 80, '{hub, usb-c}', DATE '2024-07-20', true),
('SKU-1004', '显示器支架', 'accessories', 259.00, 45, '{ergonomic}', DATE '2024-09-01', true),
('SKU-1005', 'HDMI 线缆', 'cable', 39.00, 500, '{hdmi, cable}', DATE '2025-01-15', true),
('SKU-1006', 'USB-C 线缆', 'cable', 49.00, 420, '{usb-c, cable}', DATE '2025-02-01', true),
('SKU-1007', '机械键盘 TKL', 'peripherals', 429.00, 0, '{keyboard, tkl}', DATE '2025-06-10', false),
('SKU-1008', '笔记本支架', 'accessories', 159.00, 60, '{ergonomic, laptop}', NULL, true),
('SKU-1009', '雷电 4 硬盘盒', 'storage', 899.00, 12, '{thunderbolt}', NULL, true),
('SKU-1010', '鼠标垫', 'accessories', 29.00, 800, '{mouse, pad}', DATE '2025-08-01', true);条件表达式最常用的六类:
-- 1. 比较运算
SELECT sku, name, price FROM products WHERE price > 200;
-- 2. 逻辑组合
SELECT sku, name, price FROM products
WHERE category = 'peripherals' AND price < 400;
-- 3. 集合判断
SELECT sku, name, category FROM products
WHERE category IN ('cable', 'storage');
-- 4. 区间判断
SELECT sku, name, price FROM products
WHERE price BETWEEN 100 AND 300;
-- 5. 模式匹配
SELECT sku, name FROM products WHERE name LIKE '机械%';
-- 6. 空值判断
SELECT sku, name, launched FROM products WHERE launched IS NULL;详细说明
比较运算符与逻辑组合
| 运算符 | 含义 | 示例 |
|---|---|---|
= | 等于 | price = 199.00 |
<> 或 != | 不等于 | category <> 'cable' |
> < >= <= | 大小比较 | stock >= 100 |
AND | 逻辑与,两侧都为真才为真 | is_active AND stock > 0 |
OR | 逻辑或,任一为真即为真 | category = 'cable' OR price < 50 |
NOT | 逻辑非 | NOT is_active |
IS NULL / IS NOT NULL | 判空 | launched IS NULL |
IS TRUE / IS FALSE / IS UNKNOWN | 布尔判等(含 NULL) | is_active IS TRUE |
AND 优先级高于 OR,混用时务必加括号,否则语义与阅读直觉不符:
-- 危险!实际含义是:category = 'cable' OR (price < 100 AND is_active)
SELECT sku, name FROM products
WHERE category = 'cable' OR price < 100 AND is_active;
-- 正确:意图明确
SELECT sku, name FROM products
WHERE (category = 'cable' OR price < 100) AND is_active;布尔列可以直接作为条件,不必写 = true:
-- 简洁写法
SELECT sku, name FROM products WHERE is_active;
-- 完整写法(等价)
SELECT sku, name FROM products WHERE is_active = true;
-- 判假:不能用 = false(虽然可以,但 NULL 会被排除),推荐 IS FALSE
SELECT sku, name FROM products WHERE is_active IS FALSE;NULL 的条件判断
NULL 在条件中必须使用 IS NULL / IS NOT NULL,用 = 判断永远不成立:
-- 错误!结果永远为 0 行(NULL = NULL 的结果是 NULL,不是 true)
SELECT sku, name FROM products WHERE launched = NULL;
-- 正确
SELECT sku, name FROM products WHERE launched IS NULL;NULL 参与的算术与字符串拼接结果也是 NULL,可以用 COALESCE 提供兜底值:
-- launched 为 NULL 时显示"未上架"
SELECT sku, name, COALESCE(launched::text, '未上架') AS launched_text
FROM products
WHERE launched IS NULL;
-- 注意!任何值与 NULL 拼接结果都是 NULL,整列都会变成 NULL
SELECT name, name || launched::text AS wrong_concat FROM products LIMIT 3;
-- 正确:用 COALESCE 或 concat()(concat 会忽略 NULL)
SELECT name, concat(name, ' ', launched) AS safe_concat FROM products LIMIT 3;在排序与聚合中,NULL 的处理规则与条件判断不同,需要单独记忆:
| 场景 | NULL 的行为 |
|---|---|
ORDER BY ... ASC(默认) | NULL 排在最后(最大) |
ORDER BY ... DESC | NULL 排在最前 |
NULLS FIRST / NULLS LAST | 显式指定,覆盖默认行为 |
count(*) | 统计所有行,包含 NULL 行 |
count(列) | 忽略该列为 NULL 的行 |
sum / avg / max / min | 忽略 NULL;若全为 NULL 则返回 NULL |
GROUP BY | 所有 NULL 归为同一组 |
UNIQUE 约束 | 默认允许多个 NULL 并存(NULL 互不相等) |
模式匹配:LIKE 与正则
LIKE 支持两个通配符:% 匹配任意长度(含零)字符,_ 匹配单个字符。
-- 前缀匹配:能用上索引(前提是索引为普通 B-tree 且数据库 locale 为 C,或建了 text_pattern_ops 索引)
SELECT sku, name FROM products WHERE name LIKE '机械%';
-- 包含匹配:两侧都有 %,B-tree 索引失效
SELECT sku, name FROM products WHERE name LIKE '%线缆%';
-- 单字符通配
SELECT sku, name FROM products WHERE sku LIKE 'SKU-100_';
-- 大小写不敏感匹配
SELECT sku, name FROM products WHERE name ILIKE '%usb%';
-- 转义通配符本身:匹配含下划线的字符串
SELECT sku, name FROM products WHERE sku LIKE 'SKU\_10%' ESCAPE '\';LIKE 的能力有限,复杂模式用正则表达式:
-- 名称以"机械"或"无线"开头
SELECT sku, name FROM products WHERE name ~ '^(机械|无线)';
-- 大小写不敏感正则
SELECT sku, name FROM products WHERE name ~* 'usb';
-- SKU 符合 SKU-四位数字
SELECT sku, name FROM products WHERE sku ~ '^SKU-[0-9]{4}$';三种匹配方式的对比:
| 方式 | 语法 | 大小写 | 索引支持 | 适用场景 |
|---|---|---|---|---|
LIKE | LIKE 'ab%' | 敏感 | 前缀可走 B-tree | 简单前缀/后缀匹配 |
ILIKE | ILIKE 'ab%' | 不敏感 | 需 pg_trgm 或表达式索引 | 用户模糊搜索 |
| 正则 | ~ ~* !~ | 可选 | 需 pg_trgm 或表达式索引 | 复杂模式校验 |
| 相似度 | %(pg_trgm) | — | gin_trgm_ops | 拼写容错搜索 |
-- 用 pg_trgm 加速模糊搜索
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_products_name_trgm ON products USING gin (name gin_trgm_ops);
-- 相似度匹配(阈值默认 0.3)
SELECT sku, name, similarity(name, '机械键盘') AS sim
FROM products
WHERE name % '机械键盘'
ORDER BY sim DESC;集合判断:IN、ANY、EXISTS
IN 用于判断值是否属于一个集合;ANY/ALL 用于与数组或子查询比较。
-- IN:枚举若干可能值
SELECT sku, name FROM products WHERE category IN ('cable', 'storage');
-- NOT IN 的陷阱:集合中含 NULL 时结果恒为空
SELECT sku, name FROM products WHERE category NOT IN ('cable', NULL); -- 返回 0 行
-- 安全写法:先排除 NULL,或用 NOT EXISTS
SELECT sku, name FROM products
WHERE category NOT IN (SELECT category FROM products WHERE category IS NOT NULL);
-- = ANY:等价于 IN
SELECT sku, name FROM products WHERE category = ANY (ARRAY['cable', 'storage']);
-- <> ALL:等价于 NOT IN(同样有 NULL 陷阱)
SELECT sku, name FROM products WHERE category <> ALL (ARRAY['cable', 'storage']);BETWEEN 是闭区间(含两端),且对日期类型容易产生"漏掉当天数据"的错误:
-- 价格在 100 到 300 之间(含 100 和 300)
SELECT sku, name, price FROM products WHERE price BETWEEN 100 AND 300;
-- 等价写法
SELECT sku, name, price FROM products WHERE price >= 100 AND price <= 300;
-- 日期范围:BETWEEN 到 DATE 会漏掉当天有时间部分的数据
SELECT sku, name FROM products
WHERE launched BETWEEN DATE '2024-01-01' AND DATE '2024-12-31'; -- 仅含 2024 年
-- 推荐写法:半开区间 [起, 止)
SELECT sku, name FROM products
WHERE launched >= DATE '2024-01-01' AND launched < DATE '2025-01-01';排序:ORDER BY
ORDER BY 支持多列、表达式、别名、序号,以及自定义排序规则。
-- 单列升序(ASC 是默认值,可省略)
SELECT sku, name, price FROM products ORDER BY price;
-- 单列降序
SELECT sku, name, price FROM products ORDER BY price DESC;
-- 多列:先按分类升序,同分类内按价格降序
SELECT sku, category, price FROM products ORDER BY category ASC, price DESC;
-- 按表达式排序
SELECT sku, name, price, stock FROM products ORDER BY price * stock DESC;
-- 按输出列别名排序
SELECT sku, price * stock AS inventory_value FROM products ORDER BY inventory_value DESC;
-- 按 SELECT 列表序号排序(不推荐,可读性差)
SELECT sku, name, price FROM products ORDER BY 3 DESC;NULL 的排序位置可以用 NULLS FIRST / NULLS LAST 显式控制:
-- 未上架的排在最后(与 ASC 默认一致,但显式声明更清晰)
SELECT sku, name, launched FROM products
ORDER BY launched ASC NULLS LAST;
-- 未上架的排在最前,优先处理待上架商品
SELECT sku, name, launched FROM products
ORDER BY launched ASC NULLS FIRST;
-- 更直观的写法:先按是否为空分组,再按日期
SELECT sku, name, launched FROM products
ORDER BY (launched IS NULL), launched;用 CASE WHEN 实现自定义优先级排序,例如让缺货商品排在最前:
SELECT sku, name, stock, price
FROM products
ORDER BY
CASE
WHEN stock = 0 THEN 1 -- 缺货最优先
WHEN stock < 100 THEN 2 -- 库存偏低
ELSE 3 -- 充足
END,
price DESC;排序的稳定性问题需要特别注意:当排序键存在重复值时,PostgreSQL 不保证相同键值行的相对顺序。分页查询时必须追加一个唯一列作为兜底排序键:
-- 不稳定:category 有重复值,分页时可能出现行重复或遗漏
SELECT sku, category FROM products ORDER BY category LIMIT 3;
-- 稳定:追加唯一的 id 作为最终排序键
SELECT id, sku, category FROM products ORDER BY category, id LIMIT 3;DISTINCT 与分页
DISTINCT 去除结果集中的重复行;DISTINCT ON 是 PostgreSQL 扩展,用于"每组只取一行"。
-- 去重:列出所有出现过的分类
SELECT DISTINCT category FROM products ORDER BY category;
-- 多列去重:组合唯一
SELECT DISTINCT category, is_active FROM products ORDER BY category, is_active;
-- DISTINCT ON:每个分类只取价格最高的一行(PostgreSQL 扩展)
SELECT DISTINCT ON (category) category, sku, name, price
FROM products
ORDER BY category, price DESC; category | sku | name | price
-------------+----------+--------------+--------
accessories | SKU-1004 | 显示器支架 | 259.00
cable | SKU-1006 | USB-C 线缆 | 49.00
peripherals | SKU-1007 | 机械键盘 TKL | 429.00
storage | SKU-1009 | 雷电 4 硬盘盒 | 899.00注意:
DISTINCT ON的ORDER BY必须以DISTINCT ON的列开头,否则报错。
分页使用 LIMIT + OFFSET:
-- 第 1 页:每页 3 条
SELECT id, sku, name FROM products ORDER BY id LIMIT 3 OFFSET 0;
-- 第 2 页
SELECT id, sku, name FROM products ORDER BY id LIMIT 3 OFFSET 3;
-- 第 3 页
SELECT id, sku, name FROM products ORDER BY id LIMIT 3 OFFSET 6;OFFSET 在大偏移量下性能很差,因为它需要先扫描并丢弃前 N 行。深分页应改用键集分页(Keyset Pagination):
-- 深分页优化:记住上一页最后一行的 (id),直接从该位置继续
SELECT id, sku, name
FROM products
WHERE id > 6 -- 上一页最后一行的 id
ORDER BY id
LIMIT 3;两种分页方式对比:
| 对比项 | LIMIT + OFFSET | 键集分页(Keyset) |
|---|---|---|
| 语法 | OFFSET n | WHERE key > 上次最后值 |
| 大偏移性能 | 差,需扫描丢弃 n 行 | 好,直接定位 |
| 能否跳页 | 可以,任意页 | 只能顺序翻页 |
| 数据变动影响 | 可能重复或遗漏 | 稳定 |
| 适用场景 | 后台管理少量页 | 信息流、无限滚动 |
实战示例
下面用一个"商品筛选列表"场景,串起条件与排序的常用组合。
-- 步骤 1:筛选"上架中、库存充足、价格在 50~500 之间"的商品
SELECT sku, name, category, price, stock
FROM products
WHERE is_active
AND stock > 0
AND price BETWEEN 50 AND 500
ORDER BY price DESC; sku | name | category | price | stock
----------+--------------+-------------+--------+-------
SKU-1001 | 机械键盘 | peripherals | 349.00 | 120
SKU-1004 | 显示器支架 | accessories | 259.00 | 45
SKU-1003 | USB-C 扩展坞 | accessories | 199.00 | 80
SKU-1008 | 笔记本支架 | accessories | 159.00 | 60
SKU-1002 | 无线鼠标 | peripherals | 129.00 | 300
(5 rows)注意 SKU-1007(429.00)虽然价格落在区间内,但它 is_active = false 且 stock = 0,因此被排除;SKU-1009(899.00)则超出价格上限。
-- 步骤 2:按关键词模糊搜索(大小写不敏感)
SELECT sku, name, price
FROM products
WHERE name ILIKE '%支架%'
ORDER BY price DESC; sku | name | price
----------+------------+--------
SKU-1004 | 显示器支架 | 259.00
SKU-1008 | 笔记本支架 | 159.00
(2 rows)-- 步骤 3:按标签数组筛选(数组包含运算符 @>)
SELECT sku, name, tags
FROM products
WHERE tags @> ARRAY['ergonomic'] -- 标签中含 ergonomic
ORDER BY price DESC; sku | name | tags
----------+------------+------------------------
SKU-1004 | 显示器支架 | {ergonomic}
SKU-1008 | 笔记本支架 | {ergonomic,laptop}
(2 rows)-- 步骤 4:找出"待上架"商品(launched 为 NULL),并排在前面
SELECT sku, name, launched
FROM products
ORDER BY (launched IS NULL) DESC, launched, sku; sku | name | launched
----------+---------------+------------
SKU-1008 | 笔记本支架 | (null)
SKU-1009 | 雷电 4 硬盘盒 | (null)
SKU-1001 | 机械键盘 | 2024-03-01
SKU-1002 | 无线鼠标 | 2024-05-12
...-- 步骤 5:分页取第 2 页(每页 4 条),并用 id 保证稳定排序
SELECT id, sku, name, price
FROM products
ORDER BY price DESC, id
LIMIT 4 OFFSET 4; id | sku | name | price
----+----------+--------------+--------
5 | SKU-1005 | HDMI 线缆 | 39.00
6 | SKU-1006 | USB-C 线缆 | 49.00
10 | SKU-1010 | 鼠标垫 | 29.00
3 | SKU-1003 | USB-C 扩展坞 | 199.00上例可见:OFFSET 分页结果是"跳过前 4 条后按同序取 4 条",因此不会出现"第二页价格一定低于第一页"的直觉结果——分页结果依赖于排序键的完整性与稳定性。
-- 步骤 6:统计各条件的命中行数,快速核对筛选逻辑
SELECT
count(*) AS total,
count(*) FILTER (WHERE is_active) AS active_cnt,
count(*) FILTER (WHERE stock > 0) AS in_stock_cnt,
count(*) FILTER (WHERE launched IS NULL) AS pending_cnt,
count(*) FILTER (WHERE category IN ('cable','storage')) AS cable_storage_cnt
FROM products; total | active_cnt | in_stock_cnt | pending_cnt | cable_storage_cnt
-------+------------+--------------+-------------+-------------------
10 | 9 | 9 | 2 | 3-- 步骤 7:组合排序——缺货优先、其次按库存升序、最后按价格降序
SELECT sku, name, stock, price
FROM products
WHERE is_active
ORDER BY
(stock = 0) DESC, -- 缺货排最前
stock ASC,
price DESC
LIMIT 5; sku | name | stock | price
----------+------------+-------+--------
SKU-1009 | 雷电 4 硬盘盒 | 12 | 899.00
SKU-1004 | 显示器支架 | 45 | 259.00
SKU-1008 | 笔记本支架 | 60 | 159.00
SKU-1003 | USB-C 扩展坞 | 80 | 199.00
SKU-1002 | 无线鼠标 | 300 | 129.00
(5 rows)注意事项
WHERE中不能使用SELECT定义的列别名。因为WHERE在SELECT之前逻辑求值。ORDER BY与GROUP BY可以使用别名(但有例外,如GROUP BY中别名与列名冲突时优先按列名解析)。
-- 错误!WHERE 中别名不存在
SELECT price * 1.13 AS price_tax FROM products WHERE price_tax > 200;
-- 正确:重复表达式,或用子查询
SELECT price * 1.13 AS price_tax FROM products WHERE price * 1.13 > 200;
-- 也可以:子查询
SELECT * FROM (SELECT price * 1.13 AS price_tax FROM products) t WHERE price_tax > 200;NOT IN遇到NULL会静默返回空结果。这是最隐蔽的一类查询错误。
-- 错误!子查询结果若含 NULL,整条语句返回 0 行
SELECT sku FROM products
WHERE category NOT IN (SELECT category FROM products_archive);
-- 正确:排除 NULL
SELECT sku FROM products
WHERE category NOT IN (SELECT category FROM products_archive WHERE category IS NOT NULL);
-- 更推荐:用 NOT EXISTS,天然不受 NULL 影响
SELECT p.sku FROM products p
WHERE NOT EXISTS (
SELECT 1 FROM products_archive a WHERE a.category = p.category
);LIKE '%...%'无法使用 B-tree 索引。前导通配符会使索引失去作用,大表上会导致全表扫描。需要模糊搜索时使用pg_trgm的 GIN 索引,或改用全文检索。- 在
WHERE中对列做运算或函数调用会导致索引失效。这称为"条件不可索引化(Non-Sargable)"。
-- 错误!对列使用函数,索引失效
SELECT sku, name FROM products WHERE upper(category) = 'CABLE';
-- 正确:把函数作用在常量侧;若确需函数索引,则创建表达式索引
CREATE INDEX idx_products_category_upper ON products (upper(category));
SELECT sku, name FROM products WHERE upper(category) = 'CABLE';BETWEEN是闭区间,用日期时注意边界语义。BETWEEN '2024-01-01' AND '2024-12-31'对timestamptz列会漏掉 12 月 31 日当天 00:00 之后的数据,推荐用半开区间>= AND <。ORDER BY不带唯一列时排序不稳定。分页、LIMIT、数据导出等依赖顺序的场景,务必追加唯一键。OFFSET大偏移量性能差。OFFSET 100000需要扫描并丢弃 10 万行。深分页改用键集分页。DISTINCT有排序开销。它需要对结果集排序或哈希去重,在对大表使用时注意代价;如果只是想判断是否存在,用EXISTS更高效。IN列表过长会拖慢解析与规划。几千个值的IN列表应改为VALUES+JOIN,或使用临时表、= ANY(数组)。
-- 不推荐:超长 IN 列表
SELECT sku FROM products WHERE sku IN ('SKU-1001', 'SKU-1002', /* ...数千个... */ 'SKU-9999');
-- 推荐:改为数组或 JOIN 临时表
SELECT p.sku FROM products p
JOIN unnest(ARRAY['SKU-1001','SKU-1002','SKU-9999']::text[]) AS t(sku)
ON t.sku = p.sku;最佳实践
- 条件从左到右按"选择性强 → 弱"排列仅影响可读性,不影响性能。 优化器会自行重排条件,不必为"性能"而刻意调整
WHERE中条件的顺序;应按业务逻辑分组、加括号提升可读性。 - 判空统一用
IS NULL/IS NOT NULL,避免= NULL与NOT IN (含 NULL 的子查询)两个经典陷阱。子查询排除用NOT EXISTS。 - 时间范围统一用半开区间
>= 起 AND < 止。 这样无论列类型是date还是timestamptz,语义都清晰且不漏数据。 - 为高频过滤列建索引,并保持条件"可索引化"。 不要在
WHERE中对列做函数运算;必要时创建表达式索引或函数索引。
-- 为常用过滤组合建复合索引(等值列在前,范围列在后)
CREATE INDEX idx_products_active_category_price
ON products (is_active, category, price);
-- 部分索引:只索引活跃商品,体积更小、更高效
CREATE INDEX idx_products_active_name ON products (name) WHERE is_active;
-- 表达式索引:支持大小写不敏感等值查询
CREATE INDEX idx_products_sku_lower ON products (lower(sku));- 需要"每组取一行"时用
DISTINCT ON(PostgreSQL 扩展),比ROW_NUMBER()窗口函数写法简洁;但要注意ORDER BY必须以DISTINCT ON列开头。
-- 每个分类最贵的一件商品
SELECT DISTINCT ON (category) category, sku, name, price
FROM products
WHERE is_active
ORDER BY category, price DESC;- 分页排序键必须是"唯一且稳定"的组合。 常见做法是
ORDER BY 创建时间 DESC, id DESC,这样即使创建时间重复也不会错乱。 - 深分页用键集分页。 用
WHERE (sort_col, id) < (上一页末值, 末 id)的行值比较语法,可一次定位,无需扫描丢弃。
-- 键集分页:行值比较写法(PostgreSQL 支持元组比较)
SELECT id, sku, name, price
FROM products
WHERE (price, id) < (199.00, 3) -- 上一页最后一行的 (price, id)
ORDER BY price DESC, id DESC
LIMIT 4;- 用
EXPLAIN (ANALYZE, BUFFERS)验证条件是否走索引。 看到Seq Scan且过滤后行数远小于表行数,说明索引缺失或条件不可索引化。
EXPLAIN (ANALYZE, BUFFERS)
SELECT sku, name FROM products WHERE category = 'cable' AND price < 100;下一节
继续学习:聚合查询与分组