How can I use IF and CONCAT within SELECT - mysql

I have this Adjacency List Model table
Table:
CREATE TABLE node_structure_data (
id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(455) NOT NULL,
parent_id INT(10) UNSIGNED DEFAULT NULL,
PRIMARY KEY (id),
FOREIGN KEY (parent_id) REFERENCES node_structure_data (id)
ON DELETE CASCADE ON UPDATE CASCADE
);
Output:
id title parent_id
1 Division NULL
2 Site 1 1
3 Paper 2
4 ms1 3
5 Site 2 1
6 Paper 5
7 ms2 6
8 Site 3 1
9 Paper 8
10 ms3 9
So I have the following query that duplicates a Site 1 e.g. and its children.
In this case, the children are Paper with parent_id = 2 and ms1 with parent_id = 3
INSERT INTO node_structure_data (title,parent_id)
WITH recursive max_id AS (
SELECT MAX(id) AS id FROM node_structure_data
),
child_nodes AS (
SELECT
n.id,
title,
parent_id,
m.id+1 AS new_id,
parent_id AS new_parent_id
FROM
node_structure_data n
CROSS JOIN
max_id AS m
WHERE
title='Site 1'
UNION ALL
SELECT
n.id,
n.parent_id,
n.title,
#row_num:=IF(#row_num=0,c.new_id,0) + 1 + #row_num AS new_id,
c.new_id
FROM
child_nodes c
INNER JOIN
node_structure_data n ON n.parent_id = c.id
CROSS JOIN (
SELECT #row_num:=0 AS rn
) AS vars
)
SELECT title,new_parent_id FROM child_nodes ORDER BY new_id;
Output:
id title parent_id
1 Division NULL
2 Site 1 1
3 Paper 2
4 ms1 3
5 Site 2 1
6 Paper 5
7 ms2 6
8 Site 3 1
9 Paper 8
10 ms3 9
11 Site 1 1
12 Paper 11
13 ms1 12
As you can see Site 1 and its children got duplicated with a new unique id.
However for the duplicated Site title I want to have a prefix text Copy of for the DUPLICATED Site 1 title
I only want that prefix for a Site/parent_id = 1
So that the duplicated nodes should look like this:
id title parent_id
1 Division NULL
2 Site 1 1
3 Paper 2
4 ms1 3
5 Site 2 1
6 Paper 5
7 ms2 6
8 Site 3 1
9 Paper 8
10 ms3 9
11 Copy of Site 1 1
12 Paper 11
13 ms1 12
I have tried to implement the IF and CONCAT in the query but for some reason, it doesn't work, I don't get any errors but the output stays the same.
IF(n.title LIKE '%Site%', CONCAT("Copy of ", n.title), n.title),
If the title contains the text Site then I want to contact the prefix and the site title otherwise no concat.
Any ideas?
Any help is appreciated!!!

This solution shows how to insert a copy of a subtree and re-identify the descendants.
INSERT INTO node_structure_data (id, title, parent_id)
WITH RECURSIVE subtree AS (
SELECT
id,
(SELECT MAX(id) FROM node_structure_data) AS last_id,
CONCAT('Copy of ', title) AS title,
parent_id
FROM node_structure_data
WHERE id = 2 -- i.e. title = 'Site 1'
UNION ALL
SELECT
n.id,
s.last_id,
n.title,
n.parent_id
FROM subtree s
JOIN node_structure_data n ON s.id = n.parent_id
), new_id AS (
SELECT
id,
last_id + ROW_NUMBER() OVER (ORDER BY id) AS new_id,
title,
parent_id
FROM subtree
)
SELECT
n.new_id AS id,
n.title,
COALESCE(p.new_id, n.parent_id) AS parent_id
FROM new_id n
LEFT JOIN new_id p ON n.parent_id = p.id
Note that starting MySQL 8 setting user variables within expressions is deprecated and will be removed in a future release.
The following fiddle shows the results of each CTE - db<>fiddle

Related

Adjacency list model duplicate parent & children

