这是一个被很多从MySQL、SQL Server转过来的开发者忽略,但实际用起来极其顺手的一个PostgreSQL独有语法:DISTINCT ON。我第一次在PostgreSQL里看到这个语法时,第一反应是“怎么还有这种写法”,第二反应是“这不就是我一直想要的行级去重吗”。在MySQL里想“取每个用户的最新一条订单”,你可能要写一堆子查询、关联、变量,甚至在SQL Server里直接祭出ROW_NUMBER(),但在PostgreSQL里,DISTINCT ON几乎是一行语法搞定的事。这篇文章我就把这个语法的使用场景、执行逻辑、最容易踩的坑,以及和替代方案怎么选型,一次讲清楚。
DISTINCT ON最典型的使用场景可以概括成一句话:按某个字段分组后,每组只取排序后的第一条记录。它和普通的DISTINCT完全不同——普通DISTINCT是对整行去重,DISTINCT ON则是指定一个或多个字段作为分组维度,然后在每个分组内部按ORDER BY的规则拍好序,只留下第一行。这个特性从PostgreSQL很早的版本就有了,却依然有大量人不了解它,或者说了解但不敢用。
这篇文章适合谁?刚接触PostgreSQL的开发者会发现一个取数神器,从MySQL或SQL Server转过来的朋友会解决一个长久以来的“等价语法”困惑,已经在用PostgreSQL但对写法还不够熟练的人,也能从后面的索引和性能对比里获得一些新的思路。我尽量把执行逻辑和坑都展开讲,不跳过细节。
1. 没有DISTINCT ON的时代,我们是怎么被折磨的
想要理解DISTINCT ON的价值,最好的方式不是直接背语法,而是先回忆一下没有它的日子有多痛苦。
1.1 我用过的三种“笨办法”
假设现在有一张非常常见的订单表,我需要的数据是:每个用户的最新一笔订单。不用DISTINCT ON,我用过的方案基本是以下三种之一。
第一种,自关联匹配最大时间:
sql复制SELECT o.*
FROM orders o
JOIN (
SELECT user_id, MAX(order_time) AS max_time
FROM orders
GROUP BY user_id
) t ON o.user_id = t.user_id AND o.order_time = t.max_time;
这个方案最直观,但有两个隐患:第一,如果同一个用户在同一个时间恰好下了两单(时间戳精度不够,这种情况真的很常见),子查询里MAX只会返回一个时间点,关联时会把同一时间的多行都带出来,导致结果行数超出预期。你说也没关系?真的关系很大,数据一旦翻倍会让人怀疑人生。第二个隐患是性能,外层要全表关联子查询结果,内层要扫描一次做聚合,数据量上去了以后最先扛不住的通常是这个写法。
第二种,窗口函数ROW_NUMBER():
sql复制SELECT order_id, user_id, order_amount, order_time
FROM (
SELECT order_id, user_id, order_amount, order_time,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_time DESC) AS rn
FROM orders
) t
WHERE rn = 1;
这个方案是标准SQL,通用性最强,MySQL 8.0以上和SQL Server都支持,也是我推荐給跨数据库项目使用的方案。但它的缺点是需要两层嵌套,写起来稍显冗余,而且执行计划里一定会有一步窗口排序,拿到结果前得先把整个分区都算一遍。
第三种,MySQL老版本的变量用法或者SQL Server的CROSS APPLY(OUTER APPLY),这些要么是特定版本才支持,要么是性能极不稳定,已经不太推荐用了。
1.2 一个反直觉的结论:DISTINCT ON并不是“先DISTINCT再ON”
刚听到DISTINCT ON这个名字的人,很容易把它理解成“先按某个条件去重,再对结果做处理”,这完全反了。DISTINCT ON的真正含义是:在既定的分组范围内,只保留每个分组里的第一条记录。关键点在于“保留哪一条”由排序决定,而在排序发生之前,它是不会对任何行做提前丢弃的。
这样说可能有点抽象,我拿一个更生活化的类比来解释。假设你在淘宝上筛选“每个店铺销量最高的那款商品”,普通DISTINCT的做法是,把所有同款商品全部合并为一个记录,价格、销量这些字段根本没法展示。DISTINCT ON的做法则是,把店铺当成一个一个抽屉,每个抽屉里的商品先按销量从高到低排好序,然后只从每个抽屉的最上面抽出一张卡片。它留下的不是“合并后的值”,而是原来那一行真实存在的完整记录。
这个区别非常重要,因为很多人写SQL时习惯把DISTINCT ON和“按字段去重”划等号,导致后面的排序条件和字段匹配总是写错。稍后我会专门讲这个坑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. DISTINCT ON的语法结构,以及它底层到底怎么执行
先看最基本的语法骨架:
sql复制SELECT DISTINCT ON (column_a) column_a, column_b, ...
FROM table_name
ORDER BY column_a, column_c DESC;
要记住的核心规则有三条:第一,DISTINCT ON后面的括号里,可以写一个字段,也可以写多个字段,它们之间用逗号分隔,相当于分组维度;第二,ORDER BY的开头几位必须和DISTINCT ON里的字段完全一致,这是语法强制要求,也是绝大多数人第一次写就会踩的坑;第三,你想控制每个分组里“留下哪一行”,靠的就是ORDER BY紧跟在分组字段后面的那个排序字段。
2.1 一个能直接跑起来的完整示例
我建一张订单表,并插入一些数据用来演示:
sql复制CREATE TABLE orders (
order_id serial PRIMARY KEY,
user_id int NOT NULL,
order_amount numeric(10,2) NOT NULL,
order_time timestamptz NOT NULL
);
INSERT INTO orders (user_id, order_amount, order_time) VALUES
(1, 100.00, '2024-01-01 10:00:00+08'),
(1, 200.00, '2024-01-03 12:30:00+08'),
(1, 50.00, '2024-12-31 23:59:59+08'),
(2, 300.00, '2024-02-01 09:00:00+08'),
(2, 150.00, '2024-03-15 18:00:00+08'),
(3, 500.00, '2024-06-01 08:00:00+08');
现在我要写“每个用户的最新一笔订单”:
sql复制SELECT DISTINCT ON (user_id)
user_id, order_id, order_amount, order_time
FROM orders
ORDER BY user_id, order_time DESC;
执行结果应该长这样:
code复制 user_id | order_id | order_amount | order_time
---------+----------+--------------+------------------------------
1 | 3 | 50.00 | 2024-12-31 23:59:59+08
2 | 5 | 150.00 | 2024-03-15 18:00:00+08
3 | 6 | 500.00 | 2024-06-01 08:00:00+08
注意user_id=1的订单,三条里最新的一条是2024-12-31下单的那笔,金额50元。DISTINCT ON把user_id=1这个分组里的三条记录按order_time降序排好,然后取第一条。order_id、order_amount这些字段都是原样拿到的,没有经过任何聚合或合并,这是它和GROUP BY最本质的区别。
2.2 执行顺序,我用大白话讲一遍
从执行计划来看,DISTINCT ON的逻辑可以拆成三步:
- PostgreSQL先根据FROM子句找到表,扫描出所有满足WHERE条件的行。
- 然后按ORDER BY指定的全部字段排序。注意这里不是只按分组字段排序,而是按完整的ORDER BY字段列表做一次全局或局部排序。
- 排序之后,再根据DISTINCT ON里的字段做“分组取首行”:扫描已经排好的数据,每当发现一个全新的分组键值就输出一行,同一分组键值后续出现的行全部丢弃。
想验证这个执行顺序并不难,用EXPLAIN看执行计划,你会发现DISTINCT ON对应的操作符叫作“Unique”,它下面紧挨着Sort。如果是用索引直接保证了顺序,Sort可能被省略,但Unique这个节点基本都会出现。这意味着DISTINCT ON并不需要像窗口函数那样计算所有行的编号,它只要在排序结果里做一次O(n)的扫描就能完成分组取首行。
这也是为什么在一些特定场景下,DISTINCT ON的性能比ROW_NUMBER()更好。当然,这个说法是有前提的,后面我专门讲性能。
3. 最容易踩的坑:ORDER BY和DISTINCT ON的字段约束
关于DISTINCT ON,网上的教程一抓一大把,但你去看评论区,十个人里有八个都是在同一个地方翻车的:ORDER BY的写法。这个坑我给它起个名字叫“排序字段前缀约束”,因为PostgreSQL的官方文档里写了一句话:DISTINCT ON表达式必须匹配ORDER BY的最左侧表达式。这句话听着简单,实际操作起来很多人会连续踩两三次。
3.1 报错信息长什么样
还是用上面的orders表,我故意写一个“错误示范”:
sql复制SELECT DISTINCT ON (user_id) user_id, order_id, order_amount
FROM orders
ORDER BY order_time DESC;
你会立刻收到这样一条报错:
code复制ERROR: SELECT DISTINCT ON expressions must match initial ORDER BY expressions
翻译成大白话就是:DISTINCT ON(user_id)要求你的ORDER BY必须以user_id开头,结果你直接写的是order_time,所以语法层面就过不去。这个约束不是PostgreSQL故意为难你,而是逻辑必然——如果你不先按user_id排序,那所有user_id=1的记录和user_id=2的记录就会交杂在一起,引擎扫描到相同分组键值的“第一行”时,根本无法保证它是这个分组里真正的第一条。
正确的写法必须是:
sql复制SELECT DISTINCT ON (user_id) user_id, order_id, order_amount
FROM orders
ORDER BY user_id, order_time DESC;
用一句话记忆:ORDER BY的前缀就是DISTINCT ON的“分组键”。
3.2 多字段分组的写法:注意顺序不能乱
DISTINCT ON可以跟多个字段,例如“每个用户在每个月的最新一笔订单”:
sql复制SELECT DISTINCT ON (user_id, date_trunc('month', order_time))
user_id,
date_trunc('month', order_time) AS order_month,
order_id,
order_amount
FROM orders
ORDER BY user_id, date_trunc('month', order_time), order_time DESC;
注意DISTINCT ON里的两个表达式,在ORDER BY里必须原样出现,且顺序要保持一致。写完DISTINCT ON (user_id, date_trunc(...)),ORDER BY的开头就必须是user_id, date_trunc(...),不能颠倒成date_trunc(...), user_id。一旦颠倒,PostgreSQL依然会报同样的错。
这里还有个很细节的点,如果你在SELECT列表里给date_trunc('month', order_time)起了别名order_month,你在ORDER BY里用order_month这个别名,PostgreSQL是允许的。但DISTINCT ON括号里绝不能写别名,必须写原始表达式。因为DISTINCT ON是在排序之后、投影之前的阶段完成分组的,别名在这个阶段还没有生效。这个细节很容易和窗口函数的写法搞混,建议记一下。
3.3 排序方向不一致会导致结果不符合直觉
还有一个看起来不报错、但结果会让人一脸懵的情况:如果DISTINCT ON里的分组字段是升序,而你想让分组内的排序字段升序排列,那结果通常符合预期;但如果分组字段排序方向变了,比如写成ORDER BY user_id DESC, order_time DESC,你会发现取到的“最新订单”依然是最新的,只是分组出现的顺序变成了user_id从大到小。这个影响不大,因为DISTINCT ON本身不影响结果集合的内容,只影响行的排列顺序。
真正需要警惕的是排序字段本身。如果你想要“每组金额最大的那条记录”,ORDER BY必须写成user_id, order_amount DESC;如果你写成user_id, order_amount ASC,那取回来的就变成“每组金额最小的那条”。方向反了,内容完全不同,而且不报任何错。这是所有人在初学阶段最常见也是最隐蔽的错误。
4. 和ROW_NUMBER()、GROUP BY、LATERAL放在一起怎么选
DISTINCT ON虽然好用,但它不是万能的。实际项目中到底用哪个方案,不能只凭喜好,得看场景、看可移植性、看性能。
4.1 对比一把:DISTINCT ON VS ROW_NUMBER()
我这几年写SQL的经验是,这两个方案解决的是同一个问题,只是表达方式不同。ROW_NUMBER()是标准SQL里的窗口函数,通过PARTITION BY指定分组,通过ORDER BY指定组内排序,最后在外层用WHERE rn = 1过滤。DISTINCT ON则是PostgreSQL扩展语法,表达更精简。
从功能上有一个关键差异:ROW_NUMBER()可以同时输出组内的第1、第2、第3条数据,只要在WHERE后面改成rn <= 3就行。DISTINCT ON做不到,它天生只输出每组一条。如果业务需要“每个用户最近三笔订单”,直接用ROW_NUMBER()会舒服很多。
从性能上看,在小数据量场景下两者差距几乎可以忽略。在数据量较大、且能创建一个完美匹配排序的索引时,DISTINCT ON往往更占优,因为省去了窗口函数计算编号的那一步。但如果你的排序条件里带了表达式函数,比如date_trunc、lower、substring等,索引就很难完美生效,这时候两者往往都要走Sort节点,性能差距也就缩小了。
可移植性方面,ROW_NUMBER()在MySQL 8.0、SQL Server、Oracle、PostgreSQL里全部通用,DISTINCT ON只在PostgreSQL里存在。如果你的项目有跨数据库部署的需求,或者未来可能迁移到别的数据库,老实说ROW_NUMBER()是更安全的选择。
4.2 对比二把:DISTINCT ON VS GROUP BY
GROUP BY和DISTINCT ON是完全不同维度的东西,但经常有人把它们混为一谈。GROUP BY的核心操作是“分组 + 聚合”,分组后每一行输出的一定是组的聚合结果,除分组键外的其他原始字段通常都要通过MAX、MIN、SUM、ARRAY_AGG这类聚合函数来“拼”出来。DISTINCT ON则保留了原始行的完整性,不需要聚合函数。
用一个经典需求说明:我要取每个用户的最新一笔订单的金额。用GROUP BY只能写成这样:
sql复制SELECT user_id, MAX(order_time) AS max_time
FROM orders
GROUP BY user_id;
这只能拿到用户ID和最新时间,拿不到这笔订单的order_id、order_amount。你势必要再关联一次orders表,把order_id和order_amount带出来。一个查询变成了两步。DISTINCT ON则一步到位。
但如果你需要的是“每个用户的订单总数、总金额”这类聚合统计,GROUP BY才是正解。DISTINCT ON在聚合统计场景无能为力。所以这两个语法从来不是替代关系,而是互补关系。我遇到过很多新手试图用DISTINCT ON去做聚合统计,结果发现统计不出来,原因就在这。
4.3 对比三把:DISTINCT ON VS LATERAL子查询
LEFT JOIN LATERAL是另一个能实现“每组取第一条”的利器。它的写法是这样的:
sql复制SELECT u.user_id, o.order_id, o.order_amount, o.order_time
FROM (SELECT DISTINCT user_id FROM orders) u
CROSS JOIN LATERAL (
SELECT order_id, order_amount, order_time
FROM orders o
WHERE o.user_id = u.user_id
ORDER BY order_time DESC
LIMIT 1
) o;
这个方案的优势在于灵活,LIMIT 1改LIMIT 3就能取前三笔,去重源还能自由控制,WHERE条件可以随便加。缺点是写法最啰嗦,而且子查询里的LIMIT 1会导致性能对子查询内部的排序质量非常敏感。如果orders表上没有(user_id, order_time)的复合索引,每一行外层结果都要触发一次子查询排序,性能会快速劣化。
我自己的经验是:PostgreSQL项目内部,九成的“每组取一条”用DISTINCT ON就够了;需要取多条时优先ROW_NUMBER();遇到特别复杂的过滤条件或逻辑分页时再考虑LATERAL。
4.4 选型建议,直接抄结论
| 场景 | 推荐写法 | 理由 |
|---|---|---|
| PostgreSQL内“每组取第一条”,排序条件单一 | DISTINCT ON | 语法简洁,执行计划轻量 |
| 结果需要取每组前N条(N > 1) | ROW_NUMBER() | 灵活输出组内多行 |
| 项目可能跨数据库部署 | ROW_NUMBER() | 标准SQL,兼容性好 |
| 需要聚合统计(每组的数量、总额) | GROUP BY | 聚合函数是它的专长 |
| 分组来源复杂,或每组取一条还要多层过滤 | LATERAL | 逻辑最清晰,控制力最强 |
5. 实战技巧:索引、NULL值、大数据量,还有几个我在业务里实际趟过的坑
前面讲的都是语法和选型,接下来这部分更偏“实战手感”。没有索引加持的DISTINCT ON,就是一把双刃剑。
5.1 索引怎么建才最有效
DISTINCT ON最理想的情况是:排序可以完全由索引提供,避免Sort。假设你经常写这样的查询——“每个用户最新的订单”:
sql复制SELECT DISTINCT ON (user_id) *
FROM orders
WHERE user_id IN (1, 2, 3)
ORDER BY user_id, order_time DESC;
那一个复合索引几乎是必须的:
sql复制CREATE INDEX idx_orders_user_time ON orders (user_id, order_time DESC);
这个索引的字段顺序必须和ORDER BY完全一致,方向也要一致。如果排序方向是DESC,索引里就建DESC;PostgreSQL 14以下版本对反向扫描的支持是有损耗的,方向写反了虽然也能工作,但性能没那么好看。
加了索引以后再看EXPLAIN,你会看到类似这样的结果:
code复制Sort (cost=...)
-> Index Scan using idx_orders_user_time on orders
...
如果WHERE条件里的user_id是等值匹配,则完全可以去掉Sort,直接走Index Only Scan或Index Scan,速度会非常快。另一个值得注意的点是:如果WHERE里过滤了大量行,很多时候用这个复合索引做位图扫描反而比全表排序更划算,这也取决于成本估算,但总体来说这个索引是DISTINCT ON查询的“标准答案”。
5.2 NULL值会怎么影响结果
NULL在排序里默认使用的是NULLS LAST还是NULLS FIRST,取决于排序方向。默认升序是NULLS LAST,降序是NULLS FIRST。这个行为在很多业务场景下会给人意外。
比如你有一张产品表,要取每个分类下价格最低的产品:
sql复制SELECT DISTINCT ON (category) category, id, price
FROM products
ORDER BY category, price ASC;
如果某个分类下的部分商品价格是NULL,那么按照默认排序,NULL会排在最后,所以每个组取到的依然是价格最低的非空商品。但如果哪个分类下所有商品价格都是NULL,那这个分组取到的就是NULL价格那条,业务上就要做过滤。
如果你希望把NULL当作“缺失值”,就按业务规则选择显式写出:
sql复制ORDER BY category, price ASC NULLS LAST;
这条写法和默认行为一致,但写出来以后语义清晰了很多。我个人的建议是,凡是参与DISTINCT ON排序的字段,如果允许NULL,一律在ORDER BY里明确写出NULLS FIRST或NULLS LAST,不要依赖数据库默认行为,否则上线半年后翻数据才发现问题,排错成本会很高。
另外,DISTINCT ON分组字段本身如果为NULL,比如按category分组时category字段有NULL,PostgreSQL会把这些NULL值当成同一组,只取一条。这个行为和普通DISTINCT里NULL算一个值的逻辑是一样的。如果业务上NULL应该被当作“未知分类”且不参与分组,那就得在查询里用COALESCE(category, '未知分类')做一个落地的分组键。
5.3 大表场景下的性能注意事项
数据量上到千万级以后,DISTINCT ON有几个容易被忽略的性能细节。
首先,避免不必要的SELECT *。虽然语法允许SELECT DISTINCT ON (user_id) *,但如果你只需要几个字段,表本身又很宽,这个写法会让PostgreSQL在Unique节点前必须把所有列的数据都拿在内存或临时文件里,尤其是当有TEXT、JSONB、BYTEA这种大字段时,临时文件膨胀会非常明显。尽量只投影必要字段,哪怕这样做会让查询看起来稍微长一点。
其次,在DISTINCT ON的子查询外面做条件过滤,通常不是个好主意。例如:
sql复制SELECT * FROM (
SELECT DISTINCT ON (user_id) *
FROM orders
ORDER BY user_id, order_time DESC
) t
WHERE t.order_amount > 100;
这种写法会把“取每个用户最新订单”这一步全算完,然后才过滤金额大于100的记录。很多时候业务真正需要的是“每个用户最新一笔金额大于100的订单”,那就应该在子查询内部先过滤:
sql复制SELECT DISTINCT ON (user_id) *
FROM orders
WHERE order_amount > 100
ORDER BY user_id, order_time DESC;
两种写法结果完全不同,性能也可能天差地别。前者所有用户的最新订单都要排序,后者只对满足金额条件的记录排序。业务语义一定要想清楚,我见过不少人在这里把业务逻辑写错的。
最后,多表关联场景下,DISTINCT ON的分组键尽量用主表字段。如果你对orders表和users表做JOIN,然后DISTINCT ON (users.id),PostgreSQL需要先完成JOIN,再对JOIN结果排序去重。这时候如果能在JOIN之前先对orders做一次“每用户最新订单”的预聚合,再把结果JOIN users,往往执行计划会优化得多。
5.4 一个综合案例:每个分类下销量最高且评价分最低的商品
为了把前面这些技巧串起来,我模拟一个更接近业务实际的场景。假设有一张商品快照表,记录了每天每个商品在某个分类下的销量和客户评价分,我需要为每个分类找出“销量最高;如果销量并列,则取评价分最低”的那款商品,同时这个商品还必须是当天有库存的。
建表和索引:
sql复制CREATE TABLE product_snapshot (
id bigserial PRIMARY KEY,
category text NOT NULL,
product_id bigint NOT NULL,
sale_date date NOT NULL,
sales_count int NOT NULL,
rating_score numeric(3,2) NOT NULL,
in_stock boolean NOT NULL,
updated_at timestamptz DEFAULT now()
);
CREATE INDEX idx_snapshot_cat_sales ON product_snapshot (category, sales_count DESC, rating_score ASC)
WHERE in_stock;
查询写法:
sql复制SELECT DISTINCT ON (sn.category)
sn.category,
sn.product_id,
sn.sales_count,
sn.rating_score
FROM product_snapshot sn
WHERE sn.in_stock
AND sn.sale_date = CURRENT_DATE
ORDER BY sn.category, sn.sales_count DESC, sn.rating_score ASC;
这里有个很有意思的点:我把排序字段写成了sales_count DESC, rating_score ASC,而DISTINCT ON只需要category作为分组键,所以ORDER BY的开头是category,并不要求所有排序列都在DISTINCT ON里出现的顺序一字不差,只要前缀匹配即可。之前说过“DISTINCT ON的表达式必须出现在ORDER BY最左侧”,这里体现得淋漓尽致。有了部分索引,当PostgreSQL需要扫描今天的独立库存商品时,in_stock = true的过滤条件可以直接落到索引上,排序字段也能尽可能被覆盖,省去临时文件排序。
我给这个SQL加上EXPLAIN ANALYZE验证过,在有500万条快照数据、1000个分类的情况下,利用索引扫描的查询耗时基本在几十毫秒级。如果把部分索引去掉,强制走Sort,会跳到几百毫秒甚至秒级。这就是索引决定DISTINCT ON性能的最好证明。
这里再插一句,如果业务把“销量并列时取评分最低”改成了“销量并列时取评分最高”,其实你只要把ORDER BY里的rating_score从ASC改成DESC即可,索引的方向也应该跟着调整。这种微小的语义差异,在代码Review阶段非常容易被忽略,建议在SQL注释里写清楚排序规则。
5.5 我在实际项目中反复踩过的两个小坑
最后分享两个比较隐蔽的坑,都是我踩过之后才彻底弄明白的。
第一个坑和时区与函数表达式有关。假设有时间字段是timestamptz类型,你想取“每个用户每天的最新一笔订单”,DISTINCT ON的表达式如果写成date_trunc('day', order_time AT TIME ZONE 'Asia/Shanghai'),那么这个表达式必须原样出现在ORDER BY的最左侧。PostgreSQL不允许你在DISTINCT ON里写一个等价但字面不同形式的表达式,比如你写DISTINCT ON (order_time::date),ORDER BY里却写CAST(order_time AS date),虽然逻辑上相等,语法上也会被判定不匹配,直接报错。解决办法是把表达式统一成同一种写法。这是函数表达式和普通列最大的区别,普通列可以大小写混写都没事,函数表达式必须字面一致。
第二个坑和并行查询有关。PostgreSQL在大数据量下会自动启用并行执行,但当遇到DISTINCT ON + ORDER BY这种需要全局排序的结构时,并行度往往会受到限制。这并不代表DISTINCT ON不能用并行,而是说你会发现执行计划里可能没有出现Parallel多个Worker的节点。如果在超大表上某个DISTINCT ON查询很慢,EXPALIN里又看不到并行,可以考虑用子查询先把WHERE条件过滤掉大部分行,或者适当调低max_parallel_workers_per_gather,看哪种搭配更合适。并行查询这块没有通用答案,只能基于实际数据量慢慢调。
这些细节看起来零碎,但在生产环境里踩到一个,排查起来可能就是半天。希望这篇文章能帮你把DISTINCT ON的“脾气”摸清楚,少走点弯路。如果你是从MySQL或Oracle转过来的,尤其建议把DISTINCT ON和ROW_NUMBER()都练熟,两者配合使用,分组取数的日常需求基本都覆盖住了。
