DISTINCT 去重与 LIMIT/OFFSET 分页
本教程共 50 篇 · 第 25 篇 · 更新于 2026-07-31 · 约 8 分钟阅读
25. DISTINCT 去重与 LIMIT/OFFSET 分页
本节目标:掌握用 DISTINCT 去掉重复结果,用 LIMIT 和 OFFSET 做分页,了解标准的 FETCH FIRST 写法,以及 PostgreSQL 特色的 DISTINCT ON 与 WITH TIES。
查询返回的结果里常常有重复值,或者你只想看”前几条”。这一章就讲怎么去重和怎么翻页。
先把示例数据准备好。注意自增列用的是 v18 推荐的写法:
CREATE TABLE orders (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id INT,
amount NUMERIC(10,2),
status VARCHAR(20),
created_at DATE
);
INSERT INTO orders (user_id, amount, status, created_at) VALUES
(1, 99.00, 'paid', '2025-02-01'),
(2, 150.50, 'shipped', '2025-04-10'),
(3, 20.00, 'pending', '2025-06-11'),
(1, 300.00, 'paid', '2025-08-22'),
(4, 75.25, 'cancelled','2025-10-05'),
(2, 60.00, 'shipped', '2025-11-02');
DISTINCT 去掉重复行
DISTINCT 放在 SELECT 后面,作用是:对结果里完全相同的行,只保留一行。最常见用途是”看某一列都有哪些不同取值”。
-- 看看订单都有哪些状态(重复的只留一条)
SELECT DISTINCT status
FROM orders
ORDER BY status;
查出来会是 cancelled、paid、pending、shipped 各一条,而不是原来的多行。
NotePostgreSQL 把
NULL也当成”相同的”,所以多行NULL去重后只会留一个NULL。
多列去重
DISTINCT 也可以跟多列。这时它看的是”这几列组合起来”是否重复,只有组合完全一样才合并。
-- 看有哪些 (user_id, status) 的组合
SELECT DISTINCT user_id, status
FROM orders
ORDER BY user_id, status;
比如用户 1 有两笔 paid 订单,组合 (1, paid) 只会出现一次。
Tip想对”所有列”去重,可以写
SELECT DISTINCT *。但生产中很少这样用,通常是针对具体列去重。
DISTINCT ON:取每组的第一行(PostgreSQL 特色)
这是 PostgreSQL 才有的扩展,标准 SQL 没有。DISTINCT ON (列) 会按指定列分组,每组只返回”排在最前面”的那一行。它几乎总是要配 ORDER BY 才有意义。
-- 每个用户只保留金额最高的一笔订单
SELECT DISTINCT ON (user_id) user_id, id, amount
FROM orders
ORDER BY user_id, amount DESC;
执行逻辑:先按 user_id 分组,组内按 amount DESC 排,每组取第一行。所以每个用户拿到的就是他最大那笔订单。
Warning
DISTINCT ON后面的列,必须和ORDER BY最前面的列一致(上面都是user_id)。顺序写错会直接报错。它和标准DISTINCT是两回事,别混用。
LIMIT 限制返回条数
LIMIT 放在查询末尾,告诉数据库”最多给我 N 行”。它常和 ORDER BY 搭配,先排好序再取前几名。
-- 金额最高的前 3 笔订单
SELECT id, user_id, amount
FROM orders
ORDER BY amount DESC
LIMIT 3;
Warning不写
ORDER BY时,行的顺序是”未指定”的,每次可能不一样。要取稳定的”前几条”,一定要先ORDER BY。
OFFSET 做分页
OFFSET n 表示”先跳过 n 行,再开始返回”。配合 LIMIT,就是经典的分页。
-- 第 1 页:每页 2 条,跳过 0 行
SELECT id, user_id, amount
FROM orders
ORDER BY id
LIMIT 2 OFFSET 0;
-- 第 2 页:跳过前 2 行
SELECT id, user_id, amount
FROM orders
ORDER BY id
LIMIT 2 OFFSET 2;
翻页公式很简单:第 p 页(p 从 1 开始)、每页 size 条,就写 LIMIT size OFFSET (p-1)*size。
NotePostgreSQL 先执行
OFFSET再执行LIMIT。所以偏移量很大时,数据库其实要把前面那些行也扫一遍,深翻页会偏慢。数据量大又常翻深页时,建议改用”游标翻页”(按上一页最后一条的 id 继续往后取)。
FETCH FIRST:更标准的写法
LIMIT 好用,但它不是标准 SQL。FETCH FIRST 是 SQL 标准(SQL:2008)里的分页写法,跨数据库更通用。
-- 取前 3 行(ROW 和 ROWS 同义,FIRST 和 NEXT 同义)
SELECT id, user_id, amount
FROM orders
ORDER BY amount DESC
FETCH FIRST 3 ROWS ONLY;
-- 跳过前 2 行再取 2 行,相当于 LIMIT 2 OFFSET 2
SELECT id, user_id, amount
FROM orders
ORDER BY id
OFFSET 2 ROWS
FETCH FIRST 2 ROWS ONLY;
Tip如果你希望程序以后可能换数据库,用
FETCH FIRST更稳妥;只在 PostgreSQL 里跑,LIMIT写起来更短。
FETCH FIRST … WITH TIES:并列也一起取
PostgreSQL 13 起,FETCH FIRST 还支持 WITH TIES:当最后一名有并列时,把并列的行也一起带上,不会漏掉。
-- 取金额最高的 3 行;若有并列第 3,也一并返回
SELECT id, user_id, amount
FROM orders
ORDER BY amount DESC
FETCH FIRST 3 ROWS WITH TIES;
Tip想要”榜单不漏并列”的效果,
WITH TIES比LIMIT更合适。注意它必须配合ORDER BY使用。
常见错误
- 用
LIMIT却不写ORDER BY,结果顺序不定,每次可能不同。 - 以为查询慢是
LIMIT的锅,其实OFFSET越大越慢(前面行白扫一遍)。 - 分页越翻越深越慢,大数据量场景建议改用基于 id 的游标翻页。
DISTINCT 与聚合搭配
DISTINCT 还能放在聚合函数里面,对要参与聚合的值先去重。比如”每个用户涉及过几种不同状态”:
-- 每个用户涉及的不同状态种类数
SELECT user_id, COUNT(DISTINCT status) AS status_kinds
FROM orders
GROUP BY user_id
ORDER BY user_id;
Note
COUNT(DISTINCT 列)和整行DISTINCT是两回事:前者只对那一列去重后计数,后者对整行去重。别混淆。
深翻页更好用的做法:游标翻页
前面说了 OFFSET 越大越慢。更稳的做法是”记住上一页最后一条的排序值,从它之后取”:
-- 假设上一页最后一条 id 是 4,取它之后的 2 条
SELECT id, user_id, amount
FROM orders
WHERE id > 4
ORDER BY id
LIMIT 2;
这种方式不管翻多深都很快,因为它靠索引定位起点,不用扫前面的行。缺点是只能”往后翻”,不能任意跳到第 N 页。
一点执行顺序备忘
完整查询里各子句大致按这个顺序跑:FROM → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT/OFFSET。注意 DISTINCT 在 SELECT 之后、ORDER BY 之前执行,所以它能和 ORDER BY 一起用,顺序不会打架。
Note小提醒:LIMIT / OFFSET 后面除了写数字,在程序里也常写成参数(如
LIMIT $1 OFFSET $2)。但别写成会随行变化的子查询,那会报语法错。分页的”每页大小”和”页码”本质就是这两个值的组合。
小结
DISTINCT去重,可单列也可多列组合。DISTINCT ON (列)是 PostgreSQL 扩展,取每组排在最前的第一行,须与ORDER BY首列一致。LIMIT n取前 n 行,配合ORDER BY才有意义。OFFSET n跳过 n 行,和 LIMIT 一起做分页;偏移越大越慢。FETCH FIRST n ROWS ONLY是标准分页写法;加WITH TIES可保留并列行。