How do I update a column based on the previous value? - mysql

I have read a lot of answers here but I couldn't adapt to my needs.
I have this table below where I would like to update the BALANCE column:
balance = old.balance + new.amount
+----+----------------+---------+------------+-------------+---------------------+------------------+--------+----------+---------+
| ID | TRANSACTION_ID | BANK_ID | ACCOUNT_ID | CUSTOMER_ID | CREATED | DESCRIPTION | AMOUNT | CURRENCY | BALANCE |
+----+----------------+---------+------------+-------------+---------------------+------------------+--------+----------+---------+
| 1 | T1 | 2 | 2 | 1 | 2018-04-22 00:00:00 | TRANSACTION TEST | 100.00 | GBP | NULL |
| 2 | T2 | 2 | 2 | 1 | 2018-04-22 00:00:00 | TRANSACTION TEST | 125.00 | GBP | NULL |
| 3 | T3 | 2 | 2 | 1 | 2018-04-22 00:00:00 | TRANSACTION TEST | -73.00 | GBP | NULL |
+----+----------------+---------+------------+-------------+---------------------+------------------+--------+----------+---------+
This is the result I would like is shown below:
I got it executing:
SET #balance:=0;
UPDATE TRANSACTIONS SET BALANCE = (#balance := #balance + AMOUNT) WHERE ID > 0;
There is no way to fire the statement above after a new column inserted?
+----+----------------+---------+------------+-------------+---------------------+------------------+--------+----------+---------+
| ID | TRANSACTION_ID | BANK_ID | ACCOUNT_ID | CUSTOMER_ID | CREATED | DESCRIPTION | AMOUNT | CURRENCY | BALANCE |
+----+----------------+---------+------------+-------------+---------------------+------------------+--------+----------+---------+
| 1 | T1 | 2 | 2 | 1 | 2018-04-22 00:00:00 | TRANSACTION TEST | 100.00 | GBP | 100.00 |
| 2 | T2 | 2 | 2 | 1 | 2018-04-22 00:00:00 | TRANSACTION TEST | 125.00 | GBP | 225.00 |
| 3 | T3 | 2 | 2 | 1 | 2018-04-22 00:00:00 | TRANSACTION TEST | -73.00 | GBP | 152.00 |
+----+----------------+---------+------------+-------------+---------------------+------------------+--------+----------+---------+
I tried using trigger:
DELIMITER $$
CREATE TRIGGER updateBalance AFTER INSERT ON TRANSACTIONS
FOR EACH ROW
BEGIN
SET NEW.BALANCE = BALANCE + NEW.AMOUNT;
END $$
DELIMITER ;
And I got the error:
Error Code: 1362. Updating of NEW row is not allowed in after trigger
I am new in SQL and MySQL and I believe this is a common task for advanced users.

Values that can be calculated from other (materialized) values shouldn't be materialized as this can lead to inconsistencies.
Remove the column all together.
ALTER TABLE transactions
DROP balance;
And create a view instead:
CREATE VIEW transactions_with_balance
AS
SELECT t1.*,
(SELECT sum(t2.amount)
FROM transactions t2
WHERE t2.bank_id = t1.bank_id
AND t2.account_id = t1.account_id
AND t2.id <= t1.id) balance
FROM transactions t1;
db<>fiddle
If you're using MySQL version 8 or higher you can also replace the subquery by the windowed version of sum()
CREATE VIEW transactions_with_balance
AS
SELECT t1.*,
sum(amount) OVER (PARTITION BY t1.bank_id,
t1.account_id
ORDER BY t1.id) balance
FROM transactions t1;
db<>fiddle
The column customer_id also seems misplaced in the table as I suppose there is an account table where the customer that account belongs to is stored in a foreign key to the customer table. So you can get the customer via the accoount_id.

You can do this with a BEFORE INSERT trigger, summing all the transaction amounts for the given CUSTOMER_ID and adding the new AMOUNT value to get the balance:
DELIMITER $$
CREATE TRIGGER updateBalance BEFORE INSERT ON transactions
FOR EACH ROW
BEGIN
SET NEW.BALANCE = NEW.AMOUNT +
COALESCE((SELECT SUM(AMOUNT)
FROM transactions
WHERE CUSTOMER_ID = NEW.CUSTOMER_ID), 0);
END $$
DELIMITER ;
Demo on dbfiddle
Note you may want to further qualify the sum with
AND BANK_ID = NEW.BANK_ID
and/or
AND ACCOUNT_ID = NEW.ACCOUNT_ID
as necessary to distinguish exactly which records to read the previous transactions from.

Related

Update first occurrence of value in a time interval

I'm trying to set the value of another column on the first occurrence of any value in a username column in monthly intervals, if there's another column with an specific value.
create table table1
(
username varchar(30) not null,
`date` date not null,
eventid int not null,
firstFlag int null
);
insert table1 (username,`date`, eventid) values
('john','2015-01-01', 1)
, ('kim','2015-01-01', 1)
, ('john','2015-01-01', 1)
, ('john','2015-01-01', 1)
, ('john','2015-03-01', 2)
, ('john','2015-03-01', 1)
, ('kim','2015-01-01', 1)
, ('kim','2015-02-01', 1);
This should result in:
| username | date | eventid | firstFlag |
|----------|------------|---------|-----------|
| john | 2015-01-01 | 1 | 1 |
| kim | 2015-01-01 | 1 | 1 |
| john | 2015-01-01 | 1 | (null) |
| john | 2015-01-01 | 1 | (null) |
| john | 2015-03-01 | 2 | 1 |
| john | 2015-03-01 | 1 | (null) |
| kim | 2015-01-01 | 1 | (null) |
| kim | 2015-02-01 | 1 | 1 |
I've tried using joins as described here, but it updates all rows:
update table1 t1
inner join
( select username,min(`date`) as minForGroup
from table1
group by username,`date`
) inr
on inr.username=t1.username and inr.minForGroup=t1.`date`
set firstFlag=1;
As a1ex07 points out, it would need another per row unique constrain to update the rows I need to:
update table1 t1
inner join
( select id, username,min(`date`) as minForGroup
from table1
where eventid = 1
group by username,month(`date`)
) inr
on inr.id=t1.id and inr.username=t1.username and inr.minForGroup=t1.`date`
set firstFlag=1;
Add an Id column, and use it on the join on constrains.
To allow only those that satisfies a specific condition on another column you need the where clause inside the subquery, otherwise it would try to match different rows as the subquery would return rows with eventid=2 while the update query would return only those with eventid=1.
To use yearly intervals instead of monthly, change the group by statement to use years.

Database to track visits vs dynamic targets

I'm designing a new database to track accounts achieved visits vs monthly targets. The final report shall be requested by start/end dates and one account to show the months inbetween with monthly target and sum of visits.
The complication started when I knew the number of accounts is more than 10 thousands and the targets should be changed monthly or not for the only changed accounts targets(i.e each target will have start and end date. if no end date then the target is always valid). At this point I lost and I need help
For simplicity I will assume I have table with dates periods and simplest situation as follow
accounts
+----+---------+
|id | name |
+----+---------+
| 1 | account1|
| 2 | account2|
+----+---------+
targets
+---+------------+------------+-----------+----------------+
|id | account_id | start_date | end_date | monthly_target |
+---+------------+------------+-----------+----------------+
|1 | 1 | 1-1-2016 | 31-1-2016 | 5 |
|2 | 1 | 1-2-2016 | 31-5-2016 | 4 |
|3 | 1 | 1-7-2016 | null | 7 |
|4 | 2 | 1-1-2016 | null | 10 |
+---+------------+------------+-----------+----------------+
visits
+---+-----------+------------+
|id | date | account_id |
+----------------------------+
|1 | 15-1-2016 | 1 |
|2 | 20-1-2016 | 1 |
|3 | 10-5-2016 | 1 |
|3 | 20-5-2016 | 1 |
|4 | 20-5-2016 | 2 |
+---+-----------+------------+
calendar (Optional)
----------+----------+
|start | end |
----------+----------+
|1-1-2016 | 31-1-2016|
|1-2-2016 | 29-2-2016|
|1-3-2016 | 31-3-2016|
|1-4-2016 | 30-4-2016|
|1-5-2016 | 31-5-2016|
|1-6-2016 | 30-6-2016|
|1-7-2016 | 31-7-2016|
|1-8-2016 | 31-7-2016|
+---------+----------+
Expected report for account1 coverage from 1-4-2016 to 31-7-2016
+---------+-----------+--------+----+
|start | end | target | sum|
+---------+-----------+--------+----+
|1-4-2016 | 30-4-2016 | 4 | 0 |
|1-5-2016 | 31-5-2016 | 4 | 2 |
|1-6-2016 | 30-6-2016 | 0 | 0 |
|1-7-2016 | 31-7-2016 | 7 | 0 |
+---------+-----------+--------+----+
I can accept changing my initial design if it causes problems but assuming the design of targets table is the most practical design for system admin.
I need help in SQL needed to generate the final report.
I modified the range of dates in targets to have an explicit end date even if that means end of year. This way, avoiding the null, the sql could range ok. It also uses the ISO 8601 Standard for dates. And it is implemented in a Stored Proc that takes 3 parameters: account_id, start and end date.
Alias v, the derived table, prevents double counts versus a flat out LEFT JOIN against the visits table. For instance, that 2 would be an errant 7 without that strategy. So it used the LAST_DAY() function.
Schema:
create table accounts
( id int not null,
name varchar(100) not null
);
insert accounts values
(1,'account1'),
(2,'account2');
-- drop table targets;
create table targets
( id int not null,
account_id int not null,
start_date date not null,
end_date date not null,
monthly_target int not null
);
-- truncate targets;
insert targets values
(1,1,'2016-01-01','2016-01-31',5),
(2,1,'2016-02-01','2016-05-31',4),
(3,1,'2016-07-01','2016-12-31',7),
(4,2,'2016-01-01','2016-12-31',10);
create table visits
( id int not null,
date date not null,
account_id int not null
);
-- truncate visits;
insert visits values
(1,'2016-01-15',1),
(2,'2016-01-20',1),
(3,'2016-05-10',1),
(4,'2016-05-20',1),
(5,'2016-05-20',2);
create table calendar
( start date not null,
end date not null
);
insert calendar values
('2016-01-01','2016-01-31'),
('2016-02-01','2016-02-29'),
('2016-03-01','2016-03-31'),
('2016-04-01','2016-04-30'),
('2016-05-01','2016-05-31'),
('2016-06-01','2016-06-30'),
('2016-07-01','2016-07-31'),
('2016-08-01','2016-08-31'),
('2016-09-01','2016-09-30'),
('2016-10-01','2016-10-31'),
('2016-11-01','2016-11-30'),
('2016-12-01','2016-12-31');
Stored Proc:
DROP PROCEDURE IF EXISTS uspGetRangeReport007;
DELIMITER $$
CREATE PROCEDURE uspGetRangeReport007
( p_account_id INT,
p_start DATE,
p_end DATE
)
BEGIN
SELECT c.start,c.end,
IFNULL(t.monthly_target,0) as target,
-- IFNULL(sum(v.id),0) as visits
IFNULL(v.theCount,0) as visits
FROM calendar c
LEFT JOIN targets t
ON account_id=p_account_id
AND c.start BETWEEN t.start_date AND t.end_date
AND c.end BETWEEN t.start_date AND t.end_date
LEFT JOIN
( SELECT LAST_DAY(date) as lastDayOfMonth,
count(id) as theCount
FROM VISITS
WHERE account_id=p_account_id
GROUP BY LAST_DAY(date)
) v
ON v.lastDayOfMonth BETWEEN c.start AND c.end
WHERE c.start BETWEEN p_start AND p_end
AND c.end BETWEEN p_start AND p_end
GROUP BY c.start,c.end,t.monthly_target
ORDER BY c.start;
END;$$
DELIMITER ;
Test:
call uspGetRangeReport007(1,'2016-04-01','2016-07-31');
+------------+------------+--------+--------+
| start | end | target | visits |
+------------+------------+--------+--------+
| 2016-04-01 | 2016-04-30 | 4 | 0 |
| 2016-05-01 | 2016-05-31 | 4 | 2 |
| 2016-06-01 | 2016-06-30 | 0 | 0 |
| 2016-07-01 | 2016-07-31 | 7 | 0 |
+------------+------------+--------+--------+
SELECT c.start,
c.end,
t.monthly_target AS target,
(
SELECT COUNT(*)
FROM visits
WHERE `date` BETWEEN c.start AND c.end
AND account_id = ? -- Specify '1'
) AS `sum` -- Correlated subquery for counting visits
FROM Calendar AS c
JOIN targets AS t ON c.start_date >= t.start_date
AND ( t.end_date IS NULL
OR c.start_date < t.end_date )
WHERE c.start >= ? -- Specify date range
AND c.end <= ?

Find and Delete Duplicate rows in MySQL

I'm having trouble finding duplicates in a database table with the following setup:
==========================================================================
| stock_id | product_id | store_id | stock_qty | updated_at |
==========================================================================
| 9990 | 51 | 1 | 13 | 2014-10-25 16:30:01 |
| 9991 | 90 | 2 | 5 | 2014-10-25 16:30:01 |
| 9992 | 161 | 1 | 3 | 2014-10-25 16:30:01 |
| 9993 | 254 | 1 | 18 | 2014-10-25 16:30:01 |
| 9994 | 284 | 2 | 12 | 2014-10-25 16:30:01 |
| 9995 | 51 | 1 | 11 | 2014-10-25 17:30:02 |
| 9996 | 90 | 2 | 5 | 2014-10-25 17:30:02 |
| 9997 | 161 | 1 | 3 | 2014-10-25 17:30:02 |
| 9998 | 254 | 1 | 16 | 2014-10-25 17:30:02 |
| 9999 | 284 | 2 | 12 | 2014-10-25 17:30:02 |
==========================================================================
Stock updates are imported into this table every hour, I'm trying to find duplicate stock entries (any rows which have a matching product id and store id) so I can delete the oldest. The query below is my attempt, by comparing product ids and store ids on a join like this I can find one set of duplicates:
SELECT s.`stock_id`, s.`product_id`, s.`store_id`, s.`stock_qty`, s.`updated_at`
FROM `stock` s
INNER JOIN `stock` j ON s.`product_id`=j.`product_id` AND s.`store_id`=j.`store_id`
GROUP BY `stock_id`
HAVING COUNT(*) > 1
ORDER BY s.updated_at DESC, s.product_id ASC, s.store_id ASC, s.stock_id ASC;
While this query will work, it doesn't find ALL duplicates, only 1 set, which means if an import goes awry and isn't noticed until the morning, there's a possibility that we'll be left with tons of duplicate stock entries. My MySQL skills are sadly lacking and I'm at a complete loss about how to find and delete all duplicates in a fast, reliable manner.
Any help or ideas are welcome. Thanks
You can use this query:
DELETE st FROM stock st, stock st2
WHERE st.stock_id < st2.stock_id AND st.product_id = st2.product_id AND
st.store_id = st2.store_id;
This query will delete older record having same product_id and store_id and will keep latest record.
A self join on store_id, product_id and 'is older' in combination with DISTINCT should give you all rows where also a newer version exists:
> SHOW CREATE TABLE stock;
CREATE TABLE `stock` (
`stock_id` int(11) NOT NULL,
`product_id` int(11) DEFAULT NULL,
`store_id` int(11) DEFAULT NULL,
`stock_qty` int(11) DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`stock_id`)
> select * from stock;
+----------+------------+----------+-----------+---------------------+
| stock_id | product_id | store_id | stock_qty | updated_at |
+----------+------------+----------+-----------+---------------------+
| 1 | 1 | 1 | 1 | 2001-01-01 12:00:00 |
| 2 | 2 | 2 | 1 | 2001-01-01 12:00:00 |
| 3 | 2 | 2 | 1 | 2002-01-01 12:00:00 |
+----------+------------+----------+-----------+---------------------+
> SELECT DISTINCT s1.stock_id, s1.store_id, s1.product_id, s1.updated_at
FROM stock s1 JOIN stock s2
ON s1.store_id = s2.store_id
AND s1.product_id = s2.product_id
AND s1.updated_at < s2.updated_at;
+----------+----------+------------+---------------------+
| stock_id | store_id | product_id | updated_at |
+----------+----------+------------+---------------------+
| 2 | 2 | 2 | 2001-01-01 12:00:00 |
+----------+----------+------------+---------------------+
> DELETE stock FROM stock
JOIN stock s2 ON stock.store_id = s2.store_id
AND stock.product_id = s2.product_id
AND stock.updated_at < s2.updated_at;
Query OK, 1 row affected (0.02 sec)
> select * from stock;
+----------+------------+----------+-----------+---------------------+
| stock_id | product_id | store_id | stock_qty | updated_at |
+----------+------------+----------+-----------+---------------------+
| 1 | 1 | 1 | 1 | 2001-01-01 12:00:00 |
| 3 | 2 | 2 | 1 | 2002-01-01 12:00:00 |
+----------+------------+----------+-----------+---------------------+
Or you can use a stored Procedure:
DELIMITER //
DROP PROCEDURE IF EXISTS removeDuplicates;
CREATE PROCEDURE removeDuplicates(
stockID INT
)
BEGIN
DECLARE stockToKeep INT;
DECLARE storeID INT;
DECLARE productID INT;
-- gets the store and product value
SELECT DISTINCT store_id, product_id
FROM stock
WHERE stock_id = stockID
LIMIT 1
INTO
storeID, productID;
SELECT stock_id
FROM stock
WHERE product_id = productID AND store_id = storeID
ORDER BY updated_at DESC
LIMIT 1
INTO
stockToKeep;
DELETE FROM stock
WHERE product_id = productID AND store_id = storeID
AND stock_id != stockToKeep;
END //
DELIMITER ;
And afterwards call it for every pair of the product id and store id via a cursor procedure:
DELIMITER //
CREATE PROCEDURE updateTable() BEGIN
DECLARE done BOOLEAN DEFAULT FALSE;
DECLARE stockID INT UNSIGNED;
DECLARE cur CURSOR FOR SELECT DISTINCT stock_id FROM stock;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done := TRUE;
OPEN cur;
testLoop: LOOP
FETCH cur INTO stockID;
IF done THEN
LEAVE testLoop;
END IF;
CALL removeDuplicates(stockID);
END LOOP testLoop;
CLOSE cur;
END//
DELIMITER ;
And then just call the second procedure
CALL updateTable();

Complex sql, get rows only if date is older and group by 3 columns

I was wondering if this query if possible. I have this table:
+----+-------+--------+--------+------------+
| id | plate | agency | status | assigned |
+----+-------+--------+--------+------------+
| 1 | UB10 | 0 | 3 | 2010-01-02 |
| 2 | UB10 | 2 | 2 | 2010-01-03 |
| 3 | UB10 | 5 | 1 | 2010-01-04 |
+----+-------+--------+--------+------------+
In this way everything is OK and I use all the records. The main issue is the following situation:
+----+-------+--------+--------+------------+
| id | plate | agency | status | assigned |
+----+-------+--------+--------+------------+
| 1 | UB12 | 0 | 3 | 2010-01-02 |
| 2 | UB12 | 2 | 1 | 2010-01-03 |
| 3 | UB12 | 5 | 2 | 2010-01-04 |
+----+-------+--------+--------+------------+
I need a sql query where I'm able to skip the records with the status = 1 only if I have records with the same plate and there is a newest record with different status. In the above example I only need the first and third record.
It's possible?
Thanks very much.
EDITED FOR GRAMMAR
you can do this with a prepared statement like so
SET #sql = "CREATE VIEW v_test AS (SELECT * FROM my_table WHERE status <> 1)";
SET #union = " UNION (SELECT * FROM my_table ORDER BY assigned DESC LIMIT 1)";
SELECT #final := case status WHEN 1 then CONCAT(#sql, #union) ELSE #sql END
FROM
( SELECT status
FROM my_table
WHERE id =
( SELECT id FROM my_table
ORDER BY assigned DESC LIMIT 1
)
) t;
PREPARE stmt FROM #final;
EXECUTE stmt;
SELECT * FROM v_test;
DEMO WHERE LAST ROW HAS STATUS <> 1
DEMO WHERE LAST ROW HAS STATUS = 1
DEMO WITH A VIEW

Sum of two columns by id to one field

i need to make sum of two columns by id to one field by that has the same id
for example : i want the Balance which is income - expenses = balance
Table Transactions:
=========================================================
| id | idBudget | expenses || income |
=====+========+===============+=====|=====+========+====|
| 1 | 2 | 10 || 0 |
|----+--------+---------------+-----||----+--------+----|
| 2 | 3 | 200 || 0 |
|----+--------+---------------+-----||----+--------+----|
| 3 | 2 | 1 || 100 |
|----+--------+---------------+-----||----+--------+----|
| 4 | 2 | 0 || 1000 |
|----+--------+---------------+-----||----+--------+----|
Table Budget:
=====================================
| idBudget | Balance |
=====+========+===============+=====|
| 2 | 1090 |
|----+--------+---------------+-----|
| 3 | -200 |
|----+--------+---------------+-----|
i tired to use Triggers but i think i don't know how to implement it
CREATE TABLE starting_balance AS (SELECT * FROM budget);
DROP TABLE budget;
CREATE VIEW budget AS (
SELECT
sb.idBudget,
sb.balance + SUM(t.income - t.expenses) AS balance
FROM starting_balance sb
LEFT JOIN transactions t ON (t.idBudget = sb.idBudget)
GROUP BY
sb.idBudget,
sb.balance
);
In other words, use a view instead of a table. You can update the starting balance from time to time (and either delete or flag the transactions you no longer need!), and you could use a materialized view.
Something like this?
INSERT INTO Budget(SELECT idBudget, SUM(income)-SUM(expenses) as Balance
FROM Transactions GROUP BY idBudget )
delimiter //
CREATE TRIGGER balance AFTER INSERT ON Transactions
FOR EACH ROW
BEGIN
INSERT INTO Budget(SELECT NEW.idBudget, SUM(NEW.income)-SUM(NEW.expenses) as Balance
FROM Transactions GROUP BY idBudget )
END;
delimiter ;