0

Problem to insert running total in MySQL transactional database. need your help for solutions and opinion. Table structure of my table is,

create table `wtacct` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `ACCOUNT_NO` varchar(16),
  `AMOUNT` float(16,2),
  `BALANCE` float(16,2)
);

[Please note other fields have been removed to make it simple example]

I am doing Transaction as,

  • Dr 10 USD from account 1001 and
  • Cr 10 USD to account 2002

Insert query

INSERT INTO wtacct (ID, ACCOUNT_NO, AMOUNT, BALANCE) 
VALUES ('', 1001, -10, 100), ('', 2002, 10, 5000);

I want the Balance as,
BALANCE of Account no 1001 = Last transaction Balance of account 1001 - 10.

My solutions and limitations

Solution 1

In insert statement put sub query in balance field:

select balance from wtacct where account_no=1001 and id in(select max(id) from wtacct where account_no=1001)

Limitation: Mysql does not support same table select query (wtacct) where inserting the data (wtacct).

Solution 2

Using insert into select statement

insert into wtacct select '' ID, 1001 ACCOUNT_NO, -10 AMOUNT, (BALANCE-10) BALANCE where account_no=1001 and id in(select max(id) from wtacct where account_no=1001)

Limitation: For first transaction there is no record in wtacct for the account 1001 so select query will not return any record for first transaction.

Solution 3

Taking balance in variable and use it in insert statement.

select @balance1001 :=balance from wtacct
  where account_no=1001 and id in(select max(id) from wtacct where account_no=1001)

select @balance2002 :=balance from wtacct
  where account_no=2002 and id in(select max(id) from wtacct where account_no=2002)

INSERT INTO wtacct (ID, ACCOUNT_NO, AMOUNT, BALANCE) 
VALUES ('', 1001, -10, @balance1001-10), ('', 2002, 10, @balance2002+10);

Limitation: there is a chance to be change the balance in time between select and insert query execution. also its costly, 3 query execution required.

Solution 4

Insert and then update Balance

INSERT INTO wtacct (ID, ACCOUNT_NO, AMOUNT, BALANCE) 
VALUES ('', 1001, -10, 0);

UPDATE wtacct set balance = (ifnull(Select balance from wtacct where account_no=1001 and id in(select max(id) from wtacct where id <last_insert_id() and account_no=1001),0) -10) 
  where id =last_insert_id() and account_no=1001

........

Limitation: query is costly. its required 4 (two insert and 2 update) query execution. note last_insert_id() is php function

Solution 5

Using a trigger on insert statement. In the trigger, the balance will be updated calculating last transaction value and insert amount.

Limitation: Trigger not support transaction behavior and may fail.

Please give your solution and opinion on the above solutions. Please note in the above example their may be some syntax error/error. Please ignore them.

Faiz
  • 13
  • 2
  • Hello Faiz, please clean up your question to remove unnecessary information. Try to focus specifically on the question at hand. This is way too much text for someone to easily extract a coherent question from. –  Apr 15 '16 at 16:48
  • Dear Tara, thank you. problem is: problem to insert running total in mysql transactional database. i drew real scenario so its looks clumsy. sorry for that. – Faiz Apr 15 '16 at 16:55
  • No need to put a tag in the title, also I cleaned up most of the body. The queries could use some better formatting. – Mr. Polywhirl Apr 15 '16 at 17:00
  • thanks Mr. Polywhirl for formatting. its is now more understandable. – Faiz Apr 15 '16 at 17:08

2 Answers2

1

A big limitation I didn't see listed is a potential race condition, where two rows are being inserted into the table at the same time. There's a chance that the two inserts will both get the current "balance" from the same previous row.

One question: do you also have a separate "current balance" table that keeps a single value of the current "balance" for each account? Or are you only relying on the "balance" from the previous transaction.

Personally, I would track the current balance on a separate "account balance" table. And I would use BEFORE INSERT/UPDATE triggers to maintain the value in that row, and use that to return the current balance for the account.

For example, I would define a trigger like this which gets fired when a row is inserted into `wtacct` table:

