Use a GROUP_CONCAT and JOIN in a subquery - mysql

I have a DB under MySQL with one main table with unique id, few tables that list different choices (with one id column, one text column to describe the item). So, for one record in the main table, I can have multiple choices associated to the choice table.
I'd like to create a View where all information could be visible, using GROUP_CONCAT to concatenate into one field the different choices from a given 'choice' table. However, my query repeats many times each list of choices when the record is related to other multiple choices from another 'choice' table. The query returns all the possible combinations between those choices...
Here my query (reduced to 2 'choice' tables -t_age, t_animal- for the example)
SELECT general.id_g,
GROUP_CONCAT(CAST(t_age AS CHAR) SEPARATOR ', ') AS age,
GROUP_CONCAT(CAST(t_animal AS CHAR) SEPARATOR ', ') AS animal
FROM general
LEFT JOIN interm_age
INNER JOIN t_age ON interm_age.id_age = t_age.id_age
ON general.id_g = interm_age.id_g
LEFT JOIN interm_animal
INNER JOIN t_animal ON interm_animal.id_animal = t_animal.id_animal
ON general.id_g = interm_animal.id_g
GROUP BY id_g;
I tried to include each CONCAT/JOIN within a subquery into a main SELECT as followed, but MySQL tells me "returns more than 1row", which is the case indeed. And?
SELECT id_g, (
SELECT GROUP_CONCAT(CAST(t_age AS CHAR) SEPARATOR ', ')
FROM general
LEFT JOIN interm_age
INNER JOIN t_age ON interm_age.id_age = t_age.id_age
ON general.id_g = interm_age.id_g
GROUP BY general.id_g )
FROM general;
[EDIT]
In more details, this is my DB (with FK)
general
-----------
id_g | date
902 | 2016/01/01
956 | 2016/02/01
959 | 2016/02/01
interm_age
-----------
id_age | id_g
1 | 902
3 | 902
1 | 956
4 | 956
interm_animal
-----------
id_animal | id_g
1 | 902
5 | 902
5 | 959
7 | 959
t_age
-----------
id_age | age
1 | <10y
3 | >10y
4 | >60y
t_animal
-----------
id_animal | animal
1 | bird
5 | mammal
7 | insect
And I would like something like :
id_g | date | age | animal
902 | 2016/01/01 | <10y, >10y | bird, mammal
and so on...
Thanks in advance!

Although it would be better if you provided some sample data, actual results, and expected results, I'll have a stab at the solution.
The issue probably is that you have multiple matching records in the joined tables, which leads to duplication of values within group_concat().
As MySQL documentation on group_concat() indicates, you can use the distinct keyword to remove duplicate values:
To eliminate duplicate values, use the DISTINCT clause.
SELECT general.id_g,
GROUP_CONCAT(DISTINCT CAST(t_age AS CHAR) SEPARATOR ', ') AS age,
GROUP_CONCAT(DISTINCT CAST(t_animal AS CHAR) SEPARATOR ', ') AS animal
FROM general
LEFT JOIN interm_age
INNER JOIN t_age ON interm_age.id_age = t_age.id_age
ON general.id_g = interm_age.id_g
LEFT JOIN interm_animal
INNER JOIN t_animal ON interm_animal.id_animal = t_animal.id_animal
ON general.id_g = interm_animal.id_g
GROUP BY id_g;

Try this
SELECT general.id_g,
GROUP_CONCAT(CAST(t_age AS CHAR) SEPARATOR ', ') AS age,
GROUP_CONCAT(CAST(t_animal AS CHAR) SEPARATOR ', ') AS animal
FROM general
LEFT JOIN interm_age
INNER JOIN t_age ON interm_age.id_age = t_age.id_age AND general.id_g = interm_age.id_g
LEFT JOIN interm_animal
INNER JOIN t_animal ON interm_animal.id_animal = t_animal.id_animal AND general.id_g = interm_animal.id_g
GROUP BY id_g;
Hope this helps.

Related

How to display a query in a certain way?

