CALL in stored procedure doesn't work - mysql

I have next data set in table rel_user_article
+---------+------------+
| user_id | article_id |
+---------+------------+
| 1 | -1 |
| 97 | 153 |
+---------+------------+
This table implies next logic: each author must have at least 1 article and each article must have at least 1 author. When author hasn't articles, than table must have fake relation:(uid, -1)
When author adds his first article than this fake relation must be deleted.
I have stored procedures for creating new relation and deleting fake ones.
Deleting fake relations looks like this:
CREATE PROCEDURE `rel_delete_fake`(IN `ids` TEXT, IN `tbl` VARCHAR(255),
IN `fake_f` VARCHAR(255), IN `real_f` VARCHAR(255))
MODIFIES SQL DATA
proc: begin
if (`ids` = '') then
leave proc;
end if;
set #s = concat('DELETE FROM ', `tbl`, ' WHERE (', `fake_f`,
' = "-1" AND ', `real_f`, ' IN (', `ids`, '))');
prepare qr from #s;
execute qr;
deallocate prepare qr;
end proc
Creating "real" relations looks like this:
CREATE DEFINER=`root`#`localhost` PROCEDURE `rel_create`(IN `ids1` TEXT,
IN `ids2` TEXT, IN `tbl` VARCHAR(255), IN `field1` VARCHAR(255),
IN `field2` VARCHAR(255))
MODIFIES SQL DATA
proc: begin
set #cur = 0;
set #id1_cur = 0;
set #id2_cur = 0;
set #id1_cur_old = NULL;
set #id2_cur_old = NULL;
if (`ids1` = '' or `ids2` = '') then
leave proc;
end if;
set #sql_str = concat('insert into ', `tbl`, ' (', `field1`, ', ', `field2`, ") values (?, ?)");
prepare qr from #sql_str;
loop1: loop
set #cur = #cur + 1;
set #id1_cur = substring_index(substring_index(`ids1`, ',', #cur), ',', -1);
set #id2_cur = substring_index(substring_index(`ids2`, ',', #cur), ',', -1);
if (#id1_cur = #id1_cur_old and #id2_cur = #id2_cur_old) then
leave proc;
end if;
execute qr using #id1_cur, #id2_cur;
set #id1_cur_old = #id1_cur;
set #id2_cur_old = #id2_cur;
end loop loop1;
deallocate prepare qr;
-- deleting fake records that became needless
-- even this doesn't work
call `rel_delete_fake`('1','rel_user_article','article_id','user_id');
leave proc;
-- deleting fake records that became needless
call `rel_delete_fake`(`ids1`, `tbl`, `field2`, `field1`);
call `rel_delete_fake`(`ids2`, `tbl`, `field1`, `field2`);
end proc
Procedure for deleting fake relations works. Procedure for creating relations only creates new record but don't even call rel_delete_fake procedure.
For test I'm issuing next call:
call `rel_create`('1','153','rel_user_article','user_id','article_id');
and have in result:
+---------+------------+
| user_id | article_id |
+---------+------------+
| 1 | -1 |
| 1 | 153 |
| 97 | 153 |
+---------+------------+
Why this (1, -1) not deleted?

I found mistake. There must be "leave loop1;" in place of "leave proc;" in loop.

Related

Concatenating tablenames and columns in joined SQL query

How can i use the content of switches.address as a table-name, and switches.pin as a column-name to perform some sort of joined query on "switches" that that gives me (in the below case) the value of PIN3 from 0x68?
something like
SELECT name, state FROM switches
with 0x68.PIN3 as state
CREATE TABLE switches (
name varchar(20), address varchar(20), pin varchar(20));
INSERT INTO switches VALUES
("Lights_Kitchen", "0x68", "PIN3");
| name | address | pin |
+----------------+---------+------+
| Lights_Kitchen | 0x68 | PIN3 |
CREATE TABLE `0x68` (
PIN1 INT, PIN2 INT, PIN3 INT, PIN4 INT);
INSERT INTO `0x68` VALUES
(0,0,1,0)
| PIN1 | PIN2 | PIN3 | PIN4 |
+------+------+------+------+
| 0 | 0 | 1 | 0 |
and so on..
I'm not entirely sure about your condition but you can try PREPARED STATEMENTS. Perhaps something like this:
SET #tbl = NULL;
SET #col = NULL;
SET #sql = NULL;
SELECT address INTO #tbl FROM switches WHERE address='0x68';
SELECT pin INTO #col FROM switches WHERE address='0x68';
SELECT #tbl, #col;
SET #sql := CONCAT('SELECT ',#col,' FROM `',#tbl,'`;');
SELECT #sql;
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE stmt;
There are three variables #tbl, #col & #sql here and each are quite self-explanatory. The processes are:
Set all variables with NULL value: just in case that the variables has been used and still holds the last value being set.
Assigning variables with table & column value that need to be used in the final query.
Setting #sql variable with a generated query concatenated with the table and column variables.
Prepare, execute and deallocate the statement.
P/S: Those SELECT #tbl, #col and SELECT #sql query in between are just for checking for what has been set in the variables and not required in the final query structure.
Demo fiddle

MYSQL concat char to create table name with select

I tried to use such code
select
price
from
(select concat("_",`id`) -- to set table name
as
dta
from
druk
where
date >= '2021-02-01' and date < '2021-03-01') d
If I put * instead price I get for example "_5438" - table name. One or more. In this way I can't get price.
I tried to use variables from examples I found, but some of them mysql do'es not recognize. What should I do to make proper, dynamic table name with concat?
You can use dynamic sql for this.
First you get the table nane into #dta and after that constructs the actual query which you run as prepare statements
But this onlöy works, if your forwst select only goves back 1 eow as result, that is why it is LIMIT 1
SELECT concat("_",`id`)
INTO
#dta
FROM
druk
WHERE
date >= '2021-02-01' AND `date` < '2021-03-01' LIMIT 1;
SET #sql := CONCAT('SELECT price FROM ',#dta );
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
Still this does look like a poor design. to have many tables with the same design
For multiple ids you need a stored procedure, it can look like this).
But i didn_'t test it
CREATE TABLE druk(`id` INT , `date` date)
INSERT INTO druk VALUES (1,now()),(2,now())
CREATE TABLE _1 (price DECIMAL(8,3))
INSERT INTO _1 VALUE (2.3),(4.6),(7.9)
CREATE TABLE _2 (price DECIMAL(8,3))
INSERT INTO _2 VALUE (0.3),(1.6),(3.9)
CREATE PROCEDURE createpricelist( IN _datefrom varchar(29),IN _dateto varchar(20)
)
BEGIN
DECLARE finished INTEGER DEFAULT 0;
DECLARE _id VARCHAR(29);
DEClARE curid
CURSOR FOR
SELECT concat("_",`id`) -- to set table name
FROM
druk
WHERE
`date` >= _datefrom AND `date` < _dateto;
-- declare NOT FOUND handler
DECLARE CONTINUE HANDLER
FOR NOT FOUND SET finished = 1;
SET #datefrom := _datefrom;
SET #dateto := _dateto;
DROP TEMPORARY TABLE IF EXISTS tableprice;
CREATE TEMPORARY TABLE tableprice (price DECIMAL(8,2));
OPEN curid;
getprice: LOOP
FETCH curid INTO _id;
IF finished = 1 THEN
LEAVE getprice;
END IF;
SET #od = _id;
SET #sql := CONCAT('INSERT INTO tableprice (price) select price from ',_id);
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END LOOP getprice;
CLOSE curid;
-- Diplay temporary table you can use it outside the procedure
SELECT * FROM tableprice;
END
✓
CALL createpricelist(NOW() -INTERVAL 1 DAY,NOW())
| price |
| ----: |
| 2.30 |
| 4.60 |
| 7.90 |
| 0.30 |
| 1.60 |
| 3.90 |
✓
db<>fiddle here

