Eu implementei a estratégia de desnormalização de dados usando RULEs do postgresql. Escolhi regras em vez de gatilhos por motivos de desempenho.
O esquema é estruturado assim:
- O aplicativo tem muitos clientes
- Cliente tem muitos projetos
- O projeto tem muitos usuários
Uma parte do sistema está armazenando hits
para cada usuário na stats
tabela. Hit é uma métrica imaginária, não é realmente relevante. O sistema pode coletar muitas dessas métricas. Existem muitos registros na tabela de estatísticas (> 1.000.000 por dia).
Quero saber quantos acessos são por usuário, por projeto, por cliente e por aplicativo em determinado dia.
Para fazê-lo funcionar rapidamente, agrupei as estatísticas por dia e armazenei a saída na tabela user_hits. Durante este processo, também o application_id, client_id e project_id foram adicionados (como colunas) e os índices apropriados criados.
Quero otimizar ainda mais o processo agrupando as coisas por project_id, client_id e finalmente application_id. O pipeline de dados é assim:
stats -> user_hits -> project_hits -> client_hits -> application_hits
Quero ter certeza de que, ao excluir os dados de user_hits
um determinado dia, os dados project_hits
dessa mesma data também serão excluídos. Este processo deve se propagar até a última tabela da cadeia.
Eu defini estas regras simples:
CREATE RULE delete_children AS ON DELETE TO user_hits
DO ALSO
DELETE FROM project_hits WHERE day = OLD.day;
CREATE RULE delete_children AS ON DELETE TO project_hits
DO ALSO
DELETE FROM client_hits WHERE day = OLD.day;
CREATE RULE delete_children AS ON DELETE TO client_hits
DO ALSO
DELETE FROM application_hits WHERE day = OLD.day;
No entanto, quando emito uma declaração como esta:
DELETE FROM user_hits WHERE day = current_date;
Espero que ele execute essas 3 consultas em retorno:
DELETE FROM project_hits WHERE day = current_date;
DELETE FROM client_hits WHERE day = current_date;
DELETE FROM application_hits WHERE day = current_date;
No entanto, isso não acontece.
Ele conclui a operação, mas leva alguns minutos para fazer isso (com dados de teste). Com dados reais, leva horas, enquanto executar essas 3 consultas manualmente leva alguns milissegundos. O tempo gasto parece proporcional ao número de combinações (usuários x projetos x clientes x aplicativos).
Qual é o problema aqui? Estou esquecendo de algo? Isso pode ser implementado com gatilhos de maneira otimizada?
Script de exemplo incluído que reproduz o problema:
https://gist.github.com/assembler/5151102
ATUALIZAÇÃO: A transição de user_hits
para project_hits
(e assim por diante) é feita pelo processo de trabalho em segundo plano (uma vez que envolve entrar em contato com serviços de terceiros para obter informações adicionais). É inteligente o suficiente para recalcular tudo para datas perdidas. Portanto, a única coisa que preciso é uma maneira de EXCLUIR registros em cascata de maneira otimizada.
ATUALIZAÇÃO: stats
a tabela é preenchida diariamente. O único cenário possível é excluir incondicionalmente os dados do dia inteiro e substituí-los por novos valores.
ATUALIZAÇÃO: notei que o número de linhas afetadas (extraídas da explain
instrução) é exatamente igual ao produto das linhas afetadas em user_hits
, project_hits
, client_hits
e application_hits
tabelas (centenas de milhões de linhas).
Acontece que funciona assim:
- eu corro
DELETE FROM user_hits WHERE day = current_date;
- Para cada linha na
user_hits
tabela, RULE é acionado, o que exclui TODAS as linhas deproject_hits
- Para cada linha de
project_hits
, RULE é acionado, o que exclui TODAS as linhas declient_hits
- Para cada linha de
client_hits
, RULE é acionado, o que exclui TODAS as linhas deapplication_hits
Portanto, o número de operações é igual ao produto da contagem das linhas afetadas nessas tabelas.
Da próxima vez, inclua a saída EXPLAIN em vez de nos fazer procurá-la em seus scripts. Não há garantia de que meu sistema esteja usando o mesmo plano que o seu (embora com seus dados de teste seja provável).
O sistema de regras aqui está funcionando corretamente. Primeiro, desejo incluir minhas próprias consultas de diagnóstico (observe que não executei EXPLAIN ANALYZE, pois estava interessado apenas em qual plano de consulta foi gerado):
Se seus dados forem parecidos com os dados existentes, nem as regras nem os gatilhos funcionarão muito bem. Melhor será uma stored procedure que você passa um valor e ela apaga tudo que você quiser.
Primeiro, vamos observar que os índices aqui não levarão a lugar nenhum porque em todos os casos você está puxando metade das tabelas (eu adicionei índices no dia em todas as tabelas para ajudar o planejador, mas isso não fez nenhuma diferença real).
Você precisa começar com o que está fazendo com as REGRAS. As RULEs basicamente reescrevem as consultas e o fazem usando as formas mais robustas possíveis. Seu código também não corresponde ao seu exemplo, embora corresponda melhor à sua pergunta. Você tem regras em tabelas que se propagam para regras em outras tabelas que se propagam para regras em outras tabelas
Portanto, quando você
delete from user_hits where [criteria]
, as regras transformam isso em um conjunto de consultas:Agora, você pode pensar que poderíamos pular a varredura em client_hits no primeiro, mas não é isso que acontece aqui. O problema é que você pode ter dias em user_hits e application_hits que não estão em client_hits, então você realmente precisa verificar todas as tabelas.
Now here there is no magic bullet. A trigger isn't going to work much better because, while it gets to avoid scanning every table, it gets fired every row that gets deleted so you basically end up with the same nested loop sequential scans that are currently killing performance. It will work a bit better because it will delete rows along the way rather than rewriting the query along the way, but it isn't going to perform very well.
A much better solution is to just define a stored procedure and have the application call that. Something like:
On the test data this runs in 280 ms on my laptop.
One of the hard things regarding RULEs is remembering what they are and noting that the computer cannot, in fact, read your mind. This is why I would not consider them a beginner's tool.
E a seguir: crie algumas regras para
INSERT
... E não se esqueça dos casos desagradáveis dosUPDATE
principais arquivos.OK, já faz muito tempo, mas se você executar um
EXPLAIN
podemos ver se minha lembrança está correta. Acho que o plano para as consultas subsidiárias está sendo criado na hora errada, antes que o planejador possa levar em consideração os índices. Acho que você está recebendo Table Scans.Dito isso, você comparou que uma chave estrangeira de exclusão em cascata padrão é muito lenta? E que uma regra seria mais rápida?
[Editar após comentários]
Lendo os documentos com atenção, parece que (ao contrário de um gatilho) quando uma regra é usada, ela será (na ausência da cláusula where adicionada) aplicada a toda a tabela original?!