Estou usando o PostgreSQL, mas acho que a maioria dos bancos de dados de ponta deve ter alguns recursos semelhantes e, além disso, que as soluções para eles podem inspirar soluções para mim, portanto, não considere isso específico do PostgreSQL.
Sei que não sou o primeiro a tentar resolver esse problema, então acho que vale a pena perguntar aqui, mas estou tentando avaliar os custos de modelar dados contábeis de forma que cada transação seja fundamentalmente equilibrada. Os dados contábeis são apenas anexos. A restrição geral (escrita em pseudo-código) aqui pode ser mais ou menos assim:
CREATE TABLE journal_entry (
id bigserial not null unique, --artificial candidate key
journal_type_id int references journal_type(id),
reference text, -- source document identifier, unique per journal
date_posted date not null,
PRIMARY KEY (journal_type_id, reference)
);
CREATE TABLE journal_line (
entry_id bigint references journal_entry(id),
account_id int not null references account(id),
amount numeric not null,
line_id bigserial not null unique,
CHECK ((sum(amount) over (partition by entry_id) = 0) -- this won't work
);
Obviamente, tal restrição de verificação nunca funcionará. Ele opera por linha e pode verificar todo o banco de dados. Portanto, sempre falhará e será lento ao fazê-lo.
Então, minha pergunta é qual é a melhor maneira de modelar essa restrição? Eu basicamente olhei para duas ideias até agora. Querendo saber se estes são os únicos, ou se alguém tem uma maneira melhor (além de deixá-lo no nível do aplicativo ou em um procedimento armazenado).
- Eu poderia pegar emprestado uma página do conceito do mundo da contabilidade sobre a diferença entre um livro de entrada original e um livro de entrada final (diário geral versus razão geral). A esse respeito, eu poderia modelar isso como uma matriz de linhas de diário anexadas à entrada do diário, impor a restrição na matriz (em termos do PostgreSQL, selecione sum(amount) = 0 de unnest(je.line_items). Um gatilho pode expandir e salve-os em uma tabela de itens de linha, onde as restrições de colunas individuais podem ser aplicadas com mais facilidade e onde os índices, etc., podem ser mais úteis. Essa é a direção em que estou inclinado.
- Eu poderia tentar codificar um gatilho de restrição que aplicaria isso por transação com a ideia de que a soma de uma série de 0s sempre será 0.
Estou pesando isso contra a abordagem atual de impor a lógica em um procedimento armazenado. O custo da complexidade está sendo pesado contra a ideia de que a prova matemática de restrições é superior aos testes de unidade. A principal desvantagem do nº 1 acima é que os tipos como tuplas são uma daquelas áreas no PostgreSQL em que se depara com comportamento inconsistente e mudanças nas suposições regularmente e, portanto, espero que o comportamento nessa área mude com o tempo. Projetar uma futura versão segura não é tão fácil.
Existem outras maneiras de resolver esse problema que serão dimensionados para milhões de registros em cada tabela? Estou esquecendo de algo? Existe uma troca que eu perdi?
Em resposta ao ponto de Craig abaixo sobre as versões, no mínimo, isso terá que ser executado no PostgreSQL 9.2 e superior (talvez 9.1 e superior, mas provavelmente podemos usar o 9.2 direto).
Como temos que abranger várias linhas, isso não pode
CHECK
ser implementado com uma restrição simples .Também podemos descartar restrições de exclusão . Isso abrangeria várias linhas, mas apenas verificaria a desigualdade. Operações complexas como uma soma em várias linhas não são possíveis.
A ferramenta que parece melhor se adequar ao seu caso é
CONSTRAINT TRIGGER
(ou até mesmo simplesTRIGGER
- a única diferença na implementação atual é que você pode ajustar o tempo do gatilho comSET CONSTRAINTS
.Então essa é a sua opção 2 .
Uma vez que podemos contar com a restrição sendo aplicada o tempo todo, não precisamos mais verificar a tabela inteira. Verificar apenas as linhas inseridas na transação atual - ao final da transação - é suficiente. O desempenho deve ser ok.
Tambem como
... só precisamos nos preocupar com as linhas recém- inserida . (Assumindo
UPDATE
ouDELETE
não são possíveis.)Eu uso a coluna do sistema
xid
e comparo com a funçãotxid_current()
- que retorna oxid
da transação atual. Para comparar os tipos, é necessário lançar ...Isso deve ser razoavelmente seguro.Considere esta resposta posterior relacionada com um método mais seguro:Demonstração
Deferred , por isso só é verificado no final da transação.
testes
Funciona.
Falha:
Funciona. :)
Se você precisar impor sua restrição antes do final da transação, poderá fazê-lo em qualquer ponto da transação, mesmo no início:
Mais rápido com gatilho simples
Se você operar com várias linhas
INSERT
, é mais eficaz acionar por instrução - o que não é possível com acionadores de restrição :Em vez disso, use um gatilho simples e atire
FOR EACH STATEMENT
para...SET CONSTRAINTS
.EXCLUIR possível
Em resposta ao seu comentário: Se
DELETE
for possível, você pode adicionar um gatilho semelhante fazendo uma verificação de saldo de toda a tabela após um DELETE ter ocorrido. Isso seria muito mais caro, mas não importa muito, pois raramente acontece.A solução do SQL Server a seguir usa apenas restrições. Estou usando abordagens semelhantes em vários lugares do meu sistema.