Hierarchical Data in MySQL Using the Adjacency List Model
I have this table named node_structur_data
CREATE TABLE node_structure_data (
id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(455) NOT NULL,
parent_id INT(10) UNSIGNED DEFAULT NULL,
PRIMARY KEY (id),
FOREIGN KEY (parent_id) REFERENCES node_structure_data (id)
ON DELETE CASCADE ON UPDATE CASCADE
);
Output:
id title parent_id
1 Division NULL
2 Site 1 1
3 Paper 2
4 ms1 3
How can I duplicate a node and its children?
For example Site 1
The id & parent_id should be unique but the title should stay the same.
Expected Output:
id title parent_id
1 Division NULL
2 Site 1 1
3 Paper 2
4 ms1 3
5 Site 1 1
6 Paper 5
7 ms1 6
The following approach first estimates the new max and then uses a recursive cte to find all children of the desired node 'Site 1' and determine their new possible parent_id if there were no other concurrent writes to the table.
I would recommend running the following in a transaction and locking the table during the operation to prevent concurrent table modifications.
To test this approach I added some additional sample data which I have included below, however you may see the approach in a demo with your initial sample data here
See output of working db fiddle below:
Schema (MySQL v8.0)
CREATE TABLE node_structure_data (
id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(455) NOT NULL,
parent_id INT(10) UNSIGNED DEFAULT NULL,
PRIMARY KEY (id),
FOREIGN KEY (parent_id) REFERENCES node_structure_data (id)
ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO node_structure_data
(`id`, `title`, `parent_id`)
VALUES
('1', 'Division', NULL),
('2', 'Site 1', '1'),
('3', 'Paper', '2'),
('4', 'ms1', '3'),
('5', 'ms2', '3'),
('6', 'os1', '4'),
('7', 'os2', '4'),
('8', 'gs1', '1'),
('9', 'hs1', '3'),
('10','js1','9');
Query #1
select 'Before Insert';
Before Insert
Before Insert
Query #2
select * from node_structure_data;
id
title
parent_id
1
Division
2
Site 1
1
3
Paper
2
4
ms1
3
5
ms2
3
6
os1
4
7
os2
4
8
gs1
1
9
hs1
3
10
js1
9
Query #3
select 'Possible Data Changes';
Possible Data Changes
Possible Data Changes
Query #4
with recursive max_id AS (
SELECT MAX(id) as id FROM node_structure_data
),
child_nodes AS (
SELECT
n.id,
title,
parent_id,
m.id+1 as new_id,
parent_id as new_parent_id
FROM
node_structure_data n
CROSS JOIN
max_id as m
WHERE
title='Site 1'
UNION ALL
SELECT
n.id,
n.title,
n.parent_id,
#row_num:=IF(#row_num=0,c.new_id,0) + 1 + #row_num as new_id,
c.new_id
FROM
child_nodes c
INNER JOIN
node_structure_data n ON n.parent_id = c.id
CROSS JOIN (
SELECT #row_num:=0 as rn
) as vars
)
SELECT * FROM child_nodes;
id
title
parent_id
new_id
new_parent_id
2
Site 1
1
11
1
3
Paper
2
12
11
4
ms1
3
13
12
5
ms2
3
14
12
9
hs1
3
15
12
6
os1
4
16
13
7
os2
4
17
13
10
js1
9
18
15
Query #5 - Performing actual insert
INSERT INTO node_structure_data (title,parent_id)
with recursive max_id AS (
SELECT MAX(id) as id FROM node_structure_data
),
child_nodes AS (
SELECT
n.id,
title,
parent_id,
m.id+1 as new_id,
parent_id as new_parent_id
FROM
node_structure_data n
CROSS JOIN
max_id as m
WHERE
title='Site 1'
UNION ALL
SELECT
n.id,
n.title,
n.parent_id,
#row_num:=IF(#row_num=0,c.new_id,0) + 1 + #row_num as new_id,
c.new_id
FROM
child_nodes c
INNER JOIN
node_structure_data n ON n.parent_id = c.id
CROSS JOIN (
SELECT #row_num:=0 as rn
) as vars
)
SELECT title,new_parent_id FROM child_nodes ORDER BY new_id;
There are no results to be displayed.
Query #6
select 'AFTER INSERT';
AFTER INSERT
AFTER INSERT
Query #7
select * from node_structure_data;
id
title
parent_id
1
Division
2
Site 1
1
3
Paper
2
4
ms1
3
5
ms2
3
6
os1
4
7
os2
4
8
gs1
1
9
hs1
3
10
js1
9
11
Site 1
1
12
Paper
11
13
ms1
12
14
ms2
12
15
hs1
12
16
os1
13
17
os2
13
18
js1
15
View on DB Fiddle
Let me know if this works for you.

Aggregate rows by id comparing column values

I have the following table that groups users by their permissions
userIds permissions
4,5,7,8 100,1600,500,501,502,400,401,1500,1501
The numbers in the permissions column are the sections ids.
Some of these sections may have other data associated which I retrieved and stored in another table.
sectionId userId resourceId
100 4 NULL
1600 4 NULL
500 4 NULL
501 4 NULL
502 4 NULL
400 4 NULL
401 4 1
1500 4 NULL
1501 4 NULL
100 5 NULL
1600 5 NULL
500 5 NULL
501 5 NULL
502 5 NULL
400 5 NULL
401 5 1,2
1500 5 NULL
1501 5 NULL
100 7 NULL
1600 7 NULL
500 7 NULL
501 7 NULL
502 7 NULL
400 7 NULL
401 7 2
1500 7 NULL
1501 7 NULL
100 8 NULL
1600 8 NULL
500 8 NULL
501 8 NULL
502 8 NULL
400 8 NULL
401 8 1
1500 8 NULL
1501 8 NULL
My goal is to compare, for each user in the userIds column of the first table (splitted by comma), every row of the second table in order to check if each user has the same resourceId value for that specific sectionId.
If one or more users have the same resourceId value for each section I want to keep them group together, otherwise they need to be on different rows.
This is the output I'm expecting from the sample data provided:
userIds permissions
4,8 100,1600,500,501,502,400,401,1500,1501
5 100,1600,500,501,502,400,401,1500,1501
7 100,1600,500,501,502,400,401,1500,1501
UPDATE
I managed to get the desidered output in the following way:
-- Numbers table creation
DROP temporary TABLE IF EXISTS tally;
CREATE temporary TABLE tally
(
n INT NOT NULL auto_increment PRIMARY KEY
);
INSERT INTO tally
(n)
SELECT NULL
FROM (SELECT 0 AS N
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) a,
(SELECT 0 AS N
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) b;
-- Split users by comma from first table
DROP temporary TABLE IF EXISTS tmppermissions2;
CREATE temporary TABLE tmppermissions2
(
userid VARCHAR(255) NOT NULL,
permissions TEXT NOT NULL
);
INSERT INTO tmppermissions2
SELECT userid,
permissions
FROM (SELECT Substring_index(Substring_index(t.userids, ',', tally.n), ',', -1
)
userId,
t.permissions
permissions
FROM tally
INNER JOIN tmppermissions t
ON Char_length(t.userids) - Char_length(
REPLACE(t.userids, ',',
'')) >=
tally.n - 1
ORDER BY n) AS split;
-- Gets the users with the same permissions
DROP temporary TABLE IF EXISTS sharedprofiles;
CREATE temporary TABLE sharedprofiles
(
userids VARCHAR(255) NOT NULL,
permissions TEXT NOT NULL,
profileid INT(11)
);
INSERT INTO sharedprofiles
SELECT Group_concat(userid),
permissions,
NULL
FROM tmppermissions2
WHERE userid NOT IN (SELECT split.userid
FROM (SELECT Substring_index(Substring_index(r.userids,
',',
t.n), ',', -1)
userId
FROM tally t
INNER JOIN tmppermissions r
ON Char_length(r.userids)
- Char_length(
REPLACE(r.userids, ',',
'')) >=
t.n - 1
WHERE Position(',' IN r.userids) > 0
ORDER BY n) AS split
WHERE split.userid IN (SELECT *
FROM (SELECT Group_concat(userid
ORDER
BY userid ASC)
AS
users
FROM
tmpcurrentresources2
GROUP BY resourceid,
sectionid
ORDER BY users) b
WHERE Position(',' IN b.users) =
0))
GROUP BY permissions
ORDER BY Group_concat(userid);
-- Gets the users with specific permissions
DROP temporary TABLE IF EXISTS singleprofiles;
CREATE temporary TABLE singleprofiles
(
userid VARCHAR(255) NOT NULL,
permissions TEXT NOT NULL,
profileid INT(11)
);
INSERT INTO singleprofiles
SELECT userid,
permissions,
NULL
FROM tmppermissions2
WHERE userid IN (SELECT split.userid
FROM (SELECT Substring_index(Substring_index(r.userids, ',',
t.n),
',', -1)
userId
FROM tally t
INNER JOIN tmppermissions r
ON Char_length(r.userids) -
Char_length(
REPLACE(r.userids, ',',
'')) >=
t.n - 1
WHERE Position(',' IN r.userids) > 0
ORDER BY n) AS split
WHERE split.userid IN (SELECT *
FROM (SELECT Group_concat(userid
ORDER BY
userid ASC)
AS
users
FROM tmpcurrentresources2
GROUP BY resourceid,
sectionid
ORDER BY users) b
WHERE Position(',' IN b.users) = 0))
ORDER BY userid;
-- Merge the results
SELECT *
FROM sharedprofiles
UNION
SELECT *
FROM singleprofiles;
I'm wondering if there is a more concise way to accomplish the same result.
The solution (as I suspect you already know) is to normalise your schema.
So instead of...
userIds permissions
4,5 100,1600,500
...you might have
userIds permissions
4 100
4 1600
4 500
5 100
5 1600
5 500

Use Order By, and Group by on a Union result from 2 different columns

This My Table
ID Value SenderID RecieverID
1 Hello There 2 7
2 etc etc 7 5
3 etc 2 6
4 ee 7 2
5 asdas 2 7
6 asdas 2 5
7 asdas 7 5
What I want is the value from either senderID or receiverID from all the rows in which a specific value let say 2 occurs in any of these 2 columns
I used this query
SELECT `SenderID` FROM `messages` WHERE `RecieverID` = 2
UNION
SELECT `ReceiverID` FROM `messages` WHERE `SenderID` = 2
Gives unique answer but in wrong order
like this
ReceiverID
7
6
5
I'm expecting the answer of this query to be ordered by ID DESC in which a specific sender or reciever id occures for example in my tables the msg between senderid 2 and reverid 7 is at id 5 and latest id btweend sendr2 and 6 is at id 3 and btweed sndr2 and 5 it is ID 7 sot the above answer should be sorted like this 5, 7, 6 instead of 7,6,5
This one will order the sender/receiver IDs by their most recent conversation:
SELECT senderID -- , MAX(ID) as maxID -- uncomment to see maxID
FROM (
SELECT ID, `SenderID` FROM `messages` WHERE `RecieverID` = 2
UNION ALL
SELECT ID, `RecieverID` FROM `messages` WHERE `SenderID` = 2
) as sub
GROUP BY (SenderID)
ORDER BY MAX(ID) DESC
http://sqlfiddle.com/#!9/6d7bc0/1
you need to use inner query in a brackets in order to set what the order by refers to:
SELECT id, senderID from
(SELECT ID, `SenderID` FROM `messages` WHERE `RecieverID` = 2
UNION
SELECT ID, `RecieverID` as senderId FROM `messages` WHERE `SenderID` = 2
) as A
GROUP BY (SenderID)
order by ID ASC
SELECT ID, IF(`SenderID` = 2,`RecieverID`,`SenderID`)
FROM `messages`
WHERE `RecieverID` = 2 OR `SenderID` = 2
group by IF(`SenderID` = 2,`RecieverID`,`SenderID`)
order by ID ASC

MySQL - Select only last record of each foreign key

I have 3 tables that go like this (stripped version):
Create table A (
AID Int NOT NULL,
PRIMARY KEY (AID)
)
Create table B (
BID Int NOT NULL,
AID Int NOT NULL,
PRIMARY KEY (BID),
FOREIGN KEY (AID) REFERENCES A(AID)
)
Create table C (
CID Int NOT NULL,
BID Int NOT NULL,
Price Decimal(12,4) NOT NULL,
PRIMARY KEY (CID),
FOREIGN KEY (BID) REFERENCES B(BID)
)
Each record in A can only have one up-to-date record in B (last ID is considered as latest) for example: we have a record in A that has 3 associated records in B, but only the last record is considered to be up-to-date and only that one should be used.
Each time a record in B is updated (in this case inserted), all records in C are duplicated and point to the newly created record in B.
How could I get the SUM of C.Price of all A records.
So far I got: (but distinct doesn't seem to work)
SELECT SUM(Price)
FROM C
INNER JOIN (
SELECT DISTINCT t2.AID, t2.BID
FROM (
SELECT BID, AID
FROM B
INNER JOIN (
SELECT AID
FROM A
-- some other statements go here
) t1
ON t1.AID = AID
ORDER BY BID DESC
) t2
) t3
ON t3.BID = C.BID
I hope I explained well enough. :/
The following should do what you want:
select b.aid, sum(c.price)
from b join
c
on c.bid = b.bid
where b.bid = (select max(b2.bid) from b b2 where b2.aid = b.aid)
group by b.aid;
The join brings the tables together. Note that a is not necessary -- unless you want rows from a that have no corresponding row in b (in which case, a would go first with a left join).
The where clause chooses the most recent b record for each aid. The group by is used to get the sum.
Each time a record in B is updated (in this case inserted), all
records in C are duplicated and point to the newly created record in
B.
This means that there is only record in C having the latest BID value. Which means, you don't need to SUM them, you only need to fetch the latest record.
Just to illustrate with sample data :
Table A :
AID
1
2
3
Table B :
BID AID
1 1
2 1
3 1
4 2
5 2
6 2
7 3
8 3
9 3
Table C :
CID BID Price
1 1 12
2 2 12
3 3 20
4 4 2
5 5 1
6 6 12
7 7 22
8 8 21
9 9 23
You can use this :
SELECT ab.AID,
c.Price
FROM (SELECT a.aid, MAX(b.BID) AS maxB
FROM a a
INNER JOIN b b ON a.AID = b.AID
GROUP BY a.AID
) ab
INNER JOIN C c ON ab.maxB = c.BID
Observe that there is no sum here.
This will give you the result :
AID PRICE
1 20
2 12
3 23
You can see this here -> http://sqlfiddle.com/#!9/4943cd/3
Gordon's answer would also give you the same result -> http://sqlfiddle.com/#!9/4943cd/2
You can see that irrespective of whether SUM is used or not, the result is the same.
NOTE : However, Gordon's answer (which is the marked answer here) is incorrect since it doesn't take into account the fact that if any values are removed from table A, then your result set would have redundant values.
You can observe this if you slightly change the original data set as below :
Table A :
AID
1
Table B :
BID AID
1 1
2 1
3 1
4 2
5 2
6 2
7 3
8 3
9 3
Table C :
CID BID Price
1 1 12
2 2 12
3 3 20
4 4 2
5 5 1
6 6 12
7 7 22
8 8 21
9 9 23
Gordon's query would yield you the result :
AID PRICE
1 20
2 12
3 23
which is incorrect as there are no AID values 2 and 3 anymore.
You need to join on table A to fetch currently existing values of AID in table A.
Hope this helps!!!

MySQL query limit per join

I tried asking question before, but it's hard to ask in specific without right terminology I am not quite familiar with. So here is an example
Take this query for example:
(
SELECT *
FROM comments
WHERE depth = 0
ORDER BY id DESC
LIMIT 2
)
UNION ALL
(
SELECT c.*
FROM comments c JOIN
(
SELECT id
FROM comments
WHERE depth = 0
ORDER BY id DESC
LIMIT 2
) p ON c.parent_id = p.id
LIMIT 5
)
id parent_id depth title
1 0 0 Title 1
2 0 0 Title 2
3 1 1 Title 3
4 1 1 Title 4
5 1 1 Title 5
6 1 1 Title 6
7 1 1 Title 7
I get two depth 0 rows and in join I get 5 child elements of those two returned queries as well. What I would like to get is to get 5 child elements of each of those two queries, total of 10 rows (of depth 1). For example:
id parent_id depth title
1 0 0 Title 1
2 0 0 Title 2
3 1 1 Title 3
4 1 1 Title 4
5 1 1 Title 5
6 1 1 Title 6
7 1 1 Title 7
8 2 1 Title 8
9 2 1 Title 9
10 2 1 Title 10
11 2 1 Title 11
12 2 1 Title 12
Is that even possible with adjacency list and a requirement to return everything as union (flat)?
edit:
Thanks to Bill Karwin's answer, I got it working now. I wonder still if there is a shorter way to write this. I have 6 (0-5) depth levels, so my query is rather long (and probably not optimal). Here is what it looks like for three levels (you can imagine what the full one looks like).
-- DEPTH LEVEL 0
(
SELECT * FROM (
SELECT *, 1 as _rn, #parent:=0
FROM comments
WHERE depth = 0
ORDER BY id DESC
LIMIT 2
) as D0
)
union all
-- DEPTH LEVEL 1
(
SELECT *
FROM (
SELECT c.*, #row:=IF(#parent=c.comment_id,#row+1,1) AS _rn, #parent:=c.comment_id
FROM (SELECT #parent:=null) AS _init
STRAIGHT_JOIN comments c
INNER JOIN
(
SELECT id
FROM comments
WHERE depth = 0
ORDER BY id DESC
LIMIT 2
) p ON c.comment_id = p.id
) AS _ranked
WHERE _ranked._rn <= 5
)
union all
-- DEPTH LEVEL 2
(
SELECT *
FROM (
SELECT c.*, #row:=IF(#parent=c.comment_id,#row+1,1) AS _rn, #parent:=c.comment_id
FROM (SELECT #parent:=null) AS _init
STRAIGHT_JOIN comments c
INNER JOIN
(
(
SELECT *
FROM (
SELECT c.*, #row:=IF(#parent=c.comment_id,#row+1,1) AS _rn, #parent:=c.comment_id
FROM (SELECT #parent:=null) AS _init
STRAIGHT_JOIN comments c
INNER JOIN
(
SELECT id
FROM comments
WHERE depth = 0
ORDER BY id DESC
LIMIT 2
) p ON c.comment_id = p.id
) AS _ranked
WHERE _ranked._rn <= 2
)
) p ON c.comment_id = p.id
) AS _ranked
WHERE _ranked._rn <= 2
)
You can't do this with LIMIT, because LIMIT is applied after the result set is completely finished, after all joining, grouping, sorting, etc.
You're using a variation of the greatest-n-per-group type of query. It's tricky to do this in MySQL because MySQL doesn't support the ROW_NUMBER() window function supported by many other SQL databases.
Here's a workaround for MySQL, in which user-defined variables can take the place of partitioned row numbers:
SELECT *
FROM (
SELECT c.*, #row:=IF(#parent=c.parent_id,#row+1,1) AS _rn, #parent:=c.parent_id
FROM (SELECT #parent:=null) AS _init
STRAIGHT_JOIN comments c
INNER JOIN
(
SELECT id
FROM comments
WHERE depth = 0
ORDER BY id DESC
LIMIT 2
) p ON c.parent_id = p.id
) AS _ranked
WHERE _ranked._rn <= 5