In mysql triggers, when I do a "after update" on table A and then use "for each row", will it run the body of the trigger for each row in A every time a row gets updated in A, or is it saying to apply the trigger to every row in A and then if a row gets updated, it will only run the body code for that updated row only?
Thanks
FOR EACH ROW means for each of the matched row that gets either updated or deleted.
Trigger body won't loop through the entire table data unless there is a where condition in the query.
A working example is demonstrated below:
Create sample tables:
drop table if exists tbl_so_q23374151;
create table tbl_so_q23374151 ( i int, v varchar(10) );
-- set test data
insert into tbl_so_q23374151
values (1,'one'),(2,'two' ),(3,'three'),(10,'ten'),(11,'eleven');
-- see current data in table**:
select * from tbl_so_q23374151;
+------+--------+
| i | v |
+------+--------+
| 1 | one |
| 2 | two |
| 3 | three |
| 10 | ten |
| 11 | eleven |
+------+--------+
5 rows in set (0.00 sec)
Sample table to record loop count in trigger body:
-- let us record, loop count of trigger, in a table
drop table if exists tbl_so_q23374151_rows_affected;
create table tbl_so_q23374151_rows_affected( i int );
select count(*) as rows_affected from tbl_so_q23374151_rows_affected;
+---------------+
| rows_affected |
+---------------+
| 0 |
+---------------+
Define a delete trigger:
drop trigger if exists trig_bef_del_on_tbl_so_q23374151;
delimiter //
create trigger trig_bef_del_on_tbl_so_q23374151 before delete on tbl_so_q23374151
for each row begin
set #cnt = if(#cnt is null, 1, (#cnt+1));
/* for cross checking save loop count */
insert into tbl_so_q23374151_rows_affected values ( #cnt );
end;
//
delimiter ;
Now, test a delete operation:
delete from tbl_so_q23374151 where i like '%1%';
-- now let us see what the loop count was
select #cnt as 'cnt';
+------+
| cnt |
+------+
| 3 |
+------+
Now, check the trigger effect on main table:
-- now let us see the table data
select * from tbl_so_q23374151;
+------+-------+
| i | v |
+------+-------+
| 2 | two |
| 3 | three |
+------+-------+
2 rows in set (0.00 sec)
select count(*) as rows_affected from tbl_so_q23374151_rows_affected;
+---------------+
| rows_affected |
+---------------+
| 3 |
+---------------+
1 row in set (0.00 sec)
Literally - mysql will run your trigger code for every row which was affected by the SQL statement.
And NOT for each row in table A by itself.
It's just a MySQL syntax quirk and it's virtually meaningless. MySQL triggers require the FOR EACH ROW syntax. You'll get a syntax error without it. They work exactly the same as Standard SQL (e.g., SQLite) triggers without FOR EACH ROW.
https://dev.mysql.com/doc/refman/8.0/en/create-trigger.html
Trigger is not applied to each row, it just say to execute trigger body for each affected table row.
FOR EACH ROW says when it should be executed, not where create trigger.
Related
I need to limit the number of rows inserted in a table of my DB.
I 'd like to implement a logic to check if the limit is reached when a new insert is executed: if the max number of records is reached I will delete the oldest record in the table.
I tried to implement this with a trigger as suggested here, but I'm getting an error:
ERROR 1442: 1442: Can't update table 'tableName' in stored function/trigger because it is already used by statement which invoked this stored function/trigger.
So how can I implement this?
NOTE I'm using MySQL v5.7.25
Elaborating on my comment, why not just fill up the table, and then only execute updates?
E.g.
DROP TABLE IF EXISTS my_table;
CREATE TABLE my_table
(id SERIAL PRIMARY KEY
,val CHAR(1)
,ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
INSERT INTO my_table (val) VALUES ('a');
INSERT INTO my_table (val) VALUES ('b');
INSERT INTO my_table (val) VALUES ('c');
SELECT * FROM my_table;
+----+------+---------------------+
| id | val | ts |
+----+------+---------------------+
| 1 | a | 2019-05-20 09:54:09 |
| 2 | b | 2019-05-20 09:54:15 |
| 3 | c | 2019-05-20 09:54:19 |
+----+------+---------------------+
3 rows in set (0.00 sec)
UPDATE my_table SET val = 'd' ORDER BY ts, id LIMIT 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
SELECT * FROM my_table;
+----+------+---------------------+
| id | val | ts |
+----+------+---------------------+
| 1 | d | 2019-05-20 09:54:37 |
| 2 | b | 2019-05-20 09:54:15 |
| 3 | c | 2019-05-20 09:54:19 |
+----+------+---------------------+
Within a stored function or trigger, it is not permitted to modify a table that is already being used (for reading or writing) by the statement that invoked the function or trigger.
MySQL does not allow this, see Restrictions
All other major DBMS support this feature so hopefully MySQL will add this support soon.
In this case you may handle this in pragmatically whatever you prefer.
Before insert data just check total number of row of that table. If reach rows limit, then call delete method.
After several google searches, I have come here.
I have a MYSQL database table name users.
I have two columns, account_balance and earned_total.
I have several places in the PHP end where I update the account_balance when user does something. I would like to have a record of how much in total he has earned so far. So I have created a earned_total column.
Using trigger (or any other method) without modifying my PHP code how can I update the earned_total column too when account_balance column gets updated?
Remember When the user withdraws, the earned_total value should not be decreased.
drop table if exists t;
create table t(account_balance int,earned_amount int);
drop trigger if exists t;
delimiter $$
create trigger t before update on t
for each row
begin
insert into debug_table(msg,msg2) values (old.earned_amount,new.earned_amount);
if new.account_balance > old.account_balance then
set new.earned_amount = new.earned_amount + (new.account_balance - old.Account_balance);
end if;
insert into debug_table(msg,msg2) values (old.earned_amount,new.earned_amount);
end $$
delimiter ;
truncate table t;
truncate table debug_table;
insert into t values (10,10);
update t set account_balance = Account_balance + 10 where 1 = 1;
update t set account_balance = Account_balance - 10 where 1 = 1;
select * from t;
+-----------------+---------------+
| account_balance | earned_amount |
+-----------------+---------------+
| 10 | 20 |
+-----------------+---------------+
1 row in set (0.00 sec)
select * from debug_table;
+----+------+------+
| id | msg | MSG2 |
+----+------+------+
| 1 | 10 | 10 |
| 2 | 10 | 20 |
| 3 | 20 | 20 |
| 4 | 20 | 20 |
+----+------+------+
4 rows in set (0.00 sec)
Note debug_table is not necessary to the solution , it's there to show that the first thing mysql does is write all OLD. values to NEW. values.
Personally I wouldn't store earned_amount, there's to much scope for error, and it can be easily calculated. Imagine for example a positive amount was contrad/reversed that should reduce the earned amount but there is no way of distinguishing this from a real withdrawal.
SELECT time
FROM posts
ORDER BY time ASC;
This will order my posts for me in a list. I would like to reorder the table itself making sure that there are no missing table ids. Thus, if I delete column 2, I can reorder so that row 3 will become row 2.
How can I do this? Reorder a table by its date column so there is always an increment of 1, no non-existing rows.
Disclaimer: I don't really know why you would need to do it, but if you do, here is just one of many ways, fairly independent of the engine or the server version.
Setup:
CREATE TABLE t (
`id` int(11) NOT NULL AUTO_INCREMENT,
`time` time DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
INSERT INTO t (`time`) VALUES ('13:00:00'),('08:00:00'),('02:00:00');
DELETE FROM t WHERE id = 2;
Initial condition:
SELECT * FROM t ORDER BY `time`;
+----+----------+
| id | time |
+----+----------+
| 3 | 02:00:00 |
| 1 | 13:00:00 |
+----+----------+
2 rows in set (0.00 sec)
Action:
CREATE TRIGGER tr AFTER UPDATE ON t FOR EACH ROW SET #id:=#id+1;
ALTER TABLE t ADD COLUMN new_id INT NOT NULL AFTER id;
SET #id=1;
UPDATE t SET new_id=#id ORDER BY time;
DROP TRIGGER tr;
Result:
SELECT * FROM t ORDER BY `time`;
+----+--------+----------+
| id | new_id | time |
+----+--------+----------+
| 3 | 1 | 02:00:00 |
| 1 | 2 | 13:00:00 |
+----+--------+----------+
2 rows in set (0.00 sec)
Cleanup:
Further you can do whatever is more suitable for your case (whatever is faster and less blocking, depending on other conditions). You can update the existing id column and then drop the extra one:
UPDATE t SET id=new_id;
ALTER TABLE t DROP new_id;
SELECT * FROM t ORDER BY `time`;
+----+----------+
| id | time |
+----+----------+
| 1 | 02:00:00 |
| 2 | 13:00:00 |
+----+----------+
2 rows in set (0.00 sec)
Or you can drop the existing id column and promote new_id to the primary key.
Comments:
A natural variation of the same approach would be to wrap it into a stored procedure. It's basically the same, but requires a bit more text. The benefit of it is that you could keep the procedure for the next time you need it.
Assuming you have a unique index on id, a temporary column new_id is needed in a general case, because if you start updating id directly, you can get a unique key violation. It shouldn't happen if your id is already ordered properly, and you are only removing gaps.
I have a table 'usage' with the following structure:
Job | Start Time | End Time | Session
And I have another table 'sessions' with the following structure to keep track of sessions:
Session Label | Start Time | End Time
I want to have a trigger such that when there is an entry in the first table, the trigger will check the start time and end time of other table. If there is a label already that exists, the session column of the first table needs to be updated with the session label, otherwise a new entry should be added to the second table and the corresponding label should be entered into the session column of second table. I have never worked with SQL Triggers before and have no idea how to achieve this.
I tried:
CREATE TRIGGER test AFTER INSERT ON `usage`
FOR EACH ROW
WHEN (SELECT RIGHT(`usage`.starttime,5) != SELECT RIGHT(`sessions`.starttime,5))
BEGIN
SET `usage`.sessionlabel = `A`
END;
Logic:
for(newrecordinusagetable)
{
//check if same starttime exists in any record of session table
if(true)
{
//do nothing
}
else
{
//add the new starttime to the session table
}
PS : Do not bother about the endtime or label.
Here's an attempt based on the limited info provided:
DELIMITER $$
CREATE TRIGGER usage_session_trigger
AFTER INSERT ON `usage`
FOR EACH ROW
BEGIN
-- Count sessions with same start time
DECLARE session_count INT;
SET session_count = (SELECT COUNT(*)
FROM sessions
WHERE RIGHT(starttime, 5) = RIGHT(NEW.starttime, 5));
-- If none found, insert one.
IF (session_count = 0) THEN
INSERT INTO sessions (sessionlabel, starttime, endtime)
VALUES (NEW.`session`, NEW.starttime, NEW.endtime);
END IF;
END;
$$
DELIMITER ;
Notes
Am assuming you had good reason to compare the last 5 characters of the times in the example so have repeated it here. (Couldn't guess why 5 though!)
May also need to consider whether similar triggers are needed for updates (+ possibly deletes?)
I'm not commenting on whether what you're asking to do is the correct approach - just attempting to answer the question.
From better solution perspective it would be better to implement this in application logic when data is inserted into usage table, instead of creating trigger to achieve this. To demonstrate your sceanrio, I have used a stored procedure to insert into usage table. You can modify it accordingly.
-- create usage table
create table `usage` ( job varchar(100),
start_time date,
end_time date,
`session` varchar(100)
);
-- create `sessions` table
create table `sessions` ( session_label varchar(100) ,
start_time date,
end_time date );
-- create procedure prc_insert_into_usage
delimiter $$
Create Procedure prc_insert_into_usage
(
IN p_job varchar(100),
IN p_start_time date,
IN p_end_time date
)
Begin
Declare v_session varchar(100);
-- check if record is there in the session table
-- it is assumed session table will have only one record for composite key (start_time, end_time)
Select session_label into v_session
from `sessions`
where start_time = p_start_time and end_time = p_end_time;
IF (v_session IS NULL ) THEN
-- if session_label is generated using auto increment key in sessions table then
-- last_insert_id() can be used to fetch that value after insert into sessions table
-- which can be used while inserting into usage table
-- example below
-- Insert into `sessions` (start_time,end_time) values (p_start_time,p_end_time);
-- set #v_session = last_insert_id();
-- Insert into `usage` values (p_job,p_start_time,p_end_time,#v_session);
-- dummy logic to create the session_label
-- dummy logic is used you can replace it with whatever logic you need
set #v_session = concat('sess_',left(uuid(),8));
-- insert record in both table (first in session table and then in usage table)
Insert into `sessions` values (#v_session,p_start_time,p_end_time);
Insert into `usage` values (p_job,p_start_time,p_end_time,#v_session);
else
-- record already present in sessions table for this session label
-- so insert record only in usage table
Insert into `usage` values (p_job,p_start_time,p_end_time,#v_session);
End If;
End;
$$
-- call this procedure to insert data into usage table
-- sample execution below when both tables have no rows
-- below call will insert 1 rows in both table having same session label
mysql> call prc_insert_into_usage('job_a','2015-01-01','2015-01-02');
Query OK, 1 row affected (0.00 sec)
mysql> select * from `usage`;
+-------+------------+------------+---------------+
| job | start_time | end_time | session |
+-------+------------+------------+---------------+
| job_a | 2015-01-01 | 2015-01-02 | sess_abc376bf |
+-------+------------+------------+---------------+
1 row in set (0.00 sec)
mysql> select * from `sessions`;
+---------------+------------+------------+
| session_label | start_time | end_time |
+---------------+------------+------------+
| sess_abc376bf | 2015-01-01 | 2015-01-02 |
+---------------+------------+------------+
1 row in set (0.01 sec)
-- below call will insert only in usage table as row already present in sessions table
mysql> call prc_insert_into_usage('job_b','2015-01-01','2015-01-02');
Query OK, 1 row affected (0.00 sec)
mysql> select * from `usage`;
+-------+------------+------------+---------------+
| job | start_time | end_time | session |
+-------+------------+------------+---------------+
| job_a | 2015-01-01 | 2015-01-02 | sess_abc376bf |
| job_b | 2015-01-01 | 2015-01-02 | sess_abc376bf |
+-------+------------+------------+---------------+
2 rows in set (0.00 sec)
mysql> select * from `sessions`;
+---------------+------------+------------+
| session_label | start_time | end_time |
+---------------+------------+------------+
| sess_abc376bf | 2015-01-01 | 2015-01-02 |
+---------------+------------+------------+
1 row in set (0.00 sec)
-- below call will again insert rows in both table
mysql> call prc_insert_into_usage('job_c','2015-01-02','2015-01-04');
Query OK, 1 row affected (0.01 sec)
mysql> select * from `usage`;
+-------+------------+------------+---------------+
| job | start_time | end_time | session |
+-------+------------+------------+---------------+
| job_a | 2015-01-01 | 2015-01-02 | sess_abc376bf |
| job_b | 2015-01-01 | 2015-01-02 | sess_abc376bf |
| job_c | 2015-01-02 | 2015-01-04 | sess_dcfa6853 |
+-------+------------+------------+---------------+
3 rows in set (0.00 sec)
mysql> select * from `sessions`;
+---------------+------------+------------+
| session_label | start_time | end_time |
+---------------+------------+------------+
| sess_abc376bf | 2015-01-01 | 2015-01-02 |
| sess_dcfa6853 | 2015-01-02 | 2015-01-04 |
+---------------+------------+------------+
2 rows in set (0.00 sec)
mysql>
I have a table which will potentially store hundreds of thousands of integers:
desc id_key_table;
+----------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------------+--------------+------+-----+---------+-------+
| id_key | int(16) | NO | PRI | NULL | |
+----------------+--------------+------+-----+---------+-------+
From a program, I have a large set of integers. I'd like to see which of these integers are NOT in the above id_key column.
So far I've come up with the following approaches:
1) Iterate through each integer and perform a:
select count(*) count from id_key_table where id_key = :id_key
When count is 0 the id_key is missing from the table.
This seems like a horrible, horrible way to do it.
2) Create a temporary table, insert each of the values into the temporary table, and perform a JOIN on the two tables.
create temporary table id_key_table_temp (id_key int(16) primary key );
insert into id_key_table_temp values (1),(2),(3),...,(500),(501);
select temp.id_key
from id_key_table_temp temp left join id_key_table as main
on temp.id_key = main.id_key
where main.killID is null;
drop table id_key_table_temp;
This seems like the best approach, however, I'm sure there is a far better approach I haven't thought of yet. I'd prefer to not have to create a temporary table and use one query to determine which integers are missing.
Is there a proper query for this type of search?
(MySQL)
Using your code in the second example given in the question, I created two stored procedures (SP): 1 SP to load a sample table of prime numbers as keys, the other SP to find the missing integers.
Here is the first SP:
DELIMITER $$
DROP PROCEDURE IF EXISTS `test`.`CreateSampleTable` $$
CREATE PROCEDURE `test`.`CreateSampleTable` (maxinttoload INT)
BEGIN
DECLARE X,OKTOUSE,MAXLOOP INT;
DROP TABLE IF EXISTS test.id_key_table;
CREATE TABLE test.id_key_table (id_key INT(16)) ENGINE=MyISAM;
SET X=2;
WHILE X <= maxinttoload DO
INSERT INTO test.id_key_table VALUES (X);
SET X = X + 1;
END WHILE;
ALTER TABLE test.id_key_table ADD PRIMARY KEY (id_key);
SET MAXLOOP = FLOOR(SQRT(maxinttoload));
SET X = 2;
WHILE X <= MAXLOOP DO
DELETE FROM test.id_key_table WHERE MOD(id_key,X) = 0 AND id_key > X;
SELECT MIN(id_key) INTO OKTOUSE FROM test.id_key_table WHERE id_key > X;
SET X = OKTOUSE;
END WHILE;
OPTIMIZE TABLE test.id_key_table;
SELECT * FROM test.id_key_table;
END $$
DELIMITER ;
Here is the second SP:
DELIMITER $$
DROP PROCEDURE IF EXISTS `test`.`GetMissingIntegers` $$
CREATE PROCEDURE `test`.`GetMissingIntegers` (maxinttoload INT)
BEGIN
DECLARE X INT;
DROP TABLE IF EXISTS test.id_key_table_temp;
CREATE TEMPORARY TABLE test.id_key_table_temp (id_key INT(16)) ENGINE=MyISAM;
SET X=1;
WHILE X <= maxinttoload DO
INSERT INTO test.id_key_table_temp VALUES (X);
SET X = X + 1;
END WHILE;
ALTER TABLE test.id_key_table_temp ADD PRIMARY KEY (id_key);
SELECT temp.id_key FROM test.id_key_table_temp temp
LEFT JOIN test.id_key_table main USING (id_key)
WHERE main.id_key IS NULL;
END $$
DELIMITER ;
Here is the Sample Run of First SP using the number 25 to create prime numbers:
mysql> CALL test.CreateSampleTable(25);
+-------------------+----------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+-------------------+----------+----------+----------+
| test.id_key_table | optimize | status | OK |
+-------------------+----------+----------+----------+
1 row in set (0.16 sec)
+--------+
| id_key |
+--------+
| 2 |
| 3 |
| 5 |
| 7 |
| 11 |
| 13 |
| 17 |
| 19 |
| 23 |
+--------+
9 rows in set (0.17 sec)
mysql>
Here is the run of the second SP using 25 as the full list to compare:
mysql> CALL test.GetMissingIntegers(25);
+--------+
| id_key |
+--------+
| 1 |
| 4 |
| 6 |
| 8 |
| 9 |
| 10 |
| 12 |
| 14 |
| 15 |
| 16 |
| 18 |
| 20 |
| 21 |
| 22 |
| 24 |
| 25 |
+--------+
16 rows in set (0.03 sec)
Query OK, 0 rows affected (0.05 sec)
mysql>
While this solution is OK for small samples, big lists become a major headache. You may want to keep the temp table (don't use CREATE TEMPORARY TABLE over and over, use CREATE TABLE just once) permamnently loaded with the numbers 1 .. MAX(id_key) and populate that permanent temp table via a trigger on id_key_table.
Just a question because I am curious: Are you doing this to see if auto_increment keys from a table can be reused ???