在每个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 ONexpression式。 您可以添加额外的expression式到ORDER BY从每组对等中select一个特定的行。 我添加了id作为最后一项打破关系:

    “从共享最高total每个组中挑选具有最小id的行”。

    如果total可以为NULL,那么最可能需要非空值最大的行。 添加NULLS LAST就像演示。 细节:

    • PostgreSQL按datetime ascsorting,先是null?
  • SELECT列表不受DISTINCT ONORDER BY中的expression式的限制。 (在上面的简单情况下不需要):

    • 不必DISTINCT ONORDER 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.49.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 ):

  1. select整个表格,在这种情况下将产生5958行。

     A: 567.218 ms B: 386.673 ms 
  2. 使用条件在WHERE customer BETWEEN x AND y导致1000行。

     A: 249.136 ms B: 55.111 ms 
  3. 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)这样的综合索引。

备注:

  1. t1,t2是可以根据数据库删除的子查询别名。

  2. 注意 :从2017年1月起,在此编辑中, using (...)子句目前不支持MS-SQL和Oracle数据库。您必须on t2.id = purchase.id将其扩展到例如on t2.id = purchase.idon t2.id = purchase.id语法适用于SQLite,MySQL和PostgreSQL。