Estou analisando um aplicativo legado que gerencia objetos armazenados em um banco de dados PostgreSQL.
Cada objeto possui vários conjuntos de propriedades que são armazenados como objetos JSONB. Cada um desses conjuntos de propriedades descreve um aspecto diferente do objeto (pode haver de 1 a 10 conjuntos de propriedades por objeto).
Em relação a isso, diferentes departamentos geralmente se preocupam apenas com alguns desses conjuntos de propriedades (aspectos), portanto o número de conjuntos de propriedades varia dependendo de quem está envolvido durante o ciclo de vida de cada objeto.
Cada vez que algo muda em uma propriedade definida pelo aplicativo front-end (que está além do meu controle), ele é enviado de volta ao banco de dados como um novo documento JSON.
No momento, a tabela (para efeito de argumentação) está estruturada da seguinte forma:
TABLE x (
object_id INTEGER,
aspect_id INTEGER,
doc_ts TIMESTAMP,
doc JOSNB,
PRIMARY KEY (object_id, aspect_id, doc_ts)
)
A entrada de dados é tratada por um procedimento, que armazena uma cópia do documento em uma tabela, e tem a seguinte aparência:
CREATE OR REPLACE PROCEDURE ingest.generic_object(IN in_object_id integer, IN in_aspect_id integer, IN in_doc_ts TIMESTAMP, IN in_doc JSONB DEFAULT NULL::JSONB)
LANGUAGE plpgsql
AS $procedure$
BEGIN
INSERT INTO ingest.x ( object_id, aspect_id, doc_ts, doc)
VALUES (in_object_id, in_aspect_id, in_doc_ts, in_doc)
;
END;
$procedure$;
Agora vamos passar para o problema que estou tentando resolver. Esta tabela continua crescendo com o tempo e pode ficar muito grande, muito rapidamente. O processo de manutenção mantém APENAS um registro para cada combinação de object_id e aspecto_id (ou seja, apenas aquele com o valor doc_ts mais recente) e leva muito tempo para ser executado.
Estou pensando em dividir os dados em duas tabelas. Como:
TABLE ingest.x (
object_id INTEGER,
aspect_id INTEGER,
doc_ts TIMESTAMPZ,
doc JOSNB,
PRIMARY KEY (object_id, aspect_id, doc_ts)
);
TABLE ingest.x_history (
object_id INTEGER,
aspect_id INTEGER,
doc_ts TIMESTAMP,
doc JOSNB,
PRIMARY KEY (object_id, aspect_id, doc_ts)
);
E então altere a função de ingestão para algo parecido com:
CREATE OR REPLACE PROCEDURE ingest.generic_object(IN in_object_id integer, IN in_aspect_id integer, IN in_doc_ts TIMESTAMP, IN in_doc JSONB DEFAULT NULL::JSONB)
LANGUAGE plpgsql
AS $procedure$
BEGIN
WITH
dd AS (
DELETE FROM ingest.x
WHERE object_id= in_object_id
AND aspect_id = in_aspect_id
RETURNING *
),
di AS (
INSERT INTO ingest.x_history ( object_id, aspect_id, doc_ts, doc)
SELECT object_id, aspect_id, doc_ts, doc FROM dd
)
INSERT INTO ingest.x ( object_id, aspect_id, doc_ts, doc)
VALUES (in_object_id, in_aspect_id, in_doc_ts, in_doc)
;
END;
$procedure$;
Isso me permitiria simplificar SIGNIFICATIVAMENTE o processo de limpeza, até:
TRUNCATE TABLE ingest.x_history;
Minhas perguntas são as seguintes:
Considerando a alteração acima, o AUTOVACUUM levará a alguma reutilização do espaço ocupado pelos registros excluídos em
ingest.x
?Quais parâmetros de tabela
ingest.x
devo ajustar para melhorar a reutilização do espaço liberado pelos registros excluídos? - Supondo que a resposta para 1 seja SIM. Tenho em mente o FILL FACTOR juntamente com limites de AUTOVACUUM mais direcionados.Você consegue ver uma abordagem alternativa para alcançar um processo de limpeza mais fácil e eficaz?
Observação:
- Tudo isso está rodando no PostgreSQL v15
- Os registros de documentos podem ser redigidos dezenas de vezes durante a fase de criação – primeiros dias. Depois, uma vez a cada dois dias/semanas durante o período de revisão. Mudanças podem acontecer depois disso, mas são muito raras.
- Os dados brutos podem envelhecer muito rapidamente, e é por isso que TRUNCAR o histórico não é um problema.
Edição 1: Os documentos JSON variam entre 100kb e 2MB. Acredito que sejam direcionados ao TOAST. Além disso, o banco de dados está em um sistema de arquivos ZFS e, como tal, a compactação PostgreSQL está desativada, aproveitando a compactação ZFS.
Atenciosamente
Sim, vai - é para isso que serve o AUTOVACUUM - parar o inchaço da mesa!
"Ajustar" vários parâmetros da tabela depende do seu próprio uso, das suas próprias taxas de tabela
INSERT
e do tamanho dos registros. Este é outro parâmetro que pode influenciar o processo. A menos que você tenha uma pergunta mais específica sobre isso, é difícil dar uma resposta definitiva.UPDDATE
DELETE
fillfactor
Esta é uma ciência completa - na seção Parâmetros de armazenamento da
CREATE TABLE
parte do manual, existem 17 parâmetros que começam comautovacuum_...
e mais dois que começam comvacuum_...
.A resposta para qualquer problema em ciência da computação normalmente é “Depende!”
As tabelas de histórico são a solução "clássica" para esse problema e você pode, é claro, como diz,
TRUNCATE
elas (ou colocá-las em algum tipo de "armazenamento refrigerado"). Você também pode considerarPARTITION
suas tabelas - você terá que ler sobre isso e ver se elas se adequam ao seu caso de uso específico.Além do Fine Manual, você também pode ler isto - um e-book do Postgrespro - pesos pesados no mundo PostgreSQL que tem um capítulo inteiro sobre
AUTOVACUUM
, aqui (um e-book do Japão - um pouco mais antigo) também pode valer a pena dar uma olhada. Nenhum dos e-books trata de particionamento - o FM e seu mecanismo de busca favorito devem resolver o problema. O conceito relacionado de congelamento também é abordado nos e-books - mais uma vez, o livro do Postgrespro tem um capítulo inteiro!