How to update related table MYSQL? - mysql

There are two tables: orders, orders_history.
orders
________
id | status
orders_history
id | order_id | status | user_id
The orders_history contains history of all user's actions. At the same time orders.status contains the last status from orders_history.status.
I make these queries in transation:
transaction start
insert into orders_history...
$status = select status from order_history order by id desc limit 1;
update orders set status = $status where orders.id = id
My question is:
Should I use transaction and is it properly way to do that?
What if several transactions try to insert, update orders_history for the same order_id.

As suggested in the comments above you could use a trigger to update the orders table -
DELIMITER $$
CREATE TRIGGER `update_order_status` AFTER INSERT ON `orders_history`
FOR EACH ROW
UPDATE `orders` SET `status` = NEW.status WHERE id = NEW.order_id;
$$
DELIMITER ;
The better option would be to not store the redundant status in orders and just query for most recent status in orders_history.
SELECT orders.id, (SELECT status FROM orders_history oh WHERE orders.id = oh.order_id ORDER BY id DESC LIMIT 1) AS status
FROM orders

The design pattern I might use in this case is...
Table 1: History -- this is an audit trail of everything that has gone on. (Think: All the checks written and deposits made to a checking account.)
Table 2: Current -- this is the current status of the information. (Think, current account balance, status, etc.)
Whenever something happens (eg, a check clears):
START TRANSACTION;
INSERT INTO History ...;
UPDATE Current ...;
COMMIT;
In the case of a checking account, something different is needed if your account is overdrawn, so let's make the transaction more complex:
START TRANSACTION;
SELECT balance FROM Current WHERE acct = 123 FOR UPDATE;
if would be overdrawn then
email user
UPDATE Current SET status = 'overdrawn' acct = 123;
...
else
INSERT INTO History ...;
UPDATE Current ...;
endif
COMMIT;
I prefer to put the "business logic" clearly in one place, not hidden in a Trigger. (I might use a Trigger for monitoring or logging, but not for the main purpose of the tables.)

Related

Is there a way to get the number of comments for each user and update it in the number_of_comments column automatically?

I have two tables in MySQL like this
Users -> user_id , user_name , number_of_comments
Comments -> comment_id , comment , user_id
Is there a way to get the number of comments for each user and update it in the number_of_comments column automatically?
Not recommended, but solves nevertheless. For learning purposes only.
CREATE TRIGGER tr_ai_update_n_of_comments
AFTER INSERT ON comments
FOR EACH ROW
UPDATE users
SET number_of_comments = ( SELECT COUNT(*)
FROM comments
WHERE comments.user_id = NEW.user_id )
WHERE user_id = NEW.user_id;
If the rows in comments may be updated (with user_id value changing) and/or deleted then create similar AFTER DELETE and AFTER UPDATE triggers.
PS. I strongly recommend you to remove users.number_of_comments column at all and calculate actual comments amount value by according query when needed.
If you agree that the value may be approximate (slightly different from the exact one), then you can use an incremental trigger.
CREATE TRIGGER tr_ai_update_n_of_comments
AFTER INSERT ON comments
FOR EACH ROW
UPDATE users
SET number_of_comments = number_of_comments + 1
WHERE user_id = NEW.user_id;
But just in case, provide for the creation of a service stored procedure (or event) that will periodically recalculate the accumulated value.

MySQL update a master field only if condition is true

