É bom manter um valor que seja atualizado em uma tabela?

31

Estamos desenvolvendo uma plataforma para cartões pré-pagos, que basicamente contém dados sobre cartões e seus saldos, pagamentos, etc.

Até agora, tínhamos uma entidade do cartão que possui uma coleção de entidades da conta e cada conta tem um valor que é atualizado a cada depósito / retirada.

Há um debate agora na equipe; alguém nos disse que isso infringe as 12 regras do Codd e que atualizar seu valor a cada pagamento é um problema.

Isto é realmente um problema?

Se for, como podemos corrigir isso?

Mithir
fonte
3
Há uma extensa discussão técnica sobre este tema aqui na DBA.SE: escrita de um esquema de banco simples
Nick Chammas
1
Quais das regras de Codd sua equipe citou aqui? As regras foram sua tentativa de definir um sistema relacional e não mencionaram a normalização explicitamente. Codd discutiu a normalização em seu livro O modelo relacional para gerenciamento de banco de dados .
Iain Samuel McLean Elder

Respostas:

30

Sim, isso não é normalizado, mas ocasionalmente projetos não normalizados vencem por razões de desempenho.

No entanto, eu provavelmente abordaria um pouco diferente, por razões de segurança. (Isenção de responsabilidade: atualmente não trabalho, nem nunca trabalhei no setor financeiro. Só estou jogando isso lá fora.)

Tenha uma tabela para os saldos registrados nos cartões. Isso teria uma linha inserida para cada conta, indicando o saldo lançado ao final de cada período (dia, semana, mês ou o que for apropriado). Indexe esta tabela por número de conta e data.

Use outra tabela para manter transações pendentes, que são inseridas em tempo real. No final de cada período, execute uma rotina que inclua as transações não lançadas no último saldo final da conta para calcular o novo saldo. Marque as transações pendentes como lançadas ou verifique as datas para determinar o que ainda está pendente.

Dessa forma, você tem um meio de calcular o saldo do cartão sob demanda, sem precisar resumir todo o histórico da conta e, colocando o recálculo do saldo em uma rotina de lançamento dedicada, pode garantir que a segurança da transação desse recálculo seja limitada a um único local (e também limita a segurança na tabela de saldo, para que somente a rotina de lançamento possa gravar nele).

Depois, basta manter o máximo de dados históricos necessários para auditoria, atendimento ao cliente e requisitos de desempenho.

db2
fonte
1
Apenas duas notas rápidas. Primeiro, é uma descrição muito boa da abordagem de agregação de log-instantâneo que eu estava sugerindo acima, e talvez mais clara do que eu era. (Voto a favor de você). Em segundo lugar, suspeito que você esteja usando o termo "publicado" de maneira estranha aqui, para significar "parte do saldo final". Em termos financeiros, postado geralmente significa "aparecer no saldo contábil atual" e, portanto, parecia valer a pena explicar isso, para não causar confusão.
precisa saber é o seguinte
Sim, provavelmente há muitas sutilezas que estou perdendo. Estou apenas me referindo a como as transações parecem ser "lançadas" na minha conta corrente no fechamento dos negócios e o saldo atualizado de acordo. Mas eu não sou contador; Eu apenas trabalho com vários deles.
db2 29/01
Isso também pode ser um requisito para SOX ou algo semelhante no futuro. Não sei exatamente que tipo de requisitos de microtransação você deve registrar, mas gostaria de perguntar a alguém que sabe quais são os requisitos de relatórios posteriormente.
Jcolebrand
Eu estaria inclinado a manter dados perpétuos, por exemplo, saldo no início de cada ano, para que o instantâneo dos "totais" nunca seja substituído - a lista simplesmente será anexada (mesmo que o sistema permaneça em uso por tempo suficiente para cada conta acumular 1.000 totais anuais [ MUITO otimista], que dificilmente seriam incontroláveis). Manter muitos totais anuais permitiria que o código de auditoria confirmasse que as transações entre os últimos anos tiveram os efeitos adequados sobre os totais [transações individuais podem ser eliminadas após cinco anos, mas seriam bem examinadas até então].
supercat
17