I'm currently new to queries and below I have a query that I have made
QUERY:
select TITLE_ID, TITLE,NAME, JOB_CATEGORY AS ROLE
FROM MOVIES
NATURAL JOIN NEW_NAMES
WHERE JOB_CATEGORY = 'writer'
OR JOB_CATEGORY = 'director'
ORDER BY TITLE_ID ASC;
Which Displays:
TITLE_ID | TITLE | NAME | ROLE |
753595 | 2F2F | ROB | WRITER |
753595 | 2F2F | YAS | DIRECTOR|
However I would like it to display in this format below:
TITLE_ID | TITLE | WRITER | DIRECTOR|
753595 | 2F2F | ROB | YAS |
You join your NEW_NAMES table in twice. Once for director, once for writer. When you join a table more than once it's necessary that you give the table an Alias, here we use writer and director.
select TITLE_ID, TITLE,writer.NAME as writer_name, director.NAME as director_name
FROM MOVIES
LEFT OUTER JOIN NEW_NAMES as writer
ON MOVIES.TITLE_ID = writer.TITLE_ID
AND writer.JOB_CATEGORY = 'writer'
LEFT OUTER JOIN NEW_NAMES as director
ON MOVIES.TITLE_ID = director.TITLE_ID
AND director.JOB_CATEGORY = 'director'
ORDER BY TITLE_ID ASC;
I've made some assumptions in those ON clauses about which columns in your tables you are joining on. You may need to edit that.
According to error message you got, it is about Oracle, not MySQL.
Anyway, such a (classic?) principle should work: aggregate!
SQL> with
2 -- sample data
3 movies (title_id, title_name) as
4 (select 753595, '2F2F' from dual union all
5 select 123456, '1Z1Z' from dual
6 ),
7 new_names (title_id, job_category, name) as
8 (select 753595, 'writer', 'ROB' from dual union all
9 select 753595, 'director', 'YAS' from dual
10 )
11 -- query you need
12 select m.title_id,
13 m.title_name,
14 max(case when n.job_category = 'writer' then n.name end) as writer,
15 max(case when n.job_category = 'director' then n.name end) as director
16 from movies m left join new_names n on m.title_id = n.title_id
17 group by m.title_id, m.title_name
18 order by m.title_id;
TITLE_ID TITL WRI DIR
---------- ---- --- ---
123456 1Z1Z
753595 2F2F ROB YAS
SQL>

Count 2 columns for occurrence

So basically, I require a query that will return display name, amount of kills and amount of deaths.
I have two tables that I need to pull from.
The two tables are
player
id | name
2334324 | user
4353454 | user2
where id is their unique identifier and name is their display name.
The second table is:
player_kill
id | killer | victim |
1 | 2334324 | 4353454 |
2 | 2334324 | 4353454 |
3 | 4353454 | 2334324 |
where killer / victim columns contain the unique identifier of the player table.
I'd like to be able to count the occurrences of player id in the killer and victim so that the query returns:
name | kills | deaths
user | 2 | 1
user2| 1 | 2
where the number under kills would be the amount of occurrences the playerid has in the killer column and same for deaths
Hope I provided enough information.
What I have so far:
SELECT `player`.`name`, COUNT(DISTINCT `player_kill`.`id`) as `kills`, COUNT(DISTINCT `player_kill`.`id`) as `deaths`
FROM `player`
LEFT JOIN `player_kill` ON `player`.`id`=`player_kill`.`killer`
LEFT JOIN `player_kill` ON `player`.`id`=`player_kill`.`victim`
WHERE `player`.`id` = `player_kill`.`killer` AND `player`.`id` = `player_kill`.`victim`
GROUP BY `player`.`id`;
Try
SELECT
p.name,
count(distinct pk1.id) as kills,
count(distinct pk2.id) as deaths
FROM player p
LEFT JOIN player_kill pk1 ON pk1.killer = p.id
LEFT JOIN player_kill pk2 ON pk2.victim = p.id
group by p.name
http://sqlfiddle.com/#!9/649504/15/0
See if this works:
SELECT `player`.`name`,
COUNT(DISTINCT k.`id`) as `kills`,
COUNT(DISTINCT v.`id`) as `deaths`
FROM `player`
LEFT JOIN `player_kill` AS k ON `player`.`id` = k.`killer`
LEFT JOIN `player_kill` AS v ON `player`.`id` = v.`victim`
GROUP BY `player`.`id`;
If not, then we may need to make the COUNTs into subqueries.

How to replace the IDs with their corresponding values in SQL?