Is there a way to update SQL in such a way, for example.
Master Table has a column called INVOICESCOUNT.
When an invoice is deleted successfully, then the INVOICESCOUNT is decreased.
For example, a SQL psuedo-code statement like this:
Delete From Invoices where INVOICE=500;
Update Customers SET INVOICECOUNT=INVOICECOUNT-1 WHERE Customer=1 (if prior statement returns 1 affected row);
I need it to be embedded within the same SQL statement instead of having the source code handling executing the 2 statements separately.
Thanks for any advice. Please also let me know if there is any minimal MySQL version requirement if there is such a solution.
UPDATE with more info: note that the list of Customers I present to the user can be very different each time, example, CustomerGroupID=? or CustomerCreated within a certain date, so the Customers query cannot be cached efficiently, as such I prefer to update the INVOICECOUNT (as it will be hit on many times in an hour by different users listing different groups of customers).
Better idea: instead have a VIEW that shows you Customer's invoice counts:
CREATE VIEW CustomersInfo AS
SELECT
CustomerId,
COUNT(*) AS InvoiceCount
FROM
Invoices
GROUP BY
CustomerId
;
Then you'd use it like so:
SELECT
c.CustomerId,
COALESCE( ci.InvoiceCount, 0 ) AS InvoiceCount
FROM
Customers AS c
LEFT OUTER JOIN CustomersInfo AS ci ON c.CustomerId = ci.CustomerId
(Don't use an INNER JOIN, otherwise Customers without any invoices won't be in the output).
You can use triggers for that
CREATE TABLE Invoices(INVOICE INT)
CREATE TABLe Customers(Customer int,INVOICECOUNT int)
INSERT INTO Customers VALUES (1,1)
CREATE TRIGGER del_after AFTER DELETE ON Invoices
FOR EACH ROW
Update Customers SET INVOICECOUNT=INVOICECOUNT-1 WHERE Customer=1
Delete From Invoices where INVOICE=500;
SELECT * FROM Customers
Customer | INVOICECOUNT
-------: | -----------:
1 | 1
INSERT INTO Invoices VALUES (400)
Delete From Invoices where INVOICE=500;
SELECT * FROM Customers
Customer | INVOICECOUNT
-------: | -----------:
1 | 1
INSERT INTO Invoices VALUES (500)
Delete From Invoices where INVOICE=500;
SELECT * FROM Customers
Customer | INVOICECOUNT
-------: | -----------:
1 | 0
db<>fiddle here
As was mentioned in the comments, you probably don't need to store the invoice count in a table column, however if you MUST have the invoice count column for any reason your best bet might be a stored procedure.
DELIMITER //
CREATE PROCEDURE removeinvoice
BEGIN
DELETE FROM invoices WHERE invoice=500;
UPDATE customers SET invoicecount = (SELECT COUNT(*) FROM invoices);
END//
DELIMITER ;
Then you just call that stored procedure.
CALL removeinvoice;

Trigger to delete all but x rows per index

So I have two tables: posts and server_options.
posts consists of:
no
date_created
server_id
user_id
etc
server_options consists of:
no
posts_per_user
server_id
etc...
A user is only allowed server_options.posts_per_user posts per server. To limit this, I have a trigger executed before insert and update to posts:
CREATE DEFINER=`root`#`localhost` TRIGGER `bi_posts` BEFORE INSERT ON `posts` FOR EACH ROW BEGIN
SELECT posts_per_user INTO #posts_per_user FROM `server_options` WHERE server_id = NEW.server_id LIMIT 1;
SELECT COUNT(0) INTO #post_count FROM `posts` WHERE server_id = NEW.server_id AND user_id = NEW.user_id;
IF #post_count >= #posts_per_user
THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Cannot add or update row: post limit exceeded.';
END IF;
END
Simple enough. However, what I'd like to do is also add a trigger to server_options on update. If posts_per_user changes, it should remove any "excess" posts for that server from posts.
For example. If some users have 5 posts in a server, and the posts_per_user is reduced to 3, it should delete the oldest and keep only 3 for that user for that server.
Any pointers on where to begin? no is AI PK, so we can sort by that rather than date_added to make things easier.
I was thinking this (it works), but to me it seems like a bad approach:
CREATE DEFINER=`root`#`localhost` TRIGGER `au_server_options` AFTER UPDATE ON `server_options` FOR EACH ROW BEGIN
IF NEW.posts_per_user < OLD.posts_per_user
THEN
DELETE FROM `posts`
WHERE NEW.posts_per_user <= (
SELECT count(0)
FROM (SELECT no, server_id, user_id FROM posts) AS posts_temp
WHERE
`posts`.`server_id` = `posts_temp`.`server_id` AND
`posts`.`user_id` = `posts_temp`.`user_id` AND
`posts`.`no` > `posts_temp`.`no`
ORDER BY `posts`.`no`
);
END IF;
END
I'm not looking at huge amounts of entries. Maybe a 100,000 or so.
Thanks, y'all!

