Limit number of rows inserted in a MySQL DB table - mysql

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.

Related

MySQL reorder row id by date

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.

Trigger with 2 tables after update

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>

MySQL SELECT returns NULL for NOT NULL column in stored procedure

I have a stored procedure in which I'm trying to loop over a number of IDs in a table and insert them into another table... problem is the ID turns out as NULL in the loop.
For debugging purposes I have created a table called test, it has two columns named var_name and value. I also made a stored procedure like this:
CREATE DEFINER=`root`#`localhost` PROCEDURE `myProcedure`(#parent INT(11))
BEGIN
INSERT INTO `test`
(`var_name`, `value`)
VALUES
('parent', #parent);
INSERT INTO test (`var_name`, `value`)
SELECT 'id', `id`
FROM `mytable`
WHERE `parent` = #parent;
END
The table mytable has a lot of columns but id is the primary key and obviously NOT NULL, parent allows NULL. The id, parent and value columns are all INT(11).
The following statement:
CALL myProcedure(1);
Produces the following result in test:
+----------+-------+
| var_name | value |
+----------+-------+
| 'parent' | 1 |
| 'id' | NULL |
| 'id' | NULL |
| 'id' | NULL |
| 'id' | NULL |
| 'id' | NULL |
| 'id' | NULL |
+----------+-------+
The number of 'id' rows match the number of rows in mytable with parent = 1, but value is always NULL. Running the following query:
SELECT `id` FROM `mytable` WHERE `parent` = 1;
Produces the expected result:
+----+
| id |
+----+
| 2 |
| 3 |
| 4 |
| 5 |
| 6 |
| 7 |
+----+
What's going on here?
Not quite sure what is wrong with the given procedure but I tried creating one similar to yours and it worked pretty well. Here what I did
mysql> create table test (var_name varchar(100),value int);
Query OK, 0 rows affected (0.10 sec)
mysql> create table mytable (id int, parent int);
Query OK, 0 rows affected (0.07 sec)
mysql> insert into mytable values (2,1),(3,1),(4,1),(5,1),(6,1),(7,1);
Query OK, 6 rows affected (0.02 sec)
Records: 6 Duplicates: 0 Warnings: 0
Then added the following procedure
delimiter //
CREATE PROCEDURE myProcedure(parent INT(11))
BEGIN
declare parent_id int ;
set #parent_id := parent ;
INSERT INTO `test`
(`var_name`, `value`)
VALUES
('parent', #parent_id);
INSERT INTO test (`var_name`, `value`)
SELECT 'id', `id`
FROM `mytable`
WHERE `parent` = #parent_id;
END; //
mysql> CALL myProcedure(1);
Query OK, 6 rows affected (0.05 sec)
mysql> select * from test ;
+----------+-------+
| var_name | value |
+----------+-------+
| parent | 1 |
| id | 2 |
| id | 3 |
| id | 4 |
| id | 5 |
| id | 6 |
| id | 7 |
+----------+-------+
7 rows in set (0.00 sec)
Only thing I changed is used a variable inside the procedure to hold the param value and use it in the query.
I lied a little bit in the question. My real stored procedure contained the portion I posted at the beginning, but after a couple of DECLARE, including:
DECLARE id INT;
which replaced the value of id in mytable...

How does "for each row" work in triggers in mysql?

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.

From a set of values, how do I find the values not stored in a table's column?

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 ???