This is my table structure:
id id_parent id_origin level name
1 0 1 1 PHP
2 0 2 1 Javascript
3 0 3 1 SMARTY
4 0 4 1 HTML
5 1 1 2 Basic
6 1 1 2 Date & Math Function
8 2 2 2 DOM
9 5 1 3 Introduction
10 5 1 3 Session & Cookies
12 2 2 2 Introduction
13 4 4 2 Basic Structure
14 6 1 3 PHP Date Function
16 3 3 2 Basic Syntax
26 4 4 2 Table
I want result like below format
Myfinalstr
-----------
PHP
PHP->Basic
PHP->Basic->Introduction
PHP->Basic->Session & Cookies
PHP->Date & Match Function
PHP->Date & Match Function->PHP Date Function
Javascript
Javascript->DOM
Javascript->Introduction
SMARTY
SMARTY->Basic Syntax
HTML
HTML->Basic Structure
HTML->Table
The following isnt a complete solution but it will get you started:
Example stored procedure call
mysql> call chapter_hier(1);
+----+----------------------+-----------+----------------------+-------+
| id | category_name | id_parent | parent_category_name | depth |
+----+----------------------+-----------+----------------------+-------+
| 1 | PHP | NULL | NULL | 0 |
| 5 | Basic | 1 | PHP | 1 |
| 6 | Date & Math Function | 1 | PHP | 1 |
| 9 | Introduction | 5 | Basic | 2 |
| 10 | Session & Cookies | 5 | Basic | 2 |
| 14 | PHP Date Function | 6 | Date & Math Function | 2 |
+----+----------------------+-----------+----------------------+-------+
6 rows in set (0.00 sec)
$result = $conn->query(sprintf("call chapter_hier(%d)", 1));
Full script and test data
drop table if exists chapters;
create table chapters
(
id smallint unsigned not null primary key,
name varchar(255) not null,
id_parent smallint unsigned null,
key (id_parent)
)
engine = innodb;
insert into chapters (id, name, id_parent) values
(1,'PHP',null),
(2,'Javascript',null),
(3,'SMARTY',null),
(4,'HTML',null),
(5,'Basic',1),
(6,'Date & Math Function',1),
(8,'DOM',2),
(9,'Introduction',5),
(10,'Session & Cookies',5),
(12,'Introduction',2),
(13,'Basic Structure',4),
(14,'PHP Date Function',6),
(16,'Basic Syntax',3),
(26,'Table',4);
drop procedure if exists chapter_hier;
delimiter #
create procedure chapter_hier
(
in p_id smallint unsigned
)
begin
declare v_done tinyint unsigned default 0;
declare v_depth smallint unsigned default 0;
create temporary table hier(
id_parent smallint unsigned,
id smallint unsigned,
depth smallint unsigned default 0
)engine = memory;
insert into hier select id_parent, id, v_depth from chapters where id = p_id;
create temporary table tmp engine=memory select * from hier;
/* http://dev.mysql.com/doc/refman/5.0/en/temporary-table-problems.html */
while not v_done do
if exists( select 1 from chapters c
inner join tmp on c.id_parent = tmp.id and tmp.depth = v_depth) then
insert into hier select c.id_parent, c.id, v_depth + 1 from chapters c
inner join tmp on c.id_parent = tmp.id and tmp.depth = v_depth;
set v_depth = v_depth + 1;
truncate table tmp;
insert into tmp select * from hier where depth = v_depth;
else
set v_done = 1;
end if;
end while;
select
c.id,
c.name as category_name,
p.id as id_parent,
p.name as parent_category_name,
hier.depth
from
hier
inner join chapters c on hier.id = c.id
left outer join chapters p on hier.id_parent = p.id
order by
hier.depth;
drop temporary table if exists hier;
drop temporary table if exists tmp;
end #
delimiter ;
Hope it helps :)
Related
I have this procedure :
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE `id_var` varchar(255);
DECLARE `cur1` CURSOR FOR
SELECT `id` FROM `clients`
WHERE `status` = 'Active';
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
DROP TABLE IF EXISTS `tblquota_nc`;
CREATE TABLE IF NOT EXISTS `tblquota_nc` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`email` varchar(255),
`pack_id` int(11) NOT NULL,
`pack_name` varchar(255) NOT NULL,
`quota` int(11) NULL,
PRIMARY KEY (`id`)
);
OPEN cur1;
read_loop: LOOP
FETCH NEXT
FROM cur1
INTO id_var;
IF done THEN
LEAVE read_loop;
END IF;
SELECT clients.id as `User ID`, `email`, `packageid` as `Pack ID`, `name` as `Pack`, (CASE
WHEN `name` = "Basic" THEN '10'
WHEN `name` = "Silver" THEN '100'
WHEN `name` = "Gold" THEN '1000'
ELSE '10'
END) as Quota INTO #mid, #mail, #p_id, #packname, #quota
FROM `clients`
INNER JOIN `tblhosting` ON clients.id = tblhosting.userid
INNER JOIN `tblproducts` ON tblhosting.packageid = tblproducts.id
WHERE clients.status = 'Active'
AND tblhosting.domainstatus = 'Active'
AND clients.id = id_var;
IF (SELECT id FROM tblquota_nc WHERE user_id = #mid) THEN
BEGIN
END;
ELSE
BEGIN
if (#mid IS NOT NULL AND #p_id IS NOT NULL AND #packname IS NOT NULL) then
INSERT INTO tblquota_nc (user_id, email, pack_id, pack_name, quota) VALUES (#mid, #mail, #p_id, #packname, #quota);
end if;
END;
END IF;
END LOOP;
CLOSE cur1;
END
It seems like I have an error because the SELECT statment return several values. I thought doing another loop with these results to make an insert into the new table. I want to make a new table from these information.
table clients:
id | email | status
----------------------------
1 | user1#mail.com | Active
2 | user2#mail.com | Inactive
3 | user3#mail.com | Active
table tblhosting
id | userid | packageid | domainstatus
------------------------------------------------
1 | 1 | 2 | Active
2 | 2 | 3 | Active
3 | 3 | 1 | Active
table tblproducts
id | name
-----------
1 | Basic
2 | Silver
3 | Gold
I expect result like :
id | user_id | email | pack_id | pack_name | quota
-----------------------------------------------------------
1 | 1 | user1#mail.com | 2 | Silver | 100
2 | 2 | user2#mail.com | 3 | Gold | 1000
3 | 3 | user3#mail.com | 1 | Basic | 10
If I put max in the case statment, it will work but will not show all data.
I don't think you need a stored procedure to do this. Just use CREATE TABLE ... SELECT syntax:
CREATE TABLE tblquota_nc (id INT AUTO_INCREMENT PRIMARY KEY) AS
SELECT c.id as user_id
, c.`email`
, h.`packageid` as pack_id
, p.`name` as pack_name
, (CASE WHEN `name` = "Basic" THEN '10'
WHEN `name` = "Silver" THEN '100'
WHEN `name` = "Gold" THEN '1000'
ELSE '10'
END) as quota
FROM `clients` c
LEFT JOIN `tblhosting` h ON c.id = h.userid
INNER JOIN `tblproducts` p ON h.packageid = p.id
ORDER BY c.id;
Output from SELECT * FROM tblquota_nc:
id user_id email pack_id pack_name quota
1 1 user1#mail.com 2 Silver 100
2 2 user2#mail.com 3 Gold 1000
3 3 user3#mail.com 1 Basic 10
Demo on dbfiddle
I have a MySQL table like this:
| CategoryId | Name | CategoryParentId |
|------------|---------------|------------------|
| 0 | Tech Support | (null) |
| 1 | Configuration | 0 |
| 2 | Questions | 1 |
| 3 | Sales | (null) |
| 4 | Questions | 3 |
| 5 | Other | (null) |
This is the output I desire when a query the ID 2 (for example):
Tech Support/Configuration/Questions
How do I do this without having to do multiple joins?
Fiddle
EDIT: Not sure if is the best way to do this, but I solved by creating a function:
DELIMITER $$
CREATE FUNCTION get_full_tree (CategoryId int) RETURNS VARCHAR(200)
BEGIN
SET #CategoryParentId = (SELECT CategoryParentId FROM category c WHERE c.CategoryId = CategoryId);
SET #Tree = (SELECT Name FROM category c WHERE c.CategoryId = CategoryId);
WHILE (#CategoryParentId IS NOT NULL) DO
SET #ParentName = (SELECT Name FROM category c WHERE c.CategoryId = #CategoryParentId);
SET #Tree = CONCAT(#ParentName, '/', #Tree);
SET #CategoryParentId = (SELECT CategoryParentId FROM category c WHERE c.CategoryId = #CategoryParentId);
END WHILE;
RETURN #Tree;
END $$
DELIMITER ;
I can now do this query:
SELECT CategoryId, get_full_tree(CategoryId) FROM category
You could create a new table, lets name it as hierarchy (could be a better name) where we would store all the ancestry of a category.
CREATE TABLE `hierarchy` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`parent` int(11) NOT NULL,
`child` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
For example in this case for Questions i.e. ID->2 we will have the below entries:
id parent child
====================
6 0 2
7 1 2
8 2 2
For the whole example the content of the table would be:
id parent child
===========================
1 0 0
2 3 3
3 5 5
4 0 1
5 1 1
6 0 2
7 1 2
8 2 2
9 3 4
10 4 4
Now whenever you want to retrieve the whole ancestry of node execute the below query:
select name from category where id in (select parent from hierarchy where child = 2 order by id ASC)
The above query will return all the ancestry names for the Questions (ID->2) i.e.
name
==================
Tech Support
Configuration
Questions
For completeness shake below is the content for category table
id Name
============================
0 Tech Support
1 Configuration
2 Questions
3 Sales
4 Questions
5 Other
N.B. This is just an idea i am sure you can definitely build more elegant solution on top of it.
If you are using MySQL 8 or above you can use Common Table Expressions for recursive queries. Query would be the following
WITH RECURSIVE CategoryPath (CategoryId, Name, path) AS
(
SELECT CategoryId, Name, Name as path
FROM category
WHERE CategoryParentId IS NULL
UNION ALL
SELECT c.CategoryId, c.Name, CONCAT(cp.path, ' / ', c.Name)
FROM CategoryPath AS cp JOIN category AS c
ON cp.CategoryId = c.CategoryParentId
)
SELECT * FROM CategoryPath ORDER BY path;
I have 3 tables:
users
user_groups
groups
A user can be in multiple (sub)groups. They are stored in the user_groups table like this
+--------------+--------------+---------------+
| id | user_id | group_id |
+--------------+--------------+---------------+
| 1 | 1 | 23 |
+--------------+--------------+---------------+
| 2 | 2 | 24 |
-----------------------------------------------
Now in my groups table, the top categories are parent_id = 0
+--------------+--------------+---------------+
| id | parent_id | name |
+--------------+--------------+---------------+
| 1 | 2 | Group 1.1 |
+--------------+--------------+---------------+
| 2 | 0 | Group 1 |
+--------------+--------------+---------------+
| 3 | 2 | Group 1.2 |
+--------------+--------------+---------------+
| 4 | 3 | Group 1.2.1 |
+--------------+--------------+---------------+
| 5 | 2 | Group 1.3 |
+--------------+--------------+---------------+
Now I want to build a query which gives me all the parent groups for all users. I did some research about recursive queries and I found this particular post:
How to create a MySQL hierarchical recursive query
But I have no idea how I should approach this when I join the tables.
This is what I got so far:
SELECT
`users`.`id`,
`users`.`first_name`,
`users`.`last_name`,
`users`.`email`,
`users`.`language`,
`groups`.`name`,
`groups`.`parent_id`
FROM `users`
LEFT JOIN `user_groups`
ON `user_groups`.`user_id` = `users`.`id`
LEFT JOIN `groups`
ON `groups`.`id` = `user_groups`.`group_id`
WHERE
`users`.`created`
BETWEEN
DATE_SUB(NOW(), INTERVAL 365 DAY) AND NOW()
But this query just gets me the name and the id of the subgroup. What I want is the top level group.
Thanks for the help!
The typical solution is to create a stored function that returns the top-level group for any given group by tracing the parentage up until it finds a row with parent_id = 0.
Then you can apply that function to each group that the user is a member of, and select the distinct set of top level groups.
Something like this should work for you:
delimiter $$
drop function if exists get_top_level_group_id $$
create function get_top_level_group_id (p_group_id int) returns int
begin
declare v_return_val int;
declare v_group_id int;
declare v_parent_id int;
declare continue handler for not found
begin
return -1;
end;
set v_group_id = p_group_id;
set v_parent_id = p_group_id;
while v_parent_id != 0
do
set v_group_id = v_parent_id;
select `parent_id`
into v_parent_id
from `groups`
where id = v_group_id;
end while;
return v_group_id;
end $$
delimiter ;
Then you can update your query like this to get those users and their distinct top-level groups:
SELECT DISTINCT
`users`.`id`,
`users`.`first_name`,
`users`.`last_name`,
`users`.`email`,
`users`.`language`,
get_top_level_group_id(`user_groups`.`group_id`) as top_level_group_id
FROM `users`
LEFT JOIN `user_groups`
ON `user_groups`.`user_id` = `users`.`id`
WHERE
`users`.`created`
BETWEEN
DATE_SUB(NOW(), INTERVAL 365 DAY) AND NOW()
I have a Categories table which has some duplicate Categories as described below,
`Categories`
+========+============+============+
| cat_id | cat_name | item_count |
+========+============+============+
| 1 | Category 1 | 2 |
| 2 | Category 1 | 1 |
| 3 | Category 2 | 2 |
| 4 | Category 3 | 1 |
| 5 | Category 3 | 1 |
+--------+------------+------------+
Here is another junction table which relates to another Items table. The item_count in the first table is the total number of items per cat_id.
`Junction`
+========+=========+
| cat_id | item_id |
+========+=========+
| 1 | 100 |
| 1 | 101 |
| 2 | 102 |
| 3 | 103 |
| 3 | 104 |
| 4 | 105 |
| 5 | 106 |
+--------+---------+
How do I add or combine those items from the duplicate Categories into ones each having maximum item_count among their duplicates? (e.g. Category 1).
Also, if the item_count is the same for those duplicate ones, then the Category with maximum cat_id will be chosen and item_count will be combined to that record. (e.g. Category 3).
Note: Instead of removing the duplicate records, the item_count will
be set to 0.
Below is the expected result.
+========+============+============+
| cat_id | cat_name | item_count |
+========+============+============+
| 1 | Category 1 | 3 |
| 2 | Category 1 | 0 |
| 3 | Category 2 | 2 |
| 4 | Category 3 | 0 |
| 5 | Category 3 | 2 |
+--------+------------+------------+
+========+=========+
| cat_id | item_id |
+========+=========+
| 1 | 100 |
| 1 | 101 |
| 1 | 102 |
| 3 | 103 |
| 3 | 104 |
| 5 | 105 |
| 5 | 106 |
+--------+---------+
In the result, there are two duplicates Category 1 and Category 3. And we have 2 scenarios,
cat_id=2 is eliminated because its item_count=1 is less than
that of cat_id=1 which is item_count=2.
cat_id=4 is eliminated even though its item_count is the same
as that of cat_id=5 since 5 is the maximum among duplicate
Category 3.
Please help me if any query that can join and update both tables in order to solve the duplicates.
Here's a SELECT. You can figure out to adapt it to an UPDATE ;-)
I've ignored the jucntion table for simplicity
SELECT z.cat_id
, z.cat_name
, (z.cat_id = x.cat_id) * new_count item_count
FROM categories x
LEFT
JOIN categories y
ON y.cat_name = x.cat_name
AND (y.item_count > x.item_count OR (y.item_count = x.item_count AND y.cat_id > x.cat_id))
LEFT
JOIN
( SELECT a.cat_id, b.*
FROM categories a
JOIN
( SELECT cat_name, SUM(item_count) new_count, MAX(item_count) max_count FROM categories GROUP BY cat_name) b
ON b.cat_name = a.cat_name
) z
ON z.cat_name = x.cat_name
WHERE y.cat_id IS NULL;
+--------+------------+------------+
| cat_id | cat_name | item_count |
+--------+------------+------------+
| 1 | Category 1 | 3 |
| 2 | Category 1 | 0 |
| 3 | Category 2 | 2 |
| 4 | Category 3 | 0 |
| 5 | Category 3 | 2 |
+--------+------------+------------+
DELIMITER $$
DROP PROCEDURE IF EXISTS cursor_proc $$
CREATE PROCEDURE cursor_proc()
BEGIN
DECLARE #cat_id INT;
DECLARE #cat_name VARCHAR(255);
DECLARE #item_count INT;
DECLARE #prev_cat_Name VARCHAR(255);
DECLARE #maxItemPerCategory INT;
DECLARE #maxItemId INT DEFAULT 0;
DECLARE #totalItemsCount INT;
-- this flag will be set to true when cursor reaches end of table
DECLARE exit_loop BOOLEAN;
-- Declare the cursor
DECLARE categories_cursor CURSOR FOR
SELECT select cat_id ,cat_name ,item_count from Categories Order By cat_name, cat_id;
-- set exit_loop flag to true if there are no more rows
DECLARE CONTINUE HANDLER FOR NOT FOUND SET exit_loop = TRUE;
-- open the cursor
OPEN categories_cursor;
-- start looping
categories_loop: LOOP
-- read the name from next row into the variables
FETCH categories_cursor INTO #cat_id, #cat_name, #item_count ;
-- close the cursor and exit the loop if it has.
IF exit_loop THEN
CLOSE categories_loop;
LEAVE categories_loop;
END IF;
IF(#prev_cat_Name <> #cat_name)
THEN
-- Category has changed, set the item_count of the 'best' category with the total items count
IF(#maxItemId > 0)
THEN
UPDATE Categories
SET Categories.item_count=#totalItemsCount
WHERE Categories.cat_id=#maxItemId;
END IF;
-- Reset Values with the actual row values
SET #maxItemPerCategory = #item_count;
SET #prev_cat_Name = #cat_name;
SET #maxItemId = #cat_id
SET #totalItemsCount = #item_count;
ELSE
-- increment the total items count
SET #totalItemsCount = #totalItemsCount + #item_count
-- if the actual row has the maximun item counts, then it is the 'best'
IF (#maxIntPerCategory < #item_count)
THEN
SET #maxIntPerCategory = #item_count
SET #maxItemId = #cat_id
ELSE
-- else, this row is not the best of its Category
UPDATE Categories
SET Categories.item_count=0
WHERE Categories.cat_id=#cat_id;
END IF;
END IF;
END LOOP categories_loop;
END $$
DELIMITER ;
It's not pretty and copied in part from Strawberry's SELECT
UPDATE categories cat,
junction jun,
(select
(z.cat_id = x.cat_id) * new_count c,
x.cat_id newcatid,
z.cat_id oldcatid
from categories x
LEFT
JOIN categories y
ON y.cat_name = x.cat_name
AND (y.item_count > x.item_count OR (y.item_count = x.item_count AND y.cat_id > x.cat_id))
LEFT
JOIN
( SELECT a.cat_id, b.*
FROM categories a
JOIN
( SELECT cat_name, SUM(item_count) new_count, MAX(item_count) max_count FROM categories GROUP BY cat_name) b
ON b.cat_name = a.cat_name
) z
ON z.cat_name = x.cat_name
WHERE
y.cat_id IS NULL) sourceX
SET cat.item_count = sourceX.c, jun.cat_id = sourceX.newcatid
WHERE cat.cat_id = jun.cat_id and cat.cat_id = sourceX.oldcatid
I think it's better to do what you want one step at time:
First, get data you need:
SELECT Max(`cat_id`), sum(`item_count`) FROM `Categories` GROUP BY `cat_name`
With these data you'll be able to check if update was correctly done.
Then, with a loop on acquired data, update:
update Categories set item_count =
(
Select Tot FROM (
Select sum(`item_count`) as Tot
FROM `Categories`
WHERE `cat_name` = '#cat_name') as tmp1
)
WHERE cat_id = (
Select MaxId
FROM (
select max(cat_id) as MaxId
FROM Categories
WHERE `cat_name` = '#cat_name') as tmp2)
Pay attention, if you run twice this code the result will be wrong.
Finally, set others Ids to 0
UPDATE Categories set item_count = 0
WHERE `cat_name` = '#cat_name'
AND cat_id <> (
Select MaxId
FROM (
select max(cat_id) as MaxId
FROM items
WHERE `cat_name` = '#cat_name0') as tmp2)
I have a table with fields like this
lev_1_id,lev_1_seq,lev_1_new_seq,lev_2_id,lev_2_seq,lev_2_new_seq
284e777e,1,null,b4dce5bb,1,null<br>
284e777e,1,null,dfd158ed,2,null<br>
284e777e,1,null,fedbf511,3,null<br>
0c7e0938,2,null,2333f431,1,null<br>
0c7e0938,2,null,808734fa,2,null<br>
0c7e0938,2,null,2504e0de,3,null<br>
And now I want to update the lev_1_new_seq and, lev_2_new_seq by reversing the values in lev_1_seq and lev_2_seq respectively.
After updating the fields, the table should look like this:
lev_1_id,lev_1_seq,lev_1_new_seq,lev_2_id,lev_2_seq,lev_2_new_seq
284e777e,1,2,b4dce5bb,1,3<br>
284e777e,1,2,dfd158ed,2,2<br>
284e777e,1,2,fedbf511,3,1<br>
0c7e0938,2,1,2333f431,1,3<br>
0c7e0938,2,1,808734fa,2,2<br>
0c7e0938,2,1,2504e0de,3,1<br>
can anyone help me with updating the fields?
Thanks in advance!
This solution works if initial ordering was done by id. 2 - the column you updating, 1 - column that needs to be in reverse order, tmp - table, id - unique key, initial sorting column.
BEGIN
DECLARE p INT;
DECLARE v INT;
DECLARE n INT DEFAULT 0;
DECLARE i INT DEFAULT 0;
SELECT COUNT(*) FROM `tmp` INTO n;
SET i=0;
WHILE i<n DO
select `1` from `tmp` order by `id` desc limit i,1 into p;
select `id` from `tmp` order by `id` limit i,1 into v;
update `tmp` set `2` = p where `id` = v;
SET i = i + 1;
END WHILE;
End
For your table: 1 = lev_1_seq, 2 = lev_1_new_seq, id = lev_1_id
It appears that you should be able to calculate the "new" sequences by deducting the exiting sequences from the relevant maximums + 1.
e.g. the max(lev_1_seq) + 1 = 3, so for an existing value of 2: 3-2 = 1, for an existing value of 1: 3-1 = 2
UPDATE table1 tu
JOIN (
SELECT
t1.lev_1_id
, m1.lev1maxseq
, MAX(t1.lev_2_seq) + 1 lev2maxseq
FROM table1 t1
CROSS JOIN (
SELECT
MAX(lev_1_seq) + 1 lev1maxseq
FROM table1
) m1
GROUP BY
t1.lev_1_id
, m1.lev1maxseq
) nv on tu.lev_1_id = nv.lev_1_id
SET
tu.lev_1_new_seq = (nv.lev1maxseq - tu.lev_1_seq)
, tu.lev_2_new_seq = (nv.lev2maxseq - tu.lev_2_seq)
;
see this sqlfiddle
results:
| lev_1_id | lev_1_seq | lev_1_new_seq | lev_2_id | lev_2_seq | lev_2_new_seq |
|----------|-----------|---------------|----------|-----------|---------------|
| 284e777e | 1 | 2 | b4dce5bb | 1 | 3 |
| 284e777e | 1 | 2 | dfd158ed | 2 | 2 |
| 284e777e | 1 | 2 | fedbf511 | 3 | 1 |
| 0c7e0938 | 2 | 1 | 2333f431 | 1 | 3 |
| 0c7e0938 | 2 | 1 | 808734fa | 2 | 2 |
| 0c7e0938 | 2 | 1 | 2504e0de | 3 | 1 |