ledger

Estudo de caso: 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.

Modelagem mais simples

1 Tabela de conta (account)

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
);

2 Tabela de transação (transaction)

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
);

3 Tabela de entradas (entry)

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)
);

4 Criação de conta

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);

5 Consulta de saldo

Igualmente é simples consultar o saldo:

select balance
	from account
	where aid = 1;

6 Transação financeira

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;

Problemas com essa modelagem mais simples

1 Locks

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.

2 Deadlock

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.

TODO: terminar a explicação sobre as modelagens mais avançadas

Benchmarks

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.

Downloads

A seguir estão arquivos com as modelagens discutidas e respectivos scripts de pgbench para medir o desempenho.

1 Estruturas das tabelas

2 Scripts de benchmarks