How to properly loop in a stored function on MySQL? - mysql

I am having some difficulty getting a pretty simple stored procedure right.
Consider the following article table snippet:
id replaced_by baseID
1 2 0
2 3 0
3 0 0
A simple hierarchical table, using copy-on-write. When an article is edited, the replaced_by field of the current article is set to the id of it's new copy.
I've added a baseID field, which in the future should store the baseID of an article.
In my example above, there is one article (eg id 3). It's baseID would be 1.
To get the baseID, I have created the following stored procedure:
DELIMITER $$
CREATE FUNCTION getBaseID(articleID INT) RETURNS INT
BEGIN
DECLARE x INT;
DECLARE y INT;
SET x = articleID;
sloop:LOOP
SELECT id INTO y FROM article WHERE replaced_by_articleID = x;
IF y IS NOT NULL THEN
SET x = y;
ITERATE sloop;
ELSE
LEAVE sloop;
END IF;
END LOOP;
RETURN x;
END $$
DELIMITER ;
It seems simple enough, until I actually call the function using:
SELECT getBaseID(3);
I would expect, the function to return 1. I'm even willing to understand it can take a slice of a second.
Instead, the machine's CPU goes up to 100% (mysqld).
I have even rewritten the same function using REPEAT .. UNTIL and with WHILE .. DO, with the same end result.
Can anyone explain why my CPU goes up 100% when it enters the loop?
Side note: I am trying to simply win time. I have created the exact same function in PHP, which performs okay, but our guess is that MySQL can do it slightly faster. We need to sift through about 18 million records. Any bit of time I can save is going to be worth it.
Thanks in advance for any assistance and/or pointers.
Solved SQL:
DELIMITER $$
CREATE FUNCTION getBaseID(articleID INT) RETURNS INT
BEGIN
DECLARE x INT;
DECLARE y INT;
SET x = articleID;
sloop:LOOP
SET y = NULL;
SELECT id INTO y FROM article WHERE replaced_by_articleID = x;
IF y IS NULL THEN
LEAVE sloop;
END IF;
SET x = y;
ITERATE sloop;
END LOOP;
RETURN x;
END $$
DELIMITER ;

From mysql :
If the query returns no rows, a warning with error code 1329 occurs (No data), and the variable values remain unchanged
So you have an infinite loop when no records found with a given x (y remains unchanged)
Try SET y = (SELECT id ....) instead or add SET y = null before your select statement (it should be the first statement in the loop)

It feels like you may be missing an index on the replaced_by column. If there no index on replaced_by, you are looking at a full table scan with every iteration.
ALTER TABLE article ADD INDEX (replaced_by);
You should also make sure the row exists before retrieving
DELIMITER $$
CREATE FUNCTION getBaseID(articleID INT) RETURNS INT
BEGIN
DECLARE x INT;
DECLARE y INT;
SET x = articleID;
sloop:LOOP
SELECT COUNT(1) INTO y FROM article WHERE replaced_by = x;
IF y > 0 THEN
SELECT id INTO y FROM article WHERE replaced_by = x;
SET x = y;
ELSE
LEAVE sloop;
END IF;
END LOOP;
RETURN x;
END $$
DELIMITER ;
Twice as many SQL calls but better safe than sorry.
Give it a Try !!!

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.

MYSQL - table not updating from Procedure

