Mathlight Mathlight - 3 months ago 9
MySQL Question

Trigger isn't working / fired while multi-row inserting

I've two tables in the DB called

KistStatus
and
LastKistStatus
. The latter will hold all the 'newest' values of
KistStatus
.

KistStatus
has around 174.000 records,
LastKistStatus
should have around 9.500 records.

If I insert the records by hand (execute the insert one by one by hand), the
LastKistStatus
will be filled properly. But If I execute the sql export file, the trigger seems to be broken.

The trigger:

DROP TRIGGER IF EXISTS `KistStatusTrigger`;
DELIMITER //
CREATE TRIGGER `KistStatusTrigger` AFTER INSERT ON `KistStatus`
FOR EACH ROW begin
DECLARE id_exists Boolean;
DECLARE time_exists Timestamp;

-- Check LastKistStatus table
SELECT 1, `Timestamp`
INTO @id_exists, @time_exists
FROM LastKistStatus
WHERE LastKistStatus.idKist = NEW.idKist;

IF @id_exists > 0
THEN
IF @time_exists <= NEW.Timestamp
THEN
UPDATE LastKistStatus SET `Timestamp` = NEW.Timestamp, `idPartij` = NEW.idPartij, `Actie` = NEW.Actie, `idTaak` = NEW.idTaak, `idBewaarplaats` = NEW.idBewaarplaats, `Rij` = NEW.Rij, `Stapel` = NEW.Stapel, `Hoogte`= NEW.Hoogte, `KnolAantal` = NEW.KnolAantal, `Gewicht` = NEW.Gewicht, `idGebruiker` = NEW.idGebruiker, `Laatste` = NEW.Laatste WHERE `idKist` = NEW.idKist;
END IF;
ELSE
INSERT INTO LastKistStatus (`idKistStatus`, `idKist`, `Timestamp`, `idPartij`, `Actie`, `idTaak`, `idBewaarplaats`, `Rij`, `Stapel`, `Hoogte`, `KnolAantal`, `Gewicht`, `idGebruiker`, `Laatste` ) VALUES (NEW.idKistStatus, NEW.idKist, NEW.Timestamp, NEW.idPartij, NEW.Actie, NEW.idTaak, NEW.idBewaarplaats, NEW.Rij, NEW.Stapel, NEW.Hoogte, NEW.KnolAantal, NEW.Gewicht, NEW.idGebruiker, NEW.Laatste);
END IF;
END
//
DELIMITER ;


Insert command (done from command line)
mysql -u *** -p TTTDB < KistStatus.sql


And the
KistStatus.sql
have a lot of lines like this:

INSERT INTO `KistStatus` (`idKistStatus`, `idKist`, `Timestamp`, `idPartij`, `Actie`, `idTaak`, `idBewaarplaats`, `Rij`, `Stapel`, `Hoogte`, `KnolAantal`, `Gewicht`, `idGebruiker`, `Laatste`) VALUES
(1, 227862, '2015-09-04 14:15:29', 40, '3', 0, 260, 2060, 33980, 1, NULL, 0, 40, 1),
(21, 229522, '2015-09-04 14:15:29', 40, '3', 0, 260, 2060, 33980, 2, NULL, 0, 40, 1),
(1083, 51102, '2015-09-05 07:33:37', 80, '3', 0, 40, 440, 5060, 1, NULL, 0, 100, 1),
(1103, 51102, '2015-09-05 07:33:44', 80, '3', 0, 40, 440, 5060, 1, NULL, 0, 100, 2),
(1123, 51102, '2015-09-05 07:33:44', 80, '3', 0, 40, 440, 5060, 1, NULL, 0, 100, 1),
(1143, 22202, '2015-09-05 07:37:33', 80, '3', 0, 40, 420, 4820, 1, NULL, 0, 100, 1);


I also have tested it with and insert per record, but that didn't changed anything (other than taking forever to insert...)

Sow what am I doing wrong? Is there a problem in my trigger? Wouldn't it be triggered properly while inserting it from the command line? I'm out of ideas at the moment

Answer

Your Problem

