ledger
Historicamente na contabilidade financeira, recursos são rastreados de diversas formas, incluindo livros-caixa, ou ledgers, e logs de transação. Naturalmente essa representação é mantida nos computadores, e transformada em tabelas, nas quais as linhas representam ou as operações sobre as contas ou os novos saldos das contas, ou ambos. Adicionalmente, o método das partidas dobradas faz com que cada operação seja representada em duas linhas, uma como um crédito e outra como um débito, nas duas contas que participaram de uma transação.
Nota: Historicamente as entradas sempre foram registradas como números positivos mais um bit de informação de crédito ou débito (soma ou subtração), que data de épocas em que nem números negativos nem zero existiam (romanos, por exemplo), e é ainda assim que se encontram muitos sistemas contábeis digitais. Mas esta explicação usa zeros e números negativos, assim como identificadores em inglês porque eu quis.
Inicialmente precisamos de uma tabela para registrar cada conta (account) e seu respectivo saldo (balance):
create table account ( -- identificadores não são criados aqui aid bigint primary key, -- saldo da conta balance numeric not null default 0 -- outros campos );
Também precisamos registrar cada transação com um identificador (tid), que virá a ser o comprovante de pagamento ou de transferência que pode ser usado posteriormente para conferir os valores e os participantes e a data (ts):
create table transaction ( -- identificador artificial tid bigint primary key generated always as identity, -- momento da transação ts timestamptz default now() not null -- outros campos );
Uma transação simples tem dois participantes. Imagine um pix de uma pessoa para outra, em que o saldo das duas contas correntes é alterado, uma com um crédito e outra com o débito, ambas do mesmo valor absoluto. Mas outras transações podem ter mais de dois participantes, como a venda de um imóvel de duas pessoas para uma, o que irá envolver três contas, com valores diferentes. Por isso, os ids de cada conta alterada e o valor das respectivas alterações são registrados em uma terceira tabela, com as entradas (entry) dessa transação:
create table entry ( -- identificador da conta aid bigint not null references account(aid), -- identificador da transação tid bigint not null references transaction(tid), -- valor da mudança na conta feito por essa transação value numeric not null, -- outros campos primary key (aid,tid) );
Uma conta real pode precisar ser criada de forma complexa, gerando o identificador com base na agência, passando por estados de criação, avaliação de crédito etc antes de ser usada. Mas neste exemplo é suficiente inserir uma linha por conta corrente nela, com algum saldo inicial opcional:
insert into account(aid, balance) values (1, 50), (2, 100);
Igualmente é simples consultar o saldo:
select balance from account where aid = 1;
Uma transferência de recursos é mais interessante e precisa de uma transação de banco de dados para executar a transação financeira. Inclusive é pela necessidade de executar transações financeiras que SGBDs trazem transações com as garantias ACID.
begin;
-- transação iniciando com locks de longa duração
-- insert on conflict do update + deadlock
insert into account (aid, balance)
values
(de_id, -valor)
on conflict do
update set balance = balance + excluded.balance;
insert into account (aid, balance)
values
(para_id, valor)
on conflict do
update set balance = balance + excluded.balance;
-- transação financeira
insert into transaction ("...")
values
(outros)
returning tid
into _tid;
-- entradas na transação
insert into entry (aid, tid, value)
values
(de_aid, _tid, -valor),
(para_aid, _tid, valor);
commit;
A transação adquire locks sobre as contas para atualizar os saldos no início da transação e mantém eles até o final. Esses locks impedem outras transações que queiram afetar essas contas de proseguirem, levando a uma serialização da execução e desempenho limitado.
Os locks nas contas são obtidos primeiro na conta que é debitada e então na conta que é creditada. É possivel que duas ou mais transações que afetem as mesmas contas no sentido contrário consigam adquirir o primeiro lock de cada transação e fiquem bloqueadas esperando uma a outra liberar a outra conta, levando a um deadlock.
Como as transações financeiras são intimamente amarradas nas transações de banco de dados, o desempenho se torna algo importante de ser mensurado em cada modelagem e monitorado em produção.
O benchmark TPC (TPC-C, TPC-B e TPC-A são diferentes versões) é desenvolvido para uniformizar a forma como diversos SGBDs medem seus desempenhos. Ainda assim, existe muita customização dos SGBDs para adequar o TPC aos seus produtos.
O PostgreSQL traz o pgbench, que traz uma modelagem de ledger próxima do TPC-B. Ele também permite criar suas modelagens e scripts de benchmark.
A seguir estão arquivos com as modelagens discutidas e respectivos scripts de pgbench para medir o desempenho.