Select first row in each GROUP BY group?
如标题所示,我想选择每一组用
具体来说,如果我有一个这样的
| 1 | SELECT * FROM purchases; | 
我的输出:
| 1 2 3 4 5 6 | id | customer | total ---+----------+------ 1 | Joe | 5 2 | Sally | 3 3 | Joe | 2 4 | Sally | 1 | 
我想查询每个
| 1 2 3 4 | SELECT FIRST(id), customer, FIRST(total) FROM purchases GROUP BY customer ORDER BY total DESC; | 
预期输出:
| 1 2 3 4 | FIRST(id) | customer | FIRST(total) ----------+----------+------------- 1 | Joe | 5 2 | Sally | 3 | 
在PostgreSQL中,这通常更简单、更快(下面是更多的性能优化):好的。
| 1 2 3 4 | SELECT DISTINCT ON (customer) id, customer, total FROM purchases ORDER BY customer, total DESC, id; | 
或更短(如果不是很清楚)的输出列的序号:好的。
| 1 2 3 4 | SELECT DISTINCT ON (2) id, customer, total FROM purchases ORDER BY 2, 3 DESC, 1; | 
如果
| 1 2 | ... ORDER BY customer, total DESC NULLS LAST, id; | 
要点
- DISTINCT ON 是标准的postgresql扩展(在整个- SELECT 列表中只定义了- DISTINCT )。好的。
- 在 - DISTINCT ON 子句中列出任意数量的表达式,组合行值定义重复项。手册:好的。
 - Obviously, two rows are considered distinct if they differ in at least 
 one column value. Null values are considered equal in this comparison.- Ok. - 大胆强调我的。好的。 
- DISTINCT ON 可与- ORDER BY 组合使用。前导表达式必须以相同的顺序匹配前导- DISTINCT ON 表达式。您可以向- ORDER BY 添加额外的表达式,以便从每个对等组中选择特定的行。我添加了EDOCX1[9]作为最后一个打破联系的项目:好的。- "从共享最高 - total 的每个组中选择具有最小- id 的行。"好的。- 要以与确定每组第一个查询的排序顺序不一致的方式对结果排序,可以将上面的查询嵌套在另一个 - ORDER BY 的外部查询中。像:好的。- PostgreSQL上的distinct on和different order by
 
- 如果 - total 可以为空,则最可能需要非空值最大的行。如图所示,加上- NULLS LAST 。细节:好的。- PostgreSQL按datetime asc排序,首先为空?
 
- SELECT 列表不受- DISTINCT ON 或- ORDER BY 表达式的任何约束。(上述简单情况下不需要):好的。- 您不必在 - DISTINCT ON 或- ORDER BY 中包含任何表达式。好的。
- 您可以在 - SELECT 列表中包含任何其他表达式。这有助于用子查询和聚合/窗口函数替换更复杂的查询。好的。
 
- 我用Postgres版本8.3–11进行了测试。但这个特性至少从7.1版开始就存在,所以基本上总是存在的。好的。 
索引
上述查询的完美索引将是一个多列索引,该索引按匹配顺序和匹配排序顺序跨越所有三列:好的。
| 1 | CREATE INDEX purchases_3c_idx ON purchases (customer, total DESC, id); | 
可能太专业了。但如果特定查询的读取性能至关重要,请使用它。如果查询中有
在为每个查询创建定制的索引之前权衡成本和收益。上述指标的潜力很大程度上取决于数据分布。好的。
使用索引是因为它提供预先排序的数据。在Postgres9.2或更高版本中,如果索引小于基础表,那么查询也可以只从索引扫描中获益。不过,必须对索引进行整体扫描。好的。
- 对于每个客户的几行( - customer 列中的高基数),这是非常有效的。更重要的是,如果您无论如何都需要经过排序的输出。随着每个客户的行数的增加,收益会减少。理想情况下,您有足够的- work_mem 来处理RAM中涉及的排序步骤,而不会溢出到磁盘。但一般情况下,将- work_mem 设得过高会产生不利影响。对于非常大的查询,考虑使用- SET LOCAL 。找到你需要多少与- EXPLAIN ANALYZE 。在排序步骤中提到"磁盘",表示需要更多:好的。- Linux上PostgreSQL中的配置参数Work-Mem
- 使用按日期和文本排序优化简单查询
 
- 对于每个客户的许多行( - customer 列中的低基数),松索引扫描(也称为"跳过扫描")将(非常)高效,但在Postgres 11之前没有实现。(计划对Postgres 12实施仅索引扫描。见这里和这里。)目前,有更快的查询技术来替代它。尤其是如果您有一个单独的表来存放唯一的客户,这是典型的用例。但如果你不这样做:好的。- 按查询优化分组以检索每个用户的最新记录
- 优化GroupWise最大查询
- 每行查询最后n个相关行
 
