This question already has answers here:
How can I return pivot table output in MySQL?
(10 answers)
Closed 8 years ago.
I have two tables: 'Project' and 'Project Monthly'.
| PROJECT_ID | TITLE | | ID | PROJECT_ID | MONTH | EXPENSE |
----------------------- --------------------------------------
| 1 | title1 | | 1 | 1 | 1 | 100 |
| 2 | title2 | | 2 | 1 | 2 | 2000 |
| 3 | title3 | | 3 | 1 | 3 | 900 |
| 4 | 1 | 4 | 900 |
| 5 | 2 | 1 | 200 |
| 6 | 2 | 2 | 200 |
| 7 | 3 | 1 | 500 |
I would like to have a table like this:
| PROJECT_ID | TITLE | MONTH_1 | MONTH_2 | MONTH_3 | MONTH_4 |
---------------------------------------------------------------
| 1 | title1 | 100 | 2000 | 900 | 900 |
| 2 | title2 | 200 | 200 | NULL| NULL|
| 3 | title3 | 500 | NULL | NULL| NULL|
Can I realize it only with JOIN and without subqueries?
Thanks!
Turning rows into columns is called pivoting. One approach is to group by each project. The group will become a single row. Inside the group, you define one column per month. The column sums up the expenses for one particular month:
select p.PROJECT_ID
, p.TITLE
, sum(case when m.month = 1 then m.expense end) as MONTH_1
, sum(case when m.month = 2 then m.expense end) as MONTH_2
...
from project p
join monthly m
on p.PROJECT_ID = m.PROJECT_ID
group by
p.PROJECT_ID
, p.TITLE
try like this
select p.PROJECT_ID,p.TITLE,
case when month=1 then EXPENSE end as Month_1,
case when month=2 then EXPENSE end as Month_2,
case when month=3 then EXPENSE end as Month_3,
case when month=4 then EXPENSE end as Month_4
from Project p inner join ProjectMonthly' pm on p.PROJECT_ID=pm.PROJECT_ID
You can do so by using case with max to get the desired result set,below query will only give the results for 4 months if you have more than 4 months then you have to write cases for all months with a max to pick greater value among the values for a month,i have used inner join so the projects that exists in ProjectMonthly only these projects will be returned if any project has no monthly data and you still want it to return the results the change inner join to left join
SELECT p.PROJECT_ID,p.TITLE,
MAX(CASE WHEN MONTH=1 THEN EXPENSE END) AS Month_1,
MAX(CASE WHEN MONTH=2 THEN EXPENSE END) AS Month_2,
MAX(CASE WHEN MONTH=3 THEN EXPENSE END) AS Month_3,
MAX(CASE WHEN MONTH=4 THEN EXPENSE END) AS Month_4
FROM Project p
INNER JOIN ProjectMonthly pm
ON p.PROJECT_ID=pm.PROJECT_ID
GROUP BY p.PROJECT_ID
Fiddle Demo
Related
I have the following tables
table anag (customer registry)
id | surname | name | phone |
----------------------------------------------
1 | Brown | Jack | +3989265781 |
2 | Smith | Bill | +3954872358 |
3 | Rogers | Stan | +3912568453 |
4 | Pickford | Eric | +3948521358 |
----------------------------------------------
table levels (table that connects each customer to his salesperson. For database registration reasons, the link between customer and seller is given by the customer's telephone number)
id | client_phone | id_seller |
--------------------------------------
1 | +3989265781 | 4 |
2 | +3954872358 | 7 |
3 | +3912568453 | 7 |
4 | +3948521358 | 8 |
--------------------------------------
table orders (contains all purchases made by customers, of course)
id | id_client | id_item | id_seller | price | status |
--------------------------------------------------------------------
1 | 1 | 2 | 4 | 12.50 | 2 |
2 | 2 | 2 | 7 | 12.50 | 2 |
3 | 2 | 3 | 7 | 10.00 | 3 |
4 | 2 | 3 | 7 | 10.00 | 3 |
5 | 2 | 4 | 7 | 20.50 | 1 |
6 | 3 | 2 | 7 | 12.50 | 1 |
7 | 3 | 5 | 7 | 19.00 | 3 |
8 | 3 | 7 | 7 | 31.00 | 2 |
9 | 4 | 1 | 8 | 5.00 | 1 |
--------------------------------------------------------------------
What I'm trying to do is get from the JOIN of these tables a complete list by seller of his customers sorted in descending order by the amount spent on orders as long as the order status is 2 or 3
Something like this (example seller id 7):
id | surname | name | amaount |
----------------------------------------
3 | Rogers | Stan | 50.00 |
2 | Smith | Bill | 32.50 |
----------------------------------------
I have tried with this query which seems correct to me, but unfortunately it returns me error in fetch_assoc()
SELECT a.id, a.surname, a.name, o.amount FROM levels AS l
JOIN anag AS a ON a.phone = l.client_phone
JOIN {
SELECT id_client, SUM(price) AS amount FROM orders
WHERE id_seller = '7' AND (status = '2' OR status = '3') GROUP BY id_client
} AS o ON o.id_client = a.id
WHERE l.id_seller = '7'
ORDER BY o.amount DESC
If I separate the subquery from the main query, both return the data I expect and it seems strange to me the JOIN between the two does not work properly
I think the only real error is using curly braces instead of parentheses:
SELECT a.id, a.surname, a.name, o.amount
FROM levels l JOIN
anag a
ON a.phone = l.client_phone JOIN
(SELECT id_client, SUM(price) AS amount
FROM orders
WHERE id_seller = '7' AND status IN ('2', '3'))
GROUP BY id_client
) o
ON o.id_client = a.id
WHERE l.id_seller = '7'
ORDER BY o.amount DESC;
In addition:
You can use IN to shorten an equality comparison to multiple values.
Although I left them in, status and id_seller look like numbers. If so, drop the single quotes. Don't mix data types.
Your question is ambiguous on what to do if the seller in orders differs from the seller in anag for a customer. This keeps your logic (the sellers need to match).
SELECT a.id, a.surname, a.name, sum(o.price) 'amount'
FROM anag a
LEFT JOIN levels l ON l.id =a.id
LEFT JOIN orders of ON o.id_seller = l.id_seller AND o.id_client = l.id
GROUP BY o.id_seller
ORDER BY amount DESC
I'm trying to combine a few tables into a row.
Team Table:
+----+-------+
| id | team |
+----+-------+
| 10 | Team1 |
| 11 | Team2 |
| 12 | Team3 |
+----+-------+
Location Table:
+----+-----------+
| id | location |
+----+-----------+
| 1 | location1 |
| 2 | location2 |
| 3 | location3 |
+----+-----------+
Stops Table:
+----+---------+-------------+---------------------+
| id | team_id | location_id | timestamp |
+----+---------+-------------+---------------------+
| 1 | 10 | 2 | 2019-11-07 15:27:42 |
| 2 | 10 | 3 | 2019-11-07 16:37:52 |
| 3 | 10 | 4 | 2019-11-07 17:47:62 |
+----+---------+-------------+---------------------+
Looking to create the desired table:
+----+---------+---------------------+---------------------+---------------------+
| id | team_id | (loc id=2) | (loc id=3) | (loc id=4) |
+----+---------+---------------------+---------------------+---------------------+
| 1 | 10 | 2019-11-07 15:27:42 | 2019-11-07 16:37:52 | 2019-11-07 17:47:62 |
| 2 | 11 | | | |
| 3 | 12 | | | |
+----+---------+---------------------+---------------------+---------------------+
There will always be a finite number of locations.
Any guidance would be greatly appreciated! I've tried a handful of LEFT JOINS, but am not getting far.
You can do conditional aggregation:
select
t.id team_id
max(case when s.location_id = 2 then timestamp end) loc_id_2,
max(case when s.location_id = 3 then timestamp end) loc_id_3,
max(case when s.location_id = 4 then timestamp end) loc_id_4
from
team t
left join stops s on s.team_id = t.id
group by t.id
If you want to generate an id column on the fly for the generated results (which makes little sense since you get one record per team_id already), then you can use row_number() (availble in MySQL 8.0 onwards):
select
row_number() over(order by t.id) id,
t.*
from (
select
t.id team_id,
max(case when s.location_id = 2 then timestamp end) loc_id_2,
max(case when s.location_id = 3 then timestamp end) loc_id_3,
max(case when s.location_id = 4 then timestamp end) loc_id_4
from
team t
left join stops s on s.team_id = t.id
group by t.id
) t
I have a pivot table with the respective fields.
district
block
MaleCount
FemaleCount
above are a column name. So, basically, I have grouped all those fileds by district and block-wise. For that, I have written the following query in MySQL.
select
ds.ds_name as district,
bs.bl_name as block,
case
when sd.gender_id = 1 then count(gender_id)
else 0
end as MaleCount,
case
when sd.gender_id = then count(gender_id)
else 0
end as FemalCount,
from studet data sd
inner join district ds on ds.district_id = sd.ds_id
inner join block bs on bs.block_id = sd.bs_id
group by bs.block_name, ds.district_name;
which gives the following result
district | block | FemalCount | MaleCount
xyz | abc | 4 | 5
| cvz | 5 | 9
ytz | tyz | 7 | 3
| awe |3 |8
The main thing is, I want the following kind of the result set.
district | block | FemalCount | MaleCount
xyz | abc | 4 | 5
| cvz | 5 | 9
total | | 9 | 14
ytz | tyz | 7 | 3
| awe | 3 | 8
total | | 10 | 11
which includes a subtotal row for Malecount and FemaleCount with a group by block and district as mention above. How could I achieve this in MySQL query?
You could add a union all (with high values for the block name to push it to the end on the order by) to calculate the totals by district so given
+----------+-----------+-------+-------+
| sudentid | gender_id | ds_id | bs_id |
+----------+-----------+-------+-------+
| 101 | 2 | 3 | 2 |
| 103 | 2 | 3 | 2 |
| 112 | 1 | 3 | 3 |
| 116 | 1 | 3 | 3 |
| 117 | 1 | 3 | 3 |
| 1 | 1 | 1 | 1 |
| 2 | 1 | 1 | 1 |
| 3 | 1 | 1 | 1 |
+----------+-----------+-------+-------+
8 rows in set (0.00 sec)
DROP TABLE IF EXISTS DISTRICT,BLOCK;
CREATE TABLE DISTRICT (ID INT, name varchar(10));
create table block(id int, name varchar(10));
insert into district values (1,'aaa'),(2,'bbb'),(3,'ccc'),(4,'ddd');
insert into block values (1,'E'),(2,'F'),(3,'G');
select
case when block = 'zzz' then 'Total'
else district
end as district,
case when block = 'zzz' then ''
else block
end as block,
malecount,femalecount
from
(
select
ds.name as district,
bs.name as block,
sum(case when gender_id = 1 then 1 else 0 end) Malecount,
sum(case when gender_id = 2 then 1 else 0 end) Femalecount
from student sd
inner join district ds on ds.id = sd.ds_id
inner join block bs on bs.id = sd.bs_id
group by ds.name, bs.name
union all
select district,'zzz', malecount,femalecount
from
(
select ds.name as district,
sum(case when gender_id = 1 then 1 else 0 end) Malecount,
sum(case when gender_id = 2 then 1 else 0 end) Femalecount
from student sd
inner join district ds on ds.id = sd.ds_id
group by ds.name
) s
) t
order by t.district,t.block
;
Result
+----------+-------+-----------+-------------+
| district | block | malecount | femalecount |
+----------+-------+-----------+-------------+
| aaa | E | 3 | 0 |
| Total | | 3 | 0 |
| ccc | F | 0 | 2 |
| ccc | G | 3 | 0 |
| Total | | 3 | 2 |
+----------+-------+-----------+-------------+
5 rows in set (0.00 sec)
Note the slightly less verbose conditional aggregation (sum(case when...)
I have a number of tables in my database.
Table: ObjectToPerson
For example if I had a number of entries below in the database:
+----+------------+------------+----------+----------+--------------+
| Id | WeekNumber | Date | PersonId | ObjectId | ObjectTypeId |
+----+------------+------------+----------+----------+--------------+
| 1 | 1 | 2015-11-04 | 1 | 1 | 1 |
| 2 | 1 | 2015-11-04 | 1 | 3 | 2 |
| 3 | 1 | 2015-11-04 | 2 | 2 | 1 |
| 4 | 1 | 2015-11-04 | 2 | 4 | 2 |
+----+------------+------------+----------+----------+--------------+
I am wanting to return the results back as two lines as follows:
+------+------------+----------+----------------------------+----------------------------+
| Week | Date | PersonId | ObjectId(ObjectTypeId = 1) | ObjectId(ObjectTypeId = 2) |
+------+------------+----------+----------------------------+----------------------------+
| 1 | 2015-11-04 | 1 | 1 | 3 |
| 1 | 2015-11-04 | 2 | 2 | 4 |
+------+------------+----------+----------------------------+----------------------------+
I am thinking of some sort of Group By query but I just can't seem to get it right.
Select * From ObjectToPerson
Left Join Objects O On O.Id = ObjectToPerson.ObjectId And ObjectToPerson.ObjectTypeId = 1
Left Join Objects O On O.Id = ObjectToPerson.ObjectId And ObjectToPerson.ObjectTypeId = 2
Can someone explain how I would get to this please?
You could use CASE to only select the ObjectId if the type is correct for the column, then use MAX/GROUP BY to group the result into a single row per person/week/date.
SELECT WeekNumber week, date, personid,
MAX(CASE WHEN ObjectTypeId=1 THEN ObjectId END) Type1,
MAX(CASE WHEN ObjectTypeId=2 THEN ObjectId END) Type2
FROM ObjectToPerson
GROUP BY week, date, personid
An SQLfiddle to test with.
You don't want two joins, you want a WHERE clause;
SELECT * FROM ObjectToPerson
LEFT JOIN Objects O ON O.Id = ObjectToPerson.ObjectId
WHERE ObjectToPerson.ObjectTypeId IN(1,2)
Senario:
I'm working with two tables, (structure below)
expenses_tb person_expenses_tb
+----+-----------+ +----+------+-------------+-----------+--------+
| id | expenses | | id | year | expenses_id | person | amount |
+----+-----------+ +----+------+-------------+-----------+--------+
| 1 | Bus | | 1 | 2007 | 1 | Will | 20 |
| 2 | Food | | 2 | 2007 | 2 | Will | 200 |
| 3 | Clothes | | 3 | 2007 | 4 | Will | 1000 |
| 4 | Girlfriend| | 4 | 2007 | 5 | Will | 20 |
| 5 | Taxi | | 5 | 2008 | 3 | Will | 500 |
+----+-----------+ | 6 | 2008 | 5 | Will | 100 |
| 7 | 2008 | 2 | Holly | 200 |
| 8 | 2007 | 5 | Holly | 850 |
| .. | 2013 | ... | .... | ... |
I have tried two different queries.
SELECT person, expenses,
CASE expense_id WHEN 1 THEN amount END AS 'bus',
CASE expense_id WHEN 2 THEN amount END AS 'food',
CASE expense_id WHEN 3 THEN amount END AS 'clothes',
CASE expense_id WHEN 4 THEN amount END AS girlfriend',
CASE expense_id WHEN 5 THEN amount END AS 'taxi'
FROM person_expenses_tb p
JOIN expenses e ON e.id=p.expenses_id
WHERE p.year = 2008
GROUP BY p.person
The query above runs fast but does not produce the desired output.
Second query i tried was
SELECT person, expenses,
(SELECT amount FROM person_expenses_tb p_bus WHERE expense_id = 1 AND p_bus.person = p.person AND year=2008) AS 'bus',
(SELECT amount FROM person_expenses_tb p_bus WHERE expense_id = 2 AND p_bus.person = p.person AND year=2008) AS 'food',
(SELECT amount FROM person_expenses_tb p_bus WHERE expense_id = 3 AND p_bus.person = p.person AND year=2008) AS 'clothes',
(SELECT amount FROM person_expenses_tb p_bus WHERE expense_id = 4 AND p_bus.person = p.person AND year=2008) AS girlfriend',
(SELECT amount FROM person_expenses_tb p_bus WHERE expense_id = 5 AND p_bus.person = p.person AND year=2008) AS 'taxi'
FROM person_expenses_tb p
JOIN expenses e ON e.id=p.expenses_id
WHERE p.year = 2008
GROUP BY p.person
this query produced the right result but its extremely slow when the [person_expenses_tb] has over 2000 records.
the desired result for 2007: I have a i pass the requested year to the query.
+--------+------+-----+------+---------+------------+------+
| person | Year | Bus | Food | Clothes | Girlfriend | Taxi |
+--------+------+-----+------+---------+------------+------+
| Will | 2007 | 20 | 20 | 0 | 1000 | 20 |
| Holly | 2007 | 0 | 0 | 0 | 0 | 850 |
| ... | ... | ... | ... | ... | ... | ... |
i would love help on how to improve the query for it to run faster and if there is another way of getting the desired output i would appreciate the help.
Thank you.
The first query is the right approach (in general). You just need aggregation functions:
SELECT person,
sum(CASE p.expenses_id WHEN 1 THEN amount END) AS bus,
sum(CASE p.expenses_id WHEN 2 THEN amount END) AS food,
sum(CASE p.expenses_id WHEN 3 THEN amount END) AS clothes,
sum(CASE p.expenses_id WHEN 4 THEN amount END) AS girlfriend,
sum(CASE p.expenses_id WHEN 5 THEN amount END) AS taxi
FROM person_expenses_tb p
WHERE p.year = 2008
GROUP BY p.person;
Your version doesn't work because of a "feature" of MySQL. p.expenses_id is not in the group by clause. In that case, MySQL chooses an arbitrary value. Hence, only one of the columns will be populated. The aggregation function fixes this problem.
Because you are using expense_id for pivoting, you don't need the join to the reference table. I also removed the single quotes from the names of the columns. Although MySQL allows this, it is bad practice. Just use single quotes for string constants. If you need to escape a name, use back quotes or double quotes.
(I also changed expense_id to expenses_id to match the naming in your sample data.)