SQL finding ancestors/decendants in a self-referencing table - mysql

I have a table which references itself, like this:
CREATE TABLE Foo (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
parent INT NULL,
name VARCHAR (30) NOT NULL,
FOREIGN KEY (parent) REFERENCES Foo(id) ON DELETE CASCADE);
Sample data:
id parent name
1 NULL a
2 NULL b
3 1 a1
4 1 a2
5 3 a1x
6 3 a2x
I want to write queries which will list the ancestors and decenders of a given row, e.g.
CALL find_ancestors('a1x')
Will return
id name
3 a1
1 a
and
CALL find_descendants('a')
Will return
id name
3 a1
5 a1x
How can I write these stored procedures for MySQL 5? Thanks
Bonus question for bounty: also select the distance of the returned row from the source and pass a maximum-distance parameter to the procedure, e.g.
CALL find_ancestors('a1x')
Will return
id name distance
3 a1 1
1 a 2
and
CALL find_ancestors_bounded('a1x',1)
Will return
id name distance
3 a1 1

Lets say we have a table with four elements, id, item, class and parent_id. We want to have the complete Ancestors of any given Item, what we need to do is a custom mysql function that will actually loop through every record looking for a match for our item parent_id, once it founds a match, if the matched item has a parent_id, it will start looping again, and so forth. Every time our function finds a match, it will store it in a comma separated string that will be returned in the end (ex: 1,2,3,4)
Our function would look something like this:
DELIMITER $$
DROP FUNCTION IF EXISTS `junk`.`GetAncestry` $$
CREATE FUNCTION `junk`.`GetAncestry` (GivenID INT) RETURNS VARCHAR(1024)
DETERMINISTIC
BEGIN
DECLARE rv VARCHAR(1024);
DECLARE cm CHAR(1);
DECLARE ch INT;
SET rv = '';
SET cm = '';
SET ch = GivenID;
WHILE ch > 0 DO
SELECT IFNULL(parent_id,-1) INTO ch FROM
(SELECT parent_id FROM pctable WHERE id = ch) A;
IF ch > 0 THEN
SET rv = CONCAT(rv,cm,ch);
SET cm = ',';
END IF;
END WHILE;
RETURN rv;
END $$
DELIMITER ;
This code is authored by RolandoMySQLDBA

Related

Writing stored procedure which flags duplicate values in a comma separated field in MySQL

