首页 / PostgreSQL 入门教程 / DISTINCT 去重与 LIMIT/OFFSET 分页

PostgreSQL 入门教程

DISTINCT 去重与 LIMIT/OFFSET 分页

本教程共 50 篇 · 第 25 篇 · 更新于 2026-07-31 · 约 8 分钟阅读

PostgreSQLPostgreSQL 入门教程DISTINCT 去重LIMITOFFSET 分页FETCH FIRST

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;

查出来会是 cancelledpaidpendingshipped 各一条,而不是原来的多行。

Note

PostgreSQL 把 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

Note

PostgreSQL 先执行 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 TIESLIMIT 更合适。注意它必须配合 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 可保留并列行。