I working on 4 tables, 1 main table and 3 small tables. The main table is called 'document' and it is referencing and ID from each of the other 3 small tables which are: importance, nature, access_level.
e.g.
(Table1) document
document_id | title | file_name | importance_id | nature_id | access_level_id
-----------------------------------------------------------------------------
1 | Food | food.docx | 1 | 1 | 1
(Table2) importance
importance_id | name
--------------------
1 | High
(Table3) nature
nature_id | name
----------------
1 | General
(Table4) nature
access_level_id | name
----------------------
1 | Public
What I wanted to do is showing all the documents (using SELECT) with their IDs replaced by their corresponding names.. like this:
document_id | title | file_name | importance | nature | access_level
-----------------------------------------------------------------------------
1 | Food | food.docx | High | General | Public
I tried to use this query but it didn't work quite well:
SELECT `document_id`, `title`, `file_name`, `date_of_archiving`, `duration_of_life`, importance.name as importance, nature.name as nature, access_level.name as access_level
FROM document, importance, nature, access_level
WHERE importance.importance_id = document.document_id
AND nature.nature_id = document.nature_id
AND access_level.access_level_id = document.access_level_id;
SO how can I make that happen?
Thanks in advance :)
Use JOINS rather than outdated syntax you are using. It is clearer and easier to debug. And to save typing use table aliases. As follows:
SELECT
document_id,
title,
file_name,
date_of_archiving,
duration_of_life,
i.NAME AS [importance],
n.name as [nature],
al.name as [access_level]
FROM
DOCUMENT D
JOIN importance i ON i.Importance_id = D.importance_id
JOIN nature n ON n.nature_id = D.nature_id
JOIN access_level al on al.access_level_id = D.access_level_id
Use inner join if you want the rows return no null values
SELECT
document_id,
title,
file_name,
date_of_archiving,
duration_of_life,
i.NAME AS [importance],
n.name as [nature],
al.name as [access_level]
FROM
DOCUMENT D
INNER JOIN importance i ON i.Importance_id = D.importance_id
INNER JOIN nature n ON n.nature_id = D.nature_id
INNER JOIN access_level al on al.access_level_id = D.access_level_id
Use this Code:
SELECT
Document_id,
Title,
File_name,
i.name AS Importance,
n.name AS Nature,
s.name AS Access_level
FROM Document d
INNER JOIN importance i ON i.importance_id = d.importance_id
INNER JOIN nature n ON n.nature_id = d.nature_id
INNER JOIN nature s ON s.access_level_id = d.access_level_id

MySql query on 3 tables not returning all rows

I have 3 tables: questions, options, comments_to_options(opt_comments).
I want to write a query that returns in each row the following values, concatenated:
A question, all options to it, all comments to each option.
My query is:
select
concat('{', '"qid":"', q.q_id, '", "qt":"', q.q_title,
'", "op":[', group_concat('{"oi":"', o.op_id, '", "ot":"', o.opt_value, '", ', oc_list, '}'
order by o.opt_upvotes desc), ']}')
as r
from questions q, options o,
(select o.op_id as ocid, concat('"oc":[', group_concat('{"oci":"', oc.opt_com_id, '", "occ":"', oc.opt_com_value, '"}'
order by oc.opt_com_added_at), ']')
as oc_list
from options o, opt_comments oc
where oc.opt_com_to=o.op_id
group by o.op_id)
as r2
where o.op_id=r2.ocid
and q.q_id=o.option_to
group by q.q_id
order by q.q_added_at desc
limit 3;
But the above query gives only those options that have at least one comment to them.
How should I modify?
You are using the old JOIN syntax with comma-separated lists of tables and subqueries. That syntax is correct, but generates INNER JOIN operations. Such joins suppress rows that don't match the join criterion.
You need to adopt the LEFT JOIN syntax. Without refactoring your entire query, I will say that you should change
FROM a,
(select something from z) AS b
WHERE a.value=b.value
to
FROM a
LEFT JOIN (select something from z) AS b ON a.value=b.value
Also, beware, you may encounter the character-string length limit in GROUP_CONCAT(). Read this:
MySQL and GROUP_CONCAT() maximum length
Use "left join".
Example:
create table opt (oid int,name varchar(100));
insert into opt values (1,'opt1');
insert into opt values (2,'opt2');
insert into opt values (3,'opt3');
create table optcom (oid int,com varchar(100));
insert into optcom values (1,'opt1_1');
insert into optcom values (1,'opt1_2');
insert into optcom values (3,'opt3_1');
When using "simple join":
select opt.*,optcom.* from opt join optcom on opt.oid=optcom.oid;
+------+------+------+--------+
| oid | name | oid | com |
+------+------+------+--------+
| 1 | opt1 | 1 | opt1_1 |
| 1 | opt1 | 1 | opt1_2 |
| 3 | opt3 | 3 | opt3_1 |
+------+------+------+--------+
When "left join":
select opt.*,optcom.* from opt left join optcom on opt.oid=optcom.oid;
+------+------+------+--------+
| oid | name | oid | com |
+------+------+------+--------+
| 1 | opt1 | 1 | opt1_1 |
| 1 | opt1 | 1 | opt1_2 |
| 2 | opt2 | NULL | NULL |
| 3 | opt3 | 3 | opt3_1 |
+------+------+------+--------+
To follow up on the above responses, the SQL amended to use outer joins:-
SELECT CONCAT('{', '"qid":"', q.q_id, '", "qt":"', q.q_title,'", "op":[', GROUP_CONCAT('{"oi":"', o.op_id, '", "ot":"', o.opt_value, '", ', oc_list, '}' ORDER BY o.opt_upvotes DESC), ']}') AS r
FROM options o
LEFT OUTER JOIN questions q
ON q.q_id = o.option_to
LEFT OUTER JOIN
(
SELECT o.op_id AS ocid,
CONCAT('"oc":[', GROUP_CONCAT('{"oci":"', oc.opt_com_id, '", "occ":"', oc.opt_com_value, '"}' ORDER BY oc.opt_com_added_at), ']') AS oc_list
FROM options o
INNER JOIN opt_comments oc
ON oc.opt_com_to=o.op_id
GROUP BY o.op_id
) r2
ON o.op_id = r2.ocid
GROUP BY q.q_id
ORDER BY q.q_added_at DESC
LIMIT 3;
Looking at this I am unsure about the join to the sub query. This appears to be bringing back an encoded string, but beyond the actual join nothing from this sub query is actually used.
As such I am unsure if that sub query is just being used to narrow down the rows returned (in which case joining against it using an INNER JOIN would be appropriate - and you may as well not bring back the encoded string), or if you have posted a cut down version of the query that you have been trying to debug.

