Multi-Join Query, Pivot? Troubles - mysql

Thank you in advance for anyone who helps with this. I know i've seen this done before without too much pain but cant seem to find the solution
My database looks something like:
`tbl_user:
----------
id ( pkey )
email
fName
lName
tbl_userSparseType:
-------------------
id ( pkey )
varName
displayName
tbl_userSparse:
---------------
id ( pkey )
value ( Value of Sparse Type )
user_id ( => tbl_user.id )
userSparseType_id ( => tbl_userSparseType.id )
with sample data:
tbl_user:
(id, email, fName, lName)
1 Bob#example.com Billy Bob
2 Betty#example.com Betty Sue
3 Beam#example.com Jim Beam
tbl_userSparseType:
(id, varName, displayName)
1 fullName Full Name
2 dayPhone Day Phone
3 nightPhone Night Phone
4 cellPhone Cell Phone
5 homeAddr Home Address
tbl_userSparse:
(id, value, user_id, userSparseType_id)
1 Billy John Bob 1 1
2 James B. Beam 3 1
3 123-234-3456 1 2
4 234-345-4567 1 4
5 24 Best st. 2 5
6 456-567-6789 3 3
I tried doing two left joins, but this gave me a tbl_user row for each sparse entry like:
(id, email, fName, lName, displayName, value)
1,"Bob#example.com","Billy","Bob","Full Name","Billy John Bob"
1,"Bob#example.com","Billy","Bob","Day Phone","123-234-3456"
1,"Bob#example.com","Billy","Bob","Cell Phone","234-345-4567"
And despite a few 45 or so minute sessions of looking, I cant find a way to get something more like the following without explicitly naming the columns, I need a dynamic way to only pull all display names that apply to the subset of tbl_user rows being queried:
WHERE tbl_user.id IN (1,2)
id | email | fName | lName | Full Name, | Day Phone | Cell Phone |
Home Address
-------------------------------------------------------------------------------------------------------
1 | Bob#example.com | Billy | Bob | Billy John Bob | 123-234-3456 | 234-345-4567 |
2 | Betty#example.com | Betty | Sue | | | | 24 Best St.
Thanks again in advance, I'm hoping this can be done without too much fuss. :\

Unfortunately MySQL does not have a PIVOT function which is basically what you are trying to do. So you will need to use an aggregate function with a CASE statement. If you know the number of columns, then you can hard-code the values:
select u.id,
u.email,
u.fname,
u.lname,
max(case when t.displayname = 'Full Name' then us.value end) FullName,
max(case when t.displayname = 'Day Phone' then us.value end) DayPhone,
max(case when t.displayname = 'Cell Phone' then us.value end) CellPhone,
max(case when t.displayname = 'Home Address' then us.value end) HOmeAddress
from tbl_user u
left join tbl_userSparse us
on u.id = us.user_id
left join tbl_userSparseType t
on us.userSparseType_id = t.id
where u.id in (1, 2)
group by u.id, u.email, u.fname,u.lname;
See SQL Fiddle With Demo
Now if you want to perform this dynamically, meaning you do not know ahead of time the columns to transpose, then you should review the following article:
Dynamic pivot tables (transform rows to columns)
Your code would look like this:
SET #sql = NULL;
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'max(case when t.displayname = ''',
t.displayname,
''' then us.value end) AS ',
replace(t.displayname, ' ', '')
)
) INTO #sql
FROM tbl_userSparse us
left join tbl_userSparseType t
on us.userSparseType_id = t.id;
SET #sql = CONCAT('SELECT u.id, u.email, u.fname, u.lname, ', #sql, '
from tbl_user u
left join tbl_userSparse us
on u.id = us.user_id
left join tbl_userSparseType t
on us.userSparseType_id = t.id
where u.id in (1, 2)
group by u.id, u.email, u.fname, u.lname');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
See SQL Fiddle with Demo

Related

Decrease the number of joins to the same parent table in mysql

I have an user table containing user info and a signature table which maps to user table with userid, as shown below
users
userid | firstname | lastname
1 | John | P
2 | Pete | C
3 | Tim | D
signs
doneBy | CheckedBy | VerifiedBy
1 | 2 | 3
Is there a better way to do this instead of multiple joins as below,
considering I would have 6 such joins
select
concat(usr1.firstname, ' ', usr1.lastname) as doneby,
concat(usr2.firstname, ' ', usr2.lastname) as checkedby,
concat(usr3.firstname, ' ', usr3.lastname) as verifiedby
from signs sgn
join users usr1 on ( usr1.userid = sgn.doneBy)
join users usr2 on ( usr2.userid = sgn.checkedBy)
join users usr3 on ( usr3.userid = sgn.verifiedBy)
You can do this with conditional aggregation using CASE EXPRESSION like this:
SELECT MAX(CASE WHEN t.doneBy = t.userId THEN t.full_name END) AS doneBy,
MAX(CASE WHEN t.CheckedBy = t.userId THEN t.full_name END) AS doneBy,
MAX(CASE WHEN t.VerifiedBy = t.userId THEN t.full_name END) AS doneBy
FROM (
SELECT u.userid,concat(u.firstname, ' ', u.lastname) as full_name,u.*
FROM signs s
INNER JOIN users u
ON(s.userid IN(s.doneBy,s.CheckedBy,s.VerifiedBy))) t
GROUP BY t.doneBy,t.checkBy,t.VerifiedBy

mysql group_concat group by on multiple fields

I have a Table member with member_id, member_name, club_name, region, zone, email as fields.
I am using the MySQL group_concat function like
SELECT group_concat(distinct m.email
SEPARATOR ', ' ) from member m group by m.club_name
This is working fine. But I would like to be able to group_concat on other fields without creating additional queries.
Is it possible to supply the other fields as parameter?
member_id member_name club_name region zone email
1 member1 A 1 1 email1#example.com
2 member2 A 1 1 email2#example.com
3 member3 B 1 1 email3#example.com
4 member4 C 1 2 email4#example.com
5 member5 D 2 1 email5#example.com
**group by club**
email1#example.com,email2#example.com
email3#example.com
email4#example.com
email5#example.com
**group by region**
email1#example.com, email2#example.com, email3#example.com, email4#example.com
email5#example.com
**group by zone**
email1#example.com, email2#example.com, email3#example.com
email5#example.com
Say every Region has 3 Zones, every zone has more than one club. Now how can I get emails which can be grouped or related to Region, Zone or Club for that matter?
It's hard to understand what are you after exactly from your question but you can try
SELECT club_name,
GROUP_CONCAT(DISTINCT email SEPARATOR ', ' ) emails,
GROUP_CONCAT(DISTINCT member_name SEPARATOR ', ' ) members
...
FROM member
GROUP BY club_name
Sample output:
| CLUB_NAME | EMAILS | MEMBERS |
------------------------------------------------------------------------
| Club1 | m1#mail.com, m2#mail.com, m3#mail.com | Jhon, Mark, Beth |
| Club2 | m4#mail.com, m5#mail.com | Helen, Thomas |
Here is SQLFiddle demo
On a side note: providing sample data and desired output in a question like this usually improves your changes of getting your answer faster and that best fits your needs.
UPDATE: You can deeply pack information using GROUP_CONCAT() using different separators if it's what you want
SELECT 'club' group_type, GROUP_CONCAT(details SEPARATOR '|') details
FROM
(
SELECT CONCAT(club_name, ';', GROUP_CONCAT(DISTINCT email)) details
FROM member
GROUP BY club_name
) a
UNION ALL
SELECT 'region' group_type, GROUP_CONCAT(details SEPARATOR '|') details
FROM
(
SELECT CONCAT(region, ';', GROUP_CONCAT(DISTINCT email)) details
FROM member
GROUP BY region
) a
UNION ALL
SELECT 'zone' group_type, GROUP_CONCAT(details SEPARATOR '|') details
FROM
(
SELECT CONCAT(zone, ';', GROUP_CONCAT(DISTINCT email)) details
FROM member
GROUP BY zone
) a
Sample output:
| GROUP_TYPE | DETAILS |
-----------------------------------------------------------------------------------------------------------------------
| club | A;email1#example.com,email2#example.com|B;email3#example.com|C;email4#example.com|D;email5#example.com |
| region | 1;email1#example.com,email2#example.com,email3#example.com,email4#example.com|2;email5#example.com |
| zone | 1;email1#example.com,email2#example.com,email3#example.com,email5#example.com|2;email4#example.com |
Here is SQLFiddle demo
If you're using php on the client side you can then easily enough unwind details column into separate records using explode() while you're iterating over the resultset.

Pivot Table Using MySQL

I have two tables Triples and Tags
Triples Table has the following Columns
id PostID TagID Value
1 1 1 Murder
2 1 2 New Brunswick
3 2 1 Theft
4 2 3 Gun
Tags Table has the following Columns
id TagName
1 Incident
2 Location
3 Weapon
I am trying to write sql to create a Pivot Table with Dynamic Headers
Output should be like this
PostID Incident Location Weapon
1 Murder New Brunswick
2 Theft Gun
Any help in writing the SQL would be appreciated. I have seen examples online but could not figure out this one
In order to pivot the data in MySQL, you will need to use both an aggregate function and a CASE expression.
If you have a known number of columns, then you can hard-code the query:
select p.postid,
max(case when t.tagname = 'Incident' then p.value end) Incident,
max(case when t.tagname = 'Location' then p.value end) Location,
max(case when t.tagname = 'Weapon' then p.value end) Weapon
from triples p
left join tags t
on p.tagid = t.id
group by p.postid;
See SQL Fiddle with Demo
But if you have an unknown number of columns, then you will need to use a prepared statement to generate dynamic SQL:
SET #sql = NULL;
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'max(CASE WHEN TagName = ''',
TagName,
''' THEN p.value END) AS `',
TagName, '`'
)
) INTO #sql
FROM tags;
SET #sql
= CONCAT('SELECT p.postid, ', #sql, '
from triples p
left join tags t
on p.tagid = t.id
group by p.postid');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
See SQL Fiddle with Demo.
Both will give the result:
| POSTID | INCIDENT | LOCATION | WEAPON |
----------------------------------------------
| 1 | Murder | New Brunswick | (null) |
| 2 | Theft | (null) | Gun |

mysql pivot table or something

Ok, I'm still a beginner in sql and can not figure this one out yet.
I have four tables: companies, persons, details, person_details.
companies:
id, compname
(1, ACME),
(2, ACME Group), ...
persons:
id, name, lastname, company id
(1, donald, duck, 1),
(2, lucky, luke, 1),
(3, mickey, mouse, 2)
details:
id, description
(1, 'weight'),
(2, 'height'),
(3, 'haircolor'), ...
person_details:
id, persons id, details id, value
(1, 1, 1, 70),
(2, 1, 3, 'red'),
(3, 2, 1, 90),
(4, 3, 2, 180)
As you can see, not all persons have all the details and the list of available details is variable.
Now, for a given arary of person ids and detail ids, I would like to get rows containing: company name and id, person name and last name, detail name and value for each of the details in the supplied array.
Let's say persons(1,2), details(1,3) should result in:
companies.id, companies.name, name, lastname, details.description, person_details.value,...
1, ACME, donald, duck, 'weight', 70, 'haircolor', 'red'
2, ACEM, lucky, luke, 'weight', 90, 'haircolor', null
Help, please...
Based on your description it seems like you want to pivot the data but unfortunately MySQL does not have a pivot function so you will need to replicate it using an aggregate function with a CASE statement.
If you know the description values ahead of time you can hard-code your query to the following:
select c.id,
c.compname,
p.name,
p.lastname,
max(case when d.description = 'weight' then pd.value end) weight,
max(case when d.description = 'haircolor' then pd.value end) haircolor,
max(case when d.description = 'height' then pd.value end) height
from companies c
left join persons p
on c.id = p.`company id`
left join person_details pd
on p.id = pd.`persons id`
left join details d
on pd.`details id` = d.id
-- where p.id in (1, 2)
group by c.id, c.compname, p.name, p.lastname
See SQL Fiddle with Demo
If you have an unknown number of values, then you can use a prepared statement to generate this dynamically similar to this:
SET #sql = NULL;
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'MAX(CASE WHEN d.description = ''',
description,
''' then pd.value end) AS ',
description
)
) INTO #sql
FROM details;
SET #sql = CONCAT('SELECT c.id,
c.compname,
p.name,
p.lastname, ', #sql, '
from companies c
left join persons p
on c.id = p.`company id`
left join person_details pd
on p.id = pd.`persons id`
left join details d
on pd.`details id` = d.id
-- where p.id in (1, 2)
group by c.id, c.compname, p.name, p.lastname');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
See SQL Fiddle with Demo
Both versions generate the result:
| ID | COMPNAME | NAME | LASTNAME | WEIGHT | HEIGHT | HAIRCOLOR |
---------------------------------------------------------------------
| 1 | ACME | donald | duck | 70 | (null) | red |
| 1 | ACME | lucky | luke | 90 | (null) | (null) |
| 2 | ACME Group | mickey | mouse | (null) | 180 | (null) |

MySql - Sql query or procedure to generate report by transposing rows into columns

This is the first time - I'm trying to do something like this - so please bear with me.
This is on MySql.
I am trying to generate a report to see which students have completed which topics and on which dates.
This is the current query that I run
select u.email,t.topic_name,tu.date_created as 'date completed'
from topic_user tu
join topic t ON tu.topic_id = t.topic_id
join user u ON tu.user_id = u.user_id
which will return results like
email | topic_name | date completed
abc#gmail.com | ABC | 03/01/2012
abc#gmail.com | DEF | 03/02/2012
abc#gmail.com | ABC | 03/08/2012
abc#gmail.com | GHI | 03/08/2012
def#gmail.com | ABC | 03/02/2012
def#gmail.com | XYZ | 03/10/2012
The way I want to generate the report is have the topic names as column headers and the date they completed it as values
email | ABC | DEF | GHI | JKL | XYZ
abc#gmail.com | 03/08/2012 | 03/02/2012 | 03/08/2012 | null | null
def#gmail.com | 03/02/2012 | null | null | null | 03/10/2012
Few things to note are:
1) All the topic names would come from the topic table - even if they have not been completed by the students - the value should appear as null
2) Incase of student abc#gmail.com - he has studied the topic ABC twice - but the report should get the latest date.
I guess I have to write a stored procedure to accomplish this. Like maybe first pull all the topic names from the topic table and then create a temp view and populate it.
I would appreciate any help you can provide.
Thanks much
I've not tested this, and my experience with MySQL is limited but I hope the below is what you are after. It dynamically creates the SELECT statement using the GROUP_CONCAT function, then executes it (This is the bit I am not certain of the way to do it in MySQL).
SET #SQL = (
SELECT CONCAT('SELECT Email,', GROUP_CONCAT(SelectText), ' FROM Topic_User tu INNER JOIN Users u ON u.User_ID = tu.User_ID GROUP BY Email')
FROM ( SELECT CONCAT(' MAX(CASE WHEN Topic_ID = ', Topic_ID, ' THEN tu.Date_Created END) AS `', Topic_Name, '`') AS SelectText
FROM Topic
) AS d);
PREPARE stmt FROM #SQL;
EXECUTE stmt
Of course if your topics are not changing very regularly you could just use:
SELECT Email,
MAX(CASE WHEN Topic_ID = 1 THEN tu.Date_Created END) AS ABC,
MAX(CASE WHEN Topic_ID = 2 THEN tu.Date_Created END) AS DEF,
MAX(CASE WHEN Topic_ID = 3 THEN tu.Date_Created END) AS GHI,
MAX(CASE WHEN Topic_ID = 4 THEN tu.Date_Created END) AS JKL,
MAX(CASE WHEN Topic_ID = 5 THEN tu.Date_Created END) AS XYZ
FROM Topic_User tu
INNER JOIN users u
ON u.User_ID = tu.User_ID
GROUP BY Email
and alter the query each time a new topic is added (This is the query produced by the process above).
You could do this with a dynamically generated crosstab query. The query you want to end up with is something like this -
SELECT
u.email,
MAX(IF(t.topic_name = 'ABC', tu.date_created, NULL)) AS 'ABC',
MAX(IF(t.topic_name = 'DEF', tu.date_created, NULL)) AS 'DEF',
etc
FROM topic_user tu
INNER JOIN topic t
ON tu.topic_id = t.topic_id
INNER JOIN user u
ON tu.user_id = u.user_id
GROUP BY u.user_id;
So, in your server side language of choice you would dynamically build your field list based on first querying for a list of topics.
You could slightly reduce the overhead of this query by using the topic_ids instead of the topic_names so that you can drop the join to the topics table -
SELECT
u.email,
MAX(IF(tu.topic_id = 1, tu.date_created, NULL)) AS 'ABC',
MAX(IF(tu.topic_id = 2, tu.date_created, NULL)) AS 'DEF',
etc
FROM topic_user tu
INNER JOIN user u
ON tu.user_id = u.user_id
GROUP BY u.user_id;