mysql use IF NOT NULL as part of transaction - mysql

I am using a MySQL transaction. If the initial where myHistoryNum result (match name+rowActive) is not NULL, then continue the transaction.
I have tried various combinations, however cannot get the insert statement to trigger when myHistoryNum is set.
history | uuid | name | rowActive
24 | abcd | Art | 1 <<< is match, trigger an insert
999 | zxcv | Art | 0 <<< no match, do not trigger insert
start transaction;
select #myHistNum := max(history), #olduuid := uuid
from mytable WHERE (name=art AND rowActive=1);
# START IS NOT NULL test
CASE WHEN #myHistNum IS NOT NULL THEN
set #histNum = #histNum + 1;
INSERT INTO mytable
.....
END; <<<ALT, tried END AS;
commit;
also tried
IF #myHistNum IS NOT NULL
THEN
.....
END IF;
commit;

If I'm not mistaken, control flow statements (like IF, WHEN) can only be used within stored programs in MySQL

Related

Trigger to invalidate cache after update

I'm trying to write a trigger to invalidate the word index for my story database. However I can't seem to figure out how to stop the trigger from firing again during the indexing operation. I know I need to place an if statement to stop the update, but I can't seem to figure out what it should look like.
CREATE TRIGGER trMarkStoryForReindex BEFORE UPDATE ON Chapters
FOR EACH ROW BEGIN
-- any update to any chapter invalidates the index for the whole story
-- we could just periodically flush the story index, but this way is
-- better.
SET New.isIndexed = FALSE;
-- delete the index for that story
-- It will get rebuilt in at most 15 minutes
DELETE FROM freq WHERE storyid = NEW.StoryId;
END;
I basically want the trigger to fire only when isIndexed is has not been set in the update statement causing the trigger.
My data model looks like so:
Chapters
id
isIndexed
StoryId
Freq
word
storyid
Here's my proposal for solution. I've tested this on SQL fiddle, and it seems to work:
-- Database setup
create table chapters (
id int unsigned not null auto_increment primary key,
isIndexed boolean default false,
storyId int not null,
index idx_storyId(storyId)
);
create table freq (
word varchar(50),
storyId int not null,
index idx_storyId(storyId)
);
delimiter //
create trigger bi_chapters before update on chapters
for each row
begin
if new.isIndexed = false then
delete from freq where storyId = new.storyId;
end if;
end //
delimiter ;
insert into freq(word, storyId)
values ('one', 1), ('two', 1), ('three', 2);
insert into chapters(isIndexed, storyId)
values (true, 1), (true, 2);
When you select the values from freq (before updating chapters) you get this:
select * from chapters;
| id | isIndexed | storyId |
|----|-----------|---------|
| 1 | false | 1 |
| 2 | true | 2 |
select * from freq;
| word | storyId |
|-------|---------|
| one | 1 |
| two | 1 |
| three | 2 |
Now, do an update to chapters and select from freq again:
update chapters
set isIndexed = false
where storyId = 1;
select * from freq;
| word | storyId |
|-------|---------|
| three | 2 |
The only modification I did is that if block that checks if the new row is updated to false. If I've understood your question correctly, this would do what you need.
SQL fiddle example

MySQL Trigger after insert and add timestamp when data changed

