Inner joining on to temp table and insert to table in MySql - mysql

I have a list of strings. Each of them have categories which are separated out by a '/'.
For example:
animals/domestic/dog
animals/domestic/cat
What I want to do with these categories is to insert into a MySql categories table.
The table has 4 columns:
id (int auto increment), category_name (nvarchar), parent_id (int), is_active (bit)
The logic around inserting these should be as follows:
The main categories (animals) should have a parent_id of 0.
The child categories will have the id of their parent as parent_id.
There cannot be two active categories with the same category name.
I have tried to implement the following logic:
Get a distinct list of strings.
From these, get a distinct list of main categories.
Insert the distinct main categories to the categories table with a parent ID of 0.
Organise each of the categories in pairs and get distinct pairs:
(animals, domestic)
(domestic, dog)
(domestic, cat)
Get the matching id for each of the parent categories and insert in to the child's parent_id
SQL:
/*INSERT ALL THE FIRST PARENT CATEGORIES WITH A PARENT ID OF 0*/
INSERT INTO categories (category_name, parent_id, is_active)
VALUES ('animals', 0, 1);
/*INSERT ALL THE CATEGORIES IN PAIRS TO TEMP TABLE*/
CREATE TEMPORARY TABLE tempcat(parent nvarchar(256), child nvarchar(256));
INSERT INTO tempcat
VALUES ('animals', 'domestic'),('domestic', 'dog'),('domestic','cat');
/*INSERT INTO THE CATEGORIES TABLE*/
INSERT INTO categories(category_name, parent_id, is_active)
SELECT tempcat.child, categories.id, 1
FROM categories
INNER JOIN tempcat
ON categories.category_name = tempcat.parent;
WHERE categories.is_active = 1;
/*DISPOSE THE TEMPORARY TABLE*/
DROP TEMPORARY TABLE tempcat;
Issue:
After the query is run I expect 4 entries in the categories table.
But I only get 2.
I can see that the temp table has correct entries before doing the last inner join.
I can't seem to figure out why the categories table wouldn't have the other two rows.
Any guidance in the right direction is highly appreciated.
Update #1
Suppose the specifications said 'There cannot be two active categories with the same category name that had the same parent IDs'.
For example, if there were two strings as (animals/domestic/cat), (animals/outdoor/cat) there should be two entries for cat with IDs of domestic and outdoor as parent_id's.

CREATE TABLE categories (id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
category_name VARCHAR(64),
parent_id INT UNSIGNED NOT NULL DEFAULT 0,
is_active CHAR(1) NULL,
UNIQUE INDEX idx_name_active (category_name));
CREATE TABLE source_data (path TEXT);
INSERT INTO source_data VALUES ('animals/domestic/dog'), ('animals/domestic/cat');
CREATE PROCEDURE update_categories_table()
BEGIN
DECLARE cnt INT DEFAULT 0;
INSERT IGNORE INTO categories (category_name, parent_id, is_active)
SELECT SUBSTRING_INDEX(path, '/', 1), 0, '1'
FROM source_data;
iteration: LOOP
SELECT COUNT(*) INTO cnt
FROM source_data
WHERE LOCATE('/', path);
IF NOT cnt THEN
LEAVE iteration;
END IF;
INSERT IGNORE INTO categories (category_name, parent_id, is_active)
SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(source_data.path, '/', 2), '/', -1),
categories.id,
'1'
FROM source_data, categories
WHERE SUBSTRING_INDEX(source_data.path, '/', 1) = categories.category_name;
UPDATE source_data
SET path = SUBSTRING(path FROM 1 + LOCATE('/', path));
END LOOP iteration;
TRUNCATE source_data;
END
call update_categories_table;
SELECT * FROM categories;
id | category_name | parent_id | is_active
-: | :------------ | --------: | :--------
1 | animals | 0 | 1
4 | domestic | 1 | 1
7 | dog | 4 | 1
8 | cat | 4 | 1
db<>fiddle here