Modify nested parents procedure code to work with name instead of ID

I found a great treasure that I looked for for months, an SQL procedure that lists all the parent categories to a child category, in order to generate breadcrumbs or provide category search suggestions. But it needs the category ID to find it's parents, I want to modify it to use the category name instead, as I am making a search box that provide search suggestions to show the category and all it's parents.
Code from this link.
CREATE PROCEDURE `getAllParentCategories`( IN idCat int, IN intMaxDepth int)
BEGIN
DECLARE chrProcessed TEXT;
DECLARE quit INT DEFAULT 0;
DECLARE done INT DEFAULT 0;
DECLARE Level INT DEFAULT 0;
DECLARE idFetchedCategory INT;
DECLARE chrSameLevelParents TEXT;
DECLARE chrFullReturn TEXT;
DECLARE cur1 CURSOR FOR SELECT parent_id FROM sb_categories WHERE website_id IN (#param);
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
SET chrFullReturn = '';
SET #param = idCat;
set chrProcessed = concat('|',idCat, '|');
myloop:LOOP
IF quit = 1 THEN
leave myloop;
END IF;
OPEN cur1;
SET chrSameLevelParents = '';
FETCH cur1 INTO idFetchedCategory;
while(not done) do
SET Level = Level + 1;
IF idFetchedCategory > 0 THEN
if NOT INSTR(chrProcessed,concat('|',idFetchedCategory, '|')) > 0 THEN
if CHAR_LENGTH(chrSameLevelParents) > 0 then
set chrSameLevelParents = concat( idFetchedCategory, ',', chrSameLevelParents );
else
set chrSameLevelParents = idFetchedCategory;
end if;
set chrProcessed = concat('|',idFetchedCategory, '|', chrProcessed );
end if;
END IF;
FETCH cur1 INTO idFetchedCategory;
end while;
CLOSE cur1;
IF Level > intMaxDepth THEN SET done =1; SET quit = 1; END IF;
if CHAR_LENGTH(chrSameLevelParents) > 0 THEN
if CHAR_LENGTH(chrFullReturn) > 0 THEN
set chrFullReturn = concat( chrFullReturn, ',', chrSameLevelParents );
ELSE
set chrFullReturn = chrSameLevelParents;
END IF;
SET #param = chrSameLevelParents;
SET chrSameLevelParents = '';
SET done = 0;
ELSE
SET quit = 1;
END IF;
END LOOP;
SET #strQuery = concat('SELECT website_id, name FROM sb_categories WHERE website_id IN (',chrFullReturn,')'); PREPARE stmt1 FROM #strQuery;
EXECUTE stmt1;
DEALLOCATE PREPARE stmt1;
END
My table structure is as simple as this:
+------------+-------------+-----------+
| website_id | name | parent_id |
+------------+-------------+-----------+
| 1 | Electronics | 0 |
+------------+-------------+-----------+
| 2 | Computers | 1 |
+------------+-------------+-----------+
| 3 | Asus | 2 |
+------------+-------------+-----------+
| 4 | Food | 0 |
+------------+-------------+-----------+
| 5 | Chicken | 4 |
+------------+-------------+-----------+
I am expecting when users search for "asus" that I get a table result showing "3-Asus, 2-Computers, 1-Electronics" in order to show in dropdown like "Electronics -> Computers -> Asus".
For now it works as expected if I use: call getAllParentCategories(3, 10) and I am hoping to get it to work like call getAllParentCategories('asus', 10), but my SQL knowledge didn't help me.
Thank you for help.
You want to change the procedure so it accepts a category name instead of category id as first argument. The output of your procedure should remain unchanged.
One solution would be to add an extra step in the query that initializes the idCat variable from the nameCat argument :
CREATE PROCEDURE `getAllParentCategories`( IN nameCat VARCHAR(255), IN intMaxDepth int)
BEGIN
...
SET chrFullReturn = '';
SELECT #param := website_id FROM sb_categories WHERE name = nameCat;
set chrProcessed = concat('|',#param, '|');
...
The rest of your code should remain unchanged.
Beware that this will work properly only as long as the category name is unique ... You would probably need to create a unique constraint on this column :
ALTER TABLE sb_categories ADD CONSTRAINT UC_name UNIQUE (name);

MySQL split multivalued strings from same table in different column into new table

I want to split multi valued strings which are from one table into a new table consisting of a primary key and the split strings result.
Example strings:
table1.field1 (primary key) = 100 , table1.field2 = 'abc,def,ghi'
In the new table (table2), the result should be like this:
**column1** **column2**
**row1** 100 'abc'
**row2** 100 'def'
**row3** 100 'ghi'
**row4** etc etc
I know how to split table1.field2, but since the data was so large, I need to insert the result automatically into table2. If I do it manually, it will take so long. Could anyone help me?
Here's a solution using a prepared statement:
DROP TABLE IF EXISTS concatenatedVals;
CREATE TABLE concatenatedVals(`key` INTEGER UNSIGNED, concatenatedValue CHAR(255));
DROP TABLE IF EXISTS splitVals;
CREATE TABLE splitVals(`key` INTEGER UNSIGNED, splitValue CHAR(255));
INSERT INTO concatenatedVals VALUES (100, 'abc,def,ghi'), (200, 'jkl,mno,pqr');
SELECT * FROM concatenatedVals;
SET #VKey := '';
SET #VExec := (SELECT CONCAT('INSERT INTO splitVals VALUES', TRIM(TRAILING ',' FROM GROUP_CONCAT(CONCAT('(', #VKey:= `key`, ', \'', REPLACE(concatenatedValue, ',', CONCAT('\'), (', #VKey, ', \'')), '\'),') SEPARATOR '')), ';') FROM concatenatedVals);
PREPARE stmt FROM #VExec;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SELECT * FROM splitVals;
Outputs:
SELECT * FROM splitVals;
+------+------------+
| key | splitValue |
+------+------------+
| 100 | abc |
| 100 | def |
| 100 | ghi |
| 200 | jkl |
| 200 | mno |
| 200 | pqr |
+------+------------+
6 rows in set (0.00 sec)
Let me know if you have any questions.
Regarding the question, how can I account for scenarios where the number of rows in my source table means the prepared statement exceeds the max-concat length, see the following example. As this uses a WHILE loop it must be inside a stored procedure. This could be adapted to allow table names and column names as arguments using further CONCATAND prepared statements to build up and execute commands dynamically. For now however, please change the table and column names from those in my example to match your data and it should work fine.
DROP TABLE IF EXISTS concatenatedVals;
CREATE TABLE concatenatedVals(`key` INTEGER UNSIGNED, concatenatedValue CHAR(255));
DROP TABLE IF EXISTS splitVals;
CREATE TABLE splitVals(`key` INTEGER UNSIGNED, splitValue CHAR(255));
INSERT INTO concatenatedVals VALUES (100, 'abc,def,ghi'), (200, 'jkl,mno,pqr'),(300, 'abc,def,ghi'), (400, 'jkl,mno,pqr'),(500, 'abc,def,ghi'), (600, 'jkl,mno,pqr'),(700, 'abc,def,ghi'), (800, 'jkl,mno,pqr'),(900, 'abc,def,ghi'), (1000, 'jkl,mno,pqr');
SELECT * FROM concatenatedVals;
DELIMITER $
DROP PROCEDURE IF EXISTS loopStringSplit$
CREATE PROCEDURE loopStringSplit()
BEGIN
DECLARE VKeyMaxLength, VConcatValMaxLength, VFixedCommandLength, VVariableCommandLength, VSelectLimit, VRowsToProcess, VRowsProcessed INT;
SET VFixedCommandLength = CHAR_LENGTH(CONCAT('INSERT INTO splitVals VALUES;'));
SET VKeyMaxLength = (SELECT MAX(CHAR_LENGTH(`key`)) FROM concatenatedVals);
SET VConcatValMaxLength = (SELECT MAX(CHAR_LENGTH(concatenatedValue)) FROM concatenatedVals);
SET VVariableCommandLength = CHAR_LENGTH('(,\'\')');
SET VSelectLimit = FLOOR((##group_concat_max_len - VFixedCommandLength) / (VKeyMaxLength + VConcatValMaxLength + VVariableCommandLength));
SET VRowsToProcess := (SELECT COUNT(*) FROM concatenatedVals);
SET VRowsProcessed = 0;
SELECT VRowsProcessed, VRowsToProcess, VSelectLimit;
WHILE VRowsProcessed < VRowsToProcess DO
SET #VKey := '';
SET #VExec := (SELECT CONCAT('INSERT INTO splitVals VALUES', TRIM(TRAILING ',' FROM GROUP_CONCAT(CONCAT('(', #VKey:= `key`, ', \'', REPLACE(concatenatedValue, ',', CONCAT('\'), (', #VKey, ', \'')), '\'),') SEPARATOR '')), ';') FROM (SELECT * FROM concatenatedVals LIMIT VRowsProcessed, VSelectLimit) A);
SELECT #VExec;
PREPARE stmt FROM #VExec;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET VRowsProcessed = VRowsProcessed + VSelectLimit;
SELECT CONCAT('Processed rows: ', VRowsProcessed);
END WHILE;
END$
DELIMITER ;
CALL loopStringSplit();
SELECT * FROM splitVals;
Regards,
James

mysql stored procedure that calls itself recursively

I have the following table:
id | parent_id | quantity
-------------------------
1 | null | 5
2 | null | 3
3 | 2 | 10
4 | 2 | 15
5 | 3 | 2
6 | 5 | 4
7 | 1 | 9
Now I need a stored procedure in mysql that calls itself recursively and returns the computed quantity.
For example the id 6 has 5 as a parent which as 3 as a parent which has 2 as a parent.
So I need to compute 4 * 2 * 10 * 3 ( = 240) as a result.
I am fairly new to stored procedures and I won't use them very often in the future because I prefer having my business logic in my program code rather then in the database. But in this case I can't avoid it.
Maybe a mysql guru (that's you) can hack together a working statement in a couple of seconds.
its work only in mysql version >= 5
the stored procedure declaration is this,
you can give it little improve , but this working :
DELIMITER $$
CREATE PROCEDURE calctotal(
IN number INT,
OUT total INT
)
BEGIN
DECLARE parent_ID INT DEFAULT NULL ;
DECLARE tmptotal INT DEFAULT 0;
DECLARE tmptotal2 INT DEFAULT 0;
SELECT parentid FROM test WHERE id = number INTO parent_ID;
SELECT quantity FROM test WHERE id = number INTO tmptotal;
IF parent_ID IS NULL
THEN
SET total = tmptotal;
ELSE
CALL calctotal(parent_ID, tmptotal2);
SET total = tmptotal2 * tmptotal;
END IF;
END$$
DELIMITER ;
the calling is like
(its important to set this variable) :
SET ##GLOBAL.max_sp_recursion_depth = 255;
SET ##session.max_sp_recursion_depth = 255;
CALL calctotal(6, #total);
SELECT #total;
Take a look at Managing Hierarchical Data in MySQL by Mike Hillyer.
It contains fully worked examples on dealing with hierarchical data.
How about avoiding procedures:
SELECT quantity from (
SELECT #rq:=parent_id as id, #val:=#val*quantity as quantity from (
select * from testTable order by -id limit 1000000 # 'limit' is required for MariaDB if we want to sort rows in subquery
) t # we have to inverse ids first in order to get this working...
join
( select #rq:= 6 /* example query */, #val:= 1 /* we are going to multiply values */) tmp
where id=#rq
) c where id is null;
Check out Fiddle!
Note! this will not work if row's parent_id>id.
Cheers!
DELIMITER $$
CREATE DEFINER=`arun`#`%` PROCEDURE `recursivesubtree`( in iroot int(100) , in ilevel int(110) , in locid int(101) )
BEGIN
DECLARE irows,ichildid,iparentid,ichildcount,done INT DEFAULT 0;
DECLARE cname VARCHAR(64);
SET irows = ( SELECT COUNT(*) FROM account WHERE parent_id=iroot and location_id=locid );
IF ilevel = 0 THEN
DROP TEMPORARY TABLE IF EXISTS _descendants;
CREATE TEMPORARY TABLE _descendants (
childID INT, parentID INT, name VARCHAR(64), childcount INT, level INT
);
END IF;
IF irows > 0 THEN
BEGIN
DECLARE cur CURSOR FOR
SELECT
f.account_id,f.parent_id,f.account_name,
(SELECT COUNT(*) FROM account WHERE parent_id=t.account_id and location_id=locid ) AS childcount
FROM account t JOIN account f ON t.account_id=f.account_id
WHERE t.parent_id=iroot and t.location_id=locid
ORDER BY childcount<>0,t.account_id;
DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = 1;
OPEN cur;
WHILE NOT done DO
FETCH cur INTO ichildid,iparentid,cname,ichildcount;
IF NOT done THEN
INSERT INTO _descendants VALUES(ichildid,iparentid,cname,ichildcount,ilevel );
IF ichildcount > 0 THEN
CALL recursivesubtree( ichildid, ilevel + 1 );
END IF;
END IF;
END WHILE;
CLOSE cur;
END;
END IF;
IF ilevel = 0 THEN
-- Show result table headed by name that corresponds to iroot:
SET cname = (SELECT account_name FROM account WHERE account_id=iroot and location_id=locid );
SET #sql = CONCAT('SELECT CONCAT(REPEAT(CHAR(36),2*level),IF(childcount,UPPER(name),name))',
' AS ', CHAR(39),cname,CHAR(39),' FROM _descendants');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DROP PREPARE stmt;
END IF;
END$$
DELIMITER ;