CREATE TRIGGER wtacct_bi
BEFORE INSERT ON wtacct
FOR EACH ROW
BEGIN
  IF NEW.amount IS NULL THEN
     SET NEW.amount = 0;
  END IF
  ;
  UPDATE acct a
     SET a.balance = (@new_balance := a.balance + NEW.amount)
   WHERE a.account_no = NEW.account_no
  ; 
  SET NEW.balance = @new_balance
  ;
END$$

The setup for that trigger...

CREATE TABLE acct 
( account_no  VARCHAR(16) NOT NULL PRIMARY KEY
, balance     DECIMAL(20,2) NOT NULL DEFAULT 0
) ENGINE=InnoDB
;

CREATE TABLE wtacct 
( id          BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT
, account_no  VARCHAR(16) NOT NULL COMMENT 'FK ref acct.account_no'
, amount      DECIMAL(20,2) NOT NULL
, balance     DECIMAL(20,2) NOT NULL 
, FOREIGN KEY FK_wtacct_acct (account_no) REFERENCES acct (account_no)
    ON UPDATE CASCADE ON DELETE RESTRICT
) ENGINE=InnoDB
;

My reason for using a separate "current balance" table is that there is only one row for the given account_no, and that row retains the current balance of the account.

The UPDATE statement in the trigger should obtain an exclusive lock on the row being updated. And that exclusive lock prevents any other UPDATE statement from simultaneously updating the same row. The execution of the UPDATE statement will add the `amount` from the current transaction row being inserted to the current balance.

If we were using Oracle or PostgreSQL, we could use a RETURNING clause to get the value that was assigned to the \'balance\' column.

In MySQL we can do a wonky workaround, using a user-defined variable. The new value we are going to assign to the column is first assigned to the user_defined variable, and then that is assigned to the column.

And we can assign the value of the user-defined variable to the `balance` column of the row being inserted into `wtacct`.

The purpose of this approach is to make the retrieval and update of the current balance in a single statement, to avoid any race conditions.

The UPDATE statement locates the row, obtains an exclusive (X) lock on the row, retrieves the current balance (value from the \'balance\' column), calculates the new current balance, and assigns it back to the \'balance\' column. Then continues to hold the lock until the transaction completes.

Once the trigger completes, the INSERT statement (which initially fired the trigger) proceeds, attempting to insert the new row into `wtacct`. If that fails, then all of the changes made by the INSERT statement and execution of the trigger are rolled back, keeping everything consistent.

Once a COMMIT or ROLLBACK is issued by the session, the exclusive (X) lock held on the row(s) in `acct` are released, and other sessions can obtain locks on that row in `acct`.

spencer7593
  • 106,611
  • 15
  • 112
  • 140
  • Answer of your question: yes, i use WMACCT table to store each account balance. fields acct, sta, bal,.... you did correct answer which is already implemented as alternative, that is, insert into wtacct (acct, amt,bal,...) values('1001', 10, (select bal from wmacct where acct='1001')).... but the problems are 1) cross check not possible and 2) wmacct and wtacct mismatch report never give any mismatch record return. i want to check wmacct bal and wtacct last bal of the account_no. for strong application level security i want to maintain two different level of entry of each transaction. thnk u. – Faiz Apr 16 '16 at 07:01
  • Very good explanation on solving race condition with the addition of balance table. Instead of using MySQL trigger, it may be easier to implement the procedure in the application itself, in my case, PHP script: before row insertion in main table, update the balance table. – kiatng Nov 18 '20 at 07:20
0

I have done it using Store Procedure for MySql

CREATE DEFINER=`root`@`%` PROCEDURE `example_add`(IN dr Int, IN cr Int)
BEGIN

DECLARE LID int;
Declare Balance decimal(16,2);

INSERT INTO example (Debit,Credit)
VALUES (dr, cr);

SET LID = LAST_INSERT_ID();

SET Balance = (select SUM(Debit) - SUM(Credit) as Balance from example);

UPDATE Example SET  Balance = Balance WHERE ID = LID;

END

Use it example_add(10,0) or example_add(0,15) then select and see the result.

Irfan Ashraf
  • 2,430
  • 21
  • 20