In MySQL 8, you can do this with a single query:
with splits as (
select 1 as n, substring_index(cats, '/', 1) as cat, cats
from strings union all
select 2 as n, substring_index(substring_index(cats, '/', 2), '/', -1) as cat, cats
from strings
where cats like '%/%' union all
select 3 as n, substring_index(substring_index(cats, '/', 3), '/', -1) as cat, cats
from strings
where cats like '%/%/%'
),
splits_n as (
select s.*, dense_rank() over (order by n, cat) as new_id
from splits s
),
splits_np as (
select s.*, sp.new_id as parent_id
from splits_n s left join
splits_n sp
on sp.cats = s.cats and sp.n = s.n - 1
)
select distinct new_id as id, cat, parent_id, 1 as is_active
from splits_np s;
Here is a db<>fiddle.
Unfortunately, this is much more painful in earlier versions.

Related

How to duplicate data from application level? Not database level

I am stuck for duplicate data from my application.
Let's say I have:
PetBooks Table:
id
book_name
Pet:
id
book_id
code
name
type
Petbooks
1 MyPet
Pet:
1 1 CAT1 Josh Cat
2 1 CAT2 Ron Cat
3 1 DOG1 Max Dog
Question 1:
How can I duplicate Petbook and all its pets?
So After I duplicate the petbook, it should look like
Petbooks
1 MyPet
2 MyPet(Copy)
Pet:
1 1 CAT1 Josh Cat
2 1 CAT2 Ron Cat
3 1 DOG1 Max Dog
4 2 CAT1 Josh Cat
5 2 CAT2 Ron Cat
6 2 DOG1 Max Dog
The solution I think is, to get all of the pet where book_id = 1, then looping and insert one by one, but it's so slow especially when there are a lot of data.
Well, you can do this somewhat painfully -- assuming that ids are assigned automatically and some column other than id is unique in the original data. Let me call that column name.
Then:
insert into petbooks (book_name)
select concat('Copy of ', book_name)
from petbooks;
Then for pets, we have to look up the new id. We can create a look-up table on the fly using aggregation:
insert into pets (book_id, code, name, type)
select pb.max_id, code, name, type)
from pets p join
(select name, min(id) as min_id, max(id) as max_id
from petbooks
group by name
) pb
on pb.min_id = p.book_id
You may handle the relationship manually one by one, and combine them to a procedure. You may use the LAST_INSERT_ID() function to get the latest id. For example:
CREATE PROCEDURE copy_petbook(petbook_id INT)
BEGIN
INSERT INTO PetBooks (book_name)
SELECT CONCAT(book_name, '(Copy)')
FROM Petbooks
WHERE id = petbook_id;
DECLARE last_insert_id BIGINT;
SET last_insert_id = LAST_INSERT_ID();
INSERT INTO Pet (book_id, code, name, type)
SELECT lid.last_insert_id , code, name, type
FROM Pet JOIN (SELECT last_insert_id) as lid
WHERE book_id = petbook_id;
END
If you want to use UUIDs for the id column, you may change the signature to CHAR(36) and then use the UUID() function:
CREATE PROCEDURE copy_petbook(petbook_id CHAR(36))
BEGIN
DECLARE uuid CHAR(36);
-- Generate an UUID which has not been used.
REPEAT
SET uuid = UUID();
UNTIL (SELECT COUNT(*) FROM PetBooks WHERE id = uuid) = 0 END REPEAT;
-- Insert rows with the UUID
INSERT INTO PetBooks (id, book_name)
SELECT u.uuid, CONCAT(book_name, '(Copy)')
FROM Petbooks JOIN (SELECT uuid) as u
WHERE id = petbook_id;
INSERT INTO Pet (book_id, code, name, type)
SELECT u.uuid, code, name, type
FROM Pet JOIN (SELECT uuid) as u
WHERE book_id = petbook_id;
END
You may generate more UUIDs in the same way if you are also using an UUID for the id column in other tables.
Finally, just call the procedure:
CALL copy_petbook(/* Put the id here. */);

get parents and children of tree folder structure in my sql < 8 and no CTEs

