Como o título sugere, eu gostaria de selecionar a primeira linha de cada conjunto de linhas agrupadas com a GROUP BY
.
Especificamente, se eu tenho uma purchases
tabela assim:
SELECT * FROM purchases;
Minha saída:
id | cliente | total --- + ---------- + ------ 1 | Joe 5 2 Sally 3 3 Joe 2 4 Sally 1
Gostaria de consultar id
a maior compra ( total
) feita por cada um customer
. Algo assim:
SELECT FIRST(id), customer, FIRST(total)
FROM purchases
GROUP BY customer
ORDER BY total DESC;
Saída esperada:
PRIMEIRO (id) | cliente | PRIMEIRO (total) ---------- + ---------- + ------------- 1 | Joe 5 2 Sally 3
sql
sqlite
postgresql
group-by
greatest-n-per-group
David Wolever
fonte
fonte
MAX(total)
?Respostas:
No Oracle 9.2+ (não 8i +, como originalmente declarado), SQL Server 2005+, PostgreSQL 8.4+, DB2, Firebird 3.0+, Teradata, Sybase, Vertica:
Suportado por qualquer banco de dados:
Mas você precisa adicionar lógica para romper laços:
fonte
ROW_NUMBER() OVER(PARTITION BY [...])
juntamente com outras otimizações, me ajudou a reduzir uma consulta de 30 segundos para alguns milissegundos. Obrigado! (PostgreSQL 9.2)total
para um cliente, a primeira consulta retornará um vencedor arbitrário (dependendo dos detalhes da implementação; aid
alteração pode ser executada a cada execução!). Normalmente (nem sempre) você deseja uma linha por cliente, definida por critérios adicionais como "aquele com o menorid
". Para corrigir, acrescenteid
àORDER BY
lista derow_number()
. Então você obtém o mesmo resultado da segunda consulta, o que é muito ineficiente para este caso. Além disso, você precisaria de outra subconsulta para cada coluna adicional.No PostgreSQL, isso geralmente é mais simples e rápido (mais otimização de desempenho abaixo):
Ou mais curto (se não tão claro) com números ordinais de colunas de saída:
Se
total
puder ser NULL (não será prejudicial de qualquer maneira, mas você desejará corresponder aos índices existentes ):Pontos principais
DISTINCT ON
é uma extensão PostgreSQL do padrão (onde apenasDISTINCT
aSELECT
lista inteira é definida).Liste qualquer número de expressões na
DISTINCT ON
cláusula, o valor da linha combinada define duplicatas. O manual:Negrito ênfase minha.
DISTINCT ON
pode ser combinado comORDER BY
. As expressões iniciais emORDER BY
devem estar no conjunto de expressões emDISTINCT ON
, mas você pode reorganizar a ordem entre elas livremente. Exemplo. Você pode adicionar expressões adicionaisORDER BY
para selecionar uma linha específica de cada grupo de pares. Ou, como o manual coloca :Eu adicionei
id
como último item para romper os laços:"Escolha a linha com a menor
id
de cada grupo que compartilhe a mais altatotal
".Para ordenar os resultados de uma maneira que não concorde com a ordem de classificação que determina a primeira por grupo, você pode aninhar a consulta acima em uma consulta externa com outra
ORDER BY
. Exemplo.Se
total
puder ser NULL, você provavelmente desejará a linha com o maior valor não nulo. AdicioneNULLS LAST
como demonstrado. Vejo:A
SELECT
lista não é restrita por expressõesDISTINCT ON
ouORDER BY
de qualquer forma. (Não é necessário no caso simples acima):Você não precisa incluir nenhuma das expressões em
DISTINCT ON
ouORDER BY
.Você pode incluir qualquer outra expressão na
SELECT
lista. Isso é fundamental para substituir consultas muito mais complexas por subconsultas e funções agregadas / janelas.Eu testei com o Postgres versões 8.3 - 12. Mas o recurso existe desde pelo menos a versão 7.1, então basicamente sempre.
Índice
O índice perfeito para a consulta acima seria um índice de várias colunas, abrangendo todas as três colunas na sequência correspondente e com a ordem de classificação correspondente:
Pode ser muito especializado. Mas use-o se o desempenho de leitura para uma consulta específica for crucial. Se você tiver
DESC NULLS LAST
na consulta, use o mesmo no índice para que a ordem de classificação corresponda e o índice seja aplicável.Efetividade / otimização de desempenho
Pese o custo e o benefício antes de criar índices personalizados para cada consulta. O potencial do índice acima depende em grande parte da distribuição dos dados .
O índice é usado porque fornece dados pré-classificados. No Postgres 9.2 ou posterior, a consulta também pode se beneficiar de uma varredura de índice apenas se o índice for menor que a tabela subjacente. O índice deve ser verificado na íntegra, no entanto.
Para poucas linhas por cliente (alta cardinalidade na coluna
customer
), isso é muito eficiente. Ainda mais se você precisar de saída classificada de qualquer maneira. O benefício diminui com um número crescente de linhas por cliente.Idealmente, você tem o suficiente
work_mem
para processar a etapa de classificação envolvida na RAM e não derramar no disco. Mas, geralmente, a configuraçãowork_mem
muito alta pode ter efeitos adversos. ConsidereSET LOCAL
para consultas excepcionalmente grandes. Encontre quanto você precisaEXPLAIN ANALYZE
. A menção de " Disco: " na etapa de classificação indica a necessidade de mais:Para muitas linhas por cliente (baixa cardinalidade na coluna
customer
), uma varredura de índice frouxa (também conhecida como "ignorar varredura") seria (muito) mais eficiente, mas isso não foi implementado até o Postgres 12. (Uma implementação para varreduras somente de índice está em desenvolvimento para o Postgres 13. Veja aqui e aqui .)Por enquanto, existem técnicas de consulta mais rápidas para substituir isso. Em particular, se você tiver uma tabela separada com clientes exclusivos, que é o caso de uso típico. Mas também se você não:
Referência
Eu tinha uma referência simples aqui, que está desatualizada agora. Substituí-lo por uma referência detalhada nesta resposta separada .
fonte
DISTINCT ON
se torna extremamente lento. A implementação sempre classifica a tabela inteira e procura duplicatas, ignorando todos os índices (mesmo se você tiver criado o índice de várias colunas necessário). Consulte explainextended.com/2009/05/03/postgresql-optimizing-distinct para obter uma possível solução.SELECT
lista.DISTINCT ON
é bom apenas para obter uma linha por grupo de pares.Referência
Testando os candidatos mais interessantes com Postgres 9.4 e 9.5 com uma mesa no meio do caminho realista de 200 mil linhas em
purchases
e 10k distintacustomer_id
( avg. 20 linhas por cliente ).Para o Postgres 9.5, realizei um segundo teste com 86446 clientes distintos. Veja abaixo ( média de 2,3 linhas por cliente ).
Configuração
Tabela principal
Eu uso um
serial
(restrição de PK adicionada abaixo) e um número inteiro,customer_id
pois essa é uma configuração mais típica. Também adicionadosome_column
para compensar tipicamente mais colunas.Dados fictícios, PK, índice - uma tabela típica também possui algumas tuplas mortas:
customer
tabela - para consulta superiorNo meu segundo teste para a versão 9.5, usei a mesma configuração, mas com
random() * 100000
para gerarcustomer_id
para obter apenas algumas linhas porcustomer_id
.Tamanhos de objeto para tabela
purchases
Gerado com esta consulta .
Consultas
1.
row_number()
em CTE, ( ver outra resposta )2.
row_number()
na subconsulta (minha otimização)3.
DISTINCT ON
( veja outra resposta )4. rCTE com
LATERAL
subconsulta ( veja aqui )5.
customer
mesa comLATERAL
( veja aqui )6.
array_agg()
comORDER BY
( veja outra resposta )Resultados
Tempo de execução para as consultas acima com
EXPLAIN ANALYZE
(e todas as opções desativadas ), o melhor de 5 execuções .Todas as consultas utilizaram uma Varredura por Índice Apenas
purchases2_3c_idx
(entre outras etapas). Alguns deles apenas para o tamanho menor do índice, outros de forma mais eficaz.A. Postgres 9.4 com 200 mil linhas e ~ 20 por
customer_id
B. O mesmo com o Postgres 9.5
C. O mesmo que B., mas com ~ 2,3 linhas por
customer_id
Referências relacionadas
Aqui está um novo teste "ogr" com 10 milhões de linhas e 60 mil "clientes" únicos no Postgres 11.5 (atual em setembro de 2019). Os resultados ainda estão alinhados com o que vimos até agora:
Referência original (desatualizada) de 2011
Eu executei três testes com o PostgreSQL 9.1 em uma tabela da vida real de 65579 linhas e índices btree de coluna única em cada uma das três colunas envolvidas e aproveitei o melhor tempo de execução de 5 execuções.
Comparando a primeira consulta do @OMGPonies (
A
) com a solução acimaDISTINCT ON
(B
):Selecione a tabela inteira, resultando em 5958 linhas neste caso.
Use a condição
WHERE customer BETWEEN x AND y
resultante em 1000 linhas.Selecione um único cliente com
WHERE customer = x
.Mesmo teste repetido com o índice descrito na outra resposta
fonte
2. row_number()
e5. customer table with LATERAL
, o que garante que o ID seja o menor?customer_id
linha com a mais altatotal
. É uma coincidência enganosa nos dados de teste da pergunta que asid
linhas selecionadas também sejam as menores porcustomer_id
.Isso é comum maior n por grupoproblema, que já possui soluções bem testadas e altamente otimizadas . Pessoalmente, prefiro a solução de junção esquerda de Bill Karwin (a postagem original com muitas outras soluções ).
Observe que muitas soluções para esse problema comum podem ser encontradas surpreendentemente em uma das fontes mais oficiais, manual do MySQL ! Consulte Exemplos de consultas comuns :: As linhas que mantêm o máximo em grupo de uma determinada coluna .
fonte
DISTINCT ON
versão é muito mais curta, mais simples e geralmente apresenta um desempenho melhor no Postgres do que as alternativas com uma autoLEFT JOIN
ou semi-anti-junçãoNOT EXISTS
. Também é "bem testado".No Postgres você pode usar
array_agg
assim:Isso fornecerá a você a
id
maior compra de cada cliente.Algumas coisas a serem observadas:
array_agg
é uma função agregada, por isso trabalha comGROUP BY
.array_agg
permite especificar uma ordem com escopo definido apenas para si mesma, para que não restrinja a estrutura de toda a consulta. Também existe uma sintaxe de como você classifica NULLs, se precisar fazer algo diferente do padrão.array_agg
maneira semelhante para sua terceira coluna de saída, masmax(total)
é mais simples.DISTINCT ON
, o usoarray_agg
permite manter o seuGROUP BY
, caso você queira isso por outros motivos.fonte
A solução não é muito eficiente, como apontado por Erwin, devido à presença de SubQs
fonte
Eu uso desta maneira (apenas postgresql): https://wiki.postgresql.org/wiki/First/last_%28aggregate%29
Então seu exemplo deve funcionar quase como está:
CAVEAT: ignora as linhas NULL
Editar 1 - Use a extensão postgres
Agora eu uso desta maneira: http://pgxn.org/dist/first_last_agg/
Para instalar no ubuntu 14.04:
É uma extensão do postgres que oferece a primeira e a última funções; aparentemente mais rápido do que o caminho acima.
Edição 2 - Ordenação e filtragem
Se você usar funções agregadas (como estas), poderá solicitar os resultados, sem a necessidade de solicitar os dados já:
Portanto, o exemplo equivalente, com pedidos, seria algo como:
Obviamente, você pode solicitar e filtrar o que considerar adequado no agregado; é uma sintaxe muito poderosa.
fonte
A pergunta:
COMO ISSO FUNCIONA! (Eu estive lá)
Queremos garantir que tenhamos apenas o total mais alto para cada compra.
Algumas coisas teóricas (pule esta parte se você quiser apenas entender a consulta)
Seja Total uma função T (cliente, ID), onde ele retorna um valor com o nome e o ID. Para provar que o total fornecido (T (cliente, ID)) é o mais alto, temos que provar que queremos provar
OU
A primeira abordagem precisará de nós para obter todos os registros para esse nome que eu realmente não gosto.
O segundo precisará de uma maneira inteligente de dizer que não pode haver registro maior que este.
Voltar para SQL
Se sairmos, uniremos a tabela ao nome e o total será menor que a tabela unida:
garantimos que todos os registros que possuem outro registro com o total mais alto para o mesmo usuário sejam associados:
Isso nos ajudará a filtrar o total mais alto para cada compra, sem a necessidade de agrupamento:
E essa é a resposta que precisamos.
fonte
Solução muito rápida
e realmente muito rápido se a tabela for indexada por id:
fonte
No SQL Server, você pode fazer isso:
Explicação: Aqui, agrupar por é feito com base no cliente e, em seguida, solicitá-lo pelo total. Cada um desses grupos recebe o número de série como StRank e estamos contratando o primeiro cliente cujo StRank é 1
fonte
Use a
ARRAY_AGG
função para PostgreSQL , U-SQL , IBM DB2 e Google BigQuery SQL :fonte
No PostgreSQL, outra possibilidade é usar a
first_value
função window em combinação comSELECT DISTINCT
:Eu criei um composto
(id, total)
, para que ambos os valores sejam retornados pelo mesmo agregado. Você pode sempre aplicarfirst_value()
duas vezes.fonte
A solução aceita por OMG Ponies "Supported by any database" tem boa velocidade em meu teste.
Aqui, forneço uma solução da mesma abordagem, mas mais completa e limpa de qualquer banco de dados. Os laços são considerados (pressupõe o desejo de obter apenas uma linha para cada cliente, até vários registros para o total máximo por cliente) e outros campos de compra (por exemplo, purchase_payment_id) serão selecionados para as linhas correspondentes reais na tabela de compras.
Suportado por qualquer banco de dados:
Essa consulta é razoavelmente rápida, especialmente quando há um índice composto como (cliente, total) na tabela de compras.
Observação:
t1, t2 são alias de subconsulta que podem ser removidos dependendo do banco de dados.
Advertência : a
using (...)
cláusula atualmente não é suportada no MS-SQL e no Oracle db a partir desta edição em janeiro de 2017. Você precisa expandi-la para, por exemplo,on t2.id = purchase.id
etc. A sintaxe USING funciona no SQLite, MySQL e PostgreSQL.fonte
Snowflake / Teradata suporta
QUALIFY
cláusula que funciona comoHAVING
para funções em janela:fonte
Se você deseja selecionar qualquer linha (por alguma condição específica) do conjunto de linhas agregadas.
Se você deseja usar outra
sum/avg
função de agregação ( ) além demax/min
. Assim, você não pode usar pista comDISTINCT ON
Você pode usar a próxima subconsulta:
Você pode substituir
amount = MAX( tf.amount )
por qualquer condição que desejar por uma restrição: Esta subconsulta não deve retornar mais de uma linhaMas se você quiser fazer essas coisas, provavelmente está procurando funções da janela
fonte
Para o SQl Server, a maneira mais eficiente é:
e não se esqueça de criar um índice clusterizado para colunas usadas
fonte