Dav*_*ver 1205 sql sqlite postgresql group-by greatest-n-per-group
正如标题所示,我想选择用a组成的每组行的第一行GROUP BY
.
具体来说,如果我有一个purchases
看起来像这样的表:
SELECT * FROM purchases;
Run Code Online (Sandbox Code Playgroud)
我的输出:
id | customer | total ---+----------+------ 1 | Joe | 5 2 | Sally | 3 3 | Joe | 2 4 | Sally | 1
我想查询每个产品id
的最大购买量(total
)customer
.像这样的东西:
SELECT FIRST(id), customer, FIRST(total)
FROM purchases
GROUP BY customer
ORDER BY total DESC;
Run Code Online (Sandbox Code Playgroud)
预期产出:
FIRST(id) | customer | FIRST(total) ----------+----------+------------- 1 | Joe | 5 2 | Sally | 3
Erw*_*ter 1044
在PostgreSQL中,这通常更简单,更快(下面的性能优化更多):
SELECT DISTINCT ON (customer)
id, customer, total
FROM purchases
ORDER BY customer, total DESC, id;
Run Code Online (Sandbox Code Playgroud)
或者更短(如果不是很清楚)带有序数的输出列:
SELECT DISTINCT ON (2)
id, customer, total
FROM purchases
ORDER BY 2, 3 DESC, 1;
Run Code Online (Sandbox Code Playgroud)
如果total
可以为NULL(不会对任何方式造成伤害,但您希望匹配现有索引):
...
ORDER BY customer, total DESC NULLS LAST, id;
Run Code Online (Sandbox Code Playgroud)
DISTINCT ON
是标准的PostgreSQL扩展(其中只定义DISTINCT
了整个SELECT
列表).
列出DISTINCT ON
子句中的任意数量的表达式,组合的行值定义重复项.手册:
显然,如果两行在至少一个列值上不同,则认为它们是不同的.在此比较中,空值被认为是相等的.
大胆强调我的.
DISTINCT ON
可以结合使用ORDER BY
.前导表达式必须以DISTINCT ON
相同的顺序匹配前导表达式.您可以添加其他表达式以ORDER BY
从每个对等组中选择特定行.我添加id
了最后一项来打破关系:
" id
从每个组中选择最小的行,选择最高的行total
."
如果ORDER BY
可以为NULL,则您很可能希望具有最大非空值的行.添加total
如证明.细节:
该NULLS LAST
列表不受表达式SELECT
或DISTINCT ON
以任何方式约束.(在上面的简单案例中不需要):
您不必在ORDER BY
或中包含任何表达式DISTINCT ON
.
您可以在ORDER BY
列表中包含任何其他表达式.这有助于用子查询和聚合/窗口函数替换更复杂的查询.
我使用Postgres版本8.3 - 11进行了测试.但是至少从版本7.1开始,该功能一直存在,所以基本上总是如此.
上述查询的完美索引将是一个多列索引,它跨越匹配顺序中的所有三列并具有匹配的排序顺序:
CREATE INDEX purchases_3c_idx ON purchases (customer, total DESC, id);
Run Code Online (Sandbox Code Playgroud)
可能太专业了.但是,如果特定查询的读取性能至关重要,请使用它.如果您SELECT
在查询中,请在索引中使用相同的内容,以便排序顺序匹配并且索引适用.
在为每个查询创建定制索引之前,权衡成本和收益.上述指数的潜力在很大程度上取决于数据分布.
使用索引是因为它提供了预先排序的数据.在Postgres 9.2或更高版本中,如果索引小于基础表,则查询也可以从仅索引扫描中受益.但是,索引必须完整扫描.
对于每个客户几行(列中的高基数DESC NULLS LAST
),这非常有效.如果你还需要分类输出,那就更是如此了.随着每个客户的行数不断增加,收益会减少.
理想情况下,您有足够的时间customer
来处理RAM中涉及的排序步骤而不会溢出到磁盘.但通常设置work_mem
过高会产生不利影响.考虑work_mem
特别大的查询.找出你需要多少SET LOCAL
.在排序步骤中提到" 磁盘: "表示需要更多:
对于每个客户的许多行(列中的低基数EXPLAIN ANALYZE
),松散的索引扫描(也称为"跳过扫描")将会(更高)更有效,但是没有实现到Postgres 11.(计划仅用于索引的扫描的实现)对于Postgres 12.请看这里和这里.)
现在,有更快的查询技术来替代它.特别是如果您有一个单独的表,其中包含唯一的客户,这是典型的用例.但如果你不这样做:
我在这里有一个简单的基准,现在已经过时了.我在这个单独的答案中用详细的基准代替了它.
OMG*_*ies 1019
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
Run Code Online (Sandbox Code Playgroud)
但是你需要添加逻辑来打破关系:
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
Run Code Online (Sandbox Code Playgroud)
Erw*_*ter 123
测试中最有趣的候选人的Postgres 9.4和9.5用的中途现实表20万行中purchases
和10K不同customer_id
(平均每用户20行).
对于Postgres 9.5,我有效地为86446个不同的客户进行了第二次测试.见下文(每位客户平均2.3行).
主表
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
);
Run Code Online (Sandbox Code Playgroud)
我使用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;
Run Code Online (Sandbox Code Playgroud)
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;
Run Code Online (Sandbox Code Playgroud)
在我的第二次 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 | |
Run Code Online (Sandbox Code Playgroud)
row_number()
在CTE,(见其他答案)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;
Run Code Online (Sandbox Code Playgroud)
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;
Run Code Online (Sandbox Code Playgroud)
DISTINCT ON
(见其他答案)SELECT DISTINCT ON (customer_id)
id, customer_id, total
FROM purchases
ORDER BY customer_id, total DESC, id;
Run Code Online (Sandbox Code Playgroud)
LATERAL
查询的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;
Run Code Online (Sandbox Code Playgroud)
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;
Run Code Online (Sandbox Code Playgroud)
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;
Run Code Online (Sandbox Code Playgroud)
上述查询的执行时间EXPLAIN ANALYZE
(以及所有选项关闭),最好是5次运行.
所有查询都使用了"仅索引扫描 " purchases2_3c_idx
(以及其他步骤).其中一些只是针对较小的索引,其他更有效.
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
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
customer_id
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
Run Code Online (Sandbox Code Playgroud)
我在PostgreSQL 9.1上运行了三次测试,在一个包含65579行的实际生命表上,在所涉及的三列中的每一列上都有单列btree索引,并且执行了5次运行的最佳执行时间.
将@OMGPonies的第一个查询(A
)与上述DISTINCT ON
解决方案(B
)进行比较:
选择整个表,在这种情况下结果为5958行.
A: 567.218 ms
B: 386.673 ms
Run Code Online (Sandbox Code Playgroud)使用条件WHERE customer BETWEEN x AND y
导致1000行.
A: 249.136 ms
B: 55.111 ms
Run Code Online (Sandbox Code Playgroud)选择一个客户WHERE customer = x
.
A: 0.143 ms
B: 0.072 ms
Run Code Online (Sandbox Code Playgroud)用另一个答案中描述的索引重复相同的测试
CREATE INDEX purchases_3c_idx ON purchases (customer, total DESC, id);
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
TMS*_*TMS 50
这是常见的最大n组问题,已经有经过充分测试和高度优化的解决方案.我个人更喜欢Bill Karwin的左连接解决方案(原始帖子中有很多其他解决方案).
请注意,对于这个常见问题的一堆解决方案可以在大多数官方来源,MySQL手册中找到!请参阅常见查询示例::保持某个列的分组最大值的行.
Pau*_*rth 27
在Postgres中你可以array_agg
像这样使用:
SELECT customer,
(array_agg(id ORDER BY total DESC))[1],
max(total)
FROM purchases
GROUP BY customer
Run Code Online (Sandbox Code Playgroud)
这将为您id
提供每个客户最大的购买.
有些事情需要注意:
array_agg
是一个聚合函数,所以它适用GROUP BY
.array_agg
允许您指定作用于自身的排序,因此它不会约束整个查询的结构.如果您需要执行与默认值不同的操作,还有关于如何对NULL进行排序的语法.array_agg
以类似的方式使用第三个输出列,但max(total)
更简单.DISTINCT ON
,使用array_agg
让你保留你的GROUP BY
,以防你出于其他原因.use*_*394 12
由于存在SubQ,Erwin指出解决方案效率不高
select * from purchases p1 where total in
(select max(total) from purchases where p1.customer=customer) order by total desc;
Run Code Online (Sandbox Code Playgroud)
非常快的解决方案
SELECT a.*
FROM
purchases a
JOIN (
SELECT customer, min( id ) as id
FROM purchases
GROUP BY customer
) b USING ( id );
Run Code Online (Sandbox Code Playgroud)
如果表由id索引,那么非常快
create index purchases_id on purchases (id);
Run Code Online (Sandbox Code Playgroud)
我用这种方式(仅限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
);
Run Code Online (Sandbox Code Playgroud)
那么你的例子应该工作,几乎为是:
SELECT FIRST(id), customer, FIRST(total)
FROM purchases
GROUP BY customer
ORDER BY FIRST(total) DESC;
Run Code Online (Sandbox Code Playgroud)
CAVEAT:忽略NULL行
现在我用这种方式: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'
Run Code Online (Sandbox Code Playgroud)
这是一个postgres扩展,为您提供第一个和最后一个功能; 显然比上述方式更快.
如果您使用聚合函数(如这些),您可以订购结果,而无需已经订购数据:
http://www.postgresql.org/docs/current/static/sql-expressions.html#SYNTAX-AGGREGATES
Run Code Online (Sandbox Code Playgroud)
所以等效的例子,排序将是这样的:
SELECT first(id order by id), customer, first(total order by id)
FROM purchases
GROUP BY customer
ORDER BY first(total);
Run Code Online (Sandbox Code Playgroud)
当然,您可以按照您认为适合的方式订购和过滤; 它的语法非常强大.
查询:
SELECT purchases.*
FROM purchases
LEFT JOIN purchases as p
ON
p.customer = purchases.customer
AND
purchases.total < p.total
WHERE p.total IS NULL
Run Code Online (Sandbox Code Playgroud)
这是如何运作的!(我去过那儿)
我们希望确保每次购买的总数最高.
一些理论上的东西(如果您只想了解查询,请跳过此部分)
设Total为函数T(customer,id),返回给定名称和id的值为了证明给定的总数(T(customer,id))是最高的,我们必须证明我们要证明
要么
第一种方法需要我们获取我不喜欢的那个名字的所有记录.
第二个需要一个聪明的方式来说没有比这个更高的记录.
回到SQL
如果我们在名称上加入表,并且总数少于连接表:
LEFT JOIN purchases as p
ON
p.customer = purchases.customer
AND
purchases.total < p.total
Run Code Online (Sandbox Code Playgroud)
我们确保所有具有相同用户总数较高的记录的记录加入:
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
Run Code Online (Sandbox Code Playgroud)
这将有助于我们过滤每次购买的最高总额,而无需分组:
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
Run Code Online (Sandbox Code Playgroud)
这就是我们需要的答案.
小智 7
first_value
在 PostgreSQL 中,另一种可能性是结合使用窗口函数SELECT DISTINCT
:
select distinct customer_id,
first_value(row(id, total)) over(partition by customer_id order by total desc, id)
from purchases;
Run Code Online (Sandbox Code Playgroud)
我创建了一个 复合(id, total)
,因此两个值都由同一聚合返回。当然,您始终可以申请first_value()
两次。
在SQL Server中,您可以执行以下操作:
SELECT *
FROM (
SELECT ROW_NUMBER()
OVER(PARTITION BY customer
ORDER BY total DESC) AS StRank, *
FROM Purchases) n
WHERE StRank = 1
Run Code Online (Sandbox Code Playgroud)
说明:这里的 分组依据是根据客户进行的,然后按总数进行订购,然后每个此类组的序列号都称为StRank,我们要取出第一个StRank为1的客户
ARRAY_AGG
对PostgreSQL,U-SQL,IBM DB2和Google BigQuery SQL使用功能:
SELECT customer, (ARRAY_AGG(id ORDER BY total DESC))[1], MAX(total)
FROM purchases
GROUP BY customer
Run Code Online (Sandbox Code Playgroud)
这样它对我有用:
SELECT article, dealer, price
FROM shop s1
WHERE price=(SELECT MAX(s2.price)
FROM shop s2
WHERE s1.article = s2.article
GROUP BY s2.article)
ORDER BY article;
Run Code Online (Sandbox Code Playgroud)
选择每件商品的最高价格
小智 6
这就是我们如何通过使用 windows 函数来实现这一点:
create table purchases (id int4, customer varchar(10), total integer);
insert into purchases values (1, 'Joe', 5);
insert into purchases values (2, 'Sally', 3);
insert into purchases values (3, 'Joe', 2);
insert into purchases values (4, 'Sally', 1);
select ID, CUSTOMER, TOTAL from (
select ID, CUSTOMER, TOTAL,
row_number () over (partition by CUSTOMER order by TOTAL desc) RN
from purchases) A where RN = 1;
Run Code Online (Sandbox Code Playgroud)
从我的测试来看,公认的 OMG Ponies 的“受任何数据库支持”解决方案具有良好的速度。
在这里,我提供了一个相同的方法,但更完整和干净的任何数据库解决方案。考虑平局(假设希望为每个客户只获取一行,甚至为每个客户的最大总数获取多个记录),并且将为购买表中的真实匹配行选择其他购买字段(例如purchase_ payment_id)。
任何数据库都支持:
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
Run Code Online (Sandbox Code Playgroud)
该查询相当快,特别是当购买表上存在诸如(客户,总计)之类的复合索引时。
评论:
t1、t2 是子查询别名,可以根据数据库删除。
警告:using (...)
截至 2017 年 1 月的编辑,MS-SQL 和 Oracle 数据库目前不支持该子句。您必须自己将其扩展为例如等on t2.id = purchase.id
。USING 语法适用于 SQLite、MySQL 和 PostgreSQL。
Snowflake/Teradata 支持QUALIFY
类似于HAVING
窗口函数的子句:
SELECT id, customer, total
FROM PURCHASES
QUALIFY ROW_NUMBER() OVER(PARTITION BY p.customer ORDER BY p.total DESC) = 1
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
945566 次 |
最近记录: |