updating two table with one single insert-SQL

Is it possible to make the following two queries to one single query?
update customers set customer_name = 'John' where customer_id=1;
update purchases set state='Accepted' where customer_id=1;
customer (table)
customer_id(PK)
customer_name
purchases (table)
customer_id(FK)
product
state
Thanks
You can execute them in a single transaction:
START TRANSACTION;
update customers set customer_name = 'John' where customer_id=1;
update purchases set state='Accepted' where customer_id=1;
COMMIT;
If something fail inside the transaction all changes are rolled back

InnoDB and count: Are helper tables the way to go?

Assume I've got an users table with 1M users on MySQL/InnoDB:
users
userId (Primary Key, Int)
status (Int)
more data
If I would want to have an exact count of the amount of users with status = 1 (denoting an activate account), what would be the way to go for big tables, I was thinking along the lines of:
usercounts
status
count
And then run an TRIGGER AFTER INSERT on users that updates the appropiate columns in usercounts
Would this be the best way to go?
ps. An extra small question: Since you also need an TRIGGER AFTER UPDATE on users for when status changes, is there a syntax available that:
Covers both the TRIGGER AFTER INSERT and TRIGGER AFTER UPDATE on status?
Increments the count by one if a count already is present, else inserts a new (status, count = 0) pair?
Would this be the best way to go?
Best (opinion-based) or not but it's definitely a possible way to go.
is there a syntax available that: covers both the TRIGGER AFTER INSERT and TRIGGER AFTER UPDATE on status?
No. There isn't a compound trigger syntax in MySQL. You'll have to create separate triggers.
is there a syntax available that: increments the count by one if a count already is present, else inserts a new (status, count = 0) pair?
Yes. You can use ON DUPLICATE KEY clause in INSERT statement. Make sure that status is a PK in usercounts table.
Now if users can be deleted even if only for maintenance purposes you also need to cover it with AFTER DELETE trigger.
That being said your triggers might look something like
CREATE TRIGGER tg_ai_users
AFTER INSERT ON users
FOR EACH ROW
INSERT INTO usercounts (status, cnt)
VALUES (NEW.status, 1)
ON DUPLICATE KEY UPDATE cnt = cnt + 1;
CREATE TRIGGER tg_ad_users
AFTER DELETE ON users
FOR EACH ROW
UPDATE usercounts
SET cnt = cnt - 1
WHERE status = OLD.status;
DELIMITER $$
CREATE TRIGGER tg_au_users
AFTER UPDATE ON users
FOR EACH ROW
BEGIN
IF NOT NEW.status <=> OLD.status THEN -- proceed ONLY if status has been changed
UPDATE usercounts
SET cnt = cnt - 1
WHERE status = OLD.status;
INSERT INTO usercounts (status, cnt) VALUES (NEW.status, 1)
ON DUPLICATE KEY UPDATE cnt = cnt + 1;
END IF;
END$$
DELIMITER ;
To initially populate usercounts table use
INSERT INTO usercounts (status, cnt)
SELECT status, COUNT(*)
FROM users
GROUP BY status
Here is SQLFiddle demo
I think there are simpler options available to you.
Just add an index to the field you'd like to count on.
ALTER TABLE users ADD KEY (status);
Now a select should be very fast.
SELECT COUNT(*) FROM users WHERE status = 1