Por outro lado, há um problema que encontramos com freqüência no software de contabilidade. Parafraseado:

Preciso realmente agregar dez anos de dados para descobrir quanto dinheiro há na conta corrente?

A resposta, claro, é não, você não. Existem algumas abordagens aqui. Um deles é armazenar o valor calculado. Eu não recomendo esta abordagem porque os bugs de software que causam valores incorretos são muito difíceis de rastrear e, portanto, evitaria essa abordagem.

Uma maneira melhor de fazer isso é o que chamo de abordagem agregação de log-instantâneo. Nessa abordagem, nossos pagamentos e usos são inserções e nunca atualizamos esses valores. Periodicamente, agregamos os dados durante um período de tempo e inserimos um registro de instantâneo calculado que representa os dados no momento em que o instantâneo se tornou válido (geralmente um período de tempo antes do presente).

Agora, isso não viola as regras do Codd porque, com o tempo, os instantâneos podem ser menos do que perfeitamente dependentes dos dados de pagamento / uso inseridos. Se tivermos instantâneos de trabalho, podemos decidir limpar dados com 10 anos de idade sem afetar nossa capacidade de calcular os saldos atuais sob demanda.

Chris Travers
fonte
2
Posso armazenar totais calculados e estou perfeitamente seguro - restrições confiáveis ​​garantem que meus números estejam sempre corretos: sqlblog.com/blogs/alexander_kuznetsov/archive/2009/01/23/…
AK
1
Não há casos extremos na minha solução - uma restrição confiável não permitirá que você esqueça nada. Não vejo nenhuma necessidade prática de quantidades NULL em um sistema da vida real que precise saber totais em execução - essas coisas se contradizem. Se você encontrar uma necessidade prática, compartilhe seu sceanrio.
AK
1
Ok, mas isso não vai funcionar como no db, que permite vários NULLs sem violar a exclusividade, certo? Além disso, sua garantia fica ruim se você limpar os dados passados, certo?
precisa saber é o seguinte
1
Por exemplo, se eu tenho uma restrição única em (a, b) no PostgreSQL, posso ter vários valores (1, nulo) para (a, b) porque cada nulo é tratado como potencialmente único, que eu acho que é semanticamente correto para desconhecido valores .....
Chris Travers
1
Em relação a "Eu tenho uma restrição única em (a, b) no PostgreSQL, posso ter vários valores (1, nulos)" - no PostgreSql, precisamos usar um índice parcial exclusivo em (a) onde b é nulo.
AK
7

Por razões de desempenho, na maioria dos casos, devemos armazenar o saldo atual - caso contrário, calculá-lo em tempo real pode eventualmente tornar-se proibitivamente lento.

Armazenamos totais de execução pré-calculados em nosso sistema. Para garantir que os números estejam sempre corretos, usamos restrições. A solução a seguir foi copiada do meu blog. Ele descreve um inventário, que é essencialmente o mesmo problema:

O cálculo dos totais em execução é notoriamente lento, seja com um cursor ou com uma junção triangular. É muito tentador desnormalizar, armazenar totais em execução em uma coluna, especialmente se você a selecionar com frequência. No entanto, como sempre, quando você desnormaliza, precisa garantir a integridade de seus dados desnormalizados. Felizmente, você pode garantir a integridade dos totais em execução com restrições - desde que todas as suas restrições sejam confiáveis, todos os totais em execução estejam corretos. Dessa forma, você pode facilmente garantir que o saldo atual (totais em execução) nunca seja negativo - a imposição de outros métodos também pode ser muito lenta. O script a seguir demonstra a técnica.

CREATE TABLE Data.Inventory(InventoryID INT NOT NULL IDENTITY,
  ItemID INT NOT NULL,
  ChangeDate DATETIME NOT NULL,
  ChangeQty INT NOT NULL,
  TotalQty INT NOT NULL,
  PreviousChangeDate DATETIME NULL,
  PreviousTotalQty INT NULL,
  CONSTRAINT PK_Inventory PRIMARY KEY(ItemID, ChangeDate),
  CONSTRAINT UNQ_Inventory UNIQUE(ItemID, ChangeDate, TotalQty),
  CONSTRAINT UNQ_Inventory_Previous_Columns UNIQUE(ItemID, PreviousChangeDate, PreviousTotalQty),
  CONSTRAINT FK_Inventory_Self FOREIGN KEY(ItemID, PreviousChangeDate, PreviousTotalQty)
    REFERENCES Data.Inventory(ItemID, ChangeDate, TotalQty),
  CONSTRAINT CHK_Inventory_Valid_TotalQty CHECK(TotalQty >= 0 AND (TotalQty = COALESCE(PreviousTotalQty, 0) + ChangeQty)),
  CONSTRAINT CHK_Inventory_Valid_Dates_Sequence CHECK(PreviousChangeDate < ChangeDate),
  CONSTRAINT CHK_Inventory_Valid_Previous_Columns CHECK((PreviousChangeDate IS NULL AND PreviousTotalQty IS NULL)
            OR (PreviousChangeDate IS NOT NULL AND PreviousTotalQty IS NOT NULL))
);
GO
-- beginning of inventory for item 1
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
VALUES(1, '20090101', 10, 10, NULL, NULL);
-- cannot begin the inventory for the second time for the same item 1
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
VALUES(1, '20090102', 10, 10, NULL, NULL);

Msg 2627, Level 14, State 1, Line 10
Violation of UNIQUE KEY constraint 'UNQ_Inventory_Previous_Columns'. Cannot insert duplicate key in object 'Data.Inventory'.
The statement has been terminated.

-- add more
DECLARE @ChangeQty INT;
SET @ChangeQty = 5;
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
SELECT TOP 1 ItemID, '20090103', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

SET @ChangeQty = 3;
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
SELECT TOP 1 ItemID, '20090104', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

SET @ChangeQty = -4;
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
SELECT TOP 1 ItemID, '20090105', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

-- try to violate chronological order

SET @ChangeQty = 5;
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
SELECT TOP 1 ItemID, '20081231', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

Msg 547, Level 16, State 0, Line 4
The INSERT statement conflicted with the CHECK constraint "CHK_Inventory_Valid_Dates_Sequence". The conflict occurred in database "Test", table "Data.Inventory".
The statement has been terminated.


SELECT ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty
FROM Data.Inventory ORDER BY ChangeDate;

ChangeDate              ChangeQty   TotalQty    PreviousChangeDate      PreviousTotalQty
----------------------- ----------- ----------- ----------------------- -----
2009-01-01 00:00:00.000 10          10          NULL                    NULL
2009-01-03 00:00:00.000 5           15          2009-01-01 00:00:00.000 10
2009-01-04 00:00:00.000 3           18          2009-01-03 00:00:00.000 15
2009-01-05 00:00:00.000 -4          14          2009-01-04 00:00:00.000 18


-- try to change a single row, all updates must fail
UPDATE Data.Inventory SET ChangeQty = ChangeQty + 2 WHERE InventoryID = 3;
UPDATE Data.Inventory SET TotalQty = TotalQty + 2 WHERE InventoryID = 3;
-- try to delete not the last row, all deletes must fail
DELETE FROM Data.Inventory WHERE InventoryID = 1;
DELETE FROM Data.Inventory WHERE InventoryID = 3;

-- the right way to update

DECLARE @IncreaseQty INT;
SET @IncreaseQty = 2;
UPDATE Data.Inventory SET ChangeQty = ChangeQty + CASE WHEN ItemID = 1 AND ChangeDate = '20090103' THEN @IncreaseQty ELSE 0 END,
  TotalQty = TotalQty + @IncreaseQty,
  PreviousTotalQty = PreviousTotalQty + CASE WHEN ItemID = 1 AND ChangeDate = '20090103' THEN 0 ELSE @IncreaseQty END
WHERE ItemID = 1 AND ChangeDate >= '20090103';

SELECT ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty
FROM Data.Inventory ORDER BY ChangeDate;