MySQL, somewhat confusingly, has two different types of variable:

  • local variables (not prefixed by @), which are strongly typed and scoped to the stored program block in which they have been declared with a DECLARE statement; and

  • user variables (prefixed by @), which are loosely typed and scoped to the session. Note that they neither need nor can be declared—they are just used directly.

You declare local variables named id_exists and time_exists, but thereafter use user variables named @id_exists and @time_exists. Since the latter are scoped to the session, they retain their values between subsequent invocations of the trigger within the same session—and, consequently, once @id_exists has been set to 1 no further records will be inserted.

The obvious solution is not to use user variables in this case, but instead to stick with your local variables: i.e. remove the @ prefixes throughout your trigger.

Better Approaches

  1.  Reduce data duplication

    Unless you really need to, don't store a copy of every column from KistStatus in LastKistStatus—you can simply store (idKist, idKistStatus) and fetch the underlying record as/when required.

    ALTER TABLE LastKistStatus
      DROP COLUMN Timestamp,
      DROP COLUMN idPartij,
      DROP COLUMN Actie,
      DROP COLUMN idTaak,
      DROP COLUMN idBewaarplaats,
      DROP COLUMN Rij,
      DROP COLUMN Stapel,
      DROP COLUMN Hoogte,
      DROP COLUMN KnolAantal,
      DROP COLUMN Gewicht,
      DROP COLUMN idGebruiker,
      DROP COLUMN Laatste;
    

    Then, whenever you referenced LastKistStatus before, you can now simply reference LastKistStatus NATURAL JOIN KistStatus in its place and get the same results.

  2.  Learn about INSERT ... ON DUPLICATE KEY UPDATE.

    Given the logic in your trigger, it would appear that LastKistStatus will only ever contain a single record with any given idKist. By enforcing this constraint in the database, you can do some clever stuff:

    ALTER TABLE LastKistStatus ADD UNIQUE KEY (idKist);
    

    Here I'm adding a UNIQUE KEY to the table. However, it's quite probable that this could (and should) be the table's PRIMARY KEY instead, which has the same behaviour (but is a bit faster, given its special status).

    Now, instead of testing whether a record already exists and then inserting or updating accordingly, you can combine the operations into one:

    INSERT INTO LastKistStatus
      (idKist, idKistStatus)
    VALUES
      (NEW.idKist, NEW.idKistStatus)
    ON DUPLICATE KEY UPDATE
      idKistStatus = NEW.idKistStatus;
    
  3. Observe that LastKistStatus is really just a property of Kist

    I presume you have a table Kist. You could simply move idKistStatus from the LastKistStatus table into that one, and do away with the LastKistStatus table altogether.

    ALTER TABLE Kist
      ADD COLUMN idKistStatus BIGINT UNSIGNED NULL DEFAULT NULL,
      ADD FOREIGN KEY (idKistStatus) REFERENCES KistStatus (idKistStatus);
    
    UPDATE Kist JOIN LastKistStatus USING (idKist)
    SET    Kist.idKistStatus = LastKistStatus.idKistStatus;
    
  4. Do away with the trigger and cache altogether

    By finding the groupwise maximum from the KistStatus table, you can locate the most recent status without maintaining LastKistStatus at all:

    SELECT * FROM KistStatus NATURAL JOIN (
      SELECT idKist, MAX(Timestamp) AS Timestamp FROM KistStatus GROUP BY idKist
    ) t;
    

    This would be sargable with an index over (idKist, Timestamp):

    ALTER TABLE KistStatus ADD INDEX (idKist, Timestamp);
    

    However, notice that it will return all records from each group that have the maximal timestamp—should you have a preference for one in particular, you will need to define the logic for identifying which.

    You can even create a view to yield the same results as you used to obtain from LastKistStatus:

    CREATE VIEW LastKistStatus AS
    SELECT * FROM KistStatus NATURAL JOIN (
      SELECT idKist, MAX(Timestamp) AS Timestamp FROM KistStatus GROUP BY idKist
    ) t;
    

    Then use it as before:

    SELECT * FROM LastKistStatus WHERE ...
    
Comments