I have a view in mysql which is made of three tables unioned together:
CREATE VIEW program_operator_jct_view AS
select
program_id,
operator_code,
'PROGRAM_OPERATOR' AS type
from
program_operator_jct
UNION
(select
program_id,
operator_code,
'PROGRAM_GROUP' AS type
from
program_operator_group_jct pg_jct,
operator_group_jct og_jct
where
pg_jct.group_id = og_jct.group_id)
From this view, I create a summary table for increased performance, which is indexed so my results from this summary table can be returned via covering indexes:
CREATE TABLE `program_operator_jct_summary` (
`program_id` int(7) NOT NULL,
`operator_code` varchar(6) NOT NULL,
PRIMARY KEY (`program_id`,`operator_code`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1
//BUILD SUMMARY PROCEDURE
delimiter //
CREATE PROCEDURE update_program_operator_jct_summary ()
BEGIN
DELETE FROM program_operator_jct_summary;
INSERT INTO program_operator_jct_summary select DISTINCT program_id, operator_code from program_operator_jct_view;
INSERT INTO trigger_record (name) VALUES ('update_program_operator_jct_summary');
END
//
I attached this procedure to the insert, update and delete triggers of the underlining tables which make up the summary table:
-program_operator_jct
-program_operator_group_jct
-operator_group_jct
EXAMPLE:
delimiter //
CREATE TRIGGER trigger_program_operator_jct_insert AFTER INSERT ON program_operator_jct
FOR EACH ROW
BEGIN
CALL update_program_operator_jct_summary ();
END
//
Here's my problem when I add (5) operators to the program_operator_jct:
INSERT INTO program_operator_jct (program_id, operator_code) VALUES
('112', '000500'),
('112', '000432'),
('112', '000501'),
('112', '000264'),
('112', '000184')
This trigger runs (5) times, if I add 100 operators this trigger runs 100 times. This is a nice place to use triggers because I don't have to worry about the summary table being out of date with the underlining tables.
However rebuilding a summary table for each value in an extended inserts is way too much of a performance hit (sometimes I add hundreds of operators to programs at a time). I want the trigger to run once after the extended inserts are performed on the underlining tables. Is this possible?
The trigger is doing its job, e.g. 'FOR EACH ROW'.
I don't believe that mysql gives you the option of running a trigger once at the end.
I'd call the stored procedure from your code after the INSERT has successfully completed.
If you're worried about forgetting, setup a cron job to run it every once in a while.
Good luck.
Related
At a high level I am already aware that you can not insert a new row to the same table and that one should consider a SPROC. Here is the use case though. There is a 3rd party web app and this MySQL DB that I have, so I have no control over the application flow. A request has come in to simplify data entry. What I do have control over is the database. The app is like a CRM and has a contacts table and there is a 2nd contact_relationships table where I am putting the trigger on. Basically the contact_relationships needs three fields. Two contactIDs (INT) from the contact table and a relationship_type varchar(45) like (Spouse, Sibling, External Family and so on).
The goal here is when a new row is added (the TRIGGER) to the contact_relationships table, that we ALSO write a 2nd row to the SAME TABLE that inverts the contactIDs and keeps the relationship_type. This ensures there is also a record relationship established for the other contact from the same single entry. (ideally should be done in the app).
I'm at my whits end over what should be a stupid simple operation. I even tried this creative implementation.
I created a new _temp table
CREATE TABLE `_temp` (
`id_temp` int(11) NOT NULL AUTO_INCREMENT,
`Contact_id` int(11) DEFAULT NULL,
`Relation_id` int(11) DEFAULT NULL,
`Relationship_Type` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id_temp`)
I have two triggers on the contact_relationships table
#1
CREATE DEFINER=`root`#`localhost` TRIGGER `contact_relationships_BEFORE_INSERT` BEFORE INSERT ON `contact_relationships` FOR EACH ROW BEGIN
insert into _temp (Contact_id,Relation_id,Relationship_Type)
values(NEW.Contact_id,NEW.Relation_id,NEW.Relationship_Type);
END
#2
CREATE DEFINER=`root`#`localhost` TRIGGER `contact_relationships_AFTER_INSERT` AFTER INSERT ON `contact_relationships` FOR EACH ROW BEGIN
delete from _temp;
END
On the _temp table I have this trigger
CREATE DEFINER=`root`#`localhost` TRIGGER `_temp_AFTER_DELETE` AFTER DELETE ON `_temp` FOR EACH ROW BEGIN
#DO Sleep(2);
# Sleep commented out as it did not work and only delayed the error
insert into contact_relationships (Relation_id,Contact_id,Relationship_Type) values(OLD.Contact_id,OLD.Relation_id,OLD.Relationship_Type);
END
So my thinking here in pseudo code is like this
When a row is inserted into the contact_relationships table write the inverted row to a temp table as I can not write it to the **same table from inside the trigger**.
AFTER the triggered inserted row is complete lets delete the row in the temp table so we can create a DELETE trigger on that other table to write the desired row into the contact_relationship.
At this stage I believe the TRIGGER and TRANSACTIONS on the contact_relationships table are DONE
On the temp Table a trigger and transaction firing AFTER any contact_relationships transactions
Results are maddening and always the same albeit internal or external. I have tried creative functions and sprocs all with the same aggravating results.
Contact Relationships : Add New
Can't update table 'contact_relationships' in stored function/trigger because it is already used by statement which invoked this stored function/trigger.
Query:
insert into `Contact_Relationships` set `Contact_id`='2', `Relation_id`='3', `Relationship_Type`='Extended Family'
I'm completely brain dead on how I may accomplish this - anyone have a way to pull this off?
dsoden.
What I would do is:
Inside the trigger definition, you can get the "inserted_id" of the relationship that was inserted ( You can get it withLAST_INSERT_ID() ).
Once you have it, you can SELECT from that record into temp variables:
SELECT `Contact_id`,`Relation_id`,`Relationship_Type` INTO #cont_id, #rel_id, #rel_type
FROM contact_relationships
WHERE id = LAST_INSERT_ID()
And then do the Insert of the new relationship in the reverse order, with the same relationship type:
INSERT INTO contact_relationships (`Contact_id`,`Relation_id`,`Relationship_Type`) VALUES ( #rel_id, #cont_id, #rel_type);
If that's what you're aiming for..
Something like:
When a relationship between Anne and John as Siblings is inserted,
insert the John,Anne,Siblings relationship as well
I'm experiencing some very strange transactional behaviour in my MYSQL application.
I've managed to reduce the problem down to a small isolated test case, the code for which I’ve included below:
-- Setup a new environment
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
DROP DATABASE IF EXISTS `testDB`;
CREATE DATABASE `testDB`;
USE `testDB`;
-- Create a table I want two procedure calls to interact with
CREATE TABLE `tbl_test` (
`id` INT(10) UNSIGNED NOT NULL
, PRIMARY KEY (`id`)
);
-- A second table purely to demonstrate the issue
CREATE TABLE `tbl_test2` (
`id` INT(10) UNSIGNED NOT NULL
);
DELIMITER $$
DROP PROCEDURE IF EXISTS `sp_test` $$
CREATE PROCEDURE `sp_test` ()
BEGIN
START TRANSACTION;
-- CRAZY LINE
SELECT * FROM `tbl_test2`;
-- Insert ignore so both calls don’t try to insert the same row
INSERT IGNORE INTO `tbl_test` (`id`) VALUES (1);
-- Sleep added to make it possible to run concurrently manually
SELECT SLEEP(1) INTO #rubbish;
-- The result I am interested in
SELECT COUNT(*) FROM `tbl_test`;
COMMIT;
END $$
DELIMITER ;
Steps to Reproduce:
Run in the above script to create a test database, two tables and a stored procedure.
In two separate connections, as near to simultaneously as possible, run the stored procedures (you can increase the SLEEP time if you need longer):
USE `testDB`;
CALL sp_test ();
The Problem
When executed concurrently over two separate connections the SELECT COUNT(*) FROM `tbl_test`; statement returns different values for the two calls.
When I follow the steps above, I get back 1 from the first of the two procedure calls and 0 from the second.
My understanding of transactional behaviour and table locking is that when the first call reaches the INSERT statement it will create a lock. The second procedure call will reach the same line but must then wait until the transaction from the first call has been committed. Increasing the sleep time reinforces this idea as the second call will take twice as long to complete. If this is the case however, then the second procedure call should pick up the insert from the first call and both results should be equal to 1.
TL;DR
I'm expecting both to equal 1
Note that I am using READ_COMMITTED as my transaction isolation level.
I've tested this using MYSQL server and MariaDB
Further Weirdness
So at this point I assumed my understanding was incorrect. However, I then noticed that by removing the line SELECT * FROM `tbl_test2`; the results suddenly produced the expected values!
I've been experimenting with the script but essentially, including a SELECT statement to any table within the database before the INSERT line causes unanticipated results. I have absolutely no idea why this is the case.
Questions
Is my understanding of the expected transactional behaviour correct?
Why on earth does the SELECT statement to an unrelated table cause the transactional locking to fail?
If anyone can shed some light on this I would be very grateful!
I have to Write one or more triggers that keep track of how many total records have been in the sakila_film table in a single variable, but I am having trouble figuring out how to do this trigger the table is taken from http://dev.mysql.com/doc/sakila/en/sakila-structure-tables-film.html .This is what I have tried the code below but I am getting an error and I don't know any other way i could do it.
create trigger records after Update on sakila_film Count(*) from sakila_film;
Where are you planning on storing this count of total rows in the film table?
If you are going to manage this with triggers, it seems like you'd need two triggers. One for INSERT, one for DELETE. An UPDATE statement won't change the number of rows in the table. (Note that triggers are NOT fired for foreign key actions; which is a concern if there's a foreign defined with a DELETE rule.) And the triggers will not be fired for a TRUNCATE statement.
Setting aside those concerns, we'd need somewhere to store the rowcount over time. As an example:
CREATE TABLE rowcount_history_film
( ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP
, rowcount INT NOT NULL
, KEY rowcount_history_film_IX1 (ts)
) Engine=MyISAM;
And then we could use trigger to insert a row into that table whenever a DML statement changes the number of rows in the film table:
DELIMITER $$
CREATE TRIGGER film_ad
AFTER DELETE ON film
FOR EACH ROW
BEGIN
INSERT INTO rowcount_history_film (ts, rowcount)
SELECT NOW(), COUNT(*) FROM film;
END$$
CREATE TRIGGER film_ai
AFTER INSERT ON film
FOR EACH ROW
BEGIN
INSERT INTO rowcount_history_film (ts, rowcount)
SELECT NOW(), COUNT(*) FROM film;
END$$
DELIMITER ;
I am trying to create a trigger (this is my first trigger, and question, so be gentle) that will insert new rows into two different tables.
* Edit *
Adding this in as I forgot to mention it until ypercube answered.
I am trying to avoid listing all of the column names, as in the real world usage the table this will be used on has a very large number of columns (not my design, too late to refactor).
* End Edit *
Here's what I have so far.
CREATE TABLE test_table (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
message VARCHAR(255)
);
CREATE TABLE test_table_copy LIKE test_table;
DELIMITER $$
CREATE TRIGGER copy_test_table_data AFTER INSERT ON test_table
FOR EACH ROW
BEGIN
INSERT INTO test_table_copy SELECT * FROM NEW;
END;$$
DELIMITER ;
Unfortunately this results in an error.
mysql> INSERT INTO test_table VALUES (1, 'This is a message');
ERROR 1146 (4502): Table 'test_db.NEW' doesn't exist
I am not quite sure what the problem is, I thought NEW referred the table data was being inserted into?
You could possibly get a list of column names in that table from information_schema views, then use them to create a prepared statement (using cursor to iterate column names) and CONCAT() function to glue together the query string. Then execute the prepared statement.
Seems very contrived even to me, and I'm not sure it would work (and if it did, how efficient it would be)
I need to insert a discount line into a table everything time a I insert a line into the same table.
Now i know that this could end in a endless loop but I have put checks in so it wont insert anything when the discount line is inserted.
Mysql doesnt seem to allow this.It doesnt even insert into the table let alone fire off the trigger
Any suggestions on how to do this?
You cannot alter a table (other than the current row) in a trigger attached to that table.
One solution is to insert into another table and have that trigger insert 2 rows into the table you're interested in.
If you make the other table a blackhole you don't have to worry about storage.
DELIMITER $$
CREATE TRIGGER ai_bh_test_each AFTER INSERT ON bh_test FOR EACH ROW
BEGIN
INSERT INTO table1 (field1, field2, ...) VALUES (new.field1, new.field2, ....);
INSERT INTO table1 ... values for the second row
END $$
DELIMITER ;
Why don't you just change your INSERT code into something like this? :
INSERT INTO table1 (field1, field2, ...)
VALUES ( #item, #price, ....)
, ( #item, #discount, ...) ;
Another thing would be to re-evaluate your data structure. The way it is now, it seems - from the limited information you have provided - that it's not normalized. You are storing two types of info in the table.
Perhaps you can combine the two rows (that are to be inserted every time) into one, by adding a few columns in the table.
Or by splitting the table into two tables, one for the "normal" item rows and one for the discount items.
It isn't allowed in MySQL.
One solution would be to let the trigger insert two times into another table. Then you would do writes and updates to the write table and reads from the trigger managed read table.
I needed to add an additional row to the same table, based on a specific condition on an aggregate of the table and was unable to update my application queries to handle it outside of the database, due to stability lock policy.
An alternative solution is to utilize Events in MySQL to read a "staging" table that holds the pending changes. This works by eliminating the circular reference that would be caused by the trigger. The event then executes the desired changes, without initiating the trigger, by using a session variable to leave the trigger early. Please modify the event timing to suit your needs, such as EVERY 5 SECOND.
Staging Table
CREATE TABLE `table_staging` (
`id` INT NOT NULL,
`value` VARCHAR(250) NOT NULL,
`added` TINYINT(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
);
Trigger
CREATE TRIGGER `table_after_insert` AFTER INSERT ON `table` FOR EACH ROW
tableTrigger: BEGIN
IF #EXIT_TRIGGER IS NOT NULL THEN
/* do not execute the trigger */
LEAVE tableTrigger;
END IF;
/* insert the record into staging table if it does not already exist */
INSERT IGNORE INTO `table_staging`(`id`, `value`)
VALUES (NEW.id, 'custom value');
END;
Event
CREATE EVENT `table_staging_add`
ON SCHEDULE
EVERY 1 MINUTE STARTS '2020-03-31 18:16:48'
ON COMPLETION NOT PRESERVE
ENABLE
COMMENT ''
DO BEGIN
/* avoid executing if the event is currently running */
IF #EXIT_TRIGGER IS NULL THEN
SET #EXIT_TRIGGER=TRUE;
/* add the values that have not already been updated */
INSERT INTO table(`value`)
SELECT
ts.value
FROM table_staging AS ts
WHERE ts.added = 0;
/* update the records to exclude them on the next pass */
UPDATE table_staging AS ts
SET ts.added = 1;
/* alternatively remove all the records from the staging table */
/* TRUNCATE table_staging; */
/* reset the state of execution */
SET #EXIT_TRIGGER=NULL;
END IF;
END;
Notes
Be sure to enable the event scheduler in your MySQL configuration (my.ini on Windows or my.cnf on Linux).
[mysqld]
event_scheduler = ON