MySql Impass - can't move forward

I currently have this sql statement that I wrote and it works but it's not quite what I want. I've been working on it for hours but can't seem to get any further.
select parent.id as parent_id, parent.subject as parent,s.id,s.subject from (select s.id, s.subject from subjects s where parent_id = 0) parent join subjects s on parent.id = s.parent_id order by parent.subject, s.subject
It's grabbing all the subjects and ordering correctly but I also want to return the parent subject (parent_id = 0) at the top of each grouping. This is because some parents may not have subjects underneath but I still need to return them. Also the ordering is off when I try to do it as I want the parent first then it's child subjects. Hope that makes sense but if not just ask.
Any help would be appreciated.
Thanks
Steve
You're talking about grouping sets of rows by their parent rows.
The only way I know how to do this in MySQL is using the GROUP_CONCAT() function which won't group the subjects by row, but rather create a grouped string.
Here's what you can do:
SELECT
a.id,
a.subject,
GROUP_CONCAT(CONCAT(b.id, ':::', b.subject) ORDER BY b.subject SEPARATOR '|||') AS subjectlist
FROM
subjects a
LEFT JOIN
subjects b ON a.id = b.parent_id
WHERE
a.parent_id = 0
GROUP BY
a.id,
a.subject
ORDER BY
a.subject
So this will give you a result set like:
id | subject | subjectlist
---------------------------------------------------------------------
2 | subj1 | 23:::childsubj1|||28:::childsubj4
3 | subj2 | 18:::childsubj8|||55:::childsubj16
4 | subj3 | NULL
Depending on what language you are using in your application, you may be able to "explode" the subjects string into arrays delimited first by ||| which separates each subject, then ::: which separates that subject's ID and name.
Obviously, the downside of this is you have to make sure that your child subject name does not contain either ||| or ::: or whichever delimiters you decide to use.
Edit: Experimentally, I came up with this alternative solution which may be closer to what you're looking for:
Try:
SELECT
c.subj,
c.id
FROM
(
SELECT
CONCAT('---> ', b.subject) AS subj,
b.id,
CONCAT(a.subject, b.subject) AS orderfactor
FROM
subjects a
INNER JOIN
subjects b ON a.id = b.parent_id
WHERE
a.parent_id = 0
UNION ALL
SELECT
subject AS subj,
id,
subject AS orderfactor
FROM
subjects
WHERE
parent_id = 0
) c
ORDER BY
c.orderfactor
This query should give you a result along the lines of:
subject | id |
----------------------------------------------------------
subj1 | 2 |
---> childsubj1 | 23 |
---> childsubj4 | 28 |
subj2 | 3 |
---> childsubj8 | 18 |
---> childsubj16 | 55 |
subj3 | 4 |
subj4 | 5 |
---> childsubj10 | 79 |