I have a folder table that joins to itself on an id, parent_id relationship:
CREATE TABLE folders (
id int(10) unsigned NOT NULL AUTO_INCREMENT,
title nvarchar(255) NOT NULL,
parent_id int(10) unsigned DEFAULT NULL,
PRIMARY KEY (id)
);
INSERT INTO folders(id, title, parent_id) VALUES(1, 'root', null);
INSERT INTO folders(id, title, parent_id) values(2, 'one', 1);
INSERT INTO folders(id, title, parent_id) values(3, 'target', 2);
INSERT INTO folders(id, title, parent_id) values(4, 'child one', 3);
INSERT INTO folders(id, title, parent_id) values(5, 'child two', 3);
INSERT INTO folders(id, title, parent_id) values(6, 'root 2', null);
INSERT INTO folders(id, title, parent_id) values(7, 'other child one', 6);
INSERT INTO folders(id, title, parent_id) values(8, 'other child two', 6);
I want a query that returns all the parents of that record, right back to the route and any children.
So if I ask for folder with id=3, I get records: 1, 2, 3, 4, 5. I am stuck how to get the parents.
The version of MYSQL is 5.7 and there are no immediate plans to upgrade so sadly CTEs are not an option.
I have created this sql fiddle
In your table design, ID and PARENT_ID corresponds to the "Adjacency List Model" for storing a tree.
There is another design, called the "Nested Set Model", which makes it easier to perform the operations you want here.
See this excellent article from Mike Hillyer describing both:
managing-hierarchical-data-in-mysql
In summary:
The tree is stored in a table like:
CREATE TABLE nested_category (
category_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
lft INT NOT NULL,
rgt INT NOT NULL
);
Finding the path from the root to a given node (here, 'FLASH'):
SELECT parent.name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.name = 'FLASH'
ORDER BY parent.lft;
Finding all children of a given node (here 'PORTABLE ELECTRONICS'):
SELECT node.name, (COUNT(parent.name) - (sub_tree.depth + 1)) AS depth
FROM nested_category AS node,
nested_category AS parent,
nested_category AS sub_parent,
(
SELECT node.name, (COUNT(parent.name) - 1) AS depth
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.name = 'PORTABLE ELECTRONICS'
GROUP BY node.name
ORDER BY node.lft
)AS sub_tree
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
AND sub_parent.name = sub_tree.name
GROUP BY node.name
HAVING depth <= 1
ORDER BY node.lft;
After renaming to your folders table
TABLE nested_category -> TABLE folders
Column category_id -> Column id
Column name -> Column title
The solution is:
CREATE TABLE folders (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(20) NOT NULL,
lft INT NOT NULL,
rgt INT NOT NULL
);
INSERT INTO folders(id, title, lft, rgt) values(1, 'root', 1, 10);
INSERT INTO folders(id, title, lft, rgt) values(2, 'one', 2, 9);
INSERT INTO folders(id, title, lft, rgt) values(3, 'target', 3, 8);
INSERT INTO folders(id, title, lft, rgt) values(4, 'child one', 4, 5);
INSERT INTO folders(id, title, lft, rgt) values(5, 'child two', 6, 7);
INSERT INTO folders(id, title, lft, rgt) values(6, 'root 2', 11, 16);
INSERT INTO folders(id, title, lft, rgt) values(7, 'other child one', 12, 13);
INSERT INTO folders(id, title, lft, rgt) values(8, 'other child two', 14, 15);
Path to the target:
SELECT parent.title
FROM folders AS node,
folders AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.title = 'target'
ORDER BY parent.lft;
Target children:
SELECT node.title, (COUNT(parent.title) - (sub_tree.depth + 1)) AS depth
FROM folders AS node,
folders AS parent,
folders AS sub_parent,
(
SELECT node.title, (COUNT(parent.title) - 1) AS depth
FROM folders AS node,
folders AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.title = 'target'
GROUP BY node.title
ORDER BY node.lft
)AS sub_tree
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
AND sub_parent.title = sub_tree.title
GROUP BY node.title
HAVING depth <= 1
ORDER BY node.lft;
See sqlfiddle
To get all the data in a single query, a union should do.
In MySQL 8.0, you can make use of the Recursive Common Table Expressions to adress this use case.
The following query gives you the parents of a given record (including the record itself):
with recursive parent_cte (id, title, parent_id) as (
select id, title, parent_id
from folders
where id = 3
union all
select f.id, f.title, f.parent_id
from folders f
inner join parent_cte pc on f.id = pc.parent_id
)
select * from parent_cte;
| id | title | parent_id |
| --- | ------ | --------- |
| 3 | target | 2 |
| 2 | one | 1 |
| 1 | root | |
And here is a slightly different query, that returns the children tree of a given record:
with recursive children_cte (id, title, parent_id) as (
select id, title, parent_id
from folders
where parent_id = 3
union all
select f.id, f.title, f.parent_id
from folders f
inner join children_cte cc on f.parent_id = cc.id
)
select * from children_cte;
| id | title | parent_id |
| --- | --------- | --------- |
| 4 | child one | 3 |
| 5 | child two | 3 |
Both queriers can be combined as follows:
with recursive parent_cte (id, title, parent_id) as (
select id, title, parent_id
from folders
where id = 3
union all
select f.id, f.title, f.parent_id
from folders f
inner join parent_cte pc on f.id = pc.parent_id
),
children_cte (id, title, parent_id) as (
select id, title, parent_id
from folders
where parent_id = 3
union all
select f.id, f.title, f.parent_id
from folders f
inner join children_cte cc on f.parent_id = cc.id
)
select * from parent_cte
union all select * from children_cte;
| id | title | parent_id |
| --- | --------- | --------- |
| 3 | target | 2 |
| 2 | one | 1 |
| 1 | root | |
| 4 | child one | 3 |
| 5 | child two | 3 |
Demo on DB Fiddle
If it's guaranteed that child nodes always have a higher id than it's parent, then you could use user variables.
Get descendants:
select f.*, #l := concat_ws(',', #l, id) as dummy
from folders f
cross join (select #l := 3) init_list
where find_in_set(parent_id, #l)
order by id
Result:
id | title | parent_id | dummy
---|-----------|-----------|------
4 | child one | 3 | 3,4
5 | child two | 3 | 3,4,5
Get ancestors (including itself):
select f.*, #l := concat_ws(',', #l, parent_id) as dummy
from folders f
cross join (select #l := 3) init_list
where find_in_set(id, #l)
order by id desc
Result:
id | title | parent_id | dummy
3 | target | 2 | 3,2
2 | one | 1 | 3,2,1
1 | root | null | 3,2,1
Demo
Note that this technique relies on undocumented evaluation order, and will not be possible in future versions.
Also it is not very performant, since both queries need a full table scan, but might be fine for smaller tables. However - for small tables I would just fetch the full table and solve the task with a recursive function in application code.
For bigger tables I would consider a more complex solution like the following stored procedure:
create procedure get_related_nodes(in in_id int)
begin
set #list = in_id;
set #parents = #list;
repeat
set #sql = '
select group_concat(id) into #children
from folders
where parent_id in ({parents})
';
set #sql = replace(#sql, '{parents}', #parents);
prepare stmt from #sql;
execute stmt;
set #list = concat_ws(',', #list, #children);
set #parents = #children;
until (#children is null) end repeat;
set #child = in_id;
repeat
set #sql = '
select parent_id into #parent
from folders
where id = ({child})
';
set #sql = replace(#sql, '{child}', #child);
prepare stmt from #sql;
execute stmt;
set #list = concat_ws(',', #parent, #list);
set #child = #parent;
until (#parent is null) end repeat;
set #sql = '
select *
from folders
where id in ({list})
';
set #sql = replace(#sql, '{list}', #list);
prepare stmt from #sql;
execute stmt;
end
Use it with
call get_related_nodes(3)
This will return
id | title | parent_id
---|-----------|----------
1 | root |
2 | one | 1
3 | target | 2
4 | child one | 3
5 | child two | 3
Demo
I expect this procedure to perform as good as a recursive CTE query. In any case you should have an index on parent_id.
I've solved this in the past with a second table, which contains the transitive closure of all paths through the tree.
mysql> CREATE TABLE folders_closure (
ancestor INT UNSIGNED NOT NULL,
descendant INT UNSIGNED NOT NULL,
PRIMARY KEY (ancestor, descendant),
depth INT UNSIGNED NOT NULL
);
Load this table with tuples of all ancestor-descendant pairs, including the ones where a node in the tree references itself (path of length 0).
mysql> INSERT INTO folders_closure VALUES
(1,1,0), (2,2,0), (3,3,0), (4,4,0), (5,5,0), (6,6,0),
(1,2,1), (2,3,1), (3,4,1), (3,5,1), (1,4,2), (1,5,2),
(6,7,1), (6,8,1);
Now you can query the tree below a given node by querying all the paths that start at the top node, and join that path's descendant to your folders table.
mysql> SELECT d.id, d.title, cl.depth FROM folders_closure cl
JOIN folders d ON d.id=cl.descendant WHERE cl.ancestor=1;
+----+-----------+-------+
| id | title | depth |
+----+-----------+-------+
| 1 | root | 0 |
| 2 | one | 1 |
| 4 | child one | 2 |
| 5 | child two | 2 |
+----+-----------+-------+
I see many people recommend the Nested Sets solution which was introduced in 1992, and became popular after Joe Celko included it in his book SQL for Smarties in 1995. But I don't like the Nested Sets technique, because the numbers aren't actually references to the primary keys of the nodes in your tree, and it requires renumbering many rows when you add or delete a node.
I wrote about the closure table method in What is the most efficient/elegant way to parse a flat table into a tree? and some of my other answers with the hierarchical-data tag.
I did a presentation about it: Models for Hierarchical Data.
I also covered this in a chapter of my book SQL Antipatterns Volume 1: Avoiding the Pitfalls of Database Programming.
if your parent_id comes always in ascending order then below query is the great solution.
if you get the result your id to null parent value then Please follow the link
http://www.sqlfiddle.com/#!9/b40b8/258 (When passing id = 6)
http://www.sqlfiddle.com/#!9/b40b8/259 (When passing id = 3)
SELECT * FROM folders f
WHERE id = 3
OR
(Parent_id <=3 AND Parent_id >=
(SELECT id FROM folders Where id <= 3 AND parent_id IS NULL Order by ID desc LIMIT 1)) OR (id <= 3 AND IFNULL(Parent_id,0) = 0)
AND id >= (SELECT id FROM folders Where id <= 3 AND parent_id IS NULL Order by ID desc LIMIT 1);
OR
You won't get your passing id to top at parent then please follow the link as below.
http://www.sqlfiddle.com/#!9/b40b8/194 (When passing id =3)
http://www.sqlfiddle.com/#!9/b40b8/208 (When passing id =6)
SELECT
*
FROM
folders f
WHERE
id = 3 OR Parent_id <=3
OR (id <= 3 AND IFNULL(Parent_id,0) = 0);
Note My solution is more or less same as #Marc Alff. Didn't realise it was already there before typing / preparing response in an editor.
It is very difficult to get a query to achieve your objective (or other typical requirements of hierarchical dataset) without use of CTEs or other hierarchical query supports (e.g. prior, connect by in Oracle). This was the main driver for databases to come up with CTEs etc.
Many many years ago when such support for modelling hierarchical entities weren't available in databases, requirements outlined by you and many other related were solved by modelling such entities slightly differently.
The concept is simple. In essence, two more attributes are introduced in the hierarchical table (or a separate table foreign keyed into hierarchical table) called left_boundary and right_boundary (call whatever you wish after all what’s in the name). For each row the values (numbers) for these attributes are so chosen that they cover the values of these attributes for all their children. In other words, a child’s left and right boundaries will be between left and right boundaries of its parents.
By the way of example
Creating this hierarchy used to be part of an early morning batch job or the boundaries were chosen so wide apart during design time that they were easily covering all depths of tree.
I am going to use this solution to achieve your objective.
Firstly I will introduce a second table (could have introduced the attributes in the same table, decided not to disturb your data model)
CREATE TABLE folder_boundaries (
id int(10) unsigned NOT NULL AUTO_INCREMENT,
folder_id int(10) unsigned NOT NULL,
left_boundary int(10) unsigned,
right_boundary int(10) unsigned,
PRIMARY KEY (id),
FOREIGN KEY (folder_id) REFERENCES folders(id)
);
The data for this table based on your dataset
NSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(1, 1, 10);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(2, 2, 9);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(3, 3, 8);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(4, 4, 4);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(5, 4, 4);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(6, 21, 25);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(7, 22, 22);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(7, 22, 22);
Here is the query to achieve what you are after
select f.id, f.title
from folders f
join folder_boundaries fb on f.id = fb.folder_id
where fb.left_boundary < (select left_boundary from folder_boundaries where folder_id = 3)
and fb.right_boundary > (select right_boundary from folder_boundaries where folder_id = 3)
union all
select f.id, f.title
from folders f
join folder_boundaries fb on f.id = fb.folder_id
where fb.left_boundary >= (select left_boundary from folder_boundaries where folder_id = 3)
and fb.right_boundary <= (select right_boundary from folder_boundaries where folder_id = 3)
Result
You can perform an union between parent rows and child rows like this :
select title, id, #parent:=parent_id as parent from
(select #parent:=3 ) a join (select * from folders order by id desc) b where #parent=id
union select title, id, parent_id as parent from folders where parent_id=3 ORDER BY id
here a sample dbfiddle
Suppose you know the maximum depth of the tree, you could "create" a loop to get what you want:
Get parent nodes:
SELECT #id :=
(
SELECT parent_id
FROM folders
WHERE id = #id
) AS folderId, vars.id
FROM (
SELECT #id := 7 AS id
) vars
INNER JOIN (
SELECT 0 AS nbr UNION ALL SELECT 1 UNION ALL SELECT 2
UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5
UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8
UNION ALL SELECT 9) temp
WHERE #id IS NOT NULL
Get child nodes:
SELECT #id :=
(
SELECT GROUP_CONCAT(id)
FROM folders
WHERE FIND_IN_SET(parent_id, #id)
) AS folderIds, vars.id
FROM (
SELECT #id := 1 AS id
) vars
INNER JOIN (
SELECT 0 AS nbr UNION ALL SELECT 1 UNION ALL SELECT 2
UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5
UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8
UNION ALL SELECT 9) temp
WHERE #id IS NOT NULL
This works by
Creating a join between a static variable subquery (SELECT #id := 1 AS id) and a static set of 10 rows in this case(maximum depth)
using a subquery in the select to traverse the tree and find all the parents or child nodes
The purpose of the join is to create a result set of 10 rows, so that the subquery in the select is executed 10 times.
Alternatively, if you do not know the maximum depth, you could replace the joined subquery with
INNER JOIN (
SELECT 1 FROM folder) temp
or in order to avoid all the union selects above, use with a limit:
INNER JOIN (
SELECT 1 FROM folder LIMIT 100) temp
References:
- Hierarchical queries in MySQL

Split values to multiple row on MySQL

Here is my data in mysql table:
+---------+-------------------+------------+
| ID | Name | Class |
+---------+-------------------+------------+
| 1, 2, 3 | Alex, Brow, Chris | Aa, Bb, Cc |
+---------+-------------------+------------+
I want split values to multiple rows to get data as the below format.
1 Alex Aa
2 Brow Bb
3 Chris Cc
How can I do that?
One trick is to join to a Tally table with numbers.
Then use SUBSTRING_INDEX to get the parts.
If you don't already have a numbers table, here's one way.
drop table if exists Digits;
create table Digits (n int primary key not null);
insert into Digits (n) values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9);
drop table if exists Nums;
create table Nums (n int primary key not null);
insert into Nums (n)
select (n3.n*100+n2.n*10+n1.n) as n
from Digits n1
cross join Digits n2
cross join Digits n3;
Then it can be used to unfold those columns
Sample data:
drop table if exists YourTable;
create table YourTable (
ID varchar(30) not null,
Name varchar(30) not null,
Class varchar(30) not null
);
insert into YourTable
(ID, Name, Class) values
('1, 2, 3', 'Alex, Brow, Chris', 'Aa, Bb, Cc')
, ('4, 5, 6', 'Drake, Evy, Fiona', 'Dd, Ee, Ff')
;
The query:
SELECT
LTRIM(SUBSTRING_INDEX( SUBSTRING_INDEX( t.ID, ',', Nums.n), ',', -1)) AS Id,
LTRIM(SUBSTRING_INDEX( SUBSTRING_INDEX( t.Name, ',', Nums.n), ',', -1)) AS Name,
LTRIM(SUBSTRING_INDEX( SUBSTRING_INDEX( t.Class, ',', Nums.n), ',', -1)) AS Class
FROM YourTable t
LEFT JOIN Nums ON n BETWEEN 1 AND (LENGTH(ID)-LENGTH(REPLACE(ID, ',', ''))+1);
Result:
Id Name Class
1 Alex Aa
2 Brow Bb
3 Chris Cc
4 Drake Dd
5 Evy Ee
6 Fiona Ff
Multiple values are not recommended for a one field but still if you want a solution you can split string by comma and insert to the table.
Please see this blog post which shows how to split https://nisalfdo.blogspot.com/2019/02/mysql-how-to-insert-values-from-comma.html#more

PostgreSQL recursive rows to JSONB map

This question is best explained with an example. So, if you have 2 tables category and event in PostgreSQL as follows: -
create table category (
id integer primary key,
type varchar(255),
label varchar (255),
parent_id integer
);
insert into category (id, type, label, parent_id)
values (1, 'organisation', 'Google', null),
(2, 'product', 'Gmail', 1),
(3, 'organisation', 'Apple', null),
(4, 'product', 'iPhone', 3),
(5, 'product', 'Mac', 3);
create table event (
id integer primary key,
name varchar (255),
category_id integer
);
insert into event (id, name, category_id)
values (1, 'add', 4),
(2, 'delete', 5),
(3, 'update', 2);
As you can see, the category table is quite dynamic and a hierarchy of categories can be defined.
What I'm trying to achieve is selecting entries of the event table and join it with the categories but flatten it to a JSON structure. I can illustrate using the following query: -
select e.*,
jsonb_build_object(
c1.type, c1.label,
c2.type, c2.label
) as categories
from event e
left join category c2 on c2.id = e.category_id
left join category c1 on c1.id = c2.parent_id
This will return: -
+----+--------+-------------+------------------------------------------------+
| id | name | category_id | categories |
+----+--------+-------------+------------------------------------------------+
| 1 | add | 4 | {"organisation": "Apple", "product": "iPhone"} |
| 2 | delete | 5 | {"organisation": "Apple", "product": "Mac"} |
| 3 | update | 2 | {"organisation": "Google", "product": "Gmail"} |
+----+--------+-------------+------------------------------------------------+
However, this approach only works when an event.category_id column references a child category which has precisely 1 parent (2 levels). Really what I'm looking for is to generate categories, regardless if (a) it doesn't have a parent category (i.e. a 1 level category) OR (b) has more than 1 parent (e.g. 3 levels). For example, if I add the following rows to the event and category tables: -
insert into category (id, type, label, parent_id)
values (6, 'module', 'Mobile', 5), /* has 2 parents */
(7, 'organisation', 'AirBNB', null); /* has no parents */
insert into event (id, name, category_id)
values (4, 'event1', 6),
(5, 'event2', 7);
... and run the query from above it will return: -
ERROR: argument 1: key must not be null
SQL state:
My gut feeling is a recursive CTE could solve this.
Update 1
create or replace function category_array(category_parent_id int) returns setof jsonb as $$
select case
when count(x) > 0 then
jsonb_agg(f.x) || jsonb_build_object (
c.type, c.label
)
else jsonb_build_object (
c.type, c.label
)
end as category_pair
from category c
left join category_array (c.parent_id) as f(x) on true
where c.id = category_parent_id
group by c.id, c.type, c.label;
$$ language sql;
... and call using this SQL ...
select *,
category_array(category_id)
from event;
... will return the following ...
+----+--------+-------------+--------------------------------------------------------------------------+
| id | name | category_id | categories |
+----+--------+-------------+--------------------------------------------------------------------------+
| 1 | add | 4 | [{"organisation": "Apple"}, {"product": "iPhone"}] |
| 2 | delete | 5 | [{"organisation": "Apple"}, {"product": "Mac"}] |
| 3 | update | 2 | [{"organisation": "Google"}, {"product": "Gmail"}] |
| 4 | event1 | 6 | [[{"organisation": "Apple"}, {"product": "Mac"}], {"module": "Mobile"}] |
| 5 | event2 | 7 | {"organisation": "AirBNB"} |
+----+--------+-------------+--------------------------------------------------------------------------+
Pretty close but not quite there just yet!
Use the concatenation operator || to build cumulative jsonb objects:
with recursive events as (
select
e.id, e.name, e.category_id as parent_id,
jsonb_build_object(c.type, c.label) as categories
from event e
left join category c on c.id = e.category_id
union all
select
e.id, e.name, c.parent_id,
categories || jsonb_build_object(c.type, c.label)
from events e
join category c on c.id = e.parent_id
)
select id, name, categories
from events
where parent_id is null
order by id;
Note that the query is not protected against circular dependencies, so you need to be sure that all paths in the table are ended with nulls.
Test the query on DbFiddle.
Alternative solution:
create or replace function get_categories(int)
returns jsonb language sql as $$
select case
when parent_id is null then
jsonb_build_object (type, label)
else
jsonb_build_object (type, label) || get_categories(parent_id)
end as categories
from category
where id = $1
$$;
select id, name, get_categories(category_id)
from event
order by id;
DbFiddle.

Recursively select all subcategories of a category

I have a table with the following fields:
node_id (int, AI)
category_id, (int)
parent_node_id (int)
How can I select all the nodes (or categories, if you wish) that hang down from a given category id. And by "hang down" I mean all the recursively stored nodes.
Example:
Category node parent
1 1 none
2 2 none
3 3 none
4 4 1
5 5 4
6 6 5
Expected output of the select:
Category node parent
1 1 none
4 4 1
5 5 4
6 6 5
exactly I don't know what you are looking for but as per my assumption.
DECLARE #Table1 TABLE
(node_id varchar(9), category_id varchar(5), parent_node_id varchar(11))
;
INSERT INTO #Table1
(node_id, category_id, parent_node_id)
VALUES
('Category1', 'node1', 'parentnone.'),
('Category2', 'node2', 'parentnone.'),
('Category3', 'node3', 'parentnone.'),
('Category4', 'node4', 'parent1.'),
('Category5', 'node5', 'parent4.'),
('Category6', 'node6', 'parent5.')
;
select node_id, category_id, parent_node_id from (
select node_id, category_id, parent_node_id,Row_number()OVER(PARTITION BY parent_node_id ORDER BY node_id desc)RN from #Table1
GROUP BY node_id, category_id, parent_node_id
)T
WHERE T.RN = 1
--ORDER BY cat desc
ORDER BY RIGHT(category_id,1)
You can create a function that will return whether the category is a child at any level to category you are interested.
CREATE FUNCTION `is_child_of`(id INT, related_to_id INT) RETURNS int(11)
BEGIN
DECLARE `exists` BOOL;
/* to avoid infinite loop */
SELECT EXISTS(SELECT `parent_id` FROM `category` WHERE `category_id` = id) INTO `exists`;
IF `exists` IS FALSE THEN
RETURN 0;
END IF;
WHILE id IS NOT NULL DO
IF id = related_to_id THEN
RETURN 1;
END IF;
SELECT `parent_id` INTO id FROM `category` WHERE `category_id` = id;
END WHILE;
RETURN 0;
END
Then just select by it's result regarding category you want to drill down.
For example for a category with id - 1
SELECT * FROM `category` WHERE `is_child_of`(category_id, 1);
I admit it is far from being efficient. It is difficult to be efficient when dealing with hierarchy in a relational database.
Assuming the table is called categories:
select
children.category_id as category,
children.node_id as node,
parent.node_id as parent_node_id
from categories parent
join categories children
on parent.node_id = children.parent_id;
That should get you somewhere.