I got a little problem with my recursive query.
I got a database of menu of a bar.
We got: Category, each category got sub-categories and each-subcategories got multiple items.
The database is this one and the query is linked inside:
CREATE TABLE category (
id int(10) unsigned NOT NULL AUTO_INCREMENT,
title varchar(255) NOT NULL,
parent_id int(10) unsigned DEFAULT NULL,
PRIMARY KEY (id),
FOREIGN KEY (parent_id) REFERENCES category (id)
ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE TABLE `items` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`cat_id` int unsigned DEFAULT NULL,
`parent_id` int unsigned DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
PRIMARY KEY (`id`),
KEY `cat_id` (`cat_id`),
KEY `sub_id` (`parent_id`),
CONSTRAINT `cat_id` FOREIGN KEY (`cat_id`) REFERENCES `category` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `sub_id` FOREIGN KEY (`parent_id`) REFERENCES `category` (`parent_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
BEGIN;
INSERT INTO `category` VALUES (1, 'Colazione', NULL);
INSERT INTO `category` VALUES (2, 'Pranzo', NULL);
INSERT INTO `category` VALUES (3, 'Primi piatti', 2);
INSERT INTO `category` VALUES (4, 'Second dish', 2);
INSERT INTO `category` VALUES (5, 'Other things for lunch', 2);
COMMIT;
-- ----------------------------
-- Records of items
-- ----------------------------
BEGIN;
INSERT INTO `items` VALUES (1, 1, NULL, 'Cornetto');
INSERT INTO `items` VALUES (2, 3, 2, 'Pasta al sugo 1');
INSERT INTO `items` VALUES (3, 3, 2, 'Pasta al sugo 2');
INSERT INTO `items` VALUES (4, 3, 2, 'Pasta al sugo 3');
INSERT INTO `items` VALUES (5, 3, 2, 'Pasta al sugo 1 X');
INSERT INTO `items` VALUES (6, 3, 2, 'Pasta al sugo 2 X');
INSERT INTO `items` VALUES (7, 4, 2, 'Pasta al sugo 3 X');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
Query:
with combine_trees as (
with make_tree as (
WITH RECURSIVE category_path AS
(
SELECT id, title, parent_id
FROM category
WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.title, c.parent_id
FROM category_path AS cp JOIN category AS c
ON cp.id = c.parent_id
)
SELECT cp.title, cp.id,
if(cp.id = category.id,
json_arrayagg(json_object('item_name', it.name)),
json_object(cp.title, json_object('items',json_arrayagg(json_array(json_object('item_name', it.name))))))
as tree
FROM category_path cp
INNER JOIN items it ON it.cat_id = cp.id
join category on category.id = ifnull(cp.parent_id, cp.id)
group by cp.title, cp.id, category.id
)
select json_arrayagg(json_object(title, json_array('items', tree))) output_json from make_tree group by id
)
select json_object('menu',group_concat(output_json)) as output from combine_trees;
https://sqlize.online/
The problem is that its not printing the result as JSON but its printing it formatted in one-string. How can we transform it in a JSON without that all the output is an unique string?
In your last line,
select json_object('menu',group_concat(output_json)) as output from combine_trees
you cannot use group_concat to combine the json arrays you get from the 2nd to last line (e.g. select json_arrayagg(...) output_json from make_tree group by id).
The arrays you get there each look like [...], and group_concat will give you [...], [...]. This is not a valid json array (which would need additional brackets around it, e.g. [[...],[...]]), but a string, and creating a json object from it will give you, well, that string as the value.
To combine your json arrays, you can use (as you did before) json_arrayagg instead of group_concat, e.g.
select json_object('menu',json_arrayagg(output_json)) as output from combine_trees
Related
This question is about selecting data across many-to-many relations in MySQL. Is related to another two questions, but with some differences:
MySQL: query with two many to many relations and duplicates
MySQL: query with two many to many relations and duplicates, with full data from subqueries
Those questions used a simple mockup database with simple many to many relations:
article
article_author
author
article_tag
tag
Now I will introduce next level of complexity. We want each author to be able to tag each of their articles. Thus, we will connect tags to the intermediate table article_author instead of to author directly.
article
article_author
author
article_author_tag
tag
Here is in MySQL:
CREATE TABLE `article` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `author` (
`id` INT NOT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
);
CREATE TABLE `tag` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `article_author` (
`id` int NOT NULL AUTO_INCREMENT,
`author_id` INT NOT NULL,
`article_id` int NOT NULL,
`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_index` (`author_id`,`article_id`),
KEY `fk_article_author_author1_idx` (`author_id`),
KEY `fk_article_author_article1_idx` (`article_id`),
CONSTRAINT `fk_article_author_article1` FOREIGN KEY (`article_id`) REFERENCES `article` (`id`),
CONSTRAINT `fk_article_author_author1` FOREIGN KEY (`author_id`) REFERENCES `author` (`id`)
);
CREATE TABLE `article_author_tag` (
`article_author_id` int NOT NULL,
`tag_id` int NOT NULL,
PRIMARY KEY (`article_author_id`,`tag_id`),
KEY `fk_article_author_tag_article_author1_idx` (`article_author_id`),
KEY `fk_article_author_tag_tag1_idx` (`tag_id`),
CONSTRAINT `fk_article_author_tag_article_author1` FOREIGN KEY (`article_author_id`) REFERENCES `article_author` (`id`),
CONSTRAINT `fk_article_author_tag_tag1` FOREIGN KEY (`tag_id`) REFERENCES `tag` (`id`)
);
INSERT INTO article (id, name) VALUES (1, 'first article'), (2, 'second article');
INSERT INTO `author` (id, name) VALUES (1, 'first author'), (2, 'second author');
INSERT INTO tag (id, name) VALUES (1, 'first tag'), (2, 'second tag');
INSERT INTO article_author (author_id, article_id) VALUES (1, 1), (2, 1);
INSERT INTO article_author_tag (article_author_id, tag_id) VALUES (1, 1), (1, 2), (2, 1), (2, 2);
And now, I want just to select the tags that authors of an article used to tag it, as a JSON array; but I can't get rid of duplicates:
SELECT
JSON_ARRAYAGG(tag.id)
FROM article_author
JOIN article_author_tag ON article_author_tag.article_author_id = article_author.id
JOIN tag ON article_author_tag.tag_id = tag.id
WHERE article_author.article_id = 1;
Here it is in a db<>fiddle: https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=253f30ecd2f87b06c3894ef02b2ee35d
Any idea how can I get rid of them?
Edit:
I can do it with CONCAT and GROUP_CONCAT, and then casting to JSON. But it looks quite hacky:
SELECT
CAST(CONCAT('[', GROUP_CONCAT(DISTINCT tag.id SEPARATOR ','), ']') AS JSON) AS tags
FROM article_author
JOIN article_author_tag ON article_author_tag.article_author_id = article_author.id
JOIN tag ON article_author_tag.tag_id = tag.id
WHERE article_author.article_id = 1;
Here it is in a db<>fiddle: https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=20087a9036acb00637be8d2f58747ba5
Any other idea will be welcome!
There is no distinct functionality for json yet (something like JSON_ARRAYAGG(distinct tag.id)), but there is a common workaround for it:
SELECT JSON_EXTRACT(JSON_OBJECTAGG(tag.id,tag.id),"$.*")
FROM article_author
JOIN article_author_tag ON article_author_tag.article_author_id = article_author.id
JOIN tag ON article_author_tag.tag_id = tag.id
WHERE article_author.article_id = 1;
JSON_OBJECTAGG works as an implict distinct, because json tags are distinct by definition, so adding {"1": 1} twice results in just one of those remaining. Afterwards, you JSON_EXTRACT just the values to get the format you intended (e.g. without the artificially added tags).
Another method would be to feed the json function with the already correct, distinct data:
SELECT JSON_ARRAYAGG(id)
FROM (
SELECT distinct tag.id
FROM article_author
JOIN article_author_tag
ON article_author_tag.article_author_id = article_author.id
JOIN tag ON article_author_tag.tag_id = tag.id
WHERE article_author.article_id = 1
) subquery;
You first prepare the data the way you want it (e.g. the distinct tag-ids), then use JSON_ARRAYAGG to format your output.
Hello,
I have 3 tables:
CREATE TABLE `invoice` (
`id` int(11) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `invoice` (`id`) VALUES
(1),
(2),
(3);
CREATE TABLE `invoice_deduction` (
`id` int(11) NOT NULL,
`invoiceId` int(11) NOT NULL,
`deductionId` int(11) NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (`invoiceId`) REFERENCES `invoice` (`id`),
FOREIGN KEY (`deductionId`) REFERENCES `invoice` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `invoice_deduction` (`id`, `invoiceId`, `deductionId`) VALUES
(1, 2, 1),
(2, 3, 1),
(3, 3, 2);
CREATE TABLE `invoice_item` (
`id` int(11) NOT NULL,
`invoiceId` int(11) NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (`invoiceId`) REFERENCES `invoice` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `invoice_item` (`id`, `invoiceId`) VALUES
(1, 1),
(2, 1),
(3, 1),
(4, 2),
(5, 2),
(6, 3),
(7, 3),
(8, 3);
For example:
The first invoice contains 3 items (total: 3 items)
The second invoice contains 3 items from first invoice (deduction) and 2 new items (total: 5 items)
The third invoice contains 3 items from first invoice (deduction), 2 items from second invoice (deduction) and 3 new items (total: 8 items)
So I want to have a query with this result:
id | count of items (with deductions)
3 | 8
2 | 5
1 | 3
I start with this query:
SELECT
i.id, COUNT(*) as countItems
FROM
invoice i JOIN invoice_item it ON i.id = it.invoiceId
GROUP BY
it.invoiceId
ORDER BY
countItems
DESC
I thank you in advance for your help.
I think you want a cumulative sum:
SELECT i.id, COUNT(*) as countItems,
SUM(COUNT(*)) OVER (ORDER BY it.id)
FROM invoice i JOIN
invoice_item it
ON i.id = it.invoiceId
GROUP BY i.id
ORDER BY countItems;
I have no idea why you are using a LEFT JOIN. I don't know why you would have invoiceId values that do not match a valid invoice. So I switched it to an inner join.
I have this query:
SELECT options.id, options.text,
COUNT(options2.id) AS num_children
FROM options
JOIN history_uids AS uids
ON uids.uid = options.id
LEFT JOIN options AS options2
ON (options.id = options2.id_parent)
LEFT JOIN history_uids AS uids1
ON uids1.uid = options2.id
WHERE (
options.id = 25
AND (uids1.active = 1 OR
options2.id IS NULL) # Problem
)
GROUP BY `options`.`id`
There is an options table, and another, history_uids, which has a column named uid with the id of every options, and an active column set to 1 or 0.
I am expecting to get a result with:
The ID and the text of the option which has the id 25, and which has active set to 1 in history_uids.
The number of options which have an id_parent equal to the id (25) and for which active in history_uids is set to 1
So whatever is this number I should get my row if it has active set to 1. I cannot understand how to achieve this: I wanna set as last condition uids1.active = 1 OR "options2 doesn't exist", but to get this I should have my active condition in the ON of options2, which is not possible because at that moment the table history_uids is not yet referenced...
In my case the row has active set to 1 and has 5 children with active set to 0, so I should get my row with num_children set at 0. Instead whatever combination I do in the JOIN or the WHERE I get either num_children set to 5 or no result at all.
Thanks for your help (and for reading!)
Here is the full structure and data for testing:
CREATE TABLE `history_uids` (
`uid` int(10) NOT NULL,
`active` tinyint(1) NOT NULL DEFAULT '1'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `history_uids` (`uid`, `active`) VALUES
(1, 1),
(2, 0),
(3, 0),
(4, 0),
(5, 0),
(6, 0);
CREATE TABLE `options` (
`id` int(10) NOT NULL,
`id_parent` int(10) DEFAULT NULL,
`text` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `options` (`id`, `id_parent`, `text`) VALUES
(1, NULL, 'parent active'),
(2, 1, 'child 1 inactive'),
(3, 1, 'child 2 inactive'),
(4, 1, 'child 3 inactive'),
(5, 1, 'child 4 inactive'),
(6, 1, 'child 5 inactive');
ALTER TABLE `history_uids`
ADD PRIMARY KEY (`uid`);
ALTER TABLE `options`
ADD PRIMARY KEY (`id`),
ADD KEY `id_parent` (`id_parent`);
ALTER TABLE `options`
ADD CONSTRAINT `ibfk_3` FOREIGN KEY (`id_parent`) REFERENCES `options` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `ibfk_4` FOREIGN KEY (`id`) REFERENCES `history_uids` (`uid`) ON DELETE CASCADE ON UPDATE CASCADE;
You need to put the active test for the child row into the ON clause, so that inactive rows will be filtered out of the LEFT JOIN. And you should be counting uids1.uid, not options2.id, since the inactive rows aren't filtered out until you join with uids1.
SELECT options.id, options.text,
COUNT(uids1.uid) AS num_children
FROM options
JOIN history_uids AS uids
ON uids.uid = options.id
LEFT JOIN options AS options2
ON (options.id = options2.id_parent)
LEFT JOIN history_uids AS uids1
ON uids1.uid = options2.id and uids1.active = 1
WHERE
uids.active = 1
GROUP BY `options`.`id`
DEMO
I am working on comment system, I have to count all replies of a single comment on several levels.
Like this:
Parent
->child
-> child
Parent
-> child
-> child
->child
My Sql is :
CREATE TABLE IF NOT EXISTS `comment` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'This is primary key of the table',
`parent_id` bigint(11) NOT NULL,
`content` text NOT NULL,
PRIMARY KEY (`comment_id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=unicode_ci AUTO_INCREMENT=8 ;
INSERT INTO `comments` (`id`, parent_id`, `content`) VALUES
(1, 0, 'Parent'),
(2, 1, 'child'),
(3, 2, 'child'),
(4, 3, 'child'),
(5, 1, 'child2'),
(6, 0, 'Parent2'),
(7, 6,'child of parent2');
Try below query:
select count(*)
from comments c0
join comments c1 on c0.id = c1.parentid
-- in case if child comment doesn't have any children, we still need to keep it
left join comments c2 on c1.id = c2.parentid
where c0.id = 1 --particular id for which we want to count children
I have two tables: categories and items. i have stored categories using nested set structure. Categories have items. Items can be only added to leaf nodes of a root category.
For eg:
Categories
Vehicles
Bikes
Bajaj
Automobiles
Art & Antiques
Amateur Art
Items can be added to category Bajaj, Automobiles and Amateur Art in this case.
Lets say there are 2 items inside Bajaj, 5 items inside Automobiles, 2 inside Amateur Art
For root level categories I want to display as follow:
- Vehicles (7 items)
- Art & Antiques (2 items)
How can I do this ?
Here is the sql dump to work with some sample data
--
-- Table structure for table `categories`
--
CREATE TABLE IF NOT EXISTS `categories` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`parent_id` int(11) DEFAULT NULL,
`title` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
`lft` int(11) NOT NULL,
`lvl` int(11) NOT NULL,
`rgt` int(11) NOT NULL,
`root` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `IDX_3AF34668727ACA70` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=28 ;
--
-- Dumping data for table `categories`
--
INSERT INTO `categories` (`id`, `parent_id`, `title`, `lft`, `lvl`, `rgt`, `root`) VALUES
(22, NULL, 'Vehicles', 1, 0, 8, 22),
(23, 22, 'Bikes', 2, 1, 5, 22),
(24, 23, 'Bajaj', 3, 2, 4, 22),
(25, 22, 'Automobiles', 6, 1, 7, 22),
(26, NULL, 'Art & Antiques', 1, 0, 4, 26),
(27, 26, 'Amateur Art', 2, 1, 3, 26);
-- --------------------------------------------------------
--
-- Table structure for table `items`
--
CREATE TABLE IF NOT EXISTS `items` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`category_id` int(11) NOT NULL,
`title` varchar(100) NOT NULL,
PRIMARY KEY (`id`),
KEY `FK_403EA91BA33E2D84` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=5 ;
--
-- Dumping data for table `items`
--
INSERT INTO `items` (`id`, `category_id`, `title`) VALUES
(1, 24, 'Pulsor 150 cc'),
(2, 24, 'Discover 125 cc'),
(3, 27, 'Art of dream'),
(4, 25, 'Toyota Car');
--
-- Constraints for dumped tables
--
--
-- Constraints for table `categories`
--
ALTER TABLE `categories`
ADD CONSTRAINT `FK_3AF34668727ACA70` FOREIGN KEY (`parent_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL;
--
-- Constraints for table `items`
--
ALTER TABLE `items`
ADD CONSTRAINT `FK_403EA91BA33E2D84` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE CASCADE;
root nodes have NULL in the field of parent_id
Update:
I was able to fetch for roots using this query:
SELECT c.id,c.title,cte.co FROM categories c
JOIN
(SELECT
c0_.id,c0_.root,COUNT(i.id) co
FROM
categories c0_
JOIN items i ON c0_.id=i.category_id
WHERE c0_.rgt = 1 + c0_.lft
GROUP BY c0_.id
) cte
ON cte.root=c.id
WHERE c.parent_id is null
The above query works for root level category. Now when the user clicks on root level category, I want to do the same.
for eg when somebody clicks on vehicles I should get:
Bikes (2)
Automobiles (5)
For that I tried :
SELECT c.id,c.title,cte.co FROM categories c
JOIN
(SELECT
c0_.id,c0_.root,COUNT(i.id) co
FROM
categories c0_
JOIN items i ON c0_.id=i.category_id
WHERE
c0_.rgt = 1 + c0_.lft
GROUP BY c0_.id
) cte
ON cte.root=c.id
WHERE c.parent_id=1
This returned empty result set. what is wrong in this query ?
SELECT parent.title,
( SELECT count(i.id) count FROM items i
WHERE category_id IN
(
SELECT child.id FROM categories child WHERE child.lft>=parent.lft AND
child.rgt<=parent.rgt AND child.root=parent.root
)
)
FROM categories parent
WHERE parent.parent_id=#parent_id;
Please inform me if this does not work
How about something like this:
SELECT COUNT(items.id),
(SELECT lookup.title
FROM categories lookup
WHERE lookup.id = categories.root)
FROM items, categories
WHERE categories.id = items.category_id
GROUP BY categories.root;
based on the input from the script above gives me:
3 | Vehicles
1 | Art & Antiques
to select for a particular root add
AND categories.root = #id
where #id is your root id you're looking for.
Alternatively if you want to select by the root name do something (scary) like this:
SELECT title, total
FROM
(SELECT COUNT(items.id) total,
(SELECT lookup.title
FROM categories lookup
WHERE lookup.id = categories.root) title
FROM items, categories
WHERE categories.id = items.category_id
GROUP BY categories.root;
) AS some_table
WHERE some_table.title = #root_name
where #root_name is the name of your root node (in quotes of course)
Something like this should work.
select c1.title, count(*) itemcount
from categories c1 join categories c2
on c2.parent_id = c1.id
join items on items.category_id = c2.id
group by c1.title