I would like to get all IDs from children in a tree with MySQL only.
I have a table like this:
ID parent_id name
1 0 cat1
2 1 subcat1
3 2 sub-subcat1
4 2 sub-subcat2
5 0 cat2
Now I'm trying to get all child IDs for cat1 (2,3,4) recursively. Is there any way how to achieve that?
There are two basic methods for doing this: adjacency lists and nested lists. Take a look at Managing Hierarchical Data in MySQL.
What you have is an adjacency list. No there isn't a way of recursively grabbing all descendants with a single SQL statement. If possible, just grab them all and map them all in code.
Nested sets can do what you want but I tend to avoid it because the cost of inserting a record is high and it's error-prone.
Here is a simple single-query MySql-solution:
SELECT GROUP_CONCAT(Level SEPARATOR ',') FROM (
SELECT #Ids := (
SELECT GROUP_CONCAT(`ID` SEPARATOR ',')
FROM `table_name`
WHERE FIND_IN_SET(`parent_id`, #Ids)
) Level
FROM `table_name`
JOIN (SELECT #Ids := <id>) temp1
) temp2
Just substitute <id> with the parent element's ID.
This will return a string with the IDs of all descendants of the element with ID = <id>, separated by ,. If you would rather have multiple rows returned, with one descendant on each row, you can use something like this:
SELECT *
FROM `table_name`
WHERE FIND_IN_SET(`ID`, (
SELECT GROUP_CONCAT(Level SEPARATOR ',') FROM (
SELECT #Ids := (
SELECT GROUP_CONCAT(`ID` SEPARATOR ',')
FROM `table_name`
WHERE FIND_IN_SET(`parent_id`, #Ids)
) Level
FROM `table_name`
JOIN (SELECT #Ids := <id>) temp1
) temp2
))
Including the root/parent element
The OP asked for the children of an element, which is answered above. In some cases it might be useful to include the root/parent element in the result. Here are my suggested solutions:
Comma-separated string of ids:
SELECT GROUP_CONCAT(Level SEPARATOR ',') FROM (
SELECT <id> Level
UNION
SELECT #Ids := (
SELECT GROUP_CONCAT(`ID` SEPARATOR ',')
FROM `table_name`
WHERE FIND_IN_SET(`parent_id`, #Ids)
) Level
FROM `table_name`
JOIN (SELECT #Ids := <id>) temp1
) temp2
Multiple rows:
SELECT *
FROM `table_name`
WHERE `ID` = <id> OR FIND_IN_SET(`ID`, (
SELECT GROUP_CONCAT(Level SEPARATOR ',') FROM (
SELECT #Ids := (
SELECT GROUP_CONCAT(`ID` SEPARATOR ',')
FROM `table_name`
WHERE FIND_IN_SET(`parent_id`, #Ids)
) Level
FROM `table_name`
JOIN (SELECT #Ids := <id>) temp1
) temp2
))
You could probably do it with a stored procedure, if that's an option for you.
Otherwise you can't do it with a single sql-statement.
Ideally you should make the recursive calls to walk the tree from your program
create table it should be look like below
DROP TABLE IF EXISTS `parent_child`;
CREATE TABLE `parent_child` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`parent_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=latin1;
insert into `parent_child`(`id`,`name`,`parent_id`)
values (1,'cat1',0),(2,'subcat1',1),
(3,'sub-subcat1',2),(4,'sub-subcat2',2),
(5,'cat2',0);
Create function for getting parent child element
DELIMITER $$
USE `yourdatabase`$$
DROP FUNCTION IF EXISTS `GetAllNode1`$$
CREATE DEFINER=`root`#`localhost` FUNCTION `GetAllNode1`(GivenID INT) RETURNS TEXT CHARSET latin1
DETERMINISTIC
BEGIN
DECLARE rv,q,queue,queue_children TEXT;
DECLARE queue_length,front_id,pos INT;
SET rv = '';
SET queue = GivenID;
SET queue_length = 1;
WHILE queue_length > 0 DO
SET front_id = queue;
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) AS qc
FROM `parent_child` 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$$
DELIMITER ;
write query for desire output
SELECT GetAllNode1(id) FROM parent_child
or
SELECT GetAllNode1(id) FROM parent_child where id =1 //for specific parent's child element
Your question seems a bit imprecise. Why do you want to have them, and what do you mean by having them, "in a tree" ?
The table you've got IS (the relational way to represent) the tree.
If you want them "in a table" with rows that hold the pairs (ID 4 , ParentID 0), then you need your SQL engine's version of recursive SQL to do this, if that engine supports it.
I wouldn't know about MySQL specifically, but my understanding is that they once planned to implement recursive SQL using the same syntax as Oracle, i.e. with CONNECT BY.
If you look in your manual's table of contents for keywords such as "recursive queries" or "CONNECT BY", I imagine you should be able to find the answer.
(Sorry for not being able to provide a more ready-to-consume answer.)
Related
So I've made a UDF in DB2 that tokenizes a String called Tokenize and returns a table with an ID and word (so each row is a word that was in the initial string). example
tokenize('University of Toronto', ' ') returns
ID Word
1 University
2 of
3 Toronto
What I'm trying to do is make another function that compares 2 strings to see how many words they have in common based on the length of the first string.
So for example 'University of Toronto' and 'University of Guelph' should return 0.66 I've gotten this to work with this code
CREATE OR REPLACE FUNCTION Parsed_Match(STRING1 VARCHAR(256), STRING2 VARCHAR(256))
RETURNS DECIMAL(5,2)
NO EXTERNAL ACTION
BEGIN
DECLARE SCORE DECIMAL(5,2);
DECLARE mymatches int;
DECLARE len int;
set mymatches = (
select count(*)
from (
select word
from table(tokenize(STRING1, ' '))
intersect all
select word
from table(tokenize(STRING2, ' '))
)
);
set len = (
select count(*)
from table(tokenize(STRING1, ' '))
);
set score = decimal(mymatches) / decimal(len);
RETURN SCORE;
END
Having to recall the tokenize code though to get the length of string1 just strikes me as wrong. Is there a way in DB2 I can store the calculated table in a variable to reuse that later on?
like I ideally want to do
set t1 = tokenize(String1);
set t2 = tokenize(String2);
set matches = (
select count(*)
from (
select word
from t1
intersect all
select word
from t2
)
);
set len = ( select count(*) from t1 );
but just can't find a way to get that to work :(
It'd probably be easiest to just create the table inline; that is, with a CTE:
WITH T1 AS (SELECT word
FROM TABLE(tokenize(STRING1, ' ')))
SELECT COUNT(*) / (SELECT COUNT(*) FROM T1) AS score
FROM (SELECT word
FROM T1
INTERSECT ALL
SELECT word
FROM TABLE(tokenize(STRING2, ' '))
)
... if you'll notice, this also allowed me to calculate the score in the same statement.
This won't help if you actually the to work with the tables outside this single statement.
I have a table with a primary key field ID. I don't want to use Identity because i need to give the User the posibility of manualy choose an ID for the new object. So my idea is:
By default in the edit view, the ID field will be 0.
If user don't change it, i need to find the first free ID and use it.
If the user change the ID, i first need to check if there's another object with that id, and in that case throw an error.
If not, use the ID choose by user.
create the new object
The question is how to query and SQL Server table to get the firts free ID number?
Examples 1:
ID
--
1
2
10
First free ID is 3
Examples 2:
ID
--
1
2
3
4
First free ID is 5
Is there a way to do that?
All i can think of is get the min and max value, create a cycle for possible values and then compare with table data, but it involves too many querys to the database.
Thanks!
You can find the first free id as the first id where there is no "next" value:
select coalesce(min(t.id) + 1, 0)
from table t left outer join
table t2
on t.id = t2.id - 1
where t2.id is null;
EDIT:
If you want to handle "1" as a potential missing value:
select (case when min(minid) > 1 then 1 else coalesce(min(t.id) + 1, 0) end)
from table t left outer join
table t2
on t.id = t2.id - 1 cross join
(select min(id) as minid from table t) const
where t2.id is null;
Test Table
CREATE TABLE ID_TABLE(ID INT)
INSERT INTO ID_TABLE VALUES
(1),(2),(10)
Stored Procedure
ALTER PROCEDURE dbo.usp_GetNextValue
#nxt_ID_Wanted INT = 0,
#nxt_ID_Available INT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
-- If user hasnt passed any value get next avilable value
IF (#nxt_ID_Wanted = 0)
BEGIN
SELECT TOP 1 #nxt_ID_Available = ID + 1
FROM
(
SELECT ID , ROW_NUMBER() OVER (ORDER BY ID ASC) AS rn
FROM ID_TABLE
)Q
WHERE ID = rn
ORDER BY ID DESC
IF (#nxt_ID_Available IS NULL)
BEGIN
SET #nxt_ID_Available = 1;
END
END
-- If user has passed a value check if it exists and raise error
ELSE IF EXISTS(SELECT 1 FROM ID_TABLE WHERE ID = #nxt_ID_Wanted)
BEGIN
RAISERROR('Selected ID value already exists',16,1)
SET #nxt_ID_Wanted = 0;
RETURN;
END
ELSE -- else just let the user have the value he/she wanted
BEGIN
SET #nxt_ID_Available = #nxt_ID_Wanted;
END
END
Execute Procedure
DECLARE #ID INT;
EXECUTE dbo.usp_GetNextValue #nxt_ID_Wanted = 6
,#nxt_ID_Available = #ID OUTPUT
SELECT #ID
This function gets first free ID for a table in PostgreSQL (PL/pgSQL):
CREATE OR REPLACE FUNCTION next_free_id_from(table_name TEXT)
RETURNS BIGINT AS
$$
DECLARE
next_id BIGINT;
BEGIN
execute 'select rn from (select id, row_number() over (order by id) as rn from ' || quote_ident(table_name) || ') as T where id > rn limit 1' into next_id;
if next_id is null then
execute 'select COALESCE(max(id), 0) + 1 from ' || quote_ident(table_name) into next_id;
end if;
return next_id;
END;
$$ language 'plpgsql' STRICT;
Execute:
select next_free_id_from('test');
Where test is a table name. This function call can be placed in DEFAULT of the primary key.
I just started working with the EVE static dump, which is just a lot of tables with data about the game, such as a list of what solar systems connect, which is what I'm dealing with.
I want to make a webpage that lets you filter out systems, and the first step is getting a list of systems nearby, with the distance to them.
I found a script that does it for MSSQL
--By Joanna Davaham http://forum.eveuniversity.org/viewtopic.php?t=44601&p=396107#p424943
--set values
DECLARE #jumpsAway INT =10
DECLARE #MiddleSystemName VARCHAR(50) = 'Aldrat'
DECLARE #Level INT =1
IF OBJECT_ID('tempdb..#map') IS NOT NULL
DROP TABLE #map
CREATE TABLE #map
(fromSolarSystemID INT, toSolarSystemID INT, Level INT)
INSERT INTO #map
SELECT -1, mSS.solarSystemID, 0 FROM mapSolarSystems mSS
WHERE mSS.solarSystemName= #MiddleSystemName
WHILE #Level <= #jumpsAway
BEGIN
INSERT INTO #map
SELECT mSSJ.fromSolarSystemID, mSSJ.toSolarSystemID, #Level FROM mapSolarSystemJumps mSSJ
WHERE mSSJ.fromSolarSystemID IN (SELECT toSolarSystemID FROM #map WHERE Level = #Level-1)
AND mSSJ.fromSolarSystemID NOT IN (SELECT fromSolarSystemID FROM #map)
SET #Level=#Level+1
END
SELECT m.*, mSS.solarSystemName, mSS.security FROM #map m
JOIN mapSolarSystems mSS ON m.toSolarSystemID=mSS.solarSystemID
--WHERE mSS.security<0.45 --uncomment to check all nearby lowsec system
I know that I could probably just use the MSSQL version of the dump, but I also want to be learning more about how to use PostgreSQL better.
I understand what it's doing and everything, but I just don't understand PL/pgSQL well enough to make it work.
My attempt is
CREATE FUNCTION near(VARCHAR, INTEGER) RETURNS TABLE(fromID INT,toID INT,jumps INT,name VARCHAR,security VARCHAR) AS $$
DECLARE --Declaration from here http://www.postgresql.org/docs/9.1/static/plpgsql-declarations.html
MiddleSystemName ALIAS FOR $1;
jumpsAway ALIAS FOR $2;
jumps INTEGER :=1;
BEGIN
--http://stackoverflow.com/questions/11979154/select-into-to-create-a-table-in-pl-pgsql
CREATE TEMP TABLE map AS
SELECT -1, mSS.solarSystemID, 0
FROM mapSolarSystems mSS
WHERE mSS.solarSystemName= MiddleSystemName;
LOOP
--http://www.postgresql.org/docs/9.1/static/plpgsql-statements.html#PLPGSQL-STATEMENTS-EXECUTING-DYN
--If you don't do it with execute, you can only do one row, I guess?
EXECUTE 'SELECT
|| mSSJ.fromSolarSystemID,
|| mSSJ.toSolarSystemID,
|| $1
|| FROM
|| mapSolarSystemJumps mSSJ
|| WHERE
|| mSSJ.fromSolarSystemID EXISTS (SELECT toSolarSystemID FROM map WHERE jumps = $1 - 1)
|| AND mSSJ.fromSolarSystemID NOT EXISTS (SELECT fromSolarSystemID FROM map)'
INTO map
USING jumps;
jumps := jumps + 1
EXIT WHEN jumps > jumpsAway;
END LOOP;
RETURN QUERY SELECT m.*,mSS.solarSystemName, mSS.security FROM JOIN mapSolarSystems mSS ON m.toSolarSystemID = mSS.solarSystemID;
END;
$$ LANGUAGE plpgsql;
And the error that produces is
Error is
ERROR: "map" is not a known variable
LINE 27: INTO map
^
Thanks for all the help.
PL/pgSQL
This should be a valid translation to plpgsql:
CREATE OR REPLACE FUNCTION f_near(_middlesystemname text, _jumpsaway int)
RETURNS TABLE(fromid int, toid int, jumps int, name text, security text) AS
$func$
DECLARE
_jumps integer;
BEGIN
CREATE TEMP TABLE map AS
SELECT -1 AS "fromSolarSystemID"
,m."solarSystemID" AS "toSolarSystemID"
,0 AS level
FROM "mapSolarSystems" m
WHERE "solarSystemName" = _middlesystemname;
-- potentially add indexes on the temp table and ANALYZE if it gets big
FOR _jumps IN 1 .. _jumpsaway LOOP
INSERT INTO map ("fromSolarSystemID", "toSolarSystemID", level)
SELECT sj."fromSolarSystemID", sj."toSolarSystemID", _jumps AS level
FROM "mapSolarSystemJumps" sj
JOIN map m ON m."toSolarSystemID" = sj."fromSolarSystemID"
AND m."level" = _jumps - 1
LEFT JOIN map mx ON mx."fromSolarSystemID" = sj."fromSolarSystemID"
WHERE mx."fromSolarSystemID" IS NULL;
END LOOP;
RETURN QUERY
SELECT m.*, s."solarSystemName", s."security"
FROM map m
JOIN "mapSolarSystems" s ON m."toSolarSystemID" = s."solarSystemID";
END
$func$ LANGUAGE plpgsql;
RECURSIVE CTE - doesn't seem to work
This short SQL query with a recursive CTE should have done it:
WITH RECURSIVE map AS (
SELECT -1 AS fromsolarsystemid, m.solarsystemid, 0 AS level
FROM mapsolarsystems m
WHERE m.solarsystemname = from_id
UNION ALL
SELECT sj.fromsolarsystemid, sj.tosolarsystemid, level + 1
FROM mapsolarsystemjumps sj
JOIN map m USING (level)
LEFT JOIN map mx USING (fromsolarsystemid)
WHERE sj.fromsolarsystemid = m.tosolarsystemid
AND mx.fromsolarsystemid IS NULL
AND m.level < 10 -- jumpsAway
)
SELECT m.*, s.solarsystemname, s.security
FROM map m
JOIN mapsolarsystems s ON m.tosolarsystemid = s.solarsystemid
-- WHERE s.security < 0.45 -- uncomment to check all nearby lowsec system
However:
ERROR: recursive reference to query "map" must not appear within an outer join
LINE 9: LEFT JOIN map mx USING (fromsolarsystemid)
Can somebody help me with this little task? What I need is a stored procedure that can find duplicate letters (in a row) in a string from a table "a" and after that make a new table "b" with just the id of the string that has a duplicate letter.
Something like this:
Table A
ID Name
1 Matt
2 Daave
3 Toom
4 Mike
5 Eddie
And from that table I can see that Daave, Toom, Eddie have duplicate letters in a row and I would like to make a new table and list their ID's only. Something like:
Table B
ID
2
3
5
Only 2,3,5 because that is the ID of the string that has duplicate letters in their names.
I hope this is understandable and would be very grateful for any help.
In your answer with stored procedure, you have 2 mistakes, one is missing space between column name and LIKE clause, second is missing single quotes around search parameter.
I first create user-defined scalar function which return 1 if string contains duplicate letters:
EDITED
CREATE FUNCTION FindDuplicateLetters
(
#String NVARCHAR(50)
)
RETURNS BIT
AS
BEGIN
DECLARE #Result BIT = 0
DECLARE #Counter INT = 1
WHILE (#Counter <= LEN(#String) - 1)
BEGIN
IF(ASCII((SELECT SUBSTRING(#String, #Counter, 1))) = ASCII((SELECT SUBSTRING(#String, #Counter + 1, 1))))
BEGIN
SET #Result = 1
BREAK
END
SET #Counter = #Counter + 1
END
RETURN #Result
END
GO
After function was created, just call it from simple SELECT query like following:
SELECT
*
FROM
(SELECT
*,
dbo.FindDuplicateLetters(ColumnName) AS Duplicates
FROM TableName) AS a
WHERE a.Duplicates = 1
With this combination, you will get just rows that has duplicate letters.
In any version of SQL, you can do this with a brute force approach:
select *
from t
where t.name like '%aa%' or
t.name like '%bb%' or
. . .
t.name like '%zz%'
If you have a case sensitive collation, then use:
where lower(t.name) like '%aa%' or
. . .
Here's one way.
First create a table of numbers
CREATE TABLE dbo.Numbers
(
number INT PRIMARY KEY
);
INSERT INTO dbo.Numbers
SELECT number
FROM master..spt_values
WHERE type = 'P'
AND number > 0;
Then with that in place you can use
SELECT *
FROM TableA
WHERE EXISTS (SELECT *
FROM dbo.Numbers
WHERE number < LEN(Name)
AND SUBSTRING(Name, number, 1) = SUBSTRING(Name, number + 1, 1))
Though this is an old post it's worth posting a solution that will be faster than a brute force approach or one that uses a scalar udf (which generally drag down performance). Using NGrams8K this is rather simple.
--sample data
declare #table table (id int identity primary key, [name] varchar(20));
insert #table([name]) values ('Mattaa'),('Daave'),('Toom'),('Mike'),('Eddie');
-- solution #1
select id
from #table
cross apply dbo.NGrams8k([name],1)
where charindex(replicate(token,2), [name]) > 0
group by id;
-- solution #2 (SQL 2012+ solution using LAG)
select id
from
(
select id, token, prevToken = lag(token,1) over (partition by id order by position)
from #table
cross apply dbo.NGrams8k([name],1)
) prep
where token = prevToken
group by id; -- optional id you want to remove possible duplicates.
another burte force way:
select *
from t
where t.name ~ '(.)\1';
trying desperately to combine 2 simple answers into specifically what i need.
sql loop and set properties
sql to set an xml value
SET #I := 0;
SELECT *,
#I := #I + 1
SET xml = UpdateXML(xml,'comic/pageNumber', '<pageNumber>'.#I.'</pageNumber>')
FROM `comics`
ORDER BY ExtractValue(xml,'comic/pageNumber')+100000 ASC
this is as close as i have come, i know the SELECT / ORDER BY works separate from trying to SET the xml property.
side note: the +100000 is a work
around to treat the value as numeric
for sorting. otherwise 11 < 2 but
100011 > 100002
i have also tried this
SET #I := 0;
UPDATE comics,
#I := #I + 1 AS newPageNumber
SET xml = UpdateXML(xml,'comic/pageNumber', '<pageNumber>'.#I.'</pageNumber>')
WHERE 1
ORDER BY ExtractValue(xml,'comic/pageNumber')+100000 ASC
i think i just don't know how to combine the SELECT and UPDATE
UPDATE comics
inner join (
select c.id, #row:=#row+1 rownum
from (select #row:=0) X cross join comics c
ORDER BY ExtractValue(xml,'comic/pageNumber')*1.0) Y on Y.id=comics.id
SET xml = UpdateXML(xml,
'comic/pageNumber',
concat('<pageNumber>',Y.rownum,'</pageNumber>'))
;
Based on this test schema and data
create table comics (id int auto_increment primary key, xml text);
insert comics select null, '<comic><name>test1</name><pageNumber>7</pageNumber><content>page 5 con</content></comic>';
insert comics select null, '<comic><name>test1</name><pageNumber>3</pageNumber><content>page 6 con</content></comic>';
insert comics select null, '<comic><name>test1</name><pageNumber>5</pageNumber><content>page 7 con</content></comic>';