I have an organization table that has an id, a parent_id, and a name column. There are roughly 50k rows in this table. There is just one top level parent and the rest are all under that. In Oracle, I am able to easily retrieve the current depth of a particular organization with the level pseudocolumn:
SELECT id, parent_id, level, name
FROM organizations
START WITH parent_id = 1
CONNECT BY PRIOR id = parent_id
I am at a loss of what the proper way to do the above in MySQL is. I need to fetch the entire tree along with the node's depth in one query.
There are a plethora of questions on StackOverflow that have to do with this, but none of them seem to have a really good answer to it, mostly links to blogs with dubious solutions. Surely this is doable in some sort of straight-forward manner?
Unfortunately modifying the table in any way is not an option, so nested sets is not a possibility.
This is totally hilarious. I just picked up a +50 bounty on a similar question literally yesterday : Using MySQL query to traverse rows to make a recursive tree
I referenced how to do this with Stored Procedures in the DBA StackExchange (October 24, 2011)
I will post the same stored procedures along with the examples from my DBA StackExchange answer:
Code to Get the Parent for Any Given Node
DELIMITER $$
DROP FUNCTION IF EXISTS `junk`.`GetParentIDByID` $$
CREATE FUNCTION `junk`.`GetParentIDByID` (GivenID INT) RETURNS INT
DETERMINISTIC
BEGIN
DECLARE rv INT;
SELECT IFNULL(parent_id,-1) INTO rv FROM
(SELECT parent_id FROM pctable WHERE id = GivenID) A;
RETURN rv;
END $$
DELIMITER ;
Code to Get the Ancenstry for Any Given Node
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 ;
Code to Get the Family Tree (or Descendants) for Any Given Node
DELIMITER $$
DROP FUNCTION IF EXISTS `junk`.`GetFamilyTree` $$
CREATE FUNCTION `junk`.`GetFamilyTree` (GivenID INT) RETURNS varchar(1024) CHARSET latin1
DETERMINISTIC
BEGIN
DECLARE rv,q,queue,queue_children VARCHAR(1024);
DECLARE queue_length,front_id,pos INT;
SET rv = '';
SET queue = GivenID;
SET queue_length = 1;
WHILE queue_length > 0 DO
SET front_id = FORMAT(queue,0);
IF queue_length = 1 THEN
SET queue = '';
ELSE
SET pos = LOCATE(',',queue) + 1;
SET q = SUBSTR(queue,pos);
SET queue = q;
END IF;
SET queue_length = queue_length - 1;
SELECT IFNULL(qc,'') INTO queue_children
FROM (SELECT GROUP_CONCAT(id) qc
FROM pctable WHERE parent_id = front_id) A;
IF LENGTH(queue_children) = 0 THEN
IF LENGTH(queue) = 0 THEN
SET queue_length = 0;
END IF;
ELSE
IF LENGTH(rv) = 0 THEN
SET rv = queue_children;
ELSE
SET rv = CONCAT(rv,',',queue_children);
END IF;
IF LENGTH(queue) = 0 THEN
SET queue = queue_children;
ELSE
SET queue = CONCAT(queue,',',queue_children);
END IF;
SET queue_length = LENGTH(queue) - LENGTH(REPLACE(queue,',','')) + 1;
END IF;
END WHILE;
RETURN rv;
END $$
To demonstrate the execution of everything, here is the sample data
USE junk
DROP TABLE IF EXISTS pctable;
CREATE TABLE pctable
(
id INT NOT NULL AUTO_INCREMENT,
parent_id INT,
PRIMARY KEY (id)
) ENGINE=MyISAM;
INSERT INTO pctable (parent_id) VALUES (0);
INSERT INTO pctable (parent_id) SELECT parent_id+1 FROM pctable;
INSERT INTO pctable (parent_id) SELECT parent_id+2 FROM pctable;
INSERT INTO pctable (parent_id) SELECT parent_id+3 FROM pctable;
INSERT INTO pctable (parent_id) SELECT parent_id+4 FROM pctable;
INSERT INTO pctable (parent_id) SELECT parent_id+5 FROM pctable;
SELECT * FROM pctable;
Here is the query to see all Parents, Ancestries, and Family Trees
SELECT
id,parent_id,
GetParentIDByID(id),
GetAncestry(id),
GetFamilyTree(id)
FROM pctable;
Give it a Try !!!
There is no recursion in SQL without CTEs/Subquery Factoring, so there is specifically and absolutely no way to do this with arbitrary level depths in "Pure SQL," especially as implemented in MySQL. See How to transform a MSSQL CTE query to MySQL?
That is why all of the answers you find are hacks, to get around this very specific limitation of the engine. You can try this solution, if you still need one: Generating Depth based tree from Hierarchical Data in MySQL (no CTEs). (Of course, the limitation means that the queries aren't weird to parse, in the LangSec sense, which allows all sorts of beneficial parsing, and eliminates some security concerns, but helps you not at all.)
A bunch of Lefts Joins that exhaust the hierarchy are all you have left, sorry to dissapoint and use a bad pun.
#idok, the function will return "Truncated incorrect DOUBLE value" error if you have same parent id for more than one child, i have found the following fix from dba and it was given by Sivakumar Natarayan
WHILE queue_length > 0 DO
IF queue_length = 1 THEN
SET front_id = queue;
SET queue = '';
ELSE
SET front_id = SUBSTR(queue,1,LOCATE(',',queue)-1);
SET pos = LOCATE(',',queue) + 1;
SET q = SUBSTR(queue,pos);
SET queue = q;
END IF;
Mysql doesn't support recursive sql. When you have only 1 dimension then you can use a Left join with itself on the parent_id = id.
Related
So for some reason, I just got this error message: `Deadlock found when trying to get lock; try restarting transaction, but I'm not quite sure why since I've never gotten this before, and it used to work fine.
Stored procedure
CREATE PROCEDURE `AddTestResultRequirement`(varTestResultId INT, RequirementNameId INT)
BEGIN
declare reqId INT;
declare linkId INT;
set linkId = -1;
IF NOT EXISTS(SELECT * FROM testcaserequirement where nameId = RequirementNameId) THEN
insert into testcaserequirement(id, nameId) values (NULL, RequirementNameId);
set reqId = last_insert_id();
ELSE
set reqId = GetRequirementNameId(RequirementNameId);
END IF;
IF NOT EXISTS(select * from testresultrequirementlink where requirementId = reqId and testresultId = varTestResultId) THEN
insert into testresultrequirementlink(id, requirementId, testresultId) values(NULL, reqId, varTestResultId);
set linkId = last_insert_id();
else
set linkId = GetRequirementTestResultLinkId(varTestResultId, reqId);
end if;
select linkId;
END
Function
CREATE FUNCTION `GetRequirementTestResultLinkId`(varTestResultId INT, RequirementId INT) RETURNS int(11)
BEGIN
RETURN (SELECT id from testresultrequirementlink where testresultid = varTestResultId and requirementId = RequirementId LIMIT 1);
END
Anyone who can see where this deadlock would appear?
I do know that deadlock means that a deadlock is when two transactions are trying to lock two locks at opposite orders, but I still can't figure out why I got it.
Hello I have the following stored procedure for MySQL but when it is executed in my ASP.NET Core application I get a Subquery returns more than 1 row error. What am I doing wrong here? The equivalent SQL Server version used to work without problems...
-- System Calculates Candidate’s Matching Score based on a Manager’s Answer Weights
CREATE PROCEDURE spSysCalcCandScore
(
IN Candidate_ID INT,
IN Manager_ID INT
)
Begin
DECLARE ansID INT;
DECLARE tempSum INT;
DECLARE Sum INT;
DECLARE Done INT DEFAULT FALSE;
DECLARE MyCursor CURSOR FOR
SELECT Answer_ID FROM Completed_Questionnaire
WHERE Candidate_ID = Candidate_ID;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET Done = TRUE;
START TRANSACTION;
OPEN MyCursor;
myloop: LOOP
FETCH MyCursor INTO ansID;
IF Done THEN
LEAVE myloop;
END IF;
SET tempSum = (SELECT Weight_Value FROM Weight WHERE (Answer_ID = ansID AND Manager_ID = Manager_ID));
SET Sum = Sum + tempSum;
END LOOP;
CLOSE MyCursor;
IF (Sum IS NULL) THEN
SET Sum = 0;
END IF;
UPDATE `Interest`
SET Matching_Score = Sum
WHERE (Candidate_ID = Candidate_ID AND Manager_ID = Manager_ID);
COMMIT;
End//
I have this procedure which loops through article values, fetch tag1 and insert it into article_tag table:
DELIMITER $$
CREATE PROCEDURE dt1()
BEGIN
DECLARE maxid INT;
DECLARE x INT;
DECLARE t VARCHAR(30);
DECLARE ntag1 int;
SET maxid = (SELECT MAX(id) FROM `article`);
SET x = (SELECT MIN(id) FROM `article`) ;
WHILE x<= maxid DO
SET t = (SELECT tag1 from `article` WHERE id=x);
SET ntag1 = (SELECT count(*) from `article_tag` WHERE tag=t);
IF ntag1 = 0
THEN
INSERT INTO `article_tag` (tag, slug, frequency) VALUES (t, t, 1);
ELSE
UPDATE `article_tag` SET frequency = frequency + 1 WHERE tag=t;
END IF;
SET x = x + 1;
END WHILE;
END$$
This works fine when there are rows with id in the while loop, but when when there are some missing ids in between (like here)
I get
Query Error: Error: ER_BAD_NULL_ERROR: Column 'tag' cannot be null
I'm wondering what is the idiomatic way to deal with such missing rows?
You can use loop with cursor, so you will only loop through existing records and do not need to check for NULL.
Something like this:
DELIMITER \\
CREATE PROCEDURE dt1()
BEGIN
DECLARE t VARCHAR(30);
DECLARE done INT DEFAULT FALSE;
DECLARE cur1 CURSOR FOR SELECT tag FROM `article` ORDER BY id;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cur1;
loop1: LOOP
FETCH cur1 INTO t;
IF done THEN
LEAVE loop1;
END IF;
SELECT COUNT(*) INTO #cnt from `article_tag` WHERE tag = t;
IF #cnt = 0 THEN
INSERT INTO `article_tag` (tag, slug, frequency) VALUES (t, t, 1);
ELSE
UPDATE `article_tag` SET frequency = frequency + 1 WHERE tag = t;
END IF;
END LOOP;
CLOSE cur1;
END
\\
DELIMITER ;
See manual for details:
https://dev.mysql.com/doc/refman/5.7/en/cursors.html
Sure, if possible I'd use INSERT ON DUPLICATE UPDATE as already mentioned.
I am making a procedure that inserts a place ("Sted") and I would like to check if the inputs are NULL. However, whenever I try to add an if-statement at the start to surround my code (marked CRASH below), it gives me an error saying my syntax is not right at "DECLARE varStedskodeID INT;" which is the part after the IF-statement I'm trying to add.
To my eyes the syntax of my if-statement is the same inside the code, but only my soon-to-be-NULL-check if-statement crashes even with just a simple IF(TRUE) THEN.
Can anyone give me a hint of what causes this one if to crash?
DROP PROCEDURE IF EXISTS InsertSted;
DELIMITER $$
CREATE PROCEDURE InsertSted(
IN inputStedsnavn VARCHAR(255),
IN inputStedstype VARCHAR(255),
IN inputKommunenavn VARCHAR(255))
BEGIN
IF(TRUE) THEN <<------ CRASH
DECLARE varStedskodeID INT;
DECLARE varKommunenr INT;
IF(SELECT COUNT(StedkodeID) FROM stedstype WHERE Kodenavn = inputStedstype LIMIT 1) = 0 THEN
INSERT INTO stedstype VALUES(DEFAULT, inputStedstype);
END IF;
SET varStedskodeID = (SELECT StedkodeID FROM stedstype WHERE Kodenavn = inputStedstype LIMIT 1);
IF(SELECT COUNT(Kommunenr) FROM kommune WHERE Kommunenavn = inputKommunenavn LIMIT 1) = 1 THEN
SET varKommunenr = (SELECT Kommunenr FROM kommune WHERE Kommunenavn = inputKommunenavn LIMIT 1);
INSERT INTO sted VALUES(DEFAULT, inputStedsnavn, varStedskodeID, varKommunenr);
END IF;
END IF; <<------ CRASH
END$$
DELIMITER ;
DECLARE is permitted only inside a BEGIN ... END compound statement
and must be at its start, before any other statements.
http://dev.mysql.com/doc/refman/5.0/en/declare.html
MySQL follows strict rules for DECLARE. You have to DECLARE variables, tables, etc... at the beginning of Stored Procedure.
Change Stored Procedure like this
DECLARE varStedskodeID INT;
DECLARE varKommunenr INT;
IF(TRUE) THEN
IF(SELECT COUNT(StedkodeID) FROM stedstype WHERE Kodenavn = inputStedstype LIMIT 1) = 0 THEN
INSERT INTO stedstype VALUES(DEFAULT, inputStedstype);
END IF;
SET varStedskodeID = (SELECT StedkodeID FROM stedstype WHERE Kodenavn = inputStedstype LIMIT 1);
IF(SELECT COUNT(Kommunenr) FROM kommune WHERE Kommunenavn = inputKommunenavn LIMIT 1) = 1 THEN
SET varKommunenr = (SELECT Kommunenr FROM kommune WHERE Kommunenavn = inputKommunenavn LIMIT 1);
INSERT INTO sted VALUES(DEFAULT, inputStedsnavn, varStedskodeID, varKommunenr);
END IF;
END IF;
I have this function in my DB
CREATE FUNCTION BookBed (pPaciente varchar(255),
pHospital bigint(20)) RETURNS BOOLEAN
BEGIN
DECLARE NumLeitosDisponiveis INT;
DECLARE vReservaOK BOOLEAN;
DECLARE dt TIMESTAMP;
SET dt = (Select now());
SET NumLeitosDisponiveis = (SELECT AVAILABLEBEDCOUNT FROM HOSPITAL WHERE ID = pHospital);
IF((SELECT NumLeitosDisponiveis) > 0) THEN
BEGIN
START TRANSACTION;
INSERT INTO RESERVATION(PERSON, HOSPITAL, DATE) VALUES (pPaciente, pHospital, dt);
UPDATE HOSPITAL
SET AVAILABLEBEDCOUNT = AVAILABLEBEDCOUNT - 1
WHERE ID = pHospital;
SET vReservaOk = true;
commit;
END;
ELSE
SET vReservaOk = false;
END IF;
RETURN vReservaOK;
END;
In the if part of my if-else statement, I would like to perform all the operations in a atomic way.
I wanted to use the START TRANSACTION command, but they are disallowed in functions and I couldn't find any other command to perform it.
Are functions atomic by default?
If not, is there any way I can implement it?
Thanks,
Oscar
EDIT: And if I have to use a function, is it possible to have transactions?
Use a stored procedure with an output parameter for returning the operation status.
DELIMITER //
CREATE PROCEDURE BookBed (
pPaciente varchar(255),
pHospital bigint(20),
OUT oReservaOK boolean)
BEGIN
DECLARE NumLeitosDisponiveis INT;
DECLARE dt TIMESTAMP;
SET dt = (Select now());
SET NumLeitosDisponiveis =
SELECT AVAILABLEBEDCOUNT FROM HOSPITAL WHERE ID = pHospital;
IF((SELECT NumLeitosDisponiveis) > 0) THEN
BEGIN
START TRANSACTION;
INSERT INTO RESERVATION(PERSON, HOSPITAL, DATE)
VALUES (pPaciente, pHospital, dt);
UPDATE HOSPITAL
SET AVAILABLEBEDCOUNT = AVAILABLEBEDCOUNT - 1
WHERE ID = pHospital;
SET oReservaOk = true;
commit;
END;
ELSE
SET oReservaOk = false;
END IF;
END//
If you are having trouble calling the stored procedure using Hibernate, then move the transaction logic to Hibernate. That is, start, commit or rollback the transaction using Hibernate constructs.
EDIT
Transactions require using the InnoDB engine as #bobobobo notes on the comments.