Como posso GROUP BY
uma coluna, enquanto ordenando apenas por outra.
Estou tentando fazer o seguinte:
SELECT dbId,retreivalTime
FROM FileItems
WHERE sourceSite='something'
GROUP BY seriesName
ORDER BY retreivalTime DESC
LIMIT 100
OFFSET 0;
Quero selecionar os últimos /n/ itens de FileItems, em ordem decrescente, com as linhas filtradas por DISTINCT
valores de seriesName
. A consulta acima apresenta erros ERROR: column "fileitems.dbid" must appear in the GROUP BY clause or be used in an aggregate function
. Eu preciso do dbid
valor para então pegar a saída dessa consulta e JOIN
na tabela de origem para pegar o restante das colunas que eu estava.
Observe que esta é basicamente a gestalt da pergunta abaixo, com muitos detalhes irrelevantes removidos para maior clareza.
Pergunta original
Eu tenho um sistema que estou migrando do sqlite3 para o PostgreSQL, porque superei em grande parte o sqlite:
SELECT
d.dbId,
d.dlState,
d.sourceSite,
[snip a bunch of rows]
d.note
FROM FileItems AS d
JOIN
( SELECT dbId
FROM FileItems
WHERE sourceSite='{something}'
GROUP BY seriesName
ORDER BY MAX(retreivalTime) DESC
LIMIT 100
OFFSET 0
) AS di
ON di.dbId = d.dbId
ORDER BY d.retreivalTime DESC;
Basicamente, quero selecionar os últimos n DISTINCT
itens no banco de dados, onde a restrição distinta está em uma coluna e a ordem de classificação está em uma coluna diferente.
Infelizmente, a consulta acima, embora funcione bem no sqlite, apresenta erros no PostgreSQL com o erro psycopg2.ProgrammingError: column "fileitems.dbid" must appear in the GROUP BY clause or be used in an aggregate function
.
Infelizmente, enquanto adicionar dbId
à cláusula GROUP BY corrige o problema (por exemplo GROUP BY seriesName,dbId
, ), isso significa que a filtragem distinta nos resultados da consulta não funciona mais, pois dbid
é a chave primária do banco de dados e, como tal, todos os valores são distintos.
Da leitura da documentação do Postgres , existe SELECT DISTINCT ON ({nnn})
, mas isso requer que os resultados retornados sejam classificados por {nnn}
.
Portanto, para fazer o que eu quero via SELECT DISTINCT ON
, eu teria que consultar todos DISTINCT {nnn}
e seus MAX(retreivalTime)
, classificar novamente em retreivalTime
vez de {nnn}
, então pegar o maior 100 e consultar usando-os na tabela para obter o restante das linhas, que eu gostaria de evitar, como o banco de dados tem ~ 175 mil linhas e ~ 14 mil valores distintos na seriesName
coluna, eu só quero os 100 mais recentes e essa consulta é um pouco crítica para o desempenho (preciso de tempos de consulta < 1/2 segundo).
Minha suposição ingênua aqui é basicamente que o banco de dados precisa apenas iterar sobre cada linha em ordem decrescente de retreivalTime
, e simplesmente parar depois de ver os LIMIT
itens, portanto, uma consulta de tabela completa não é ideal, mas não pretendo realmente entender como o banco de dados sistema otimiza internamente, e eu posso estar abordando isso completamente errado.
FWIW, ocasionalmente uso OFFSET
valores diferentes, mas longos tempos de consulta para casos em que deslocamento > ~ 500 é completamente aceitável. Basicamente, OFFSET
é um mecanismo de paginação de baixa qualidade que me permite fugir sem precisar dedicar cursores de rolagem a cada conexão, e provavelmente o revisitarei em algum momento.
Ref- Pergunta que fiz há um mês que levou a esta consulta .
Bom, mais notas:
SELECT
d.dbId,
d.dlState,
d.sourceSite,
[snip a bunch of rows]
d.note
FROM FileItems AS d
JOIN
( SELECT seriesName, MAX(retreivalTime) AS max_retreivalTime
FROM FileItems
WHERE sourceSite='{something}'
GROUP BY seriesName
ORDER BY max_retreivalTime DESC
LIMIT %s
OFFSET %s
) AS di
ON di.seriesName = d.seriesName AND di.max_retreivalTime = d.retreivalTime
ORDER BY d.retreivalTime DESC;
Funciona corretamente para a consulta conforme descrito, mas se eu remover a GROUP BY
cláusula, ela falha (é opcional no meu aplicativo).
psycopg2.ProgrammingError: column "FileItems.seriesname" must appear in the GROUP BY clause or be used in an aggregate function
Acho que fundamentalmente não estou entendendo como as subconsultas funcionam no PostgreSQL. Onde eu estou errando? Eu tinha a impressão de que uma subconsulta é basicamente apenas uma função embutida, onde os resultados são apenas alimentados na consulta principal.
Linhas consistentes
A questão importante que ainda não parece estar no seu radar:
De cada conjunto de linhas para o mesmo
seriesName
, você quer as colunas de uma linha ou apenas quaisquer valores de várias linhas (que podem ou não vir da mesma linha )?Sua resposta faz o último, você combina o máximo
dbid
com o máximoretreivaltime
, que pode vir de uma linha diferente.Para obter linhas consistentes
DISTINCT ON
, use e envolva-as em uma subconsulta para ordenar o resultado de maneira diferente:Detalhes para
DISTINCT ON
:A parte: provavelmente deveria ser
retrievalTime
, ou melhor ainda:retrieval_time
. Identificadores de maiúsculas e minúsculas sem aspas são uma fonte comum de confusão no Postgres.Melhor desempenho com rCTE
Como estamos lidando com uma grande tabela aqui, precisaríamos de uma consulta que possa usar um índice, o que não é o caso da consulta acima (exceto
WHERE sourceSite = 'mk'
)Em uma inspeção mais detalhada, seu problema parece ser um caso especial de uma varredura de índice solto . O Postgres não suporta varreduras de índice solto nativamente, mas pode ser emulado com um CTE recursivo . Há um exemplo de código para o caso simples no Postgres Wiki.
Ver:
Mas seu caso é mais complexo. Acho que encontrei uma variante para fazê-lo funcionar para você. Com base neste índice (sem
WHERE sourceSite = 'mk'
)Ou (com
WHERE sourceSite = 'mk'
)O primeiro índice pode ser usado para ambas as consultas, mas não é totalmente eficiente com a
WHERE
condição adicional. O segundo índice é de uso muito limitado para a primeira consulta. Como você tem as duas variantes da consulta, considere criar os dois índices.Eu adicionei
dbid
no final para permitir varreduras somente de índice .Essa consulta com um CTE recursivo faz uso do índice. Testei com o Postgres 9.3 e funcionou para mim: sem varredura sequencial, todas as varreduras somente de índice :
Você precisa incluir
seriesName
emORDER BY
, poisretreivaltime
não é exclusivo. "Quase" único ainda não é único.Explicação
A consulta não recursiva começa com a última linha.
A consulta recursiva adiciona a próxima linha mais recente com uma
seriesName
que ainda não está na lista, etc., até que tenhamos 100 linhas.As partes essenciais são a
JOIN
condição(b.retreivaltime, b.seriesName) < (c.retreivaltime, c.seriesName)
e aORDER BY
cláusulaORDER BY retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST
. Ambos correspondem à ordem de classificação do índice, o que permite que a mágica aconteça.Coletando
seriesName
em uma matriz para descartar duplicatas. O custo parab.seriesName <> ALL(c.foo_arr)
cresce progressivamente com o número de linhas, mas para apenas 100 linhas ainda é barato.Apenas retornando
dbid
conforme esclarecido nos comentários.Alternativa com índices parciais:
Já lidamos com problemas semelhantes antes. Aqui está uma solução completa altamente otimizada baseada em índices parciais e uma função de loop:
Provavelmente o caminho mais rápido (exceto para uma visão materializada) se feito corretamente. Mas mais complexo.
Visualização Materializada
Como você não tem muitas operações de gravação e elas não são críticas ao desempenho, conforme indicado nos comentários (deve estar na pergunta), salve as n primeiras linhas pré-computadas em uma visualização materializada e atualize-a após alterações relevantes no tabela subjacente. Em vez disso, baseie suas consultas críticas de desempenho na visualização materializada.
Poderia ser apenas um mv "fino" dos últimos 1000
dbid
ou mais. Na consulta, junte-se à tabela original. Por exemplo, se o conteúdo às vezes é atualizado, mas as n primeiras linhas podem permanecer inalteradas.Ou um mv "gordo" com linhas inteiras para retornar. Mais rápido, ainda. Precisa ser atualizado com mais frequência, obviamente.
Detalhes no manual aqui e aqui .
Ok, eu li os documentos mais e agora entendo o problema pelo menos um pouco melhor.
Basicamente, o que está acontecendo é que existem vários valores possíveis
dbid
como resultado daGROUP BY seriesName
agregação. Com SQLite e MySQL, aparentemente o mecanismo de banco de dados apenas escolhe um aleatoriamente (o que é absolutamente bom no meu aplicativo).No entanto, o PostgreSQL é muito mais conservador, então, em vez de escolher um valor aleatório, ele gera um erro.
Uma maneira simples de fazer essa consulta funcionar é aplicar uma função de agregação ao valor relevante:
Isso torna a saída da consulta totalmente qualificada e a consulta agora funciona.
Bem, na verdade acabei usando alguma lógica procedural fora do banco de dados para realizar o que queria fazer.
Basicamente, 99% do tempo, eu quero os últimos
100.200resultados. O planejador de consultas parece não otimizar para isso e, se o valor deOFFSET
for grande, meu filtro procedural será muito mais lento.De qualquer forma, usei um cursor nomeado para iterar manualmente as linhas no banco de dados, recuperando as linhas em grupos de algumas centenas. Em seguida, filtrei-os para distinção no código do meu aplicativo e fecho o cursor imediatamente depois de acumular o número de resultados distintos desejados.
O
mako
código (basicamente python). Muitas instruções de depuração restantes.Atualmente, isso recupera os 100.200 itens de série distintos mais recentes
em115~80 milissegundos (o tempo mais baixo é ao usar uma conexão local, em vez de um soquete TCP), enquanto processa aproximadamente 1.500 linhas.Venha comentários:
buildWhereQuery
is my own dynamic query builder. Yes, this is a horrible idea. Yes, I know about SQLalchemy et al. I wrote my own because A. this is a personal project that I don't expect to ever use outside of my home LAN, and B. It's a great way to learn SQL.autocommit
mode off). I have to instantiate a anonymous cursor, issue some SQL (just aBEGIN
, here), create my named cursor, use it, close it, and finally commit with the anonymous cursor.