I have a database table like this sample:
ID THINGS HAS_DUPLICATES
1 AAA, BBB, AAA NULL
2 CCC, DDD NULL
I am trying to write a stored procedure to flag duplicate values in THINGS field.
After calling the procedure the table will become like this:
ID THINGS HAS_DUPLICATES
1 AAA, BBB, AAA YES
2 CCC, DDD NO
Please be informed that I am trying to resolve it using only SQL and without normalizing my database. I am also aware of other approaches like writing PHP code.
Schema:
DROP TABLE IF EXISTS evilThings; -- orig table with dupes
CREATE TABLE evilThings
( ID INT AUTO_INCREMENT PRIMARY KEY,
THINGS TEXT NOT NULL,
HAS_DUPLICATES INT NULL
);
INSERT evilThings(ID,THINGS) VALUES
(1,"'AAA, BBB, AAA'"),
(2,"'CCC, DDD'");
CREATE TABLE notEvilAssocTable
( ai INT AUTO_INCREMENT PRIMARY KEY, -- no shuffle on inserts
ID INT NOT NULL,
THING VARCHAR(100) NOT NULL,
UNIQUE KEY `unqK_id_thing` (ID,THING) -- no dupes, this is honorable
);
Stored Proc:
DROP PROCEDURE IF EXISTS splitEm;
DELIMITER $$
CREATE PROCEDURE splitEm()
BEGIN
DECLARE lv_ID,pos1,pos2,comma_pos INT;
DECLARE lv_THINGS TEXT;
DECLARE particle VARCHAR(100);
DECLARE strs_done INT DEFAULT FALSE; -- string search done
DECLARE done INT DEFAULT FALSE; -- cursor done
DECLARE cur111 CURSOR FOR SELECT ID,THINGS FROM evilThings ORDER BY ID;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
-- Please note in the above, CURSOR stuff MUST come LAST else "Error 1337: Variable or condition decl aft curs"
-- -------------------------------------------------------------------------------------------------------------------
TRUNCATE TABLE notEvilAssocTable;
OPEN cur111;
read_loop: LOOP
SET strs_done=FALSE;
FETCH cur111 INTO lv_ID,lv_THINGS;
IF done THEN
LEAVE read_loop;
END IF;
SET pos1=1,comma_pos=0;
WHILE !strs_done DO
SET pos2=LOCATE(',', lv_THINGS, comma_pos+1);
IF pos2=0 THEN
SET pos2=LOCATE("'", lv_THINGS, comma_pos+1);
IF pos2!=0 THEN
SET particle=SUBSTRING(lv_THINGS,comma_pos+1,pos2-comma_pos-1);
SET particle=REPLACE(particle,"'","");
SET particle=TRIM(particle);
INSERT IGNORE notEvilAssocTable (ID,THING) VALUES (lv_ID,particle);
END IF;
SET strs_done=1;
ELSE
SET particle=SUBSTRING(lv_THINGS,comma_pos+1,pos2-comma_pos-1);
SET particle=REPLACE(particle,"'","");
SET particle=TRIM(particle);
INSERT IGNORE notEvilAssocTable (ID,THING) VALUES (lv_ID,particle);
SET comma_pos=pos2;
END IF;
END WHILE;
END LOOP;
CLOSE cur111; -- close the cursor
END$$
DELIMITER ;
Test:
call splitEm();
See results of split:
select * from notEvilAssocTable;
Note that position 3, the InnoDB gap (from INSERT IGNORE). It is simply the innodb gap anomaly, an expected side effect like so many of InnoDB. In this case driven by the IGNORE part that creates a gap. No problem though. It forbids duplicates in our new table for split outs. It is common. It is there to protect you.
If you did not mean to have the single quote at the beginning and end of the string in the db, then change the routine accordingly.
Here is the answer to my question, assuming the data in THINGS field are separated by a bar '|'. Our original table will be myTABLE:
ID THINGS THINGSCount THINGSCountUnique HAS_DUPLICATES
1 AAA|BBB|AAA NULL NULL NULL
2 CCC|DDD NULL NULL NULL
Step 1. Check the maximum number of values separated by a bar '|' in THINGS field:
SELECT ROUND((CHAR_LENGTH(THINGS) - CHAR_LENGTH(REPLACE(THINGS,'|',''))) / CHAR_LENGTH('|')) + 1 FROM myTABLE;
Step 2. Assuming the answer from step 1 was 7, now use the following SQL to split the data in THINGS field into rows, there are many other approaches which you can Google to do the split:
CREATE TABLE myTABLE_temp
SELECT ID, SUBSTRING_INDEX(SUBSTRING_INDEX(myTABLE.THINGS, '|', n.n), '|', -1) THINGS
FROM myTABLE JOIN
( SELECT n FROM
( SELECT 1 AS N UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 ) a ) n
ON CHAR_LENGTH(THINGS) - CHAR_LENGTH(REPLACE(THINGS, '|', '')) >= n - 1
ORDER BY ID;
Our myTABLE_temp table will be something like:
ID THINGS
1 AAA
1 BBB
1 AAA
2 CCC
2 DDD
Step 3. Here we create two new tables to hold COUNT(THINGS) and COUNT(DISTINCT THINGS) as following:
# THINGSCount
CREATE TABLE myTABLE_temp_2
SELECT ID, COUNT(THINGS) AS THINGSCount FROM myTABLE_temp GROUP BY ID;
# Remember to ADD INDEX to ID field
UPDATE myTABLE A INNER JOIN myTABLE_temp_2 B ON(A.ID = B.ID) SET A.THINGSCount = B.THINGSCount;
# THINGSCountUnique
CREATE TABLE myTABLE_temp_3
SELECT ID, COUNT(THINGS) AS THINGSCountUnique FROM myTABLE_temp GROUP BY ID;
# Remember to ADD INDEX to ID field
UPDATE myTABLE A INNER JOIN myTABLE_temp_3 B ON(A.ID = B.ID) SET A.THINGSCountUnique = B.THINGSCountUnique;
Final Step: Flag duplicate values:
UPDATE myTABLE SET HAS_DUPLICATES = IF(THINGSCount>THINGSCountUnique, 'DUPLICATES', 'NO');

How do I use a recursive procedure in a loop in MySQL

I have an employees table with demographics data. This table has two fields I need: Employee ID and Manager ID. I need to build a join table that would look like this:
Employee IDManager ID
1 2
1 3
1 4
2 3
2 4
So I need the join table to show, for each employee, all the people above them. In case of employee 1 his direct manager is 2, his manager's manager is 3 and his manager's manager's manager is 4.
I have a MySQL procedure that seems to be working when called for an employee:
CREATE PROCEDURE chainReaction (IN employee_id int, IN orig_id int)
BEGIN
DECLARE manager_id int default NULL;
SET ##SESSION.max_sp_recursion_depth = 255;
SELECT ManagerID
INTO manager_id
FROM employees
WHERE EmployeeID = employee_id;
IF( manager_id is not null) THEN
INSERT INTO joinTable(EmpID, SupID) VALUES(
orig_id,manager_id
);
CALL chainReaction(manager_id, orig_id);
end if;
END;
Problem 1:
When calling chainReaction it looks like I need to provide the employee ID twice to the procedure because otherwise the original employee ID is 'lost' and the resulting inserts for employee 1 look like this:
Employee IDManager ID
1 2
2 3
4 4
So the Employee ID doesn't stay fixed to 1 as I needed it. Again, this procedure works when called as it is now: chainReaction(1,1);
But here's problem 2:
I have some code to loop through all rows in the employee table and call chainReaction on them:
CREATE PROCEDURE RowPerRow ()
BEGIN
DECLARE n int default 0;
DECLARE i int default 0;
DECLARE employee_id int default null;
SELECT COUNT(*) FROM employees INTO n;
SET i=0;
WHILE i<n DO
SELECT EmployeeID FROM employees WHERE LIMIT i,1 INTO employee_id;
CALL chainReaction(employee_id,employee_id);
END WHILE;
END;
The problem is that if I call this RowPerRow() procedure the employee column in the join table is stuck to the first employee ID no matter what row is being processed.
Looks like my procedures work just fine, what I was missing in the RowPerRow procedure is incrementing the counter (SET i = i + 1;). Here's the code fixed.
CREATE PROCEDURE RowPerRow ()
BEGIN
DECLARE n int default 0;
DECLARE i int default 0;
DECLARE employee_id int default null;
SELECT COUNT(*) FROM employees INTO n;
SET i=0;
WHILE i<n DO
SELECT EmployeeID FROM employees WHERE LIMIT i,1 INTO employee_id;
CALL chainReaction(employee_id,employee_id);
SET i = i + 1;
END WHILE;
END;

mysql how to update a column of all rows and for each row a different column value

Here is a sample table:
id name code
----------------
1 n1
2 n2
3 n3
I want to update the code column of every row with different values, so for row of id 1 i want to add this value for code 'zb6DXBfJ', and for row id 2 'NV6Nx4St', and for row id 3 this value for code column 'q23ZMACc'. So my final table should look like this:
id name code
----------------
1 n1 zb6DXBfJ
2 n2 NV6Nx4St
3 n3 q23ZMACc
UPDATE TableName
SET Code = CASE
WHEN id = 1 THEN 'zb6DXBfJ'
WHEN id = 2 THEN 'NV6Nx4St'
WHEN id = 3 THEN 'q23ZMACc'
END;
Try this
UPDATE Table_Name WHERE id = desired_id SET code = desired_code;
Of course, you'll need to substitute Table_Name, desired_id, and desired_code as required.
Depending on where your codes come from, you can try one of the folowing:
If your codes came from another table, you can create a procedure that will "match" each line of the two table in order to update the codes. Here's an example :
First create the tables (the one you already have, and the one with the codes)
CREATE TABLE table1 (
id INT(6) UNSIGNED PRIMARY KEY,
name VARCHAR(300) NOT NULL,
code VARCHAR(300));
CREATE TABLE table2 (
id INT(6) UNSIGNED PRIMARY KEY,
code VARCHAR(300) NOT NULL);
INSERT INTO table1 (id, name) VALUES
(1, 'n1'),
(2, 'n2'),
(3, 'n3');
INSERT INTO table2 (id, code) VALUES
(1, 'zb6DXBfJ'),
(2, 'NV6Nx4St'),
(3, 'q23ZMACc');
Then create the actual procedure
delimiter //
CREATE PROCEDURE assign_strings()
BEGIN
DECLARE _id INT DEFAULT 0;
DECLARE str VARCHAR(300);
DECLARE cur CURSOR FOR SELECT id FROM table1;
open cur;
myloop:LOOP
fetch cur into _id;
SELECT code INTO str FROM table2 WHERE id = _id;
UPDATE table1 SET code = str WHERE id = _id;
end loop myloop;
close cur;
END //
delimiter ;
You can now call the procedure
CALL assign_strings();
Note that I don't know your logic to retrieve these code. Here I just assume table2.id has the code for table1.id. Its a little dumb but your logic may be more complicated.
If your codes are just random strings (non-unique) you can just use a function instead of a procedure like this :
DELIMITER //
CREATE FUNCTION get_random_string()
RETURNS VARCHAR(300)
BEGIN
RETURN 'Your_random_string';
END//
DELIMITER ;
Note that you'll need to implement your own random string strategy. You can use something like a MD5 function with a random number and substrings... whatever you need.
You can now call this function directly in an update statement like so :
UPDATE table1 set code = get_random_string();
Hope it gets you started.

mysql internal join of all sons of a parent

I have a table structured as
Id int(10)
Parent int(10) --reference I'd
Name
This table contain organization structure. Example data
Id | parent | name
1 Null organization A
2 1 Office A
3 1 Office B
4 3 Room 1
5 3 Room 2
`
This example simple map that org. A have 2 offices,
Office B have 2 rooms .
Parent field hold either null (if no parent -end node-) or an Id of parent.
I want to fetch all rows of a parent row
I want my query to return all child's and children of children.
Can I do this with one query ?
Unless you have a fixed hierarchy depth, I can't think of how you could do it with one query in MySQL (could always be mistaken). If you have hierarchy depth of less than 255 you can do it with a single call to previously created recursive procedures like below.
Inner Procedure Inner
CREATE PROCEDURE `parent_child`(in `I_Parent` int)
BEGIN
DECLARE `V_done` INT;
DECLARE `V_Id` INT;
DECLARE `V_Parent` INT;
DECLARE `V_Name` VARCHAR(45);
DECLARE `cur1` CURSOR FOR SELECT * FROM `stackoverflow`.`parent_child` WHERE `Parent` = `I_Parent`;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET `V_done` = TRUE;
INSERT INTO `stackoverflow`.`parent_child_temp` SELECT `Id`, `Parent`, `Name` FROM `stackoverflow`.`parent_child` WHERE `Parent` IS NULL AND `Id` = `I_Parent`;
OPEN `cur1`;
read_loop: LOOP
FETCH `cur1` INTO `V_Id`, `V_Parent`, `V_Name` ;
IF `V_done` THEN
LEAVE read_loop;
END IF;
INSERT INTO `stackoverflow`.`parent_child_temp` SELECT `V_Id`, `V_Parent`, `V_Name`;
CALL `stackoverflow`.`parent_child`(V_Id);
END LOOP;
CLOSE `cur1`;
END
Wrapper Procedure Inner
CREATE PROCEDURE `parent_child_wrapper`(in `I_WrapperParent` int)
BEGIN
SET ##SESSION.max_sp_recursion_depth = 255;
DROP TABLE IF EXISTS `stackoverflow`.`parent_child_temp`;
CREATE TEMPORARY TABLE `stackoverflow`.`parent_child_temp` (id int, parent int, name varchar(45));
CALL `stackoverflow`.`parent_child`(`I_WrapperParent`);
SELECT * FROM `stackoverflow`.`parent_child_temp`;
END
Call SQL
CALL stackoverflow.parent_child_wrapper(1);
References
*http://dev.mysql.com/doc/refman/5.0/en/cursors.html
*How I query (with mysql) column names that "unfortunately" have round brackets?
*http://www.sitepoint.com/cursors-mysql-stored-procedures/
*How to echo print statements while executing a sql script
*http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_max_sp_recursion_depth
*MySql :: stored procedure recursive
*http://www.tutorialspoint.com/mysql/mysql-temporary-tables.htm
*How to get depth in mysql store procedure recursion?

MySQL Routine not assigning value to variable?

I have a table a with the following columns:
id
name
a_id
where a_id is the id of another a row.
Suppose I have 3 rows with the following values:
1, 'name1', null
2, 'name2', 1
3, 'name3', 2
Ok, now I want to create a stored procedure that returns the following string:
"name3|name2|name1"
or, in general, the name value of each row that refers to another row until there is no other reference (separated by the | character).
What is wrong with the following code?
DELIMITER $$
CREATE PROCEDURE chain (IN init_name VARCHAR(100), OUT names VARCHAR(1000))
BEGIN
DECLARE next_id INT default 0;
DECLARE curr_id INT default 0; /* Just in case */
DECLARE n VARCHAR(100) default "";
SET n = init_name;
SELECT a_id
INTO next_id
FROM a
WHERE `name`=n;
SET names = n;
WHILE next_id <> NULL DO
SET curr_id=next_id; /* Again, just in case */
SELECT `name`, a_id
INTO n, next_id
FROM a
WHERE id=curr_id;
SET names = CONCAT(names, "|", n);
END WHILE;
END$$
CALL chain('name3', #names) just outputs "name3"
You need to read #names after calling your procedure :SELECT #names. Or if you want procedure to output it, do SELECT names in the body of SP (right after SET names ....)