Estou usando o Postgres 12 e no meu aplicativo tenho uma tabela que estou usando para armazenar eventos específicos que contêm informações sobre coisas que aconteceram fora do sistema e relacionadas a alguns registros no meu BD. A tabela se parece com isso:
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY,
eventable_type VARCHAR(255) NOT NULL,
eventable_id BIGINT NOT NULL,
type VARCHAR(255) NOT NULL,
data JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
);
CREATE INDEX index_events_on_eventable ON events (eventable_type, eventable_id);
Por exemplo: uma reunião foi agendada no Google Agenda. Um evento é criado nesta tabela com os detalhes do que aconteceu e o registro é associado à representação interna da reunião no BD. O data
atributo contém os detalhes do evento que também contêm um id exclusivo como:
INSERT INTO events (eventable_type, eventable_id, type, data) VALUES ('MyInternalEvent', 1234, 'GoogleCalendarEvent', '{"action": "created", "GoogleId": "abcdef1234"}'::jsonb);
INSERT INTO events (eventable_type, eventable_id, type, data) VALUES ('MyInternalEvent', 1234, 'GoogleCalendarEvent', '{"action": "updated", "GoogleId": "abcdef1234"}'::jsonb);
INSERT INTO events (eventable_type, eventable_id, type, data) VALUES ('MyInternalEvent', 1234, 'GoogleCalendarEvent', '{"action": "deleted", "GoogleId": "abcdef1234"}'::jsonb);
INSERT INTO events (eventable_type, eventable_id, type, data) VALUES ('MyInternalEvent', 5678, 'GoogleCalendarEvent', '{"action": "created", "GoogleId": "dsfsdf2343"}'::jsonb);
INSERT INTO events (eventable_type, eventable_id, type, data) VALUES ('MyInternalEvent', 5678, 'GoogleCalendarEvent', '{"action": "updated", "GoogleId": "dsfsdf2343"}'::jsonb);
INSERT INTO events (eventable_type, eventable_id, type, data) VALUES ('MyInternalEvent', 5678, 'GoogleCalendarEvent', '{"action": "deleted", "GoogleId": "dsfsdf2343"}'::jsonb);
Eu consulto a tabela de eventos assim:
SELECT * FROM events WHERE events.type = 'GoogleCalendarEvent' AND (data->>'GoogleId' = 'abcdef1234') LIMIT 1;
Em termos de cardinalidade de operações, o número de gravações é aproximadamente 3 vezes maior que o número de leituras . Ou seja: escrevemos mais do que lemos. A tabela tem cerca de 3 milhões de linhas, crescendo rapidamente. Cerca de 300 mil linhas são adicionadas à tabela todos os dias.
No momento, armazenamos apenas um outro type
evento na tabela, vamos chamá-lo de GoogleEmailEvent
. Filtrar por GoogleCalendarEvent
retornaria aproximadamente 50% dos registros na tabela. Filtrar por GoogleId
normalmente retornaria menos de 10 registros, mas realmente precisamos de apenas 1 porque todos eles estão associados ao mesmo "Eventable", como você pode ver nas inserções de exemplo.
Quero melhorar o tempo de execução da consulta, pensei em:
- adicionando um índice
WHERE data->>'GoogleId' IS NOT NULL
. Mas estou preocupado em deixar as gravações mais lentas - armazenando
data->>'GoogleId'
em uma tabela separada junto com o id do evento para permitir uma recuperação rápida. Quão eficaz isso seria? Isso também tornaria as gravações um pouco mais lentas. - indexando
created_at
e usando isso na consulta para restringir os registros na consulta de alguma forma
Detalhe importante: A grande maioria das vezes (99% das vezes ou mais) o evento correspondente é aquele que foi inserido na tabela recentemente (digamos, em 10 minutos). Posso aproveitar esses detalhes para acelerar a consulta? Adicionar resolveria ORDER BY Id DESC LIMIT 1
o problema?
Noções básicas
Você pode adicionar um índice parcial na expressão como você já ponderou:
Consulta:
Mas isso não é muito útil, ainda. O índice parcial não faz muito sentido enquanto a condição remove apenas metade das suas linhas. Ele pode ser melhorado de muitas maneiras.
Otimizar passo 1
Sua tabela está crescendo rapidamente, assim como os índices adicionados. Sua consulta precisa principalmente de entradas recentes. Adicione um timestamp de corte para reduzir o tamanho drasticamente:
Adicione o mesmo corte (ou um registro de data e hora posterior) à sua consulta para que o Postgres saiba que o índice é aplicável:
Começo com hoje (horário UTC). O índice continuará crescendo. Você tem que recriá-lo de tempos em tempos para mantê-lo pequeno. Como com um cron-job diário. Adicionei
CONCURRENTLY
para não bloquear gravações.Ainda não é o ideal. O Postgres não pode usar varreduras somente de índice com essa expressão e tem que introspectar a
jsonb
coluna (possivelmente grande?) toda vez. Além disso, a expressão torna as gravações no índice um pouco mais caras também.Otimizar passo 2
O Google ID parece estar presente o tempo todo (ou na maioria das vezes). Uma coluna dedicada seria muito melhor. Na verdade, se seu documento JSON for regular, seria muito mais eficiente armazenar todas as colunas simples em vez do documento JSON para começar. Menos armazenamento, acesso mais rápido e muito mais. É trivialmente simples e rápido adicionar uma chave de volta ao documento JSON na recuperação - ou gerar todo o documento JSON a partir de colunas simples do Postgres.
Apenas extraindo o ID do Google para uma demonstração:
O
ALTER TABLE
"truque" é a maneira mais rápida de reescrever a tabela inteira, mas bloqueia gravações simultâneas. (Eu realmente recriaria a tabela inteira com otimizações adicionais.)Agora, o índice pode ser:
Supondo que você só precise de
eventable_id
, adicionei isso com umaINCLUDE
cláusula para torná-lo um índice de cobertura. Agora, se a tabela for aspirada o suficiente, você obtém varreduras somente de índice :violino
Armazenar as longas strings 'GoogleCalendarEvent' / 'GoogleEmailEvent' como tipo repetidamente é um desperdício. Eu substituiria isso por algo mais eficiente. Etc.
Os tipos de dados e o layout da tabela também podem ser otimizados um pouco mais. Veja: