¿Está bien mantener un valor que se actualiza en una tabla?

31

Estamos desarrollando una plataforma para tarjetas prepagas, que básicamente contiene datos sobre tarjetas y su saldo, pagos, etc.

Hasta ahora teníamos una entidad de Tarjeta que tiene una colección de entidad de Cuenta, y cada Cuenta tiene un Monto, que se actualiza en cada Depósito / Retiro.

Hay un debate ahora en el equipo; alguien nos ha dicho que esto rompe las 12 Reglas de Codd y que actualizar su valor en cada pago es un problema.

¿Es esto realmente un problema?

Si es así, ¿cómo podemos solucionar esto?

Mithir
fuente
3
Hay una extensa discusión técnica sobre este tema aquí en DBA.SE: Escribir un esquema bancario simple
Nick Chammas
1
¿Cuál de las reglas de Codd citó tu equipo aquí? Las reglas fueron su intento de definir un sistema relacional, y no mencionaron la normalización explícitamente. Codd discutió la normalización en su libro El modelo relacional para la gestión de bases de datos .
Iain Samuel McLean Élder

Respuestas:

30

Sí, eso no está normalizado, pero en ocasiones los diseños no normalizados ganan por razones de rendimiento.

Sin embargo, probablemente lo abordaría de manera un poco diferente, por razones de seguridad. (Descargo de responsabilidad: actualmente no, ni he trabajado en el sector financiero. Solo estoy lanzando esto por ahí).

Tenga una tabla para los saldos publicados en las tarjetas. Esto tendría una fila insertada para cada cuenta, indicando el saldo registrado al cierre de cada período (día, semana, mes o lo que sea apropiado). Indice esta tabla por número de cuenta y fecha.

Use otra tabla para mantener las transacciones pendientes, que se insertan sobre la marcha. Al cierre de cada período, ejecute una rutina que agregue las transacciones no publicadas al último saldo de cierre de la cuenta para calcular el nuevo saldo. Marque las transacciones pendientes como publicadas, o mire las fechas para determinar qué está pendiente.

De esta manera, tiene un medio para calcular el saldo de la tarjeta a pedido, sin tener que resumir todo el historial de la cuenta, y al poner el recálculo del saldo en una rutina de contabilización dedicada, puede asegurarse de que la seguridad de la transacción de este recálculo se limite a un solo lugar (y también limitan la seguridad en la tabla de balance para que solo la rutina de contabilización pueda escribirle).

Luego, conserve la mayor cantidad de datos históricos que requiera la auditoría, el servicio al cliente y los requisitos de rendimiento.

db2
fuente
1
Solo dos notas rápidas. Primero, esa es una muy buena descripción del enfoque de registro-agregado-instantánea que sugería anteriormente, y quizás más claro de lo que era. (Te votó). En segundo lugar, sospecho que está utilizando el término "publicado" de forma algo extraña aquí, para referirse a "parte del saldo final". En términos financieros, publicado generalmente significa "aparecer en el saldo actual del libro mayor", por lo que parecía que valía la pena explicarlo, por lo que no causó confusión.
Chris Travers
Sí, probablemente hay muchas sutilezas que me estoy perdiendo. Me estoy refiriendo a cómo las transacciones parecen estar "registradas" en mi cuenta corriente al cierre del negocio, y el saldo actualizado en consecuencia. Pero no soy contador; Solo trabajo con varios de ellos.
db2
Esto también puede ser un requisito para SOX o similar en el futuro, no sé exactamente qué tipo de requisitos de micro-transacción tiene que registrar, pero definitivamente le preguntaría a alguien que sepa cuáles son los requisitos de informes para más adelante.
jcolebrand
Me inclinaría a mantener datos perpetuos para, por ejemplo, el saldo al comienzo de cada año, de modo que la instantánea de "totales" nunca se sobrescriba: la lista simplemente se agrega (incluso si el sistema se mantuvo en uso el tiempo suficiente para que cada cuenta acumular 1,000 totales anuales [ MUY optimista], eso difícilmente sería inmanejable). Mantener muchos totales anuales permitiría que el código de auditoría confirme que las transacciones entre los últimos años tuvieron los efectos apropiados en los totales [las transacciones individuales podrían purgarse después de 5 años, pero estarían bien examinadas para entonces].
supercat
17

Por otro lado, hay un problema que encontramos con frecuencia en el software de contabilidad. Parafraseado:

¿ Realmente necesito agregar diez años de datos para averiguar cuánto dinero hay en la cuenta corriente?

La respuesta, por supuesto, es no, no lo haces. Hay algunos enfoques aquí. Uno está almacenando el valor calculado. No recomiendo este enfoque porque los errores de software que causan valores incorrectos son muy difíciles de rastrear, por lo que evitaría este enfoque.

Una mejor manera de hacerlo es lo que yo llamo el enfoque agregado de registro de instantáneas. En este enfoque, nuestros pagos y usos son insertos y nunca actualizamos estos valores. Periódicamente agregamos los datos durante un período de tiempo e insertamos un registro de instantánea calculado que representa los datos en el momento en que la instantánea se hizo válida (generalmente un período de tiempo antes del presente).

Ahora, esto no rompe las reglas de Codd porque con el tiempo las instantáneas pueden ser menos que perfectamente dependientes de los datos de pago / uso insertados. Si tenemos instantáneas de trabajo, podemos decidir purgar datos de 10 años sin afectar nuestra capacidad de calcular los saldos actuales bajo demanda.

Chris Travers
fuente
2
Puedo almacenar totales acumulados
AK
1
No hay casos extremos en mi solución: una restricción confiable no le permitirá olvidar nada. No veo ninguna necesidad práctica de cantidades NULAS en un sistema de la vida real que necesite conocer los totales acumulados; estas cosas se contradicen entre sí. Si ve una necesidad práctica, comparta su sceanrio.
AK
1
Ok, pero esto no va a funcionar como lo está en los db que permiten múltiples NULL sin violar la unicidad, ¿verdad? Además, su garantía va mal si purga datos anteriores, ¿verdad?
Chris Travers
1
Por ejemplo, si tengo una restricción única en (a, b) en PostgreSQL, puedo tener múltiples valores (1, nulos) para (a, b) porque cada nulo se trata como potencialmente único, lo que creo que es semánticamente correcto para desconocido valores .....
Chris Travers
1
Con respecto a "Tengo una restricción única en (a, b) en PostgreSQL, puedo tener múltiples valores (1, nulos)" - en PostgreSql necesitamos usar un índice parcial único en (a) donde b es nulo.
AK
7

Por razones de rendimiento, en la mayoría de los casos debemos almacenar el saldo actual; de lo contrario, calcularlo sobre la marcha puede llegar a ser extremadamente lento.

Almacenamos totales acumulados precalculados en nuestro sistema. Para garantizar que los números sean siempre correctos, utilizamos restricciones. La siguiente solución ha sido copiada de mi blog. Describe un inventario, que es esencialmente el mismo problema:

Calcular los totales acumulados es notoriamente lento, ya sea que lo haga con un cursor o con una unión triangular. Es muy tentador desnormalizar, almacenar totales acumulados en una columna, especialmente si lo selecciona con frecuencia. Sin embargo, como es habitual cuando se desnormaliza, debe garantizar la integridad de sus datos desnormalizados. Afortunadamente, puede garantizar la integridad de los totales acumulados con restricciones: siempre y cuando todas sus restricciones sean confiables, todos sus totales acumulados son correctos. También de esta manera puede asegurarse fácilmente de que el saldo actual (totales acumulados) nunca sea negativo: la aplicación por otros métodos también puede ser muy lenta. El siguiente script demuestra la 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
Alaska
fuente
Se me ocurre que uno de los grandes límites de su enfoque es que calcular el saldo de una cuenta en una fecha histórica específica todavía requiere agregar, a menos que también asuma que todas las transacciones se ingresan secuencialmente por fecha (lo que generalmente es un mal suposición).
Chris Travers
@ChrisTravers todos los totales acumulados están siempre actualizados, para todas las fechas históricas. Las restricciones lo garantizan. Por lo tanto, no se necesita agregación para ninguna fecha histórica. Si tenemos que actualizar alguna fila histórica, o insertar algo retroactivo, actualizamos los totales acumulados de todas las filas posteriores. Creo que esto es mucho más fácil en postgreSql, porque tiene restricciones diferidas.
AK
6

Esta es una muy buena pregunta.

Suponiendo que tiene una tabla de transacciones que almacena cada débito / crédito, su diseño no tiene nada de malo. De hecho, he trabajado con sistemas de telecomunicaciones prepagos que han funcionado exactamente de esta manera.

Lo principal que debe hacer es asegurarse de que está haciendo una parte SELECT ... FOR UPDATEdel saldo mientras realiza INSERTel débito / crédito. Esto garantizará el saldo correcto si algo sale mal (porque la transacción completa se revertirá).

Como otros han señalado, necesitará una instantánea de los saldos en períodos de tiempo específicos para verificar que todas las transacciones en un período determinado sumen los saldos de inicio / fin del período correctamente. Escriba un trabajo por lotes que se ejecute a medianoche al final del período (mes / semana / día) para hacer esto.

Philᵀᴹ
fuente
4

El saldo es una cantidad calculada basada en ciertas reglas comerciales, por lo que sí, no desea mantener el saldo sino calcularlo a partir de las transacciones en la tarjeta y, por lo tanto, de la cuenta.

Desea realizar un seguimiento de todas las transacciones en la tarjeta para auditoría e informes de estado de cuenta, e incluso datos de diferentes sistemas más adelante.

En pocas palabras: calcule cualquier valor que deba calcularse cuando lo necesite

Stephen Senkomago Musoke
fuente
incluso si puede haber miles de transacciones? ¿Entonces tendré que volver a calcularlo cada vez? ¿No puede ser un poco duro para el rendimiento? ¿Puedes agregar un poco sobre por qué esto es un problema?
Mithir
2
@Mithir Porque va en contra de la mayoría de las reglas de contabilidad, y hace que los problemas sean imposibles de rastrear. Si solo actualiza un total acumulado, ¿cómo sabe qué ajustes se han aplicado? ¿Se acreditó esa factura una o dos veces? ¿Ya deducimos el monto del pago? Si realiza un seguimiento de las transacciones, conoce las respuestas, si realiza un seguimiento de un total, no lo hace.
JNK
44
La referencia a las reglas de Codd es que rompe la forma normal. Suponiendo que realiza un seguimiento de las transacciones EN ALGUNA PARTE (lo que creo que tendrá que hacer), y tiene un total acumulado por separado, ¿cuál es correcto si no están de acuerdo? Necesitas una única versión de la verdad. No arregle el problema de rendimiento hasta / a menos que realmente exista.
JNK
@JNK como está ahora: mantenemos las transacciones y un total, por lo que todo lo que mencionó puede rastrearse perfectamente si es necesario, el saldo total es solo para evitar que recalculemos el monto de cada acción.
Mithir
2
Ahora, no romperá las reglas de Codd si los datos antiguos solo pueden conservarse, digamos 5 años, ¿verdad? El saldo en ese punto no es simplemente la suma de los registros existentes, sino también los registros existentes desde la purga, ¿o me falta algo? Me parece que solo rompería las reglas de Codd si asumimos una retención de datos infinita, lo cual es poco probable. Dicho esto por las razones que digo a continuación, creo que almacenar un valor de actualización continua es un problema.
Chris Travers