Seleccionar a primeira linha em cada grupo por grupo?
como o título sugere, gostaria de seleccionar a primeira linha de cada conjunto de linhas agrupadas com um GROUP BY
.
especificamente, se eu tiver uma tabela purchases
que se pareça com isto:
SELECT * FROM purchases;
A Minha Saída:
id | customer | total ---+----------+------ 1 | Joe | 5 2 | Sally | 3 3 | Joe | 2 4 | Sally | 1
Eu gostaria de perguntar para o id
da maior compra (total
) feita por cada customer
. Algo do género:
SELECT FIRST(id), customer, FIRST(total)
FROM purchases
GROUP BY customer
ORDER BY total DESC;
Resultado Esperado:
FIRST(id) | customer | FIRST(total) ----------+----------+------------- 1 | Joe | 5 2 | Sally | 3
11 answers
On Oracle 9.2+ (not 8i+ as originally stated), 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
Suportado por qualquer base de dados:
Mas tens de adicionar lógica para quebrar laços. 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
In PostgreSQL this is typically simpler and faster (more performance optimization below):
SELECT DISTINCT ON (customer)
id, customer, total
FROM purchases
ORDER BY customer, total DESC, id;
Ou mais curto (se não tão claro) com números ordinais de colunas de saída:
SELECT DISTINCT ON (2)
id, customer, total
FROM purchases
ORDER BY 2, 3 DESC, 1;
Se total
pode ser nulo (não vai doer de nenhuma maneira, mas vai querer corresponder aos índices existentes):
...
ORDER BY customer, total DESC NULLS LAST, id;
Pontos principais
DISTINCT ON
é uma extensão PostgreSQL da norma (onde apenasDISTINCT
em toda a listaSELECT
está definido).-
Enumerar qualquer número de expressões na Cláusula
DISTINCT ON
, o valor combinado da linha define duplicados. o manual:Obviamente, duas linhas são consideradas distintas se diferirem pelo menos em um valor de coluna. Os valores nulos são considerados iguais nesta comparação.
Ênfase ousada minha.
-
DISTINCT ON
pode ser combinado comORDER BY
. As expressões iniciais têm de corresponder ao 'leading'DISTINCT ON
expressões na mesma ordem. Você pode adicionarexpressões adicionais aORDER BY
para escolher uma dada linha de cada grupo de pares. Eu adicioneiid
como último item para quebrar laços:"escolha a linha com o menor
id
de cada grupo que partilha o mais altototal
."Se
total
pode ser nulo, você provavelmente quer a linha com o maior valor não nulo. AdicionarNULLS LAST
como demonstrado. Detalhes: -
A lista
SELECT
não é restringida por expressões emDISTINCT ON
ouORDER BY
de qualquer forma. (Não necessário no caso simples acima):Você não tem que incluir nenhuma das expressões em
DISTINCT ON
ouORDER BY
.Você pode incluir qualquer outra expressão na lista
SELECT
. Isto é fundamental para substituir consultas muito mais complexas por subquerias e agregado / janela funcao.
Eu testei com versões Postgres 8.3-10. Mas o recurso tem estado lá pelo menos desde a versão 7.1, então basicamente sempre.
Índice
O índice perfeito para a consulta acima seria um índice multi-coluna abrangendo todas as três colunas em sequência correspondente e com ordem de ordenação correspondente:
CREATE INDEX purchases_3c_idx ON purchases (customer, total DESC, id);
Pode ser demasiado especializado para aplicações no mundo real. Mas use-o se o desempenho de leitura é crucial. Se você tem DESC NULLS LAST
na consulta, use o mesmo no índice para que o Postgres saiba que a ordem de ordenação corresponde.
Eficácia / optimização do desempenho
Você tem que pesar o custo e o benefício antes de criar um índice adaptado para cada consulta. O potencial do índice acima depende em grande medida da distribuição de Dados .O índice é utilizado porque fornece dados pré-ordenados, e em Postgres 9.2 ou mais tarde a consulta também pode beneficiar de um apenas índice pesquisar Se o índice for menor que o quadro subjacente. No entanto, o índice tem de ser escaneado na sua totalidade.
-
Para algumas linhas por cliente , isto é muito eficiente (ainda mais se precisar de saída ordenada de qualquer forma). O benefício encolhe com um número crescente de linhas por cliente.
Idealmente, você tem o suficientework_mem
para processar o passo de ordenação envolvido em RAM e não derramar para o disco. Configuração geralwork_mem
demasiado elevado pode ter efeitos adversos. ConsidereSET LOCAL
para perguntas excepcionalmente grandes. Descobre quanto precisas comEXPLAIN ANALYZE
. A menção de "Disk:" na classificação da etapa indica a necessidade de mais: -
Para muitas linhas por cliente , a índice solto pesquisar seria (muito) mais eficiente, mas isso não é implementado atualmente em Postgres (até v10).
Existem técnicas de consulta mais rápidas para substituir isto. Em particular, se você tem uma tabela separada com clientes únicos, que é o caso de uso típico. Mas também se você não:
Parâmetro de referência
Tinha aqui uma referência simples que já está ultrapassada. Substituí - o por um referência pormenorizada nesta resposta separada.Parâmetro de referência
Teste o mais interessante candidatos com Postgres 9.4 e 9.5 com um a meio caminho de mesa realistas de 200k linhas em purchases
e 10k distintas customer_id
(avg. 20 linhas por cliente ).
Configuração
Tabela principal
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
);
Uso um serial
(PK restrição adicionada abaixo) e um inteiro customer_id
, uma vez que é uma configuração mais típica. Também adicionou some_column
para compensar tipicamente mais colunas.
Dados fictícios, PK, index - uma tabela típica também tem algumas tuplas mortas:
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
tabela-para pesquisa superior
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;
No meu segundo teste para a rede de 9,5 eu usei a mesma configuração, mas com random() * 100000
para gerar customer_id
para obter apenas poucas linhas por customer_id
.
Tamanhos de objectos para a tabela purchases
Gerado com esta consulta .
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 | |
Perguntas
1. {[28] } em ETI, (ver outra resposta)
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()
em subcontingente (minha optimização)
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
(ver outra resposta)
SELECT DISTINCT ON (customer_id)
id, customer_id, total
FROM purchases
ORDER BY customer_id, total DESC, id;
4. rCTE com LATERAL
subquery (veja aqui)
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
tabela com LATERAL
(veja aqui)
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()
com ORDER BY
(ver outros resposta)
SELECT (array_agg(id ORDER BY total DESC))[1] AS id
, customer_id
, max(total) AS total
FROM purchases
GROUP BY customer_id;
Resultados
Tempo de execução para consultas acima com EXPLAIN ANALYZE
(e todas as opções desligadas), best of 5 runs .
todas as consultas utilizaram um índice apenas digitaliza em purchases2_3c_idx
(entre outros passos). Alguns deles apenas para o tamanho menor do Índice, outros mais eficazmente.
A. Postgres 9.4 com linhas de 200k e ~ 20 por 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. O mesmo acontece com os 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. igual a B., mas com ~ 2.3 linhas por 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
Referência Original (desactualizada) de 2011
Fiz três testes com PostgreSQL.9.1 numa tabela da vida real de 65579 linhas e índices btree de coluna única em cada uma das três colunas envolvidas e tomou o melhor tempo de execução de 5 corridas.Comparando @OMGPonies' primeira consulta (
A
) para o acima DISTINCT ON
solução (B
):
-
Seleccionar a tabela inteira, resulta em 5958 linhas neste caso.
A: 567.218 ms B: 386.673 ms
-
Condição de Utilização
WHERE customer BETWEEN x AND y
resultando em 1000 linhas.A: 249.136 ms B: 55.111 ms
-
Seleccione um único cliente com
WHERE customer = x
.A: 0.143 ms B: 0.072 ms
O mesmo teste repetido com o índice descrito na outra resposta
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
Este é um problema comum maior-n-Por-Grupo, que já tem soluções bem testadas e altamente optimizadas . Pessoalmente, prefiro a solução de junta esquerda de Bill Karwin (O post original de com muitas outras soluções ).
Note que um monte de soluções para este problema comum pode ser surpreendentemente encontrado na uma das fontes mais oficiais, MySQL manual! Ver exemplos de consultas comuns:: as linhas que seguram o Grupo Máximo de uma determinada coluna.
Em Postgres pode usar array_agg
assim:
SELECT customer,
(array_agg(id ORDER BY total DESC))[1],
max(total)
FROM purchases
GROUP BY customer
Isto dar-lhe-á A maior compra de cada cliente.
Algumas coisas a notar:
-
array_agg
é uma função agregada, por isso funciona comGROUP BY
. -
array_agg
permite-lhe especificar uma ordenação delimitada a si mesma, para que não restrinja a estrutura de toda a consulta. Há também uma sintaxe para como você ordenar NULLs, se você precisar fazer algo diferente do padrão.
Uma vez construímos a matriz, tomamos o primeiro elemento. (As matrizes Postgres são indexadas a 1, não indexadas a 0).
- Você poderia usar
array_agg
de uma forma semelhante para a sua terceira coluna de saída, masmax(total)
é mais simples. - Ao contrário de
DISTINCT ON
, o uso dearray_agg
permite-lhe manter o seuGROUP BY
, no caso de querer isso por outras razões.
A solução não é muito eficiente como apontado por Erwin, devido à presença de SubQs
select * from purchases p1 where total in
(select max(total) from purchases where p1.customer=customer) order by total desc;
Eu uso este caminho (apenas 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
);
Então o seu exemplo deve funcionar quase Como é:
SELECT FIRST(id), customer, FIRST(total)
FROM purchases
GROUP BY customer
ORDER BY FIRST(total) DESC;
Ressalva: ignora as linhas nulas
Editar 1-Use a extensão postgres em vez de
Agora uso este caminho: http://pgxn.org/dist/first_last_agg/
Para instalar no 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'
É uma extensão postgres que te dá o primeiro e o último funções; aparentemente mais rápido do que a maneira acima.
Editar 2-ordenação e filtragem
Se você usar funções agregadas( como estas), você pode encomendar os resultados, sem a necessidade de ter os dados já ordenados:
http://www.postgresql.org/docs/current/static/sql-expressions.html#SYNTAX-AGGREGATES
Então o exemplo equivalente, com a ordenação seria algo como:
SELECT first(id order by id), customer, first(total order by id)
FROM purchases
GROUP BY customer
ORDER BY first(total);
Claro que você pode pedir e filtrar como você considera caber dentro do agregado; é uma sintaxe muito poderosa.
Solução muito rápida
SELECT a.*
FROM
purchases a
JOIN (
SELECT customer, min( id ) as id
FROM purchases
GROUP BY customer
) b USING ( id );
E muito rapidamente se a tabela for indexada por id:
create index purchases_id on purchases (id);
A Pergunta:
SELECT purchases.*
FROM purchases
LEFT JOIN purchases as p
ON
p.customer = purchases.customer
AND
purchases.total < p.total
WHERE p.total IS NULL
COMO É QUE ISSO FUNCIONA? (Eu estive lá)
Queremos ter a certeza que só temos o total mais alto para cada compra.Algumas coisas teóricas (ignorar esta parte se só quiser compreender a Pesquisa)
Deixe o Total ser uma função T (cliente, id) onde devolve um valor dado o nome e o id Para provar que o total indicado (t (cliente, id)) é o mais elevado que temos de provar que Queremos provar:
- ÿx (cliente, id) > T (cliente, x) (Este total é superior a todos os outros total para esse cliente)
Ou
- x x t(cliente, id)
A primeira abordagem vai precisar de nós para obter todos os registros para esse nome que eu realmente não gosto.
O segundo vai precisar de uma forma inteligente de dizer que não pode haver registo maior do que este.Voltar para SQL
Se nos juntarmos à mesa sobre o nome e o total ser inferior à tabela junta:
LEFT JOIN purchases as p
ON
p.customer = purchases.customer
AND
purchases.total < p.total
Garantimos que todos os registos que tenham outro registo com o total mais elevado para o mesmo Utilizador a ser ligado:
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
Isso vai ajudar-nos a filtrar o total mais elevado para cada compra sem necessidade de agrupamento:
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
E essa é a resposta que precisamos.
A solução aceite de póneis OMG "suportada por qualquer base de dados" tem boa velocidade do meu teste.
Aqui eu forneço a mesma abordagem, mas mais completa e limpa qualquer base de dados. Os empates são considerados (assumir o desejo de obter apenas uma linha para cada cliente, mesmo vários registos para o total máximo por cliente), e outros campos de compra (por exemplo, purchase_payment_id) serão seleccionados para as linhas de correspondência reais na tabela de compra.Suportado por qualquer base de dados:
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
Esta consulta é razoavelmente rápida, especialmente quando há um índice composto como (cliente, total) na tabela de compra.
Observação:
T1, t2 são alcunhas subquery que podem ser removidas dependendo da base de dados.
Caveat : a cláusula
using (...)
actualmente não é suportada em MS-SQL e Oracle db a partir desta edição em Jan 2017. Você mesmo tem que expandi-lo para, por exemplo,on t2.id = purchase.id
etc. A sintaxe usada funciona em SQLite, MySQL e PostgreSQL.
Se você quiser selecionar qualquer linha (por sua condição específica) do conjunto de Linhas agregadas.
Se quiser usar outra função de agregação (
sum/avg
Para além demax/min
. Assim você não pode usar o clue comDISTINCT ON
Pode usar o seguinte subquery:
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
Você pode substituir amount = MAX( tf.amount )
por qualquer condição que deseje com uma restrição: esta subquota não deve devolver mais do que uma linha