ChangeDate              ChangeQty   TotalQty    PreviousChangeDate      PreviousTotalQty
----------------------- ----------- ----------- ----------------------- ----------------
2009-01-01 00:00:00.000 10          10          NULL                    NULL
2009-01-03 00:00:00.000 7           17          2009-01-01 00:00:00.000 10
2009-01-04 00:00:00.000 3           20          2009-01-03 00:00:00.000 17
2009-01-05 00:00:00.000 -4          16          2009-01-04 00:00:00.000 20
AK
fonte
Ocorre-me que um dos grandes limites da sua abordagem é que o cálculo do saldo de uma conta em uma data histórica específica ainda exige agregação, a menos que você também assuma que todas as transações são inseridas sequencialmente por data (o que geralmente é ruim suposição).
Chris Travers
@ChrisTravers todos os totais em execução estão sempre atualizados, para todas as datas históricas. Restrições garantem isso. Portanto, nenhuma agregação é necessária para datas históricas. Se tivermos que atualizar alguma linha histórica ou inserir algo antigo, atualizamos o total de todas as linhas posteriores. Eu acho que isso é muito mais fácil no postgreSql, porque possui restrições adiadas.
AK
6

Esta é uma pergunta muito boa.

Supondo que você tenha uma tabela de transações que armazene cada débito / crédito, não há nada de errado com seu design. De fato, trabalhei com sistemas de telecomunicações pré-pagos que funcionavam exatamente dessa maneira.

A principal coisa que você precisa fazer é garantir que você esteja fazendo um SELECT ... FOR UPDATEsaldo enquanto faz INSERTo débito / crédito. Isso garantirá o saldo correto se algo der errado (porque toda a transação será revertida).

Como outros já apontaram, você precisará de uma captura instantânea de saldos em períodos específicos para verificar se todas as transações em um determinado período somam os saldos inicial / final do período corretamente. Escreva um trabalho em lotes que seja executado à meia-noite no final do período (mês / semana / dia) para fazer isso.

Philᵀᴹ
fonte
4

O saldo é um valor calculado com base em certas regras de negócios; portanto, você não deseja manter o saldo, mas calculá-lo a partir das transações no cartão e, portanto, na conta.

Você deseja acompanhar todas as transações no cartão para relatórios de auditoria e extrato e até dados de sistemas diferentes posteriormente.

Bottom line - calcule todos os valores que precisam ser computados como e quando você precisar

Stephen Senkomago Musoke
fonte
mesmo se houver milhares de transações? Então, eu preciso recalculá-lo toda vez? não pode ser um pouco difícil no desempenho? você pode adicionar um pouco sobre por que isso é um problema?
Mithir
2
@Mithir Porque é contrário à maioria das regras contábeis e torna impossível rastrear problemas. Se você acabou de atualizar um total em execução, como você sabe quais ajustes foram aplicados? Essa fatura foi creditada uma ou duas vezes? Já deduzimos o valor do pagamento? Se você controla as transações, conhece as respostas; se controla um total, não.
JNK
4
A referência às regras do Codd é que ele quebra a forma normal. Supondo que você rastreie as transações EM ALGUM LUGAR (que eu acho que você precisará) e que você tenha um total em execução separado, o que é correto se eles não concordarem? Você precisa de uma única versão da verdade. Não corrija o problema de desempenho até / a menos que ele realmente exista.
JNK
@JNK do jeito que está agora - mantemos as transações e um total, para que tudo o que você mencionou possa ser rastreado perfeitamente, se necessário, o saldo total é apenas para impedir que recalculemos o valor a cada ação.
Mithir
2
Agora, não infringirá as regras do Codd se os dados antigos só puderem ser mantidos por, digamos, 5 anos, certo? O saldo nesse momento não é apenas a soma dos registros existentes, mas também os registros existentes anteriormente desde a limpeza, ou estou perdendo alguma coisa? Parece-me que apenas violaria as regras de Codd se assumirmos retenção infinita de dados, o que é improvável. Dito isto, pelas razões expostas abaixo, acho que armazenar um valor continuamente atualizado está causando problemas.
Chris Travers