基准
我这里有一个简单的基准,现在已经过时了。在这个单独的答案中,我用一个详细的基准代替了它。好的。好啊。
在Oracle 9.2+上(不是最初所说的8i+),SQL Server 2005+,PostgreSQL 8.4+,DB2,Firebird 3.0+,Teradata,Sybase,Vertica:
| 1 2 3 4 5 6 7 8 9 10 | 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 | 
任何数据库都支持:
但你需要添加逻辑来打破联系:
| 1 2 3 4 5 6 7 8 9 10 |   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 | 
基准
使用Postgres 9.4和9.5测试最有意思的候选人,在
对于Postgres9.5,我对86446个不同的客户进行了第二次测试。见下文(每个客户平均2.3行)。
安装程序主台
| 1 2 3 4 5 6 | 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 ); | 
我使用一个
虚拟数据、pk、index——典型的表也有一些死元组:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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; | 
| 1 2 3 4 5 6 7 8 9 | 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; | 
在我的第二个9.5测试中,我使用了相同的设置,但使用
与此查询一起生成。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 |                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中的
| 1 2 3 4 5 6 7 8 | 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。子查询中的
| 1 2 3 4 5 6 7 | 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; | 
三。
| 1 2 3 4 | SELECT DISTINCT ON (customer_id) id, customer_id, total FROM purchases ORDER BY customer_id, total DESC, id; | 
4。带
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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。带
| 1 2 3 4 5 6 7 8 9 | 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。
| 1 2 3 4 5 | SELECT (array_agg(id ORDER BY total DESC))[1] AS id , customer_id , MAX(total) AS total FROM purchases GROUP BY customer_id; | 
结果
在
所有查询仅在
| 1 2 3 4 5 6 | 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 2 3 4 5 6 | 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相同,但每个
| 1 2 3 4 5 6 | 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年原始(过时)基准
我用PostgreSQL 9.1在65579行的实际表上运行了三次测试,在涉及的三列中的每一列上运行了单列btree索引,并用了5次运行的最佳执行时间。将@omgponies的第一个查询(
选择整个表,本例中结果为5958行。
| 1 2 | A: 567.218 ms B: 386.673 ms | 
使用条件
| 1 2 | A: 249.136 ms B: 55.111 ms | 
用
| 1 2 | A:   0.143 ms B: 0.072 ms | 
用另一个答案中描述的索引重复相同的测试
| 1 | CREATE INDEX purchases_3c_idx ON purchases (customer, total DESC, id); | 
| 1 2 3 4 5 6 7 8 | 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 | 
这是一个常见的最大N组问题,它已经得到了很好的测试和高度优化的解决方案。就我个人而言,我更喜欢比尔·卡温(Bill Karwin)的左联解决方案(最初的帖子中有很多其他解决方案)。
请注意,对于这个常见问题的大量解决方案可以在一个最官方的资源mysql手册中找到!请参阅常见查询示例:包含特定列的按组最大值的行。
在Postgres中,您可以这样使用
| 1 2 3 4 5 | SELECT  customer, (array_agg(id ORDER BY total DESC))[1], MAX(total) FROM purchases GROUP BY customer | 
这将为您提供每个客户最大采购量的
需要注意的一些事项:
- array_agg 是一个聚合函数,因此它与- GROUP BY 一起工作。
- array_agg 允许您指定一个仅限于其自身的排序范围,因此它不会约束整个查询的结构。如果需要执行与默认值不同的操作,还可以使用语法来排序空值。
- 一旦我们构建了数组,我们就获取第一个元素。(Postgres数组是1索引的,而不是0索引的)。
- 您可以用与第三个输出列类似的方式使用array_agg ,但max(total) 更简单。
- 与DISTINCT ON 不同,使用array_agg 可以保留GROUP BY 以防出于其他原因需要。
由于存在子问题,因此该解决方案并不像erwin所指出的那样高效。
| 1 2 | 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
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | -- 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 ); | 
那么,您的示例应该几乎可以工作:
| 1 2 3 4 | SELECT FIRST(id), customer, FIRST(total) FROM purchases GROUP BY customer ORDER BY FIRST(total) DESC; | 
警告:它忽略了空行
编辑1-改为使用Postgres扩展现在我用这种方式:http://pgxn.org/dist/first-last-agg/
在Ubuntu 14.04上安装:
| 1 2 3 4 5 | 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扩展,为您提供了第一个和最后一个函数;显然比上面的方法快。
编辑2-排序和筛选如果使用聚合函数(如这些),则可以对结果进行排序,而无需对数据进行排序:
| 1 | http://www.postgresql.org/docs/CURRENT/static/sql-expressions.html#SYNTAX-AGGREGATES | 
因此,使用排序的等效示例如下:
| 1 2 3 4 | SELECT FIRST(id ORDER BY id), customer, FIRST(total ORDER BY id) FROM purchases GROUP BY customer ORDER BY FIRST(total); | 
当然,您可以按照您认为适合于聚合的方式进行排序和筛选;这是非常强大的语法。
非常快速的解决方案
| 1 2 3 4 5 6 7 8 | SELECT a.*  FROM purchases a JOIN ( SELECT customer, MIN( id ) AS id FROM purchases GROUP BY customer ) b USING ( id ); | 
如果表是按ID索引的,则速度非常快:
| 1 | CREATE INDEX purchases_id ON purchases (id); | 
查询:
| 1 2 3 4 5 6 7 8 | SELECT purchases.* FROM purchases LEFT JOIN purchases AS p ON p.customer = purchases.customer AND purchases.total < p.total WHERE p.total IS NULL | 
这是怎么回事!(我去过那里)
我们要确保每次购买的总金额都是最高的。
一些理论上的东西(如果你只想理解这个查询,跳过这部分)
total是一个函数t(customer,id),它返回一个给定名称和id的值为了证明给定的总数(t(客户,id))是最高的,我们必须证明我们也要证明
- ?x t(客户,ID)>t(客户,X)(此总数高于所有其他值该客户的合计)
或
- ??x t(客户,ID)
第一种方法需要我们获取我不喜欢的那个名字的所有记录。
第二个需要一个聪明的方法来说明没有比这个更高的记录。
返回SQL
如果我们左键联接表的名称和合计小于联接表:
| 1 2 3 4 5 |       LEFT JOIN purchases AS p  ON p.customer = purchases.customer AND purchases.total < p.total | 
我们确保将要加入的同一用户的其他记录的总数更高:
| 1 2 3 4 5 6 7 | purchases.id, purchases.customer, purchases.total, p.id, p.customer, p.total 1 , Tom , 200 , 2 , Tom , 300 2 , Tom , 300 3 , Bob , 400 , 4 , Bob , 500 4 , Bob , 500 5 , Alice , 600 , 6 , Alice , 700 6 , Alice , 700 | 
这将帮助我们筛选无需分组的每个采购的最高总额:
| 1 2 3 4 5 6 | WHERE p.total IS NULL purchases.id, purchases.name, purchases.total, p.id, p.name, p.total 2 , Tom , 300 4 , Bob , 500 6 , Alice , 700 | 
这就是我们需要的答案。
对PostgreSQL、U-SQL、IBM DB2和Google BigQuery SQL使用
| 1 2 3 | SELECT customer, (ARRAY_AGG(id ORDER BY total DESC))[1], MAX(total) FROM purchases GROUP BY customer | 
在SQL Server中,可以执行以下操作:
| 1 2 3 4 5 6 7 | SELECT * FROM ( SELECT ROW_NUMBER() OVER(PARTITION BY customer ORDER BY total DESC) AS StRank, * FROM Purchases) n WHERE StRank = 1 | 
说明:这里的分组方式是根据客户进行的,然后按总数订购,然后每个分组都有一个序列号,称为Strank,我们将选出第一个客户,Strank为1。
接受的OMG PONIES的"任何数据库支持"解决方案在我的测试中速度很快。
在这里,我提供了相同的方法,但更完整和干净的任何数据库解决方案。考虑绑定(假设希望每个客户只获得一行,甚至每个客户的最大合计有多个记录),并且将为采购表中的实际匹配行选择其他采购字段(例如采购付款ID)。
任何数据库都支持:
| 1 2 3 4 5 6 7 8 9 10 | 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月的编辑,MS-SQL和Oracle数据库目前不支持
对于SQL Server,最有效的方法是:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | WITH ids AS ( --condition for split table into groups SELECT i FROM (VALUES (9),(12),(17),(18),(19),(20),(22),(21),(23),(10)) AS v(i) ) ,src AS ( SELECT * FROM yourTable WHERE <condition> --use this as filter for other conditions ) ,joined AS ( SELECT tops.* FROM ids CROSS apply --it`s like for each rows ( SELECT top(1) * FROM src WHERE CommodityId = ids.i ) AS tops ) SELECT * FROM joined | 
别忘了为使用过的列创建聚集索引
- 如果要从聚合行集合中选择任何行(根据特定条件)。 
- 如果要使用除 - max/min 之外的另一个(- sum/avg 聚合函数。因此,你不能使用线索与- DISTINCT ON 。
可以使用下一个子查询:
| 1 2 3 4 5 6 7 8 9 10 | SELECT   ( SELECT **id** FROM t2 WHERE id = ANY ( ARRAY_AGG( tf.id ) ) AND amount = MAX( tf.amount ) ) id, name, MAX(amount) ma, SUM( ratio ) FROM t2 tf GROUP BY name | 
您可以用一个限制条件来替换
但是如果你想做这样的事情,你可能会寻找窗口函数