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) |
Related
I have a table called flux with :
INSERT INTO `flux` (`id`, `name`) VALUES
(20, 'test1'),
(21, 'test2');
And a table called flux_rules with :
INSERT INTO `flux_rules` (`id`, `id_flux`, `id_modele_scrape`, `disallow`) VALUES
(1, 20, 1, 1),
(2, 21, 1, 1);
And one more table called modele_scrape with :
INSERT INTO `modele_scrape` (`id`, `modele`) VALUES
(1, 'renault captur'),
(2, 'renault zoe');
I believe this is a pretty usual setup, but what I haven't found information on what I'm trying to achieve, which is a SELECT query that would list all rows from modele_scrape with a column for each row in flux where the value is defined by the flux_rules column (1 IF EXISTS, ELSE 0) :
modele_scrape.id modele_scrape.modele test1 test2
1 test 1 0
Right now I have working query, but it only works to join one row from flux :
SELECT id AS id,
(SELECT IF (EXISTS (SELECT id_flux, id_modele_scrape FROM flux_rules WHERE id_flux = 20 AND id_modele_scrape = modele_scrape.id), 1, 0) ) AS disallowed
FROM modele_scrape
The solution should work as to dynamically join X amount of columns where X is the amount of rows in flux (rows from this table can be added/deleted)
Generically this sounds like a desire to "pivot" your information, and this may achieved by using "conditional aggregates" (basically this means using a case expression inside an aggregate function, typically this is the max() function.
select
m.id
, m.modele
, max(case when r.r.flux_id = 1 then flux.name end) as `name1`
, max(case when r.r.flux_id = 2 then flux.name end) as `name2`
, max(case when r.r.flux_id = 3 then flux.name end) as `name3`
from modele_scrape as m
left join flux_rules as r on m.id = r.id_modele_scrape
left join flux as f on r.flux_id = f.id
group by
m.id
, m.modele
Dynamic Pivot in MySQL
To produce such a pivot dynamically, the following query will work:
SET #sql = NULL;
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
' max(case when r.id_flux = ''',
id,
''' then 1 else 0 end) ',
name
)
) INTO #sql
FROM
flux;
set #sql = CONCAT('SELECT m.id, m.modele,', #sql,
' from modele_scrape as m',
' left join flux_rules as r on m.id = r.id_modele_scrape',
' left join flux as f on r.id_flux = f.id',
' group by m.id, m.modele')
;
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
id | modele | test1 | test2
-: | :------------- | ----: | ----:
1 | renault captur | 1 | 1
2 | renault zoe | 0 | 0
see this as a working model here
NOTE: due to limited sample data I chose to produce the dynamic columns by directly querying the flux table, in the real tables this might produce more columns than you want/need so adjust the group_concat query, perhaps with joins to other tables and/or a where clause, to suit your data and requirements
I want to Join to table. the condition is I want to only join those rows which have only one row to match. eg.
books:
id | name | price
1 | book1 | 19
2 | book2 | 19
3 | book3 | 30
price_offer:
id | offer | price
1 | offer1 | 19
2 | offer2 | 30
so now if I do select query on these table:
SELECT * FROM price_offer
JOIN books ON price_offer.price = books.price
I only want to join book with id 3 as it have only one match with price_offer table.
You could use a self join for books table to pick a book with only single match
select po.*, b1.*
from price_offer po
join books b1 on po.price = b1.price
join (
select price,max(id) id
from books
group by price
having count(*) = 1
) b2 on b1.id = b2.id
Demo
Try following query:
Sample data:
create table books(id int, name varchar(10), price int);
insert into books values
(1, 'book1', 19),
(2, 'book2', 19),
(3, 'book3', 30);
create table price_offer(id int, offer varchar(10), price int);
insert into price_offer values
(1, 'offer1', 19),
(2, 'offer2', 30);
Query:
select max(b.id)
from price_offer p
left join books b on b.price = p.price
where p.id is not null
group by b.price
having count(*) = 1;
If you want to avoid nesting queries where you have to use self-joins, you can use window-functions of MySQL 8.0.11, which are exactly for cases like this
Here's the SQLFiddle with schema and data.
I'm trying to sum 2 columns, one at parent level and the other at child level.
The current query I'm using gives me the right sum amount on child level, but doubles up the amount on parent level, due to another 1-many relationship involved on the child level.
Ugh... that's a terrible explanation - here's the English version:
Joe the salesman is involved in 2 sales.
For the 1st sale, he get's 2 sets of commissions, based on 2 different commission types. I'm trying to show Joe's total sale value, alongside the total value of his applicable splits. The split value total is fine, but sale value get's doubled up because I'm obviously, grouping/joining incorrectly (see the last example below).
This is fine:
select sp.person_name, pr.description,
sum(spl.split) as SplitValue
from sale s, product pr, sales_person sp, sales_split spl
where s.product_id = pr.id
and s.id = spl.sale_id
and sp.id = spl.sales_person_id
group by sp.id;
person_name | description | SplitValue
----------- ----------- | ----------
Joe | Widget 1 | 50
Sam | Widget 1 | 10
This is also yields the correct split and sale values, but now 3 rows are displayed for Joe (i.e 2nd row is a duplicate of the 1st one) - I only want to display Joe's "Widget 1" sale once, so not correct:
select sp.person_name, pr.description,
sum(s.sale_value) as SaleValue, sum(spl.split) as SplitValue
from sale s, product pr, sales_person sp, sales_split spl, sales_split_agreement ssa
where s.id = spl.sale_id
and s.product_id = pr.id
and sp.id = spl.sales_person_id
and sp.id = ssa.sales_person_id
and spl.sales_person_id = ssa.sales_person_id
and ssa.id = spl.sales_split_agreement_id
group by sp.id, spl.id;
person_name | description | SplitValue | SaleValue
----------- ----------- ---------- ---------
Joe | Widget 1 | 10 | 20
Joe | Widget 1 | 10 | 20
Joe | Widget 2 | 30 | 30
Sam | Widget 1 | 10 | 20
Now the duplicated row is gone, but Joe's SaleValue is incorrect - it should be 50, not 70:
select sp.person_name, pr.description,
sum(spl.split) as SplitValue, sum(s.sale_value) as SaleValue
from sale s, product pr, sales_person sp, sales_split spl, sales_split_agreement ssa
where s.id = spl.sale_id
and s.product_id = pr.id
and sp.id = spl.sales_person_id
and sp.id = ssa.sales_person_id
and spl.sales_person_id = ssa.sales_person_id
and ssa.id = spl.sales_split_agreement_id
group by sp.id;
person_name | description | SplitValue | SaleValue
----------- ----------- --------- ----------
Joe | Widget 1 | 50 | 70
Sam | Widget 1 | 10 | 20
I.e. I'm after the query that will yield this result (i.e. Joe's correct SaleValue of 50):
person_name | description | SplitValue | SaleValue
----------- ----------- --------- ----------
Joe | Widget 1 | 50 | 50
Sam | Widget 1 | 10 | 20
Any help will be greatly appreciated!
UPDATE 1:
For clarity - here's the schema and test data from the fiddle:
CREATE TABLE product
(`id` int, `description` varchar(12))
;
INSERT INTO product
(`id`, `description`)
VALUES
(1, 'Widget 1'),
(2, 'Widget 2')
;
CREATE TABLE sales_person
(`id` int, `person_name` varchar(7))
;
INSERT INTO sales_person
(`id`, `person_name`)
VALUES
(1, 'Joe'),
(2, 'Sam')
;
CREATE TABLE sale
(`id` int, `product_id` int, `sale_value` int)
;
INSERT INTO sale
(`id`, `product_id`, `sale_value`)
VALUES
(1, 1, 20.00),
(2, 2, 30.00)
;
CREATE TABLE split_type
(`id` int, `description` varchar(6))
;
INSERT INTO split_type
(`id`, `description`)
VALUES
(1, 'Type 1'),
(2, 'Type 2')
;
CREATE TABLE sales_split_agreement
(`id` int, `sales_person_id` int, `split_type_id` int, `percentage` int)
;
INSERT INTO sales_split_agreement
(`id`, `sales_person_id`, `split_type_id`, `percentage`)
VALUES
(1, 1, 1, 50),
(2, 1, 2, 50),
(3, 2, 1, 50),
(4, 1, 1, 100)
;
CREATE TABLE sales_split
(`id` int, `sale_id` int, `sales_split_agreement_id` int, `sales_person_id` int, `split` int )
;
INSERT INTO sales_split
(`id`, `sale_id`, `sales_split_agreement_id`, `sales_person_id`, `split`)
VALUES
(1, 1, 1, 1, 10),
(2, 1, 2, 1, 10),
(3, 1, 3, 2, 10),
(4, 2, 4, 1, 30)
;
I think you were on to the right track, but I decided to restart and approach from the beginning. Getting the SplitValue for each person does not require all those tables. In fact, all you need are sales_split and sales_person, like this:
SELECT sp.person_name, SUM(ss.split) AS SplitValue
FROM sales_person sp
JOIN sales_split ss ON sp.id = ss.sales_person_id
GROUP BY sp.id;
Similarly, you can get the total sale value for each person with a join between sale, sales_split, and sales_person:
SELECT sp.person_name, SUM(s.sale_value) AS SaleValue
FROM sale s
JOIN sales_split ss ON ss.sale_id = s.id
JOIN sales_person sp ON sp.id = ss.sales_person_id
GROUP BY sp.id;
At this point, I realize you have an error in your expected results (for this data set). Joe does in fact have a sale value of 70, because sale id 1 (value 20), 2 (value 20), and 4 (value 30) add up to 70. However, I still think this query will help you out more than the one you have.
At this point, you can get the values for each sales_person_id by joining those two subqueries to the sales_person table. I took out the join to sales_person in the subqueries, as it became irrelevant now. It even makes the subqueries a little cleaner:
SELECT sp.person_name, COALESCE(t1.SplitValue, 0) AS SplitValue, COALESCE(t2.SaleValue, 0) AS SaleValue
FROM sales_person sp
LEFT JOIN(
SELECT ss.sales_person_id, SUM(ss.split) AS SplitValue
FROM sales_split ss
GROUP BY ss.sales_person_id) t1 ON t1.sales_person_id = sp.id
LEFT JOIN(
SELECT ss.sales_person_id, SUM(s.sale_value) AS SaleValue
FROM sale s
JOIN sales_split ss ON ss.sale_id = s.id
GROUP BY ss.sales_person_id) t2 ON t2.sales_person_id = sp.id;
Here is an SQL Fiddle example.
EDIT: I understand now why Joe's actual sale price is 50, because he split twice on sale id 1. To work around this, I first got a list of distinct sales for each sales_person like this:
SELECT DISTINCT sale_id, sales_person_id
FROM sales_split;
This way, there is only one row for sales_person_id = 1 and sale_id = 1. Then, it was easy enough to join that to the sale table and get the proper sales value for each sales_person:
SELECT t.sales_person_id, SUM(s.sale_value) AS SaleValue
FROM(
SELECT DISTINCT sale_id, sales_person_id
FROM sales_split) t
JOIN sale s ON s.id = t.sale_id
GROUP BY t.sales_person_id;
The rest of my answer above still fits. I wrote one query to get SplitValue, and one query to get SaleValue, and I joined them together. So, all I have to do now is replace the subquery I just gave you, with the incorrect subquery from further up:
SELECT sp.person_name, COALESCE(t1.SplitValue, 0) AS SplitValue, COALESCE(t2.SaleValue, 0) AS SaleValue
FROM sales_person sp
LEFT JOIN(
SELECT ss.sales_person_id, SUM(ss.split) AS SplitValue
FROM sales_split ss
GROUP BY ss.sales_person_id) t1 ON t1.sales_person_id = sp.id
LEFT JOIN(
SELECT t.sales_person_id, SUM(s.sale_value) AS SaleValue
FROM(
SELECT DISTINCT sale_id, sales_person_id
FROM sales_split) t
JOIN sale s ON s.id = t.sale_id
GROUP BY t.sales_person_id) t2 ON t2.sales_person_id = sp.id;
Here is the updated SQL Fiddle.
You mentioned in the comments that you shortened your data for brevity, which is fine. I am leaving my joins as they are, and I trust that it gives you enough direction that you can adjust them accordingly to match your proper structure.
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
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;