Eu tenho um padrão de consulta que deve ser muito comum, mas não sei como escrever uma consulta eficiente para ela. Quero pesquisar as linhas de uma tabela que correspondam à "data mais recente não depois" das linhas de outra tabela.
Eu tenho uma tabela, inventory
digamos, que representa o estoque que eu tenho em um determinado dia.
date | good | quantity
------------------------------
2013-08-09 | egg | 5
2013-08-09 | pear | 7
2013-08-02 | egg | 1
2013-08-02 | pear | 2
e uma tabela, digamos "preço", que detém o preço de uma mercadoria em um determinado dia
date | good | price
--------------------------
2013-08-07 | egg | 120
2013-08-06 | pear | 200
2013-08-01 | egg | 110
2013-07-30 | pear | 220
Como posso obter com eficiência o preço "mais recente" para cada linha da tabela de inventário, ou seja,
date | pricing date | good | quantity | price
----------------------------------------------------
2013-08-09 | 2013-08-07 | egg | 5 | 120
2013-08-09 | 2013-08-06 | pear | 7 | 200
2013-08-02 | 2013-08-01 | egg | 1 | 110
2013-08-02 | 2013-07-30 | pear | 2 | 220
Eu conheço uma maneira de fazer isso:
select inventory.date, max(price.date) as pricing_date, good
from inventory, price
where inventory.date >= price.date
and inventory.good = price.good
group by inventory.date, good
e, em seguida, junte-se a essa consulta novamente no inventário. Para tabelas grandes, mesmo a primeira consulta (sem ingressar novamente no inventário) é muito lenta. No entanto, o mesmo problema é resolvido rapidamente se eu simplesmente usar minha linguagem de programação para emitir uma max(price.date) ... where price.date <= date_of_interest ... order by price.date desc limit 1
consulta para cada uma date_of_interest
da tabela de inventário, então sei que não há impedimento computacional. No entanto, eu preferiria resolver todo o problema com uma única consulta SQL, porque isso permitiria um processamento SQL adicional no resultado da consulta.
Existe uma maneira padrão de fazer isso com eficiência? Parece que ele deve aparecer com frequência e que deve haver uma maneira de escrever uma consulta rápida para ele.
Estou usando o Postgres, mas uma resposta SQL genérica seria apreciada.
\d tbl
em psql), sua versão do Postgres e min. / máx. número de preços por bem.Respostas:
Ele depende muito das circunstâncias e necessidades exatas. Considere o meu comentário para a pergunta .
Solução simples
Com
DISTINCT ON
no Postgres:Resultado ordenado.
Ou
NOT EXISTS
no SQL padrão (funciona com todos os RDBMS que eu conheço):Mesmo resultado, mas com ordem de classificação arbitrária - a menos que você adicione
ORDER BY
.Dependendo da distribuição dos dados, requisitos e índices exatos, qualquer um deles pode ser mais rápido.
Geralmente,
DISTINCT ON
é o vencedor e você obtém um resultado classificado em cima dele. Mas, em certos casos, outras técnicas de consulta são (muito) mais rápidas ainda. Ver abaixo.Soluções com subconsultas para calcular valores máx / min são geralmente mais lentas. As variantes com CTEs são geralmente mais lentas, ainda.
Vistas simples (como propostas por outra resposta) não ajudam no desempenho no Postgres.
SQL Fiddle.
Solução adequada
Cordas e agrupamento
Primeiro de tudo, você sofre de um layout de tabela abaixo do ideal. Pode parecer trivial, mas normalizar seu esquema pode percorrer um longo caminho.
A classificação por tipos de caracteres (
text
,varchar
...) tem que ser feito de acordo com o local - o COLLATION em particular. Provavelmente, seu banco de dados usa algum conjunto local de regras (como, no meu casode_AT.UTF-8
:). Descubra com:Isso torna a classificação e as pesquisas de índice mais lentas . Quanto mais longas as cordas (nomes das mercadorias), pior. Se você realmente não se importa com as regras de agrupamento em sua saída (ou com a ordem de classificação), isso pode ser mais rápido se você adicionar
COLLATE "C"
:Observe como adicionei o agrupamento em dois lugares.
Duas vezes mais rápido no meu teste, com 20 mil linhas cada e nomes muito básicos ('good123').
Índice
Se sua consulta deve usar um índice, as colunas com dados de caracteres precisam usar um agrupamento correspondente (
good
no exemplo):Leia os dois últimos capítulos desta resposta relacionada no SO:
Você pode até ter vários índices com diferentes agrupamentos nas mesmas colunas - se também precisar de mercadorias classificadas de acordo com outro agrupamento (ou o padrão) em outras consultas.
Normalizar
Seqüências redundantes (nome de bom) também incham suas tabelas e índices, o que torna tudo ainda mais lento. Com um layout de tabela adequado, você pode evitar a maior parte do problema. Pode ficar assim:
As chaves primárias fornecem automaticamente (quase) todos os índices que precisamos.
Dependendo dos detalhes ausentes, um índice de
price
várias colunas ativado com ordem decrescente na segunda coluna pode melhorar o desempenho:Novamente, o agrupamento deve corresponder à sua consulta (veja acima).
No Postgres 9.2 ou posterior, "índices de cobertura" para verificações apenas de índice poderia ajudar um pouco mais - especialmente se suas tabelas mantiverem colunas adicionais, tornando a tabela substancialmente maior que o índice de cobertura.
Essas consultas resultantes são muito mais rápidas:
NÃO EXISTE
DISTINCT ON
SQL Fiddle.
Soluções mais rápidas
Se isso ainda não for rápido o suficiente, pode haver soluções mais rápidas.
CTE recursiva /
JOIN LATERAL
/ subconsulta correlacionadaEspecialmente para distribuições de dados com muitos preços por bem :
Vista materializada
Se você precisar executar isso com frequência e rapidez, sugiro que você crie uma visualização materializada. Eu acho que é seguro assumir que preços e estoques para datas passadas raramente mudam. Calcule o resultado uma vez e armazene um instantâneo como vista materializada.
O Postgres 9.3+ tem suporte automatizado para visualizações materializadas. Você pode implementar facilmente uma versão básica em versões mais antigas.
fonte
price_good_date_desc_idx
índice que você recomenda melhorou drasticamente o desempenho de uma consulta semelhante à minha. Meu plano de consulta passou de um custo42374.01..42374.86
para baixo0.00..37.12
!Para sua informação, usei o mssql 2008, portanto, o Postgres não terá o índice "include". No entanto, o uso da indexação básica mostrada abaixo mudará de junções de hash para junções de mesclagem no Postgres: http://explain.depesz.com/s/eF6 (sem índice) http://explain.depesz.com/s/j9x ( com índice nos critérios de junção)
Proponho dividir sua consulta em duas partes. Primeiro, uma exibição (não destinada a melhorar o desempenho) que pode ser usada em vários outros contextos que representam o relacionamento entre as datas do inventário e as datas dos preços.
Em seguida, sua consulta pode se tornar mais simples e mais fácil de manipular para outros tipos se a consulta (como usar associações à esquerda para encontrar inventário sem datas recentes de preços):
Isso produz o seguinte plano de execução: http://sqlfiddle.com/#!3/24f23/1
... Todas as verificações com uma classificação completa. Observe que o custo de desempenho das correspondências de hash ocupa grande parte do custo total ... e sabemos que as varreduras e a classificação da tabela são lentas (em comparação com a meta: o índice procura).
Agora, adicione índices básicos para ajudar nos critérios usados em sua associação (não reivindico que sejam índices ideais, mas eles ilustram o ponto): http://sqlfiddle.com/#!3/5ec75/1
Isso mostra melhorias. As operações de loop aninhado (junção interna) não ocupam mais nenhum custo total relevante para a consulta. O restante do custo agora está distribuído entre as buscas de índice (uma varredura do inventário porque estamos puxando cada linha do inventário). Mas podemos melhorar ainda mais porque a consulta puxa quantidade e preço. Para obter esses dados, após avaliar o critério de junção, é necessário realizar pesquisas.
A iteração final usa "include" nos índices para facilitar o deslizamento do plano e obter os dados adicionais solicitados diretamente do próprio índice. Portanto, as pesquisas se foram: http://sqlfiddle.com/#!3/5f143/1
Agora, temos um plano de consulta no qual o custo total da consulta é distribuído igualmente entre operações de busca de índice muito rápidas. Isso será quase o melhor que conseguir. Certamente outros especialistas podem melhorar isso ainda mais, mas a solução elimina algumas das principais preocupações:
fonte
Se você tiver o PostgreSQL 9.3 (lançado hoje), poderá usar um LATERAL JOIN.
Não tenho como testar isso, e nunca o usei antes, mas pelo que posso ver na documentação, a sintaxe seria algo como:
Isso é basicamente equivalente ao APPLY do SQL Server , e há um exemplo disso no SQL-Fiddle para fins de demonstração.
fonte
Como Erwin e outros observaram, uma consulta eficiente depende de muitas variáveis e o PostgreSQL tenta muito otimizar a execução da consulta com base nessas variáveis. Em geral, você deseja escrever primeiro para maior clareza e depois modificar para obter desempenho depois de identificar gargalos.
Além disso, o PostgreSQL tem muitos truques que você pode usar para tornar as coisas um pouco mais eficientes (índices parciais para um), portanto, dependendo da carga de leitura / gravação, você poderá otimizar isso muito longe, procurando uma indexação cuidadosa.
A primeira coisa a tentar é apenas fazer uma visualização e juntar-se a ela:
Isso deve ter um bom desempenho ao fazer algo como:
Então você pode participar disso. A consulta acabará juntando a visualização à tabela subjacente, mas, desde que você tenha um índice exclusivo em (data, boa nessa ordem ), você deve estar pronto (já que essa será uma simples pesquisa de cache). Isso funcionará muito bem com algumas linhas pesquisadas, mas será muito ineficiente se você estiver tentando digerir milhões de preços de mercadorias.
A segunda coisa que você pode fazer é adicionar à tabela de inventário uma coluna bool mais recente e
Você desejaria usar gatilhos para definir a maioria dos recentes como falsos quando uma nova linha de uma mercadoria foi inserida. Isso adiciona mais complexidade e maiores chances de erros, mas é útil.
Novamente, muito disso depende da existência de índices apropriados. Para consultas de data mais recentes, você provavelmente deve ter um índice na data e, possivelmente, um com várias colunas começando com date e incluindo seus critérios de associação.
Atualizar o comentário de Per Erwin abaixo, parece que eu não entendi isso. Relendo a pergunta, não tenho certeza do que está sendo perguntado. Quero mencionar na atualização qual é o problema em potencial que vejo e por que isso deixa isso claro.
O design do banco de dados oferecido não tem uso real do IME com ERP e sistemas de contabilidade. Funcionaria em um modelo hipotético de precificação perfeita, onde tudo o que é vendido em um determinado dia de um determinado produto tem o mesmo preço. No entanto, esse nem sempre é o caso. Não é o caso de coisas como trocas de moeda (embora alguns modelos finjam que sim). Se este é um exemplo artificial, não está claro. Se for um exemplo real, há problemas maiores com o design no nível de dados. Vou assumir aqui que este é um exemplo real.
Você não pode assumir que a data sozinha especifique o preço de um determinado produto. Os preços em qualquer empresa podem ser negociados por contraparte e, às vezes, por transação. Por esse motivo, você realmente deve armazenar o preço na tabela que efetivamente administra a entrada ou saída do estoque (a tabela de estoque). Nesse caso, sua tabela de data / mercadorias / preço apenas especifica um preço base que pode estar sujeito a alterações com base na negociação. Nesse caso, esse problema deixa de ser um problema de relatório para um que é transacional e opera em uma linha de cada tabela por vez. Por exemplo, você pode procurar o preço padrão de um determinado produto em um determinado dia como:
Com um índice de preços (bom, data), isso terá um bom desempenho.
Se este é um exemplo artificial, talvez algo mais próximo do que você está trabalhando ajudaria.
fonte
most_recent
abordagem deve funcionar bem pelo preço mais recente absolutamente . Parece que o OP precisa do preço mais recente em relação a cada data de estoque.Outra maneira seria usar a função window
lead()
para obter o intervalo de datas para cada linha no preço da tabela e depois usá-lobetween
ao ingressar no inventário. Na verdade, eu usei isso na vida real, mas principalmente porque essa foi minha primeira ideia de como resolver isso.SqlFiddle
fonte
Use uma associação do inventário ao preço com condições de associação que limitem os registros da tabela de preços apenas àqueles que estão na data ou antes da data do inventário, extraia a data máxima e onde a data é a data mais alta desse subconjunto
Portanto, para o seu preço de estoque:
Se o preço de qualquer mercadoria especificada tiver sido alterado mais de uma vez no mesmo dia e você realmente tiver apenas datas e horários nessas colunas, poderá ser necessário aplicar mais restrições nas junções para selecionar apenas um dos registros de alteração de preço.
fonte