I want to get distance between two GeoPoints (using LatLong) for that I wrote GETDISTANCE function from solution provided [MySQL Function to calculate distance between two latitudes and longitudes. If I call function independently it works like charm.
As per my understanding I cannot return ResultSet from Function in MySQL so I created Procedure and called function inside procedure As follows:
DELIMITER $$
CREATE PROCEDURE GetNearByGeoPoints(IN Lat REAL, IN Longi REAL)
BEGIN
DECLARE v_max int;
DECLARE v_counter int unsigned default 0;
SET #v_max = (SELECT COUNT(*) FROM TransmitterPointsData);
START TRANSACTION;
WHILE v_counter < v_max
DO
SELECT #coverageID :=CoverageID, #tableLatitude := Latitude, #tableLongitude :=Longitude FROM TransmitterPointsData LIMIT v_counter,1;
SET #Dist= GETDISTANCE(Lat, Longi, tableLatitude, tableLongitude);
UPDATE TransmitterPointsData SET DynamicDistance = #Dist WHERE CoverageID= #coverageID;
set v_counter=v_counter+1;
END WHILE;
COMMIT;
SELECT * FROM TransmitterPointsData;
END $$
DELIMITER ;
What I am trying to do is taking a set of LatLong parameters from user and comparing it with each set of LatLong from table. And after getting output from function I am updating TransmitterPointsData table with where condition on coverageID.
This is my first MySQL query so far I was following syntax but I do not know why I am getting all null values in DynammicDistance Column.
Thank You in Advance
Try replacing the while loop with this:
UPDATE TransmitterPointsData
SET DynamicDistance = GETDISTANCE(Lat, Longi, Latitude, Longitude)
Much shorter, and you avoid potential issues with row selection via limit + offset (which is poor style at best, and gives you a random row each time at worse).

phpMyAdmin - mariaDB roman numerals function

can anybody help me with my sorting function - seriously I don't know how can I make it work as supposed to. :( Database is in MariaDB in Xampp. I use phpMyAdmin to execute the query.
DELIMITER $$
DROP FUNCTION IF EXISTS convRomanNumeral$$
CREATE FUNCTION convRomanNumeral (numeral CHAR(4))
RETURNS INT
BEGIN
DECLARE intnum INT;
CASE numeral
WHEN "I" THEN intnum = 1;
WHEN "II" THEN intnum = 2;
END CASE;
RETURN intnum;
END;
$$
SET #iteration = -1;
UPDATE `st0gk_docman_documents`
SET created_on = DATE('2016-06-14') + INTERVAL(#iteration := #iteration + 1) SECOND
WHERE `docman_category_id` = 141 ORDER BY convRomanNumeral(SUBSTRING(SUBSTRING_INDEX(title,'/',1),' ',-2) ASC, SUBSTRING_INDEX(title,'/',-2)+0 ASC;
So what I want to achieve is to sort documents by title. Example titles are:
Document Nr I/36/2006
Document Nr II/36/2006
Document Nr I/32/2006
Document Nr II/19/2006
After sorting them by first Roman number and then by second Arabic number I want to update the date. Code below for updating by only second Arabic number works properly:
SET #iteration = -1;
UPDATE `st0gk_docman_documents`
SET created_on = DATE('2016-06-14') + INTERVAL(#iteration := #iteration + 1) SECOND
WHERE `docman_category_id` = 141 ORDER BY SUBSTRING_INDEX(title,'/',-2)+0 ASC;
I would like to use CASE to return proper variable for Roman values. I know it's not perfect but I can't even make the CASE and FUNCTION work. What I am doing wrong? All suggestions are welcome.
The best way to do this is to add another column that has a sortable equivalent of that string. And use non-SQL code to do the parsing and building of that column before inserting into the table.
First mistake that I was making it was trying to execute the whole query at once... After taking the first lodge out of the way the debugging seemed way simpler. :D
So I created my case function to convert Roman numerals:
DELIMITER $$
DROP FUNCTION IF EXISTS convRomanNumeralSubFunction$$
CREATE FUNCTION convRomanNumeralSubFunction (numeral CHAR(1))
RETURNS INT
BEGIN
DECLARE intnum INT;
CASE numeral
WHEN "I" THEN SELECT 1 INTO intnum;
WHEN "X" THEN SELECT 10 INTO intnum;
WHEN "C" THEN SELECT 100 INTO intnum;
WHEN "M" THEN SELECT 1000 INTO intnum;
WHEN "V" THEN SELECT 5 INTO intnum;
WHEN "L" THEN SELECT 50 INTO intnum;
WHEN "D" THEN SELECT 500 INTO intnum;
END CASE;
RETURN intnum;
END;
$$
After that I declared the second function needed for conversion. I don't know if You can declare function inside function... and I didn't want to waste more time on this. For sure You can declare Function inside Procedure. Anyhow. WARNING: This function is not proof of BAD numerals like IIX. Numerals like that or will be badly counted. Also AXI will not count.
DELIMITER $$
DROP FUNCTION IF EXISTS convRomanNumeral$$
CREATE FUNCTION convRomanNumeral (numeral CHAR(10))
RETURNS INT
BEGIN
DECLARE currentintnum, previntnum, intnum, counter, numerallength INT;
SET numerallength = LENGTH(numeral);
SET counter = numerallength;
SET intnum = 0;
SET previntnum = 0;
WHILE counter > 0 DO
SET currentintnum = CAST(convRomanNumeralSubFunction(SUBSTRING(numeral,counter, 1)) as integer);
IF currentintnum < previntnum THEN
SET intnum = intnum - currentintnum;
ELSE
SET intnum = intnum + currentintnum;
END IF;
SET previntnum = currentintnum;
SET counter = counter - 1;
END WHILE;
RETURN intnum;
END;
$$
So that's it. Now You can convert all kind of Roman numerals and sort them up.
Use this to test the conversion:
SELECT convRomanNumeral("XIX");
This is example sorting code that I in the end used:
SET #iteration = -1;
UPDATE `st0gk_docman_documents`
SET created_on = DATE('2016-06-07') + INTERVAL(#iteration := #iteration + 1) SECOND
WHERE `docman_category_id` = 67 ORDER BY convRomanNumeralBreak(SUBSTRING_INDEX(SUBSTRING_INDEX(title,'/',1),' ',-1)) ASC, SUBSTRING_INDEX(title,'/',-2)+0 ASC;
Also one more thing - if You'll try to excecute this on mySQL then You have to fix this line:
SET currentintnum = CAST(convRomanNumeralSubFunction(SUBSTRING(numeral,counter, 1)) as integer);
into this:
SET currentintnum = CAST(convRomanNumeralSubFunction(SUBSTRING(numeral,counter, 1)) as SIGNED);
This code could be improved but as the #Rick James stated this should be done differently - not in as db update but in different table structure and sorting mechanism.

#1305 - FUNCTION does not exist - nested loops

So, I'm getting an error about a function not being defined. It happens every time I try to use my counter variables to refer to specific entries in tables. I don't get it.
To be more clear, I was advised that in order to use loops with mysql I had to make a 'procedure' which I have done. the count and ingredientcount variables are references to the row being examined in the tables tDrinks and tUniqueingredients.
I am trying to generate a foreign key reference for the drink id from tDrinks in the table tDrinkMix. I want there to be an entry of the drink id for each instance of a unique ingredient in the drink. There are 16.5k drinks and 2.2k unique ingredients.
Right now it dies on SELECT id(count) FROM tDrinks. If I remove the (count) there it dies next on WHERE d_shopping(count).
The error thrown is #1305 and it says that the function DrinksDB.id is not defined
DROP PROCEDURE `test`//
CREATE DEFINER=`root`#`localhost` PROCEDURE `test`()
BEGIN
DECLARE count INT DEFAULT 0;
DECLARE ingredientcount INT DEFAULT 0;
WHILE count < 16532 DO
WHILE ingredientcount < 2202 DO
INSERT INTO tDrinkMix(count)
SELECT id(count) FROM tDrinks
WHERE d_shopping(count)
LIKE CONCAT('%',tUniqueingredients.ingredient(ingredientcount),'%');
SET ingredientcount = ingredientcount + 1;
END WHILE;
SET count = count + 1;
END WHILE;
END
So I'm working on refining this a bit, and I'm still not quite there. How can you tell this is my first database project? The following is getting closer I think: the procedure at least saves and looks like it might execute
delimiter //
CREATE DEFINER=`root`#`localhost` PROCEDURE `test`()
BEGIN
DECLARE count INT DEFAULT 0;
DECLARE ingredientcount INT DEFAULT 0;
WHILE count < 16532 DO
WHILE ingredientcount < 2202 DO
INSERT INTO tDrinkMix(drink_id)
SELECT id
FROM tDrinks
WHERE id = count
and
d_shopping
LIKE
(SELECT CONCAT (ingredient,'%') FROM tUniqueingredients WHERE id = ingredientcount);
SET ingredientcount = ingredientcount + 1;
END WHILE;
SET count = count + 1;
END WHILE;
END//
I believe the error is occurring because the parser is interpreting id(count) as a function. It's looking for a function in your database which I guess is DrinksDB. When you remove the (count) from id its moving to the next syntax error of d_shopping(count) and looks for a function with the name d_shopping in your database.
I appreciate I'm late in coming to this (I'm just scanning open issues) so it's probably not an issue any more. If its still a problem post a comment.

How can i use the CURSOR by using loop to set the local variables in making a function in mysql database

I am using mysql database. In that I have created a function .In that function I am setting a local variable. I am using a CURSOR in this as I have to fetch the records from the table.
Based on the fetched records the function is returning the real datatype from the if condition there.
Please explain me the steps by steps to how to create a for loop in cursor in mysql function?
My code is like :-
DELIMITER $$;
CREATE FUNCTION score_exam (e integer)
RETURNS real
BEGIN
Declare t float ;
Declare c float;
Declare d int;
Declare e int;
Declare f int;
Declare g int;
Declare h int;
DECLARE done INT DEFAULT FALSE;
DECLARE r CURSOR FOR SELECT num, quen_n, cha_nu, sele_anr, cor_ans FROM equestion WHERE exm_nm = e;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
set t=0.0;
set c=0.0;
OPEN r;
read_loop: LOOP
FETCH r INTO d,e,f,g,h;
IF done THEN
LEAVE read_loop;
END IF;
set t = t + 1.0;
IF g = h THEN
set c = c + 1.0;
END IF;
END LOOP;
IF t > 0.0 THEN
RETURN c/t;
ELSE
RETURN 0.0;
END IF;
CLOSE r;
END $$
If anyone would help me out, I will be greatful to him.
It looks like all you are trying to do is get a score based on an e-Question(Exam). Looking for total questions for the test and how many correct answers... If no questions offered for an exam, just return 0 to prevent divide by zero error. That said, it looks like you are going through way too much hoops to write such a complex function which can be done in a single query.. Also, you have an incoming parameter of "e" for the exam to be computed, yet do a fetch of "quen_n" into e which will screw up your fetching won't it? I would rename the parameter just for clarification. Your query of extra elements doesn't even appear to be used, and interpreting as "num" is a row number or pk id in the table, Quen_n is a question number from the exam, etc... The only thing you care about is how many questions were there and how many correct... So, here's the query I would write and just get the answer.
SELECT
count(*) as TotalQuestions,
sum( if( sele_anr = cor_ans, 1, 0 )) as CorrectAnswers
FROM
equestion
WHERE
exm_nm = ExamToScore
Have this as your fetch, and return from that. If no record returned from the result, return 0... if one record returned, just do the division and return that.