This is my perl sample code and what i needed is add a timestamp into columns when data changed(updated).
use strict;
use DBI;
use POSIX qw(strftime);
my $datestring = strftime "%Y-%m-%d %H:%M:%S",localtime;
my $dbh = DBI->("dbi:mysql:dbname=perldb","root","111111",{RaiseError=>1})
or die DBI::errstr();
my $mac1='mac1';
my $ip1='ip1';
my $mac2='mac2';
my $ip2='ip2';
$dbh->do("DROP TABLE IF EXISTS Test1");
$dbh->do("CREATE TABLE Test1(Id INT PRIMARY KEY AUTO_INCREMENT,IP TEXT,MAC TEXT,TIME DATETIME,PrevTime DATETIME) ENGINE=InnoDB");
$dbh->do("CREATE Trigger trigger1 AFTER INSERT ON Test1
FOR EACH ROW
BEGIN
IF OLD.MAC!=NEW.MAC
THEN SET NEW.TIME='$datestring';
END IF;
END;")
$dbh->do("INSERT INTO Test1(IP,MAC) VALUES('$ip1','$mac1') ON DUPLICATE KEY UPDATE IP=VALUE(IP)");
$dbh->do("INSERT INTO Test1(IP,MAC) VALUES('$ip2','$mac2') ON DUPLICATE KEY UPDATE IP=VALUE(IP)");
$dbh->disconnect();
The goal I want to reach is run this code once then modify $mac1 to 'mac3'.
So that it will trigger SET timestamp because the data was changed.
Also SET old one timestamp to PrevTIME, but OLD for INSERT.
Result I need maybe like this:
+----+------+------+---------------------+----------+
| Id | IP | MAC | TIME | PrevTime |
+----+------+------+---------------------+----------+
| 1 | ip1 | mac1 | 2015-03-03 12:34:56 | NULL |
| 2 | ip2 | mac2 | 2015-03-03 12:34:56 | NULL |
+----+------+------+---------------------+----------+
Then
+----+------+------+---------------------+---------------------+
| Id | IP | MAC | TIME | PrevTime |
+----+------+------+---------------------+---------------------+
| 1 | ip1 | mac3 | 2015-03-03 12:40:00 | 2015-03-03 12:34:56 |
| 2 | ip2 | mac2 | 2015-03-03 12:34:56 | NULL |
+----+------+------+---------------------+---------------------+
Anyidea? Please help me figure out,THANK YOU.
-------------------------------SOLUTION---------------------------------------
use strict;
use DBI;
use POSIX qw(strftime);
my $datestring = strftime "%Y-%m-%d %H:%M:%S",localtime;
my $dbh = DBI->("dbi:mysql:dbname=perldb","root","111111",{RaiseError=>1})
or die DBI::errstr();
my $mac1='mac1';
my $ip1='ip1';
my $mac2='mac2';
my $ip2='ip2';
$dbh->do("CREATE TABLE IF NOT EXISTS Test1(Id INT PRIMARY KEY AUTO_INCREMENT,IP TEXT,MAC TEXT,TIME DATETIME,PrevTime DATETIME,UNIQUE(IP)) ENGINE=InnoDB");
$dbh->do("CREATE Trigger trigger1 BEFORE INSERT ON Test1
FOR EACH ROW
BEGIN
SET NEW.TIME='$datestring';
END;")
$dbh->do("CREATE Trigger trigger2 BEFORE UPDATE ON Test1
FOR EACH ROW
BEGIN
IF OLD.MAC <> NEW.MAC
THEN
SET NEW.PrevTIME=OLD.TIME;
SET NEW.TIME='$datestring';
END IF;
END;")
$dbh->do("INSERT INTO Test1(IP,MAC) VALUES('$ip1','$mac1') ON DUPLICATE KEY UPDATE IP=VALUE(MAC)");
$dbh->do("INSERT INTO Test1(IP,MAC) VALUES('$ip2','$mac2') ON DUPLICATE KEY UPDATE IP=VALUE(MAC)");
$dbh->disconnect();
You actually need 2 triggers one before insert and one before update. The before insert will set the TIME to now(), while the before update will check the old and new values of mac and set the PrevTime to old TIME and TIME to now()
Here are the triggers, you can have them in your code
delimiter //
create trigger trigger1 before insert on Test1
for each row
begin
set new.TIME = now();
end ; //
delimiter //
create trigger trigger2 before update on Test1
for each row
begin
if old.MAC <> new.MAC then
set new.PrevTime = old.TIME ;
set new.TIME = now();
end if;
end ;//
Triggers are event specific.
You are using an INSERT and hence AFTER or BEFORE Trigger works defined for INSERT events.
You actually need a BEFORE UPDATE trigger.
Add a similar trigger for BEFORE TRIGGER
Example:
CREATE TRIGGER bu_on_Test BEFORE UPDATE ON Test1
FOR EACH ROW
BEGIN
IF OLD.MAC <> NEW.MAC THEN
SET NEW.TIME='$datestring';
END IF;
END;
But note that Triggers won't take dynamic input parameters like '$datestring'.
If such a value is set in your scripting language, that is for lifetime of trigger definition and is not going to change during its execution.

Why does this CASE based update cause replication skew?

Using MySQL 5.6 with statement based replication between a single master and slave, the following scenario creates replication skew:
Create this table:
CREATE TABLE `ReplicationTest` (
`TestId` int(11) NOT NULL,
`Tokens` int(11) NOT NULL,
PRIMARY KEY (`TestId`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1$$
Insert this data:
Insert into ReplicationTest (TestId, Tokens) VALUES
(1, 0),
(2, 0),
(3, 0),
(4, 0);
Create this proc:
CREATE PROCEDURE `MyDatabase`.`ReplicationTestProc` (vTestId int, pSuccessful BIT, pAmount DECIMAL(19, 4))
BEGIN
START TRANSACTION;
Update MyDatabase.ReplicationTest Set Tokens = CASE WHEN pSuccessful THEN Tokens - (pAmount * 100) ELSE Tokens END
where TestId = vTestId;
COMMIT;
END
Run these 4 statements in a single execution
call `MyDatabase`.`ReplicationTestProc`(1, 1, 1);
call `MyDatabase`.`ReplicationTestProc`(2, 1, 1);
call `MyDatabase`.`ReplicationTestProc`(3, 1, 1);
call `MyDatabase`.`ReplicationTestProc`(4, 0, 1);
You will now have different values in the ReplicationTest table between master and replication. It looks like the pSuccessful variable is being treated as a global, with the last value set the one that is being used on the replication server.
You can get a clue what's happening by observing what statements actually get logged in the binary log. View the binlog with:
$ mysqlbinlog <datadir>/mysql-bin.000001
(Of course the filename containing these statements is probably different on your system.)
The statements inside the called procedure are logged to the binary log, but constant values that are based one procedure variables are slightly modified. This is explained in http://dev.mysql.com/doc/refman/5.6/en/stored-programs-logging.html:
A statement to be logged might contain references to local procedure variables. These variables do not exist outside of stored procedure context, so a statement that refers to such a variable cannot be logged literally. Instead, each reference to a local variable is replaced by this construct for logging purposes.
Here's what we see in the binlog for your test. The references to local variables have been replaced with a string representation of the BIT values:
. . .
Update test.ReplicationTest Set Tokens = CASE
WHEN NAME_CONST('pSuccessful',_binary'' COLLATE 'binary')
THEN Tokens - ( NAME_CONST('pAmount',1.0000) * 100) ELSE Tokens END
where TestId = NAME_CONST('vTestId',1)
. . .
Update test.ReplicationTest Set Tokens = CASE
WHEN NAME_CONST('pSuccessful',_binary'' COLLATE 'binary')
THEN Tokens - ( NAME_CONST('pAmount',1.0000) * 100) ELSE Tokens END
where TestId = NAME_CONST('vTestId',2)
. . .
Update test.ReplicationTest Set Tokens = CASE
WHEN NAME_CONST('pSuccessful',_binary'' COLLATE 'binary')
THEN Tokens - ( NAME_CONST('pAmount',1.0000) * 100) ELSE Tokens END
where TestId = NAME_CONST('vTestId',3)
. . .
Update test.ReplicationTest Set Tokens = CASE
WHEN NAME_CONST('pSuccessful',_binary'\0' COLLATE 'binary')
THEN Tokens - ( NAME_CONST('pAmount',1.0000) * 100) ELSE Tokens END
where TestId = NAME_CONST('vTestId',4)
. . .
Notice how the BIT values 1 is replaced with the string literal _binary''.
When your expression evaluates that for true/false, the empty string is equivalent to false, which is why the statement doesn't update your column on the slave:
mysql> select false = _binary'';
+-------------------+
| false = _binary'' |
+-------------------+
| 1 |
+-------------------+
Why is a BIT value of 1 replaced with an empty string? BIT values are sort of strings by nature, as if using the BINARY data type. Here's an example of a BIT value corresponding to the ASCII code for 'Z' coming out as a 'Z':
mysql> select b'01011010';
+-------------+
| b'01011010' |
+-------------+
| Z |
+-------------+
How does BIT treat the value of b'1'? It's just like an ASCII value 0000001, which prints as an unprintable, zero-length string:
mysql> select b'1';
+------+
| b'1' |
+------+
| |
+------+
You can force this value to be converted to an integer by referencing it in an integer context:
mysql> select b'1'+0;
+--------+
| b'1'+0 |
+--------+
| 1 |
+--------+
But even if we were to change your stored procedure to use this trick, it's too late. The value has been converted in the binary log to an empty string _binary'' and that string can't be converted to anything but 0 in an integer context.
mysql> select _binary''+0;
+-------------+
| _binary''+0 |
+-------------+
| 0 |
+-------------+
Why isn't the BIT value converted to b'1' in the binary log like a real BIT literal? This appears to be deliberate. There's even a comment in the binary logging code for stored procedures (file sql/sp.c, function sp_get_item_value()).
/* Bit type is handled as binary string */
What I would recommend to you is to forget about using BIT for booleans in MySQL. There are too many cases of unexpected behavior of the MySQL BIT data type. For more on this, see Why you should not use BIT columns in MySQL.
Instead, use BOOLEAN or TINYINT (BOOLEAN or BOOL are really just aliases for TINYINT(1) in MySQL). I tested your procedure with the parameter changed to TINYINT, and it works as expected:
slave1 [localhost] {msandbox} (test) > select * from ReplicationTest;
+--------+--------+
| TestId | Tokens |
+--------+--------+
| 1 | -100 |
| 2 | -100 |
| 3 | -100 |
| 4 | 0 |
+--------+--------+

SQL cursor & stored procedure order arrangement

I was working on a stored procedure to update the order field in a product table.
It works only the problem now is the the last item in the loop(cur), is increased twice instead of once (so dubbeled). Like so:
+-----------------+
|product + order |
|_id | |
| | |
| 1 | 0 |
| 2 | 1 |
| etc.. | etc..|
| 36 | 35 |
| 37 | 36 |
| 38 | 38 |
| |
+-----------------+
I cant figure out why. The link table(CategoryProduct) in this case goes to 38 with a category_id of 2 CALL curorder(2);
Stored procedure:
DELIMITER //
CREATE PROCEDURE curorder(
IN catid INT
)
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE i INT DEFAULT 0;
DECLARE p INT;
DECLARE cur CURSOR FOR SELECT product_id FROM test.CategoryProduct WHERE category_id = catid;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cur;
read_loop: LOOP
FETCH cur INTO p;
UPDATE `test`.`Product` SET `order` = i WHERE `Product`.`product_id` =p;
SET i = i + 1;
IF done THEN
LEAVE read_loop;
END IF;
END LOOP;
CLOSE cur;
END //
DELIMITER ;
The Database is a Mysql Database. Any suggestions for improving the procedure are always welcome.
Thanks in advance.
EDIT:
I already tried to place the SET i STATEMENT beneath the IF STATEMENT but with no result.
You should put:
IF done THEN
LEAVE read_loop;
END IF;
Above your update statement, the last time mysql walks trough the loop is uses the old variables because there is no new 'p'. but i is incremented.
I good way to debug stored procedures is with a log table:
CREATE TABLE procedureLog
(
id INTEGER AUTO_INCREMENT,
description TEXT,
PRIMARY KEY (id)
);
For this case you can log the update parameters with the follow query:
INSERT INTO  `test`.`procedureLog` (`id` ,`description`) VALUES (null,  CONCAT('id: ', CAST(p as CHAR), ' order: ', CAST(i as CHAR)));
Good luck!

SQL IF - ELSE Statement

I have a table like this :
+-------+------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+------------+------+-----+---------+-------+
| ID | bigint(20) | NO | PRI | NULL | |
| view | bigint(20) | NO | | NULL | |
+-------+------------+------+-----+---------+-------+
Is it possible to do this with SQL's IF-ELSE statement ?
Check if there is ID=1 row in table
If there is , increase view column by 1 .
If there isn't, insert new row to table with ID=1
It should be something like this :
IF((SELECT COUNT(ID) FROM wp_viewcount WHERE ID=1) == 0)
BEGIN
INSERT INTO wp_viewcount VALUES (1,1)
END
ELSE
BEGIN
UPDATE wp_viewcount SET view=view+1 WHERE ID=1
END
The following SQL statement will result in the IF - ELSE logic you want, by using the on duplicate key syntax.
insert into wp_viewcount values(1,1) on duplicate key update view=view+1;
You can only use MySQL's if in a stored procedure. For example:
DELIMITER //
CREATE PROCEDURE `test_procedure` (IN wp_id INT)
BEGIN
IF( (SELECT COUNT(*) FROM wp_viewcount WHERE id = wp_id)<1) THEN
INSERT INTO wp_viewcount(id,view) VALUES (wp_id,1);
ELSE
UPDATE wp_viewcount SET view=view+1 WHERE ID=wp_id;
END IF;
END //
Given your use case, you might be better served by MySQL's INSERT ... ON DUPLICATE KEY UPDATE.
Why not use
if not exists (select * from wp_viewcount where id = 1)
begin
--insert logic
end
else
begin
--update logic
end