排序(ORDER BY)
ORDER BY 决定结果集的输出顺序。本节讲解单列与多列排序、ASC / DESC 与每列独立方向、NULLS FIRST / NULLS LAST 与默认行为、按表达式与别名排序、COLLATE 排序规则、中文与多语言排序、排序稳定性与分页的关系,以及排序的性能问题。学完本节,你将能写出结果确定、性能可控的排序查询。
前置知识
阅读本节前,建议先了解:WHERE 条件筛选
基础概念
ORDER BY 在逻辑执行顺序中位于 SELECT 与 DISTINCT 之后、LIMIT 之前,因此它能引用输出列别名,但不能影响 WHERE 的过滤结果。
| 术语 | 英文 | 说明 |
|---|---|---|
| 排序键 | Sort Key | ORDER BY 中用于比较的列或表达式 |
| 排序规则 | Collation | 字符串比较依据的语言文化规则 |
| 稳定性 | Determinism | 排序键取值相同时结果顺序是否确定 |
| 空值排序 | Null Ordering | NULL 排在前面还是后面 |
| 排序节点 | Sort Node | 执行计划中真正做排序的算子 |
| 增量排序 | Incremental Sort | PostgreSQL 13+ 在部分有序输入上只排序剩余键 |
NULL 的默认排序位置是理解 ORDER BY 的关键:PostgreSQL 把 NULL 视为最大值。
| 排序方向 | 默认 NULL 位置 | 等价显式写法 |
|---|---|---|
ASC(默认) | 最后 | ASC NULLS LAST |
DESC | 最前 | DESC NULLS FIRST |
注意这与部分数据库(如 MySQL 把 NULL 视为最小值)行为相反,迁移时需特别留意。
ORDER BY 的稳定性(Determinism) 决定了分页是否可靠:当排序键存在重复值时,PostgreSQL 不保证这些行的相对顺序,因此分页可能重复或遗漏。
语法与用法
SELECT 列列表
FROM 表名
[WHERE 条件]
ORDER BY 排序表达式 [ASC | DESC] [NULLS FIRST | NULLS LAST] [, ...];准备本节贯穿使用的示例表:
-- 商品表:含可空字段与重复分类,便于演示排序细节
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,
rating NUMERIC(3, 1), -- 可为 NULL(暂无评分)
launched DATE, -- 可为 NULL(未上架)
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO products (sku, name, category, price, stock, rating, launched) VALUES
('SKU-1001', '机械键盘', 'peripherals', 349.00, 120, 4.7, DATE '2024-03-01'),
('SKU-1002', '无线鼠标', 'peripherals', 129.00, 300, 4.5, DATE '2024-05-12'),
('SKU-1003', 'USB-C 扩展坞', 'accessories', 199.00, 80, 4.2, DATE '2024-07-20'),
('SKU-1004', '显示器支架', 'accessories', 259.00, 45, 4.8, DATE '2024-09-01'),
('SKU-1005', 'HDMI 线缆', 'cable', 39.00, 500, 3.9, DATE '2025-01-15'),
('SKU-1006', 'USB-C 线缆', 'cable', 49.00, 420, 4.1, DATE '2025-02-01'),
('SKU-1007', '机械键盘 TKL', 'peripherals', 429.00, 0, NULL, DATE '2025-06-10'),
('SKU-1008', '笔记本支架', 'accessories', 159.00, 60, NULL, NULL),
('SKU-1009', '雷电 4 硬盘盒', 'storage', 899.00, 12, 4.9, NULL),
('SKU-1010', '鼠标垫', 'accessories', 29.00, 800, 4.0, DATE '2025-08-01');常见排序写法:
-- 单列升序(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, price, stock, price * stock AS inventory_value
FROM products ORDER BY price * stock DESC;详细说明
单列与多列排序
多列排序时每一列可以独立指定方向,比较方式是从左到右依次比较,前一个键相等时才看后一个键。
-- 分类升序,同分类按库存降序,库存相同再按 SKU 升序
SELECT sku, category, stock
FROM products
ORDER BY category ASC, stock DESC, sku ASC; sku | category | stock
----------+-------------+-------
SKU-1010 | accessories | 800
SKU-1003 | accessories | 80
SKU-1008 | accessories | 60
SKU-1004 | accessories | 45
SKU-1005 | cable | 500
SKU-1006 | cable | 420
SKU-1002 | peripherals | 300
SKU-1001 | peripherals | 120
SKU-1007 | peripherals | 0
SKU-1009 | storage | 12
(10 rows)ASC / DESC 与每列独立方向
-- 全列同向
SELECT sku, price FROM products ORDER BY price ASC;
-- 混合方向:分类升序、价格降序
SELECT sku, category, price FROM products ORDER BY category ASC, price DESC;
-- 用表达式取反实现反向排序(不推荐,可读性差)
SELECT sku, price FROM products ORDER BY -price ASC;
-- 推荐:直接用 DESC
SELECT sku, price FROM products ORDER BY price DESC;NULLS FIRST / NULLS LAST
NULL 默认被视为最大值,因此 ASC 排最后、DESC 排最前。用 NULLS FIRST / NULLS LAST 显式覆盖:
-- 默认行为:rating 为 NULL 的排最后
SELECT sku, name, rating FROM products ORDER BY rating;
-- 显式声明(与 ASC 默认一致)
SELECT sku, name, rating FROM products ORDER BY rating ASC NULLS LAST;
-- 让未评分的商品排最前,优先处理待评价
SELECT sku, name, rating FROM products ORDER BY rating ASC NULLS FIRST;
-- DESC 默认 NULL 在前;显式让其排最后
SELECT sku, name, rating FROM products ORDER BY rating DESC NULLS LAST; sku | name | rating
----------+--------------+--------
SKU-1007 | 机械键盘 TKL |
SKU-1008 | 笔记本支架 |
SKU-1005 | HDMI 线缆 | 3.9
SKU-1010 | 鼠标垫 | 4.0
SKU-1006 | USB-C 线缆 | 4.1
...另一种控制 NULL 位置的技巧:先按"是否为空"布尔值排序,false 在前:
-- 先排非空(false < true),再按 rating 降序
SELECT sku, name, rating
FROM products
ORDER BY (rating IS NULL) ASC, rating DESC NULLS LAST;按表达式与别名排序
ORDER BY 可以使用任意表达式,也可以引用 SELECT 中定义的别名(因为它在 SELECT 之后执行)。
-- 按表达式排序
SELECT sku, price, stock FROM products ORDER BY price * stock DESC;
-- 按别名排序(推荐,语义清晰)
SELECT sku, price * stock AS inventory_value
FROM products
ORDER BY inventory_value DESC;
-- 按函数结果排序
SELECT sku, name FROM products ORDER BY length(name) DESC, name;
-- 按 CASE 表达式排序
SELECT sku, name, stock FROM products
ORDER BY CASE WHEN stock = 0 THEN 0 ELSE 1 END, stock;按列序号排序(不推荐)
ORDER BY 2 表示按 SELECT 列表的第 2 项排序。它写起来短,但可读性差且容易在修改 SELECT 列表后出错:
-- 不推荐:序号含义不直观,调整 SELECT 列表后极易出错
SELECT sku, name, price FROM products ORDER BY 3 DESC;
-- 推荐:显式写列名或别名
SELECT sku, name, price FROM products ORDER BY price DESC;按序号排序唯一有价值的场景是快速调试(例如配合 SELECT * 探查列位置),生产 SQL 中应避免。
COLLATE 指定排序规则
字符串比较受排序规则(Collation) 影响,它定义了字符的排序顺序、大小写敏感性等。PostgreSQL 支持在列定义、数据库、表达式(COLLATE 子句)三个层级指定。
-- 查看数据库默认排序规则
SELECT datcollate, datctype FROM pg_database WHERE datname = current_database(); datcollate | datctype
---------------+---------------
en_US.UTF-8 | en_US.UTF-8
(1 row)-- 用 COLLATE 在排序时临时切换规则
SELECT name FROM products ORDER BY name COLLATE "C";
-- 大小写不敏感排序:先转小写再排(通用做法)
SELECT username FROM users ORDER BY lower(username);
-- 或用 ICU 排序规则(PostgreSQL 10+,需数据库启用 ICU)
SELECT name FROM products ORDER BY name COLLATE "zh-x-icu";常用排序规则对比:
| 排序规则 | 大小写 | 中文顺序 | 适用场景 |
|---|---|---|---|
C | 大写在前(按字节) | 按 UTF-8 码点 | 追求确定性、可移植 |
en_US.UTF-8 | 大小写混排 | 按本地化规则 | 英文为主 |
zh-x-icu | 按本地化 | 拼音顺序 | 中文排序 |
zh_CN.UTF-8 | 按本地化 | 按本地化规则 | 中文(glibc) |
und-x-icu | 通用 | ICU 默认 | 多语言混合 |
-- 用 C 规则排序:数字与字母按字节序,中文按码点
SELECT name FROM products ORDER BY name COLLATE "C" LIMIT 5;
-- 用 ICU 中文规则排序:中文按拼音
SELECT name FROM products ORDER BY name COLLATE "zh-x-icu" LIMIT 5;排序规则会直接影响索引能否被用于排序:索引必须与
ORDER BY使用相同排序规则,否则仍需额外Sort节点。
排序稳定性与分页的关系
当排序键存在重复值时,相同键值行的相对顺序不确定。分页查询必须追加唯一列作为兜底排序键,否则可能出现行重复或遗漏。
-- 不稳定:accessories 分类有 4 行,顺序不确定
SELECT sku, category FROM products ORDER BY category LIMIT 3 OFFSET 0;
-- 稳定:追加唯一的 id 作为最终排序键
SELECT id, sku, category FROM products ORDER BY category, id LIMIT 3 OFFSET 0; id | sku | category
----+----------+-------------
3 | SKU-1003 | accessories
4 | SKU-1004 | accessories
8 | SKU-1008 | accessories
(3 rows)-- 深分页:不稳定排序会导致第 2 页出现重复行
-- 错误!只有 category 一个排序键
SELECT id, sku, category FROM products ORDER BY category LIMIT 3 OFFSET 3;
-- 正确:追加唯一键
SELECT id, sku, category FROM products ORDER BY category, id LIMIT 3 OFFSET 3; id | sku | category
----+----------+-------------
10 | SKU-1010 | accessories
5 | SKU-1005 | cable
6 | SKU-1006 | cable
(3 rows)排序的性能
排序主要有两种实现路径:
| 路径 | 触发条件 | 代价特征 |
|---|---|---|
| 索引有序读取 | ORDER BY 与索引列顺序、方向、排序规则一致 | 无需排序,直接按索引顺序读 |
Sort 节点 | 无匹配索引 | 需在内存或磁盘排序 |
Incremental Sort | 输入已按排序键前缀有序 | PostgreSQL 13+,只排剩余键 |
Top-N Sort | 配合 LIMIT | 只维护前 N 个元素,内存占用小 |
-- 建索引
CREATE INDEX idx_products_category_price ON products (category, price DESC);
-- 排序键与索引完全匹配:可走索引有序读取,无 Sort 节点
EXPLAIN (ANALYZE, BUFFERS)
SELECT sku, category, price FROM products
ORDER BY category, price DESC
LIMIT 5; QUERY PLAN
------------------------------------------------------------------------------------------
Limit (cost=0.14..1.63 rows=5 width=23) (actual time=0.015..0.019 rows=5 loops=1)
Buffers: shared hit=2
-> Index Scan using idx_products_category_price on products
(cost=0.14..2.99 rows=10 width=23) (actual time=0.013..0.016 rows=5 loops=1)
Buffers: shared hit=2
Planning Time: 0.112 ms
Execution Time: 0.035 ms
(6 rows)-- 排序键与索引顺序不符:需要 Sort 节点
EXPLAIN (ANALYZE, BUFFERS)
SELECT sku, category, price FROM products
ORDER BY category, price ASC; -- 索引是 price DESC QUERY PLAN
-------------------------------------------------------------------------------------
Sort (cost=1.26..1.28 rows=10 width=23) (actual time=0.035..0.036 rows=10 loops=1)
Sort Key: category, price
Sort Method: quicksort Memory: 25kB
Buffers: shared hit=1
-> Seq Scan on products (cost=0.00..1.10 rows=10 width=23)
(actual time=0.008..0.011 rows=10 loops=1)
Planning Time: 0.098 ms
Execution Time: 0.055 ms
(7 rows)小表上 Sort 代价可忽略,但大表排序可能超出 work_mem 而落盘(执行计划中会显示 Sort Method: external merge Disk: xxxx)。
-- Top-N 排序:配合 LIMIT,只在内存中维护前 N 个候选
EXPLAIN (ANALYZE, BUFFERS)
SELECT sku, price FROM products ORDER BY price DESC LIMIT 3; QUERY PLAN
---------------------------------------------------------------------
Limit (cost=1.28..1.28 rows=3 width=12) (actual time=0.026..0.027 rows=3 loops=1)
-> Sort (cost=1.26..1.28 rows=10 width=12) (actual time=0.024..0.025 rows=3 loops=1)
Sort Key: price DESC
Sort Method: top-N heapsort Memory: 25kB
-> Seq Scan on products (cost=0.00..1.10 rows=10 width=12)
(5 rows)ORDER BY 与 LIMIT 配合
LIMIT 不带 ORDER BY 时,返回的行是任意的(取决于扫描顺序),因此分页必须配 ORDER BY:
-- 错误认知:以为 LIMIT 10 返回"前 10 条"
SELECT sku, name FROM products LIMIT 10; -- 顺序不确定
-- 正确:分页必须指定确定的排序
SELECT sku, name FROM products ORDER BY id LIMIT 10;-- 分页三要素:稳定的排序键 + LIMIT + OFFSET
SELECT id, sku, name, price
FROM products
ORDER BY price DESC, id DESC
LIMIT 5 OFFSET 0;
-- 第 2 页
SELECT id, sku, name, price
FROM products
ORDER BY price DESC, id DESC
LIMIT 5 OFFSET 5;实战示例
下面用"商品列表的多维排序需求"串联本节要点。
-- 步骤 1:默认排序——价格升序,未上架商品排最后
SELECT sku, name, price, launched
FROM products
ORDER BY launched ASC NULLS LAST, price ASC; sku | name | price | launched
----------+--------------+--------+------------
SKU-1001 | 机械键盘 | 349.00 | 2024-03-01
SKU-1002 | 无线鼠标 | 129.00 | 2024-05-12
SKU-1003 | USB-C 扩展坞 | 199.00 | 2024-07-20
SKU-1004 | 显示器支架 | 259.00 | 2024-09-01
SKU-1005 | HDMI 线缆 | 39.00 | 2025-01-15
SKU-1006 | USB-C 线缆 | 49.00 | 2025-02-01
SKU-1007 | 机械键盘 TKL | 429.00 | 2025-06-10
SKU-1010 | 鼠标垫 | 29.00 | 2025-08-01
SKU-1008 | 笔记本支架 | 159.00 |
SKU-1009 | 雷电 4 硬盘盒 | 899.00 |
(10 rows)-- 步骤 2:业务优先级排序——缺货最优先,其次库存偏低,最后按评分降序
SELECT sku, name, stock, rating
FROM products
ORDER BY
CASE
WHEN stock = 0 THEN 1 -- 缺货最紧急
WHEN stock < 50 THEN 2
WHEN stock < 200 THEN 3
ELSE 4
END,
rating DESC NULLS LAST,
sku; sku | name | stock | rating
----------+--------------+-------+--------
SKU-1007 | 机械键盘 TKL | 0 |
SKU-1009 | 雷电 4 硬盘盒 | 12 | 4.9
SKU-1004 | 显示器支架 | 45 | 4.8
SKU-1003 | USB-C 扩展坞 | 80 | 4.2
SKU-1008 | 笔记本支架 | 60 |
SKU-1001 | 机械键盘 | 120 | 4.7
SKU-1002 | 无线鼠标 | 300 | 4.5
SKU-1006 | USB-C 线缆 | 420 | 4.1
SKU-1010 | 鼠标垫 | 800 | 4.0
SKU-1005 | HDMI 线缆 | 500 | 3.9
(10 rows)注意 SKU-1009 与 SKU-1004 同属"库存偏低"档,因此按评分降序时 4.9 排在 4.8 之前;SKU-1003 与 SKU-1008 同属"库存中等"档,SKU-1008 因 rating 为 NULL 排在档内最后。
-- 步骤 3:中文排序规则对比
SELECT name FROM products ORDER BY name COLLATE "C" LIMIT 4; name
---------------
HDMI 线缆
USB-C 扩展坞
USB-C 线缆
显示器支架
(4 rows)-- 步骤 4:稳定分页——按价格降序,id 作为兜底键
SELECT id, sku, price
FROM products
ORDER BY price DESC, id DESC
LIMIT 4 OFFSET 4; id | sku | price
----+----------+--------
4 | SKU-1004 | 259.00
3 | SKU-1003 | 199.00
8 | SKU-1008 | 159.00
2 | SKU-1002 | 129.00
(4 rows)-- 步骤 5:验证排序是否走索引
CREATE INDEX idx_products_category_price ON products (category, price DESC);
EXPLAIN (ANALYZE, BUFFERS)
SELECT sku, category, price
FROM products
ORDER BY category, price DESC
LIMIT 5; QUERY PLAN
--------------------------------------------------------------------------------------------
Limit (cost=0.14..1.63 rows=5 width=23) (actual time=0.018..0.022 rows=5 loops=1)
Buffers: shared hit=2
-> Index Scan using idx_products_category_price on products
(cost=0.14..2.99 rows=10 width=23) (actual time=0.016..0.019 rows=5 loops=1)
Buffers: shared hit=2
Planning Time: 0.121 ms
Execution Time: 0.039 ms
(6 rows)计划中没有 Sort 节点,说明排序由索引有序读取完成,这正是我们想要的结果。
注意事项
ORDER BY不带唯一列时排序不稳定。分页、LIMIT、数据导出等依赖顺序的场景,务必追加唯一键。
-- 不稳定
SELECT id, sku, category FROM products ORDER BY category LIMIT 3 OFFSET 3;
-- 稳定
SELECT id, sku, category FROM products ORDER BY category, id LIMIT 3 OFFSET 3;NULL默认被视为最大值。这与 MySQL 相反,迁移 SQL 时务必检查ORDER BY的NULL位置是否符合业务预期。
-- 未评分的排最后(ASC 默认)
SELECT sku, rating FROM products ORDER BY rating;
-- 未评分的排最前
SELECT sku, rating FROM products ORDER BY rating NULLS FIRST;LIMIT不带ORDER BY时结果顺序任意。分页查询必须同时写ORDER BY与LIMIT。- 按列序号排序(
ORDER BY 2)不推荐。修改SELECT列表后极易产生错误,且阅读时无法直接看出排序键。 ORDER BY中的别名可能被同名列覆盖。当别名与输入列同名时,PostgreSQL 优先按输入列解析。
-- 危险:别名与输入列同名
SELECT sku, price / 100 AS price FROM products ORDER BY price DESC;
-- 实际按 products.price 排序,而不是 price / 100
-- 安全:别名不要与列名重名
SELECT sku, price / 100 AS price_in_hundreds FROM products ORDER BY price_in_hundreds DESC;- 排序规则(Collation)不一致会导致索引无法用于排序。索引建立时的排序规则必须与
ORDER BY的规则匹配。
-- 索引默认使用列排序规则
CREATE INDEX idx_products_name ON products (name);
-- 以下查询无法用该索引排序(排序规则不同)
SELECT name FROM products ORDER BY name COLLATE "C";
-- 需要单独建对应排序规则的索引
CREATE INDEX idx_products_name_c ON products (name COLLATE "C");DISTINCT与ORDER BY组合有限制。ORDER BY的表达式必须出现在SELECT列表中。
-- 错误!
SELECT DISTINCT category FROM products ORDER BY price;
-- 正确
SELECT DISTINCT category FROM products ORDER BY category;- 大表排序可能落盘。执行计划中出现
Sort Method: external merge Disk: NNNkB表示超出work_mem,应调大work_mem或改用索引有序读取。
-- 会话级临时调大,仅对当前连接生效
SET work_mem = '64MB';
-- 查看当前设置
SHOW work_mem;ORDER BY与LIMIT组合时优化器会用 Top-N 排序,内存占用与 N 成正比,而非与表行数成正比。
最佳实践
- 分页排序键必须是"唯一且稳定"的组合。推荐
ORDER BY created_at DESC, id DESC,即使时间重复也不会错乱。
-- 稳定的分页排序
SELECT id, sku, name
FROM products
ORDER BY created_at DESC, id DESC
LIMIT 20 OFFSET 0;- 为高频排序建匹配的索引,索引列顺序与方向都要与
ORDER BY一致。
-- 支撑 ORDER BY category, price DESC
CREATE INDEX idx_products_category_price ON products (category, price DESC);- 多列排序时方向要与索引保持一致。混合方向(
a ASC, b DESC)在 PostgreSQL 中可以用(a ASC, b DESC)索引支持,但(a ASC, b ASC)索引则无法支持b DESC。
-- 匹配混合方向排序的索引
CREATE INDEX idx_products_cat_asc_price_desc ON products (category ASC, price DESC);- 用
NULLS FIRST/NULLS LAST显式声明NULL位置。即使与默认行为一致,显式写出也能让意图清晰、跨数据库迁移更安全。
SELECT sku, name, rating FROM products ORDER BY rating DESC NULLS LAST;- 优先让排序走索引有序读取,避免
Sort节点。用EXPLAIN确认是否存在Sort;若存在且数据量大,考虑调整索引或work_mem。
EXPLAIN (ANALYZE, BUFFERS)
SELECT sku, category, price FROM products ORDER BY category, price DESC LIMIT 10;- 深分页改用键集分页,避免
OFFSET扫描丢弃大量行,同时天然稳定。
-- 键集分页:从上一次的最后一行继续
SELECT id, sku, price
FROM products
WHERE (price, id) < (259.00, 4)
ORDER BY price DESC, id DESC
LIMIT 4;- 中文排序需要明确的排序规则。若数据库使用
C规则,中文按码点排序(近似按 UTF-8 字节序);需要拼音顺序时使用 ICU 规则或COLLATE "zh-x-icu"。
-- 拼音顺序(需 ICU 支持)
SELECT name FROM products ORDER BY name COLLATE "zh-x-icu";- 导出数据时始终指定
ORDER BY。否则导出的行顺序不确定,跨环境对比会出现"数据不一致"的假象。
下一节
继续学习:分页(LIMIT / OFFSET)