Hierarchy based on three tables - mysql

I would like to have rows like a single table like the following
id name parentId
My current tables are (keys between them are foreign keys)
category
id name
which is the parent of all
and
subcategory
id name catId
and the last table which is activity
activity
id name subcatId
For the parentId of category table, it would be nothing as category is the parent of all
My attempt so far have been unsuccessful
Sample Data
category
C-1 HR null
C-2 Development null
subcategory
SC-1 Hiring C-1
SC-2 Admin C-1
SC-3 Developer C-2
activity
A-1 College Hiring SC-1
A-2 Job Fair SC-1
A-3 Java Development SC-3
Result Needed
1 HR null
2 Development null
3 Hiring C-1
4 Admin C-1
5 Developer C-2
6 College SC-1
7 Job Fair SC-1
8 Java Development SC-3
I hope it is clearer. If you need any further information, please let me know
Thanks
my attempt on 2 tables
select name
from (
select c.name
from category c
union all
select b.name
from subcategory b
inner join category ca
on ca.id = b.parentId
)
I get an error saying
Every derived table must have its own alias
Do I need to add the following lines to my query
start with parent_id is null
connect by prior id = parent_id

Try something like this:
SELECT #id := #id + 1 AS id, name, parent_id
FROM ( SELECT name, NULL AS parent_id FROM category
UNION ALL
SELECT id, name, catId FROM subcategory
UNION ALL
SELECT id, name, parentId FROM activity
) q,
( SELECT #id := 0 ) w;
Used UNION ALL to concatenate the results from multiple queries.
In UNION the column names of the first query is used as column names of the result set.
Used variable #id and the assignment operator := to generate the calculated values of the column ID.
Be advised: If you use LIMIT or OFFSET, the values of column id would not be coherent.

Actually, like this should solve your problem:
select name
from (
select c.name
from category c
union all
select b.name
from subcategory b
inner join category ca
on ca.id = b.parentId
) A -- alias is required for sub-query.
You can set anything as alias provided that it won't conflict with some of MySQL reserved word. You can set like this as well ) AS A.
For you situation, I don't think it's necessary to do sub-query or inner join. If you really want the result to be like that you can just use union all:
SELECT name,'' as ID from category
UNION ALL
SELECT name,catId FROM subcategory
UNION ALL
SELECT name,subcatId FROM activity;

Related

MySQL: How to use partial matches across rows to set the final value to NULL?

I have a table with partial duplicate rows and I want to use the partial matches across the rows to set a value to NULL.
In this particular case, if the 'Name', 'Age', and 'Profession' columns are the same and the 'School' column is different, I want to set the 'School' column to NULL.
This is what my table looks like now:
Name Age Profession Salary School
John 21 Teacher 50,000 A
Lisa 24 Engineer 75,000 B
John 21 Teacher 55,000 C
This is what I want it to look like:
Name Age Profession Salary School
John 21 Teacher 50,000 NULL
Lisa 24 Engineer 75,000 B
John 21 Teacher 55,000 NULL
How can I create a query that does this? Thanks!
Ideally there should be some kind of primary key in your table. That lacking, we can try the following update join approach:
UPDATE yourTable t1
INNER JOIN
(
SELECT Name, Age, Profession
FROM yourTable
GROUP BY Name, Age, Profession
HAVING COUNT(*) > 1
) t2
ON t2.Name = t1.Name AND
t2.Age = t1.Age AND
t2.Profession = t1.Profession
SET
School = NULL;
SELECT Name, Age, Profession, Salary,
CASE WHEN COUNT(School) OVER (PARTITION BY Name, Age, Profession) = 1
THEN School
END AS School
FROM sourcetable

How to use distinct inside concat_ws() in mysql?

In mySQL I have a table:
id
name
code1
code2
code3
1
Jim
aaa
aaa
a
2
Ryan
bb
bbb
b
3
Ted
c
c
cc
expected output:
id
name
concat_code
1
Jim
aaa/a
2
Ryan
bb/bbb/b
3
Ted
c/cc
I tried the below query:
select
id,
name,
concat_ws("/",NULLIF(code1,""),NULLIF(code2,""),NULLIF(code3,"")) as concat_code
from
table1
group by id
But it gives wrong output:
id
name
concat_code
1
Jim
aaa/aaa/a
2
Ryan
bb/bbb/b
3
Ted
c/c/cc
How can I use the distinct combination inside concat_ws()?
This is one example of why having columns foo1, foo2, etc is bad database design; there should be a separate table in a one to many relationship with the original table with one foo value in each row. As is, you have to basically emulate that with unions:
select id, name, group_concat(distinct codes.code separator '/')
from table1
join (
select id,NULLIF(code1,"") as code from table1
union all
select id,NULLIF(code2,"") from table1
union all
select id,NULLIF(code3,"") from table1
) codes using (id)
group by id
or use a convoluted expression for code2, code3, etc that returns null if the code matches any of the previous codes in your concat_ws call.
Bit late, but for other people googling: try:
SELECT id, name,
CONCAT_WS("/",
NULLIF(code1,""),
CASE WHEN code2 NOT IN ('', code1) THEN code2 END,
CASE WHEN code3 NOT IN ('', code1, code2) THEN code3 END
) AS concat_code
FROM table1
The CASE is functioning as a NULLIF with multiple string values, some being the previous values to prevent duplicates.
I left out the GROUP BY id since id seems to be the primary key, that makes that statement useless.

Count group by enum including possible enum values that have 0 count

I have a table of items. One of the fields is a category (represented by an enum). Some categories have zero items.
So I did this:
select category, count(*) as total from items group by category;
+------------+-------+
| category | total |
+------------+-------+
| one | 6675 |
+------------+-------+
I want to generate a table like this (where two is the other possible enum value):
+------------+-------+
| category | total |
+------------+-------+
| one | 6675 |
+------------+-------+
| two | 0 |
+------------+-------+
How do I do this with an mysql SQL query?
Enum datatype is generally preferred for those cases where possible options (values) are not too many (prefer <= 10), and you are not going to add new options in future (atleast not very frequently). So, a good use-case for Enum is gender: (m, f, n). In your case, it would be generally better to have a Master table of all possible Categories, instead of using Enum for them. Then it is easier to do a LEFT JOIN from the Master table.
However, as asked by you:
A solution uses the enum type to generate the table, and includes 0
entries
Works for all MySQL/MariaDB versions:
We will need to get the list of all possible Enum values from INFORMATION_SCHEMA.COLUMNS:
SELECT
SUBSTRING(COLUMN_TYPE, 6, CHAR_LENGTH(COLUMN_TYPE) - 6) AS enum_values
FROM
information_schema.COLUMNS
WHERE
TABLE_NAME = 'items' -- your table name
AND
COLUMN_NAME = 'category' -- name of the column
AND
TABLE_SCHEMA = 'your_db' -- name of the database (schema)
But then, this query will give you all the enum values in comma-separated string, like below:
'one','two','three','four'
Now, we will need to convert this string into multiple rows. To achieve that, we can use a Sequence (Number series) table. You can define a permanent table in your database storing integers ranging from 1 to 100 (you may find this table helpful in many other cases as well) (OR, another approach is to use a Derived Table - check this to get an idea: https://stackoverflow.com/a/58052199/2469308).
CREATE TABLE seq (n tinyint(3) UNSIGNED NOT NULL, PRIMARY KEY(n));
INSERT INTO seq (n) VALUES (1), (2), ...... , (99), (100);
Now, we will do a JOIN between "enum values string" and seq table, based on the position of comma, to extract enum values into different rows. Note that instead of just using , (comma) to extract enum values, we would use ',' (to avoid cases when there might be a comma inside the value string). String operations utilizing Substring_Index(), Trim(), Char_Length() etc functions can be used to extract enum values. You can check this answer to get a general idea about this technique:
Schema (View on DB Fiddle)
CREATE TABLE items
(id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
category ENUM('one','two','three','four'),
item_id INT UNSIGNED) ENGINE=InnoDB;
INSERT INTO items (category, item_id)
VALUES ('one', 1),
('two', 2),
('one', 2),
('one', 3);
CREATE TABLE seq (n tinyint(3) UNSIGNED NOT NULL,
PRIMARY KEY(n));
INSERT INTO seq (n) VALUES (1),(2),(3),(4),(5);
Query #1
SELECT Trim(BOTH '\'' FROM Substring_index(Substring_index(e.enum_values,
'\',\'',
seq.n),
'\',\'', -1)) AS cat
FROM (SELECT Substring(column_type, 6, Char_length(column_type) - 6) AS
enum_values
FROM information_schema.columns
WHERE table_name = 'items'
AND column_name = 'category'
AND table_schema = 'test') AS e
JOIN seq
ON ( Char_length(e.enum_values) - Char_length(REPLACE(e.enum_values,
'\',\'',
''))
) / 3 >= seq.n - 1
| cat |
| ----- |
| one |
| two |
| three |
| four |
Now, the hard part is done. All we need to do is do a LEFT JOIN from this subquery (having all category enum values) to your items table, to get Count per category.
The final query follows (View on DB Fiddle):
SELECT all_cat.cat AS category,
Count(i.item_id) AS total
FROM (SELECT Trim(BOTH '\'' FROM Substring_index(
Substring_index(e.enum_values,
'\',\'',
seq.n),
'\',\'', -1)) AS cat
FROM (SELECT Substring(column_type, 6, Char_length(column_type) - 6)
AS
enum_values
FROM information_schema.columns
WHERE table_name = 'items'
AND column_name = 'category'
AND table_schema = 'test') AS e
JOIN seq
ON ( Char_length(e.enum_values) - Char_length(
REPLACE(e.enum_values,
'\',\'',
''))
) / 3 >= seq.n - 1) AS all_cat
LEFT JOIN items AS i
ON i.category = all_cat.cat
GROUP BY all_cat.cat
ORDER BY total DESC;
Result
| category | total |
| -------- | ----- |
| one | 3 |
| two | 1 |
| three | 0 |
| four | 0 |
Here is some fun with MySQL 8.0 and JSON_TABLE():
select c.category, count(i.category) as total
from information_schema.COLUMNS s
join json_table(
replace(replace(replace(trim('enum' from s.COLUMN_TYPE),'(','['),')',']'),'''','"'),
'$[*]' columns (category varchar(50) path '$')
) c
left join items i on i.category = c.category
where s.TABLE_SCHEMA = 'test' -- replace with your db/schema name
and s.TABLE_NAME = 'items'
and s.COLUMN_NAME = 'category'
group by c.category
It converts the ENUM type definition from information_schema to a JSON array, which is then converted by JSON_TABLE() to a table, which you then can use for a LEFT JOIN.
See demo on db-fiddle
Note: The categories should not contain any characters from ()[]'".
But seriously – Just create the categories table. There are more reasons to do that. For example you might want to render a drop-down menu with all possible categories. That would be simple with
select category from categories
I would say that it's basically bad practice to encode your enumerations into the script. Therefore, create a table with the enumerations present (and their relative keys), then it's a simple case of grouping a left joined query...
SELECT
cat.enum_name,
COUNT(data.id) AS total
FROM
category_table cat
LEFT JOIN
data_table data
ON cat.cate_id = data.cat_id
GROUP BY
cat.enum_name
using in-select subquery
select cat.categoryname
(
select count(*) -- count total
from items as i
where i.category = cat.category -- connect
) as totalcount
from cat
order by cat.categoryname
You can make a fictive dataset of the different categories and do a left join with your original table as shown below.
SELECT A.category, count(*) total FROM
(SELECT 'one' as Category
UNION ALL
SELECT 'two' as Category) A
LEFT JOIN items B
ON A.Category=B.Category
GROUP BY B.Category;
If you would prefer to get a list of all the category dynamically, then save them in another table (say All_category_table) then do a join as shown below:
SELECT A.category, count(*) total FROM
(SELECT Category FROM All_category_table) A
LEFT JOIN items B
ON A.Category=B.Category
GROUP BY B.Category;
This answer is applicable for when you do not have another table holding the possible category values.
Let's say you have a table called real_table with a not null & value constrained column category. In this column you know you can theoretically encounter 5 different values: 'CATEGORY_0', 'CATEGORY_1', 'CATEGORY_2', 'CATEGORY_3', 'CATEGORY_4':
CREATE TABLE real_table
(
id VARCHAR(255) NOT NULL
PRIMARY KEY,
category VARCHAR(255) NOT NULL
CONSTRAINT category_in CHECK (
category in ('CATEGORY_0',
'CATEGORY_1',
'CATEGORY_2',
'CATEGORY_3',
'CATEGORY_4')
)
);
But your actual data set in the table does not include any row with value 'CATEGORY_0'. So when you run a query such as:
SELECT real_table.category AS category, COUNT(*) AS cnt
FROM real_table
GROUP BY real_table.category;
you will see, that you get result like this:
category
cnt
CATEGORY_1
150
CATEGORY_2
20
CATEGORY_3
12
CATEGORY_4
1
Hmm, the 'CATEGORY_0' is omitted. Not good.
Since your categories are not backed by another table, then you must create an artificial dataset of the possible categories that looks as below:
SELECT 'CATEGORY_0' AS category_entry
UNION ALL
SELECT 'CATEGORY_1' AS category_entry
UNION ALL
SELECT 'CATEGORY_2' AS category_entry
UNION ALL
SELECT 'CATEGORY_3' AS category_entry
UNION ALL
SELECT 'CATEGORY_4' AS category_entry;
You can use this in your original query as a table to do a right join on:
SELECT all_categories.category_entry AS category,
COUNT(real_table.id) AS cnt -- important to count some non-null value, such as PK of the real_table
FROM real_table
RIGHT JOIN
(SELECT 'CATEGORY_0' AS category_entry -- not present in any row in table 'all_categories'
UNION ALL
SELECT 'CATEGORY_1' AS category_entry
UNION ALL
SELECT 'CATEGORY_2' AS category_entry
UNION ALL
SELECT 'CATEGORY_3' AS category_entry
UNION ALL
SELECT 'CATEGORY_4' AS category_entry) all_categories
ON real_table.category = all_categories.category_entry
GROUP BY all_categories.category_entry;
Now when you run the query, you should get the desired output:
category
cnt
CATEGORY_0
0
CATEGORY_1
150
CATEGORY_2
20
CATEGORY_3
12
CATEGORY_4
1
The 'CATEGORY_0' is now included with zero cnt. Nice.
Now let's say that the category column is not not null constrained and can also possibly include some other unexpected category values (e.g. 'CATEGORY_66'):
CREATE TABLE real_table
(
id VARCHAR(255) NOT NULL
PRIMARY KEY,
category VARCHAR(255) -- nullable and no constraint for valid values
);
We would like to include these null and unexpected category counts in the result set as well.
Then we must prepare the artificial dataset of the possible categories differently:
SELECT DISTINCT all_categories.category_entry
FROM (SELECT 'CATEGORY_0' AS category_entry -- not present in any row in table 'all_categories'
UNION ALL
SELECT 'CATEGORY_1' AS category_entry
UNION ALL
SELECT 'CATEGORY_2' AS category_entry
UNION ALL
SELECT 'CATEGORY_3' AS category_entry
UNION ALL
SELECT 'CATEGORY_4' AS category_entry
UNION ALL
SELECT DISTINCT category
FROM real_table AS category_entry) all_categories;
and use it as before:
SELECT distinct_categories.category_entry AS category,
COUNT(real_table.id) AS cnt -- important to count some non-null value, such as PK of the real_table
FROM real_table
RIGHT JOIN
(SELECT DISTINCT all_categories.category_entry
FROM (SELECT 'CATEGORY_0' AS category_entry -- not present in any row in table 'all_categories'
UNION ALL
SELECT 'CATEGORY_1' AS category_entry
UNION ALL
SELECT 'CATEGORY_2' AS category_entry
UNION ALL
SELECT 'CATEGORY_3' AS category_entry
UNION ALL
SELECT 'CATEGORY_4' AS category_entry
UNION ALL
SELECT DISTINCT category
FROM real_table AS category_entry) all_categories) distinct_categories
ON real_table.category = distinct_categories.category_entry
GROUP BY distinct_categories.category_entry;
Now when you run the query, the output should also include counts for additional categories and null categories
category
cnt
CATEGORY_0
0
CATEGORY_1
150
CATEGORY_2
20
CATEGORY_3
12
CATEGORY_4
1
CATEGORY_66
13
10
Both unexpected 'CATEGORY_66' (with 13 entries) as well as null category (with 10 entries) are now included in the result set
I cannot vouch for the performance of the provided queries - somebody more experienced might weigh in on that?

MySQL count results in a subquery with union

I have some queries with UNION, I want to count them and show it as a row in another query
Example: I have a table called "clients", they can buy on store1, store2 or store 3, I need to show their names and how many items they bought on a row called "sales"
SELECT name,COUNT(*) FROM(
SELECT 1 FROM store1 WHERE store1.client=clients.id
UNION
SELECT 1 FROM store2 WHERE store2.client=clients.id
UNION
SELECT 1 FROM store3 WHERE store3.client=clients.id
) sales
FROM clients
If john bought 2 items from store 2 and 1 item from store 3, and mary didn't bought anything, The expected result is something like:
name | sales
------------
john | 3
mary | 0
But what I have is this error:
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'FROM clients'
This is another attempt using another select subquery:
SELECT name,(
SELECT COUNT(*) FROM(
SELECT 1 FROM store1 WHERE store1.client=clients.id
UNION
SELECT 1 FROM store2 WHERE store2.client=clients.id
UNION
SELECT 1 FROM store3 WHERE store3.client=clients.id
) xxxx -- (mandatory table name)
) sales
FROM clients
This give me this error:
Unknown column 'clients.id' in 'where clause'
I hope you can help me, Thank you in advance!
First union the tables then filter the results and counts...
You can't reference a field more than 1 level of separation. Since store1.client is 2 levels deep and clients is at level 0, you're more than 1 level of separation and this isn't allowed.
SELECT C.name, count(1)
FROM (
SELECT 'Store1' as StoreTable, a.* FROM store1 a UNION
SELECT 'Store2', b.* FROM store2 b UNION
SELECT 'Store3', c.* FROM store3 c
) S
RIGHT JOIN clients C
on C.ID = S.Client
GROUP BY Name
This makes a few assumptions
Data structure between each store table is the same
You may need other data from stores table which is now accessible.
I might go one step further and just create a view called "Stores" joining all the "Stores" in a union to make other queries across stores simpler. and by hardcoding a "StoreTable name in the view you can always identify the source table if needed.
SELECT name,COUNT(*)
FROM clients INNER JOIN
(
SELECT client as id, 1 FROM store1
UNION
SELECT client as id, 1 FROM store2
UNION
SELECT client as id, 1 FROM store3
)
as Stores on clients.id = Stores.id
GROUP by name
Almost there. Try something like:
SELECT c.name, sum(s.qt) as qt
from clients c
join (
SELECT client.id, COUNT(*) FROM(
SELECT client, sum(1) as qt FROM store1 group by client
UNION
SELECT client, sum(1) as qt FROM store2 group by client
UNION
SELECT client, sum(1) as qt FROM store3 group by client
) as sales s on (s.client = c.id)
group by c.name
I can be confusing something around MySQL and SQLServer, I'll test and let you know if there's something different.
Update: corrected the subquery, added sum and group_by and removed the where clause. It can have performance effects if you don't want to get all clients from all stores.

Adding one extra row to the result of MySQL select query

I have a MySQL table like this
id Name count
1 ABC 1
2 CDF 3
3 FGH 4
using simply select query I get the values as
1 ABC 1
2 CDF 3
3 FGH 4
How I can get the result like this
1 ABC 1
2 CDF 3
3 FGH 4
4 NULL 0
You can see Last row. When Records are finished an extra row in this format
last_id+1, Null ,0 should be added. You can see above. Even I have no such row in my original table. There may be N rows not fixed 3,4
The answer is very simple
select (select max(id) from mytable)+1 as id, NULL as Name, 0 as count union all select id,Name,count from mytable;
This looks a little messy but it should work.
SELECT a.id, b.name, coalesce(b.`count`) as `count`
FROM
(
SELECT 1 as ID
UNION
SELECT 2 as ID
UNION
SELECT 3 as ID
UNION
SELECT 4 as ID
) a LEFT JOIN table1 b
ON a.id = b.id
WHERE a.ID IN (1,2,3,4)
UPDATE 1
You could simply generate a table that have 1 column preferably with name (ID) that has records maybe up 10,000 or more. Then you could simply join it with your table that has the original record. For Example, assuming that you have a table named DummyRecord with 1 column and has 10,000 rows on it
SELECT a.id, b.name, coalesce(b.`count`) as `count`
FROM DummyRecord a LEFT JOIN table1 b
ON a.id = b.id
WHERE a.ID >= 1 AND
a.ID <= 4
that's it. Or if you want to have from 10 to 100, then you could use this condition
...
WHERE a.ID >= 10 AND
a.ID <= 100
To clarify this is how one can append an extra row to the result set
select * from table union select 123 as id,'abc' as name
results
id | name
------------
*** | ***
*** | ***
123 | abc
Simply use mysql ROLLUP.
SELECT * FROM your_table
GROUP BY Name WITH ROLLUP;
select
x.id,
t.name,
ifnull(t.count, 0) as count
from
(SELECT 1 AS id
-- Part of the query below, you will need to generate dynamically,
-- just as you would otherwise need to generate 'in (1,2,3,4)'
UNION ALL SELECT 2
UNION ALL SELECT 3
UNION ALL SELECT 4
UNION ALL SELECT 5
) x
LEFT JOIN YourTable t
ON t.id = x.id
If the id does not exist in the table you're selecting from, you'll need to LEFT JOIN against a list of every id you want returned - this way, it will return the null values for ones that don't exist and the true values for those that do.
I would suggest creating a numbers table that is a single-columned table filled with numbers:
CREATE TABLE `numbers` (
id int(11) unsigned NOT NULL
);
And then inserting a large amount of numbers, starting at 1 and going up to what you think the highest id you'll ever see plus a thousand or so. Maybe go from 1 to 1000000 to be on the safe side. Regardless, you just need to make sure it's more-than-high enough to cover any possible id you'll run into.
After that, your query can look like:
SELECT n.id, a.*
FROM
`numbers` n
LEFT JOIN table t
ON t.id = n.id
WHERE n.id IN (1,2,3,4);
This solution will allow for a dynamically growing list of ids without the need for a sub-query with a list of unions; though, the other solutions provided will equally work for a small known list too (and could also be dynamically generated).