在每个GROUP BY组中select第一行?
正如标题所暗示的,我想select用GROUP BY
分组的每一组行的第一行。
具体来说,如果我有一个purchases
表,看起来像这样:
SELECT * FROM purchases;
id | 客户| 总 --- + ---------- + ------ 1 | 乔| 五 2 | 莎莉| 3 3 | 乔| 2 4 | 莎莉| 1
我想查询每个customer
所做的最大购买( total
)的id
。 像这样的东西:
SELECT FIRST(id), customer, FIRST(total) FROM purchases GROUP BY customer ORDER BY total DESC;
FIRST(id)| 客户| FIRST(总) ---------- + ---------- + ------------- 1 | 乔| 五 2 | 莎莉| 3
在Oracle 8i +,SQL Server 2005+,PostgreSQL 8.4+,DB2,Firebird 3.0+,Teradata,Sybase,Vertica:
WITH summary AS ( SELECT p.id, p.customer, p.total, ROW_NUMBER() OVER(PARTITION BY p.customer ORDER BY p.total DESC) AS rk FROM PURCHASES p) SELECT s.* FROM summary s WHERE s.rk = 1
任何数据库支持:
但是你需要添加逻辑来打破关系:
SELECT MIN(x.id), -- change to MAX if you want the highest x.customer, x.total FROM PURCHASES x JOIN (SELECT p.customer, MAX(total) AS max_total FROM PURCHASES p GROUP BY p.customer) y ON y.customer = x.customer AND y.max_total = x.total GROUP BY x.customer, x.total
在PostgreSQL中,这通常更简单快捷 (下面更多的性能优化):
SELECT DISTINCT ON (customer) id, customer, total FROM purchases ORDER BY customer, total DESC, id;
或者输出列的序号较短(如果不是很清楚):
SELECT DISTINCT ON (2) id, customer, total FROM purchases ORDER BY 2, 3 DESC, 1;
如果total
可以是NULL(不会伤害任何一种方式,但是你想要匹配现有的索引):
... ORDER BY customer, total DESC NULLS LAST , id;
重点
-
DISTINCT ON
是标准的PostgreSQL扩展(在整个SELECT
列表中只定义了DISTINCT
)。 -
在
DISTINCT ON
子句中列出任意数量的expression式,组合的行值定义重复项。 手册:显然,如果至less有一个列值不同,则两行被认为是不同的。 在这个比较中,空值被认为是相等的。
大胆重视我的。
-
DISTINCT ON
可以和ORDER BY
结合使用。 领先的expression式必须以相同的顺序匹配领先的DISTINCT ON
expression式。 您可以添加额外的expression式到ORDER BY
从每组对等中select一个特定的行。 我添加了id
作为最后一项打破关系:“从共享最高
total
每个组中挑选具有最小id
的行”。如果
total
可以为NULL,那么最可能需要非空值最大的行。 添加NULLS LAST
就像演示。 细节:- PostgreSQL按datetime ascsorting,先是null?
-
SELECT
列表不受DISTINCT ON
或ORDER BY
中的expression式的限制。 (在上面的简单情况下不需要):-
您不必在
DISTINCT ON
或ORDER BY
包含任何expression式。 -
您可以在
SELECT
列表中包含任何其他expression式。 这有助于用子查询和聚合/窗口函数replace更复杂的查询。
-
-
我testing版本8.3 – 10.但function至less从7.1版本,所以基本上总是那里。
指数
上述查询的完美索引将是一个多列索引,它跨越匹配序列中所有三列,并具有匹配的sorting顺序:
CREATE INDEX purchases_3c_idx ON purchases (customer, total DESC, id);
可能太专门针对真实世界的应用程序。 但是如果读取性能至关重 如果查询中有DESC NULLS LAST
,则在索引中使用相同的名称,以便Postgres知道sorting顺序匹配。
有效性/性能优化
在为每个查询创build定制索引之前,您必须权衡成本和收益。 上述指标的潜力很大程度上取决于数据分布 。
使用索引是因为它提供了预先sorting的数据,而在Postgres 9.2或更高版本中,如果索引小于基础表, 索引只能扫描索引。 但索引必须全部扫描。
-
对于每个客户几行 ,这是非常有效的(甚至如果你需要sorting输出更是如此)。 随着每位客户数量不断增加,收益也随之缩小。
理想情况下,您有足够的work_mem
来处理RAM中涉及的sorting步骤,而不是泄漏到磁盘。 通常将work_mem
设置得太高会产生不利影响。 考虑大集合上的单数查询的SET LOCAL
。 用EXPLAIN ANALYZE
查找你需要多less。 在sorting步骤中提到“ 磁盘: ”表示需要更多:- Linux上的PostgreSQL中的configuration参数work_mem
- 使用ORDER BYdate和文本优化简单的查询
-
对于每个客户的许多行 , 松散的索引扫描将会(更加)更有效率,但目前在Postgres(最多9.5)中没有实现。
有更快的查询技术来替代这一点。 特别是如果你有一个单独的表格,持有独特的客户,这是典型的用例。 但是,如果你不这样做:- 优化GROUP BY查询以检索每个用户的最新logging
- 优化分组最大查询
- 每行查询最后N个相关的行
基准
对于Postgres 9.1,我已经有了一个简单的基准,到2016年已经过时了。所以我用了一个更好,可重复的Postgres 9.4和9.5的设置,并在另一个答案中增加了详细的结果 。
基准
使用Postgres 9.4和9.5testing最有意思的候选人, purchases
一个200k行的中间真实表,以及10k不同的customer_id
( 平均每个客户20行 )。
对于Postgres 9.5,我用86446个不同的客户进行了第二次testing。 见下文( 平均每个客户2.3行 )。
build立
主表
CREATE TABLE purchases ( id serial , customer_id int -- REFERENCES customer , total int -- could be amount of money in Cent , some_column text -- to make the row bigger, more realistic );
我使用了一个serial
(PK约束下面添加)和一个整数customer_id
因为这是一个更典型的设置。 还添加了some_column
来弥补通常更多的列。
虚拟数据,PK,索引 – 一个典型的表也有一些死元组:
INSERT INTO purchases (customer_id, total, some_column) -- insert 200k rows SELECT (random() * 10000)::int AS customer_id -- 10k customers , (random() * random() * 100000)::int AS total , 'note: ' || repeat('x', (random()^2 * random() * random() * 500)::int) FROM generate_series(1,200000) g; ALTER TABLE purchases ADD CONSTRAINT purchases_id_pkey PRIMARY KEY (id); DELETE FROM purchases WHERE random() > 0.9; -- some dead rows INSERT INTO purchases (customer_id, total, some_column) SELECT (random() * 10000)::int AS customer_id -- 10k customers , (random() * random() * 100000)::int AS total , 'note: ' || repeat('x', (random()^2 * random() * random() * 500)::int) FROM generate_series(1,20000) g; -- add 20k to make it ~ 200k CREATE INDEX purchases_3c_idx ON purchases (customer_id, total DESC, id); VACUUM ANALYZE purchases;
customer
表 – 用于优越的查询
CREATE TABLE customer AS SELECT customer_id, 'customer_' || customer_id AS customer FROM purchases GROUP BY 1 ORDER BY 1; ALTER TABLE customer ADD CONSTRAINT customer_customer_id_pkey PRIMARY KEY (customer_id); VACUUM ANALYZE customer;
在我第二次testing 9.5中,我使用了相同的设置,但是使用random() * 100000
来生成customer_id
,以便每个customer_id
只获得几行。
表格purchases
对象大小
生成此查询 。
what | bytes/ct | bytes_pretty | bytes_per_row -----------------------------------+----------+--------------+--------------- core_relation_size | 20496384 | 20 MB | 102 visibility_map | 0 | 0 bytes | 0 free_space_map | 24576 | 24 kB | 0 table_size_incl_toast | 20529152 | 20 MB | 102 indexes_size | 10977280 | 10 MB | 54 total_size_incl_toast_and_indexes | 31506432 | 30 MB | 157 live_rows_in_text_representation | 13729802 | 13 MB | 68 ------------------------------ | | | row_count | 200045 | | live_tuples | 200045 | | dead_tuples | 19955 | |
查询
1.在CTE中的row_number()
,( 见其他答案 )
WITH cte AS ( SELECT id, customer_id, total , row_number() OVER(PARTITION BY customer_id ORDER BY total DESC) AS rn FROM purchases ) SELECT id, customer_id, total FROM cte WHERE rn = 1;
2.子查询中的row_number()
(我的优化)
SELECT id, customer_id, total FROM ( SELECT id, customer_id, total , row_number() OVER(PARTITION BY customer_id ORDER BY total DESC) AS rn FROM purchases ) sub WHERE rn = 1;
3. DISTINCT ON
( 查看其他答案 )
SELECT DISTINCT ON (customer_id) id, customer_id, total FROM purchases ORDER BY customer_id, total DESC, id;
4.带有子查询的rCTE( 参见这里 )
WITH RECURSIVE cte AS ( ( -- parentheses required SELECT id, customer_id, total FROM purchases ORDER BY customer_id, total DESC LIMIT 1 ) UNION ALL SELECT u.* FROM cte c , LATERAL ( SELECT id, customer_id, total FROM purchases WHERE customer_id > c.customer_id -- lateral reference ORDER BY customer_id, total DESC LIMIT 1 ) u ) SELECT id, customer_id, total FROM cte ORDER BY customer_id;
5. customer
表与LATERAL
( 见这里 )
SELECT l.* FROM customer c , LATERAL ( SELECT id, customer_id, total FROM purchases WHERE customer_id = c.customer_id -- lateral reference ORDER BY total DESC LIMIT 1 ) l;
6. array_agg()
与ORDER BY
( 请参阅其他答案 )
SELECT (array_agg(id ORDER BY total DESC))[1] AS id , customer_id , max(total) AS total FROM purchases GROUP BY customer_id;
结果
使用EXPLAIN ANALYZE
(以及所有选项closures )执行上述查询的执行时间,5次运行的最佳时间 。
所有查询都使用了索引只扫描 purchases2_3c_idx
(以及其他步骤)。 其中一些只是为了索引的较小规模,另一些则是更为有效。
答:Postgres 9.4有200k行,每个customer_id
1. 273.274 ms 2. 194.572 ms 3. 111.067 ms 4. 92.922 ms 5. 37.679 ms -- winner 6. 189.495 ms
B.和Postgres 9.5一样
1. 288.006 ms 2. 223.032 ms 3. 107.074 ms 4. 78.032 ms 5. 33.944 ms -- winner 6. 211.540 ms
C.与B.相同,但每个customer_id
约2.3行
1. 381.573 ms 2. 311.976 ms 3. 124.074 ms -- winner 4. 710.631 ms 5. 311.976 ms 6. 421.679 ms
2011年的原始(过时)基准
我在包含65579行的实际生命表上运行了PostgreSQL 9.1的三个testing,并且在涉及的三列中的每一列上运行了单列btree索引,并且花费了5次运行的最佳执行时间 。
比较@OMGPonies的第一个查询( A
)到上面的DISTINCT ON
解决scheme ( B
):
-
select整个表格,在这种情况下将产生5958行。
A: 567.218 ms B: 386.673 ms
-
使用条件在
WHERE customer BETWEEN x AND y
导致1000行。A: 249.136 ms B: 55.111 ms
-
select一个客户与
WHERE customer = x
A: 0.143 ms B: 0.072 ms
在另一个答案中描述的指数重复相同的testing
CREATE INDEX purchases_3c_idx ON purchases (customer, total DESC, id);
1A: 277.953 ms 1B: 193.547 ms 2A: 249.796 ms -- special index not used 2B: 28.679 ms 3A: 0.120 ms 3B: 0.048 ms
这是最常见的每组问题,它已经经过了很好的testing和高度优化的解决scheme 。 就个人而言,我更喜欢Bill Karwin的左连接解决方案 ( 原来的post有很多其他解决scheme )。
请注意,这个常见问题的一堆解决scheme可以在MySQL官方最权威的源代码中find。 查看常见查询的示例::行保持某个列的最大组的最大值 。
在Postgres中,你可以像这样使用array_agg
:
SELECT customer, (array_agg(id ORDER BY total DESC))[1], max(total) FROM purchases GROUP BY customer
这会给你每个客户的最大购买的id
。
有些事情要注意:
-
array_agg
是一个聚合函数,所以它适用于GROUP BY
。 -
array_agg
可以让你指定一个array_agg
于自身的sorting,所以它不会限制整个查询的结构。 如果你需要做一些不同于默认值的东西,那么还有如何对NULL进行sorting的语法。 - 一旦我们build立了数组,我们把第一个元素。 (Postgres数组是1索引,而不是0索引)。
- 您可以使用
array_agg
类似的方式为您的第三个输出列,但max(total)
更简单。 - 与
DISTINCT ON
不同,使用array_agg
可以让你保留你的GROUP BY
,以防其他原因。
由于存在SubQ,Erwin指出解决scheme效率不高
select * from purchases p1 where total in (select max(total) from purchases where p1.customer=customer) order by total desc;
我使用这种方式(仅限postgresql): https : //wiki.postgresql.org/wiki/First/last_%28aggregate%29
-- Create a function that always returns the first non-NULL item CREATE OR REPLACE FUNCTION public.first_agg ( anyelement, anyelement ) RETURNS anyelement LANGUAGE sql IMMUTABLE STRICT AS $$ SELECT $1; $$; -- And then wrap an aggregate around it CREATE AGGREGATE public.first ( sfunc = public.first_agg, basetype = anyelement, stype = anyelement ); -- Create a function that always returns the last non-NULL item CREATE OR REPLACE FUNCTION public.last_agg ( anyelement, anyelement ) RETURNS anyelement LANGUAGE sql IMMUTABLE STRICT AS $$ SELECT $2; $$; -- And then wrap an aggregate around it CREATE AGGREGATE public.last ( sfunc = public.last_agg, basetype = anyelement, stype = anyelement );
那么你的例子应该几乎是这样工作的:
SELECT FIRST(id), customer, FIRST(total) FROM purchases GROUP BY customer ORDER BY FIRST(total) DESC;
CAVEAT:它忽略了NULL行
编辑1 – 使用postgres扩展
现在我用这种方式: http : //pgxn.org/dist/first_last_agg/
要安装在Ubuntu 14.04上:
apt-get install postgresql-server-dev-9.3 git build-essential -y git clone git://github.com/wulczer/first_last_agg.git cd first_last_app make && sudo make install psql -c 'create extension first_last_agg'
这是一个Postgres扩展,它提供了第一个和最后一个function。 显然比上述方式更快。
编辑2 – sorting和筛选
如果您使用聚合函数(如这些),您可以订购结果,而不需要已经订购的数据:
http://www.postgresql.org/docs/current/static/sql-expressions.html#SYNTAX-AGGREGATES
所以等价的例子,与sorting会是这样的:
SELECT first(id order by id), customer, first(total order by id) FROM purchases GROUP BY customer ORDER BY first(total);
当然,您可以根据您认为合适的顺序进行订购和过滤。 这是非常强大的语法。
非常快的解决scheme
SELECT a.* FROM purchases a JOIN ( SELECT customer, min( id ) as id FROM purchases GROUP BY customer ) b USING ( id );
如果table被id索引,真的非常快:
create index purchases_id on purchases (id);
被接受的OMG Ponies的“任何数据库支持”解决scheme在我的testing中都有很好的速度。
在这里,我提供了一个相同的方法,但更完整和清洁的任何数据库解决scheme。 考虑关系(假设每个客户只需要一行,甚至每个客户最多可以有多个logging),其他采购字段(例如purchase_payment_id)将被select用于采购表中的真实匹配行。
任何数据库支持:
select * from purchase join ( select min(id) as id from purchase join ( select customer, max(total) as total from purchase group by customer ) t1 using (customer, total) group by customer ) t2 using (id) order by customer
这个查询是相当快的,特别是在购买表上有一个像(customer,total)这样的综合索引。
备注:
-
t1,t2是可以根据数据库删除的子查询别名。
-
注意 :从2017年1月起,在此编辑中,
using (...)
子句目前不支持MS-SQL和Oracle数据库。您必须on t2.id = purchase.id
将其扩展到例如on t2.id = purchase.id
等on t2.id = purchase.id
语法适用于SQLite,MySQL和PostgreSQL。