Tenho uma dúvida sobre o design de uma tabela de histórico no Postgres.
A configuração é que eu tenho uma tabela que contém uma lista de necessidades. Um local recalcula os itens sob demanda a cada cinco minutos e envia essa lista para o Postgres. A lista "quente" atual é então acessível a vários aplicativos cliente para puxar. Portanto, a cada cinco minutos, as linhas relacionadas a um local específico são excluídas e repovoadas com o que está quente. Imagine uma tela na parede de um armazém onde as pessoas olham para cima para ver tarefas urgentes, esse tipo de coisa. Esta é mais ou menos uma tabela de filas/avisos, não uma tabela de armazenamento real.
O que estamos rastreando na lista de itens sob demanda são peças específicas, com IDs. É valioso para nós coletar dados (ou pelo menos estatísticas) ao longo do tempo. Podemos descobrir que itens específicos aparecem na lista todos os dias, enquanto outros aparecem apenas raramente. Isso pode ajudar a orientar as escolhas de compra e tal.
Esse é o plano de fundo, estou no Postgres 11.5, então sem colunas geradas. A estratégia descrita abaixo parece correta ou pode ser melhorada? A tabela base é chamada need
e a tabela de histórico é chamadaneed_history
need
-- Armazena os dados de interesse
-- Tem um NOW()
atribuído como parte created_dts
da INSERT
configuração da tabela.
-- Tem um PER STATEMENT
acionador after para obter a 'tabela de transição' de linhas excluídas.
-- O gatilho de instrução INSERTS INTO
need_history
para preservar os dados.
need_history
-- É quase um clone de necessidade, mas com alguns campos extras. Especificamente, deleted_dts
, atribuído NOW()
como padrão quando os dados são inseridos, e duration_seconds
que armazena o número ~ de segundos que o registro existiu na tabela de necessidade.
-- Como este é o PG 11.5, não há colunas geradas, então precisarei de um EACH ROW
gatilho para calcular duration_seconds
.
Mais curto:
need
com um gatilho de exclusão de nível de instrução que envia para need_history
.
need_history
com um gatilho de nível de linha para calcular duration_seconds
, pois não tenho colunas geradas disponíveis no PG 11.x.
E, para responder à pergunta óbvia, não, não preciso armazenar o duration_seconds
valor derivado, pois ele pode ser gerado dinamicamente, mas, neste caso, quero desnormalizar para simplificar uma variedade de consultas, classificações e resumos .
Meu cérebro também está dizendo "pergunte sobre fatores de preenchimento ", e não sei por quê.
Abaixo está o código de configuração inicial, caso o resumo acima não esteja claro. Eu não enviei nenhum dado por isso ainda, então pode ter falhas.
Eu ficaria grato por qualquer conselho ou recomendação sobre a melhor forma de fazer isso no Postgres.
BEGIN;
DROP TABLE IF EXISTS data.need CASCADE;
CREATE TABLE IF NOT EXISTS data.need (
id uuid NOT NULL DEFAULT NULL,
item_id uuid NOT NULL DEFAULT NULL,
facility_id uuid NOT NULL DEFAULT NULL,
hsys_id uuid NOT NULL DEFAULT NULL,
total_qty integer NOT NULL DEFAULT 0,
available_qty integer NOT NULL DEFAULT 0,
sterile_qty integer NOT NULL DEFAULT 0,
still_need_qty integer NOT NULL DEFAULT 0,
perc_down double precision NOT NULL DEFAULT '0',
usage_ integer NOT NULL DEFAULT 0,
need_for_case citext NOT NULL DEFAULT NULL,
status citext NOT NULL DEFAULT NULL,
created_dts timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT need_id_pkey
PRIMARY KEY (id)
);
ALTER TABLE data.need OWNER TO user_change_structure;
COMMIT;
/* Define the trigger function to copy the deleted rows to the history table. */
CREATE FUNCTION data.need_delete_copy_to_history()
RETURNS trigger AS
$BODY$
BEGIN
/* need.deleted_dts is auto-assigned on INSERT over in need, and
need.duration_seconds is calculated in an INSERT trigger (PG 11.5, not PG 12, no generated columns). */
INSERT INTO data.need_history
(id,
item_id,
facility_id,
hsys_id,
total_qty,
available_qty,
sterile_qty,
still_need_qty,
perc_down,
usage_,
need_for_case,
status,
created_dts)
SELECT id,
item_id,
facility_id,
hsys_id,
total_qty,
available_qty,
sterile_qty,
still_need_qty,
perc_down,
usage_,
need_for_case,
status,
created_dts
FROM deleted_rows;
RETURN NULL; -- result is ignored since this is an AFTER trigger
END;
$BODY$
LANGUAGE plpgsql;
/* Bind a trigger event to the function. */
DROP TRIGGER IF EXISTS trigger_need_after_delete ON data.need;
CREATE TRIGGER trigger_need_after_delete
AFTER DELETE ON data.need
REFERENCING OLD TABLE AS deleted_rows
FOR EACH STATEMENT EXECUTE FUNCTION data.need_delete_copy_to_history();
/* Define the table. */
BEGIN;
DROP TABLE IF EXISTS data.need_history CASCADE;
CREATE TABLE IF NOT EXISTS data.need_history (
id uuid NOT NULL DEFAULT NULL,
item_id uuid NOT NULL DEFAULT NULL,
facility_id uuid NOT NULL DEFAULT NULL,
hsys_id uuid NOT NULL DEFAULT NULL,
total_qty integer NOT NULL DEFAULT 0,
available_qty integer NOT NULL DEFAULT 0,
sterile_qty integer NOT NULL DEFAULT 0,
still_need_qty integer NOT NULL DEFAULT 0,
perc_down double precision NOT NULL DEFAULT '0',
usage_ integer NOT NULL DEFAULT 0,
need_for_case citext NOT NULL DEFAULT NULL,
status citext NOT NULL DEFAULT NULL,
created_dts timestamptz NOT NULL DEFAULT NULL,
deleted_dts timestamptz NOT NULL DEFAULT NOW(),
duration_seconds int4 NOT NULL DEFAULT 0,
CONSTRAINT need_history_id_pkey
PRIMARY KEY (id)
);
ALTER TABLE data.need_history OWNER TO user_change_structure;
COMMIT;
/* Define the trigger function to update the duration count.
In PG 12 we'll be able to do this with a generated column...easier. */
CREATE OR REPLACE FUNCTION data.need_history_insert_trigger()
RETURNS trigger AS
$BODY$
BEGIN
/* Use DATE_TRUNC seconds to get just the whole seconds part of the timestamps. */
NEW.duration_seconds =
EXTRACT(EPOCH FROM (
DATE_TRUNC('second', NEW.deleted_dts) -
DATE_TRUNC('second', NEW.created_dts)
));
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
/* Bind a trigger event to the function. */
DROP TRIGGER IF EXISTS trigger_need_history_before_insert ON data.need_history;
CREATE TRIGGER trigger_need_history_before_insert
BEFORE INSERT ON data.need_history
FOR EACH ROW EXECUTE FUNCTION data.need_history_insert_trigger();```
Isso parece bom.
A parte difícil de implementar uma fila em SQL não é a historização, mas como gerenciar a própria fila (adicionar, localizar e remover itens). Se houver muito tráfego, você provavelmente precisará de configurações agressivas de autovacuum para a tabela de filas.
Eu particionaria a tabela de histórico. O que as pessoas geralmente esquecem de projetar é como se livrar de dados antigos. A tabela de histórico pode ficar grande e você não precisará dos dados indefinidamente. Se você particionou a tabela (de modo que haja entre 10 e algumas centenas de partições), será fácil se livrar dos dados antigos.
Não vejo nada de errado nisso. Como diz Laurenz, você deve considerar desde o início como irá excluir da tabela de histórico quando chegar a hora.
O fator de preenchimento informa às operações INSERT ou COPY para deixar espaço suficiente em cada bloco para que UPDATE possa encaixar as novas versões de linhas no mesmo bloco da versão antiga. Você não descreve nenhuma operação UPDATE e as operações DELETE não exigem nenhum espaço adicional no bloco (elas atualizam as linhas no local para marcá-las como excluídas). Portanto, não há necessidades especiais aqui para definir o fator de preenchimento na mesa.