Temporary Table in Stored Functions? - mysql

I'm writing a function that I need to use either a TABLE variable for (I hear they don't exist in MySQL) or a temporary table.
However, it seems that temporary tables only seem to work in stored procedures, not functions. I keep getting this error:
Explicit or implicit commit is not allowed in stored function or
trigger.
What I'm trying to build is a solution to an earlier question of mine. It's a function that receives a start date, an end date, and a comma-deliminated string. It first finds all the months between the start and end date and saves them as individual records in the first temporary table. It then parses out the comma-deliminated string and saves those into a second temporary table. Then it does a select join on the two, and if records are present, it returns true, otherwise false.
My intention is to use this as part of another queries WHERE clause, so it needs to be a function and not a stored procedure.
How can I use temporary tables in stored functions? And if I can't, what can I do instead?
Here's my (currently broken) function (or as a gist):
-- need to parse out a string like '4,2,1' and insert values into temporary table
-- MySQL doesn't have a native string split function, so we make our own
-- taken from: http://blog.fedecarg.com/2009/02/22/mysql-split-string-function/
DROP FUNCTION IF EXISTS SPLIT_STR;
CREATE FUNCTION SPLIT_STR(x VARCHAR(255), delim VARCHAR(12), pos INT) RETURNS VARCHAR(255)
RETURN REPLACE(SUBSTRING(SUBSTRING_INDEX(x, delim, pos), LENGTH(SUBSTRING_INDEX(x, delim, pos -1)) + 1), delim, '');
-- need to find all months between the start and end date and insert each into a temporary table
DROP FUNCTION IF EXISTS months_within_range;
DELIMITER //
CREATE FUNCTION months_within_range(starts_at DATE, ends_at DATE, filter_range VARCHAR(255)) RETURNS TINYINT
BEGIN
DROP TABLE IF EXISTS months_between_dates;
DROP TABLE IF EXISTS filter_months;
CREATE TEMPORARY TABLE months_between_dates (month_stuff VARCHAR(7));
CREATE TEMPORARY TABLE filter_months (filter_month VARCHAR(7));
SET #month_count = (SELECT PERIOD_DIFF(DATE_FORMAT(ends_at, "%Y%m"), DATE_FORMAT(starts_at, "%Y%m")));
-- PERIOD_DIFF only gives us the one month, but we want to compare to, so add one
-- as in, the range between 2011-01-31 and 2011-12-01 should be 12, not 11
INSERT INTO months_between_dates (month_stuff) VALUES (DATE_FORMAT(starts_at, "%Y-%m"));
SET #month_count = #month_count + 1;
-- start he counter at 1, since we've already included the first month above
SET #counter = 1;
WHILE #counter < #month_count DO
INSERT INTO months_between_dates (month_stuff) VALUES (DATE_FORMAT(starts_at + INTERVAL #counter MONTH, "%Y-%m"));
SET #counter = #counter + 1;
END WHILE;
-- break up the filtered string
SET #counter = 1;
-- an infinite loop, since we don't know how many parameters are in the filtered string
filters: LOOP
SET #filter_month = SPLIT_STR(filter_range, ',', #counter);
IF #filter_month = '' THEN LEAVE filters;
ELSE
INSERT INTO filter_months (filter_month) VALUES (#filter_month);
SET #counter = #counter + 1;
END IF;
END LOOP;
SELECT COUNT(*) INTO #matches FROM months_between_dates INNER JOIN filter_months ON months_between_dates.month_stuff = filter_months.filter_month;
IF #matches >= 1 THEN RETURN 1;
ELSE RETURN 0;
END//
DELIMITER ;

drop table statements cause an implicit commit, which is not allowed in a mysql function. drop temporary table doesn't cause the commit though. if you're not worried about regular (non-temporary) tables named months_between_dates or filter_months existing you should be able to change
DROP TABLE IF EXISTS months_between_dates;
DROP TABLE IF EXISTS filter_months;
to
DROP TEMPORARY TABLE IF EXISTS months_between_dates;
DROP TEMPORARY TABLE IF EXISTS filter_months;

Related

Mysql - Return table from stored procedure into a variable?

Thanks to this answer https://stackoverflow.com/a/8180159/16349298 , i'm able to translate
a string into a temporary table (usable for WHERE <id> IN <tmpTable>.<colomn>)
The only modification i made is at the end (The select) :
CREATE PROCEDURE stringToTmpTable(IN inputString VARCHAR(255), IN sep VARCHAR(255))
BEGIN
declare pos int; -- Keeping track of the next item's position
declare item varchar(100); -- A single item of the input
declare breaker int; -- Safeguard for while loop
-- The string must end with the delimiter
if right(inputString, 1) <> sep then
set inputString = concat(inputString, sep);
end if;
DROP TABLE IF EXISTS MyTemporaryTable;
CREATE TEMPORARY TABLE MyTemporaryTable ( columnName varchar(100) );
set breaker = 0;
while (breaker < 2000) && (length(inputString) > 1) do
-- Iterate looking for the delimiter, add rows to temporary table.
set breaker = breaker + 1;
set pos = INSTR(inputString, sep);
set item = LEFT(inputString, pos - 1);
set inputString = substring(inputString, pos + 1);
insert into MyTemporaryTable values(item);
end while;
SELECT * FROM MyTemporaryTable;
END
I would like to use this process in a function or procedure in order to call it in any procedure that needs it.
So here is the problem :
I don't know how to store the result of this procedure into a variable : i can't use the SELECT * INTO #p FROM ...; like CALL stringToTmpTable(<string>,<separator>) INTO #table;
An other way would be to add OUT parameter to stringToTmpTable() but it can't return multiple rows. Unfortunatly the amount of parameters in the string is variable so i can't define as much variable as there is parameters in the string.
Finally the FIND_IN_SET() isn't the solution i need.
In the worst case I could copy / past the stringToTmpTable() process in any other procedure that needs it, but that doesn't seem like the best way to me.
Any suggestions ?
"i'm able to translate a string into a temporary table" too, but I am using a different method:
SET #input = 'Banana, Apple, Orange, Pears';
WITH RECURSIVE cte1 as (
select
#input as s,
substring_index(substring_index(#input,',',1),',',-1) as w,
length(#input)-length(replace(#input,',','')) x
union all
select
substring_index(s,',',-x),
trim(substring_index(substring_index(substring_index(s,',',-x),',',1),',',-1)) as w,
x-1 x
from cte1 where s<>'' and x>0
)
select * from cte1
DBFIDDLE
But it's a bit of a problem to determine the real problem you have, which is causing you to ask this question. So this is not an answer, but just a different way of selecting all words from a comma-delimted string.

Create trigger in mysql on insert where it compares fields of added row

I am new with mysql triggers, I have 2 tables in a database, one is called tasks and the other is task_rules.
Once a new task_rule is inserted, I want to compare the field time (which is a time object) to the current time.
if it is greater than the current time, I want to add a new row in tasks and set rid (in tasks) to id of the newly added rule, and the time field in tasks to the time field of the newly added row.
I am getting many syntax errors and i didnt know how to create this trigger.
BEGIN
DECLARE #time TIME
DECLARE #freq VARCHAR(400)
#time = NEW.time
#freq = NEW.frequency
IF (#time > NOW()) AND (#freq == 'daily') THEN
INSERT INTO task_rules ('rid', 'time') VALUES (NEW.id, #time)
END IF
END
Im doing it using phpmyadmin
1) user defined variable (those preceded with #) should not be declared see How to declare a variable in MySQL? 2) to assign a value to a variable you have to use the SET statement 3) every statement must be terminated - if you are using phpmyadmin and the default terminator is set to ; change it and terminate your statements in the trigger with ; see - https://dev.mysql.com/doc/refman/8.0/en/stored-programs-defining.html 4) null safe equals in mysql is not == from memory this should be <=> see https://dev.mysql.com/doc/refman/8.0/en/comparison-operators.html 5) you should probably set delimiters before and after the trigger 6) column names should be escaped with back ticks not single quotes. 7) for each row clause missing before begin statement.
try this
drop trigger if exists t;
delimiter $$
create trigger t after insert on task
for each row
BEGIN
DECLARE vtime TIME;
DECLARE vfreq VARCHAR(400);
set time = NEW.time;
set freq = NEW.frequency;
IF (vtime > NOW()) AND (vfreq <=> 'daily') THEN
INSERT INTO task_rules (`rid`, `time`) VALUES (NEW.id, vtime);
END IF;
END $$
delimiter ;
And do review https://dev.mysql.com/doc/refman/8.0/en/trigger-syntax.html

mysql procedure with if condition

I'm in my first databases class and I'm trying to write a conditional block for a mysql procedure.
This is the procedure:
delimiter //
CREATE PROCEDURE add_ascent(IN cid INT, IN pid INT)
BEGIN
DECLARE count_ascents INT;
SET count_ascents = 0;
SELECT COUNT(`cid`) INTO count_ascents FROM ascents WHERE `cid`=cid AND `pid`=pid;
IF count_ascents < 1 THEN
INSERT INTO ascents (`cid`, `pid`) VALUES (cid, pid);
UPDATE climbers SET climbers.ascents = climbers.ascents + 1 WHERE climbers.id=cid;
UPDATE problems SET problems.ascents = problems.ascents + 1 WHERE problems.id=pid;
END IF;
END;
//
delimiter ;
The goal of the procedure is to only perform the insert and updates if the (cid, pid) pair is not in the the ascents database. After testing, the program doesn't seem to go into the if block at all.
FYI, you might want to consider using an UPSERT, instead of "select/if/insert". For example, mySQL offers INSERT ON DUPLICATE KEY UPDATE.
Here, I suggest:
giving your parameters a DIFFERENT name than the column name, for example iCid and iPid, then
Typing SELECT COUNT(cid) INTO count_ascents FROM ascents WHERE cid=iCid AND pid=iPid and checking the result.

Should it be possible to execute an SQL function in a check constraint within DB2 z/OS

Simple version of the DDL:
create function rm00dv1.no_concurrent_schedules()
returns integer
LANGUAGE SQL
READS SQL DATA
NO EXTERNAL ACTION
NOT DETERMINISTIC
BEGIN
declare num_overlaps integer;
select count(*)
into num_overlaps
from
rm00dv1.schedules a
where
a.id != 0
and
exists (
select 1
from rm00dv1.schedules b
where
b.id = 0 -- matches the key of a given record
and rm00dv1.isConcurrent(b.schdl_eff_dt, b.schdl_trm_dt, a.schdl_eff_dt, a.schdl_trm_dt) != 0
);
return num_overlaps;
end;
Table:
create table rm00dv1.schedules (
id int not null,
schdl_eff_dt date not null,
schdl_trm_dt date not null,
info_chg_ts timestamp(6) not null with default
)
in RM00DV1.TSRMDV01 ;
alter table rm00dv1.schedules add constraint no_schedule_overlap
check ((schdl_trm_dt < '01/01/2015')
or
rm00dv1.no_concurrent_schedules() <= 0);
I am getting an SQL00551N - no execution privilege and that is odd because I can execute the function in a select statement.
Any idea to solve this problem?
Thanks.
Looks like you can't. I'm looking at the DB2 10 for z/OS reference for ALTER TABLE reference and it says the following under CHECK (check-condition): "A check-condition is a search condition, with the following restrictions: ... must not contain... Built-in or user-defined functions...".
Since your function looks like it won't convert to a check condition, defining triggers on the table might be the next best option.
I learned that AFTER triggers do not get a -746 like BEFORE triggers do. I had really wanted to use a CONSTRAINT because that best captures the intent for people who come after me, with a BEFORE trigger to terminate the active schedules. But, it looks like a sequence of triggers is going to be the way to go. It is a bit clunky because the triggers all have to be created separately and you have to look at them together to get the intent, and because correct behavior is dependent on their creation order. Yes, it is documented that they will be executed in the order of their creation.
Happy path termination of rows without a specified termination date:
CREATE TRIGGER terminate_no_trm
after
INSERT ON schedules
referencing new as new
FOR EACH ROW
MODE DB2SQL
BEGIN ATOMIC
update schedules
set
schdl_trm_dt = max(schdl_eff_dt, new.schdl_eff_dt - 1 days) -- prob not necessary, but don't set the trm before the eff
, info_chg_ts = new.info_chg_ts
where
new.keyCombo = keyCombo
and
schdl_trm_dt = '9999-12-31'
and schdl_eff_dt < new.schdl_eff_dt;
end
Prevent insert of rows if that insert causes an overlap:
CREATE TRIGGER no_overlapping_schedules_i
after
insert ON schedules
referencing new as n
FOR EACH ROW
MODE DB2SQL
when (num_concurrent_schedules(n.keyCombo) > 0)
begin atomic
SIGNAL SQLSTATE '75001' (
'Concurrent schedules detected: '
concat ' ' concat cast(n.keyCombo as varchar(32))
concat ': ' concat cast(n.schdl_eff_dt as varchar(32))
concat ' to ' concat cast(n.schdl_trm_dt as varchar(32))
);
end
and prevent UPDATE if that would result in an overlap
CREATE TRIGGER no_overlapping_schedules_u
after
update ON schedules
referencing new as n
FOR EACH ROW
MODE DB2SQL
when (num_concurrent_schedules(n.keyCombo) > 0)
begin atomic
SIGNAL SQLSTATE '75001' (
'Concurrent schedules detected: '
concat ' ' concat cast(n.keyCombo as varchar(32))
concat ': ' concat cast(n.schdl_eff_dt as varchar(32))
concat ' to ' concat cast(n.schdl_trm_dt as varchar(32))
);
end
Thanks for the ideas.

MySQL stored procedure performance issue when using cursor and temporary table

I tried to replace heavy Java method which runs multiple requests to the DB with stored SQL procedure.
It's doing its work but I expected much higher performance improvement.
The logic of the procedure(as well as Java method):
Get list of IDs from table1(purpose)
Iterate the list and get average value of a field from table2(record) for each id
Return list
of pairs id/average_value
Are there any efficiency issues in the procedure?
DROP PROCEDURE IF EXISTS test1.getGeneralAverage;
CREATE DEFINER=root#localhost PROCEDURE getGeneralAverage()
BEGIN
DECLARE p_id BIGINT(20);
DECLARE exit_loop BOOLEAN;
DECLARE cur CURSOR FOR
SELECT purpose_id FROM purpose
WHERE purpose.type = 'GENERAL'
AND (SELECT COUNT(*) > 0 FROM record
WHERE record.purpose_id=purpose.purpose_id) is true;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET exit_loop = TRUE;
CREATE TEMPORARY TABLE IF NOT EXISTS general_average
(id BIGINT(20), average DOUBLE) ENGINE=memory;
TRUNCATE TABLE general_average;
OPEN cur;
average_loop: LOOP
FETCH cur INTO p_id;
INSERT INTO test1.general_average (id, average)
VALUES (p_id, (SELECT AVG(amount) FROM record
WHERE record.purpose_id=p_id));
IF exit_loop THEN
CLOSE cur;
LEAVE average_loop;
END IF;
END LOOP average_loop;
INSERT INTO test1.general_average (id, average)
VALUES (0,
(select avg(amount) from record where purpose_type='CUSTOM'));
SELECT * FROM general_average;
END
Several patterns to change...
Try to avoid CURSORs; usually the entire operation can be done with a single SQL statement. That will be much faster.
INSERT ... VALUES (0, ( SELECT ... ) ) --> INSERT ... SELECT 0, ...
Don't use a TEMP table when you can simply deliver the results. In your case, you may need a UNION ALL to deliver the two chunks at once.