Merge multiple query results into one using case - mysql

I have a user-table which contains users information with the fields user_id, fullname, age, username, password and other is tests-table with the fields id, user_id, test_type
When I write the below query
select users.user_id, fullname,
(CASE WHEN test_table.user_id=users.id and test_table.type = 'objectives' THEN 'yes' ELSE 'no' END) AS written_objectives,
(CASE WHEN test_table.user_id=users.id and test_table.type = 'theory' THEN 'yes' ELSE 'no' END) AS written_theory,
from users
LEFT JOIN test_table ON users.id = test_table.user_id
WHERE users.user_id = 1
I get the results like this
user_id|fullname| test_type | written_objectives| written_theory
1 | Ben. | objectivs | yes | no
1 | Ben. | theory | no | yes
But I want the results like this
user_id|fullname| test_type | written_objectives| written_theory
1 | Ben. | objectivs | yes | yes
From the above scenario, the user with id of 1 has submitted both objectives and theory.

You can do conditional aggregation:
select u.user_id, u.fullname,
max(tt.type = 'objectives') written_objectives,
max(tt.type = 'theory' ) written_theory
from users u
left join test_table tt on u.id = tt.user_id
where u.user_id = 1
group by u.user_id
This generates 0/1 values rather than 'yes'/'no' - which I find more expressive. But if you really want thoses strings, then:
select u.user_id, u.fullname,
case when max(tt.type = 'objectives') then 'yes' else 'no' end written_objectives,
case when max(tt.type = 'theory' ) then 'yes' else 'no' end written_theory
from users u
left join test_table tt on u.id = tt.user_id
where u.user_id = 1
group by u.user_id
Notes:
there is no need to repeat the join condition in the conditional expression - the left join handles that already
table aliases make the query easier to write and read

Related

COnditional JOIN query in MySQL

I have two mysql tables
user:
|--------------------------------|
| id | name | type | ruser_type |
|--------------------------------|
| 1 | Admin | a | |
| 2 | | r | c |
|--------------------------------|
customer
|-------------------------|
| id | name | user_id |
|-------------------------|
| 1 | Sam | 2 |
|-------------------------|
If user.type is 'a' or 's', then its admin user whose name is in user table.
If user.type is 'r' and ruser_type is 'c', then its regular user which has a relation in customer table where customer.user_id = user.id
I want a query which would run a conditional join.
If user.type is 'a' or 's', then name would be fetched from user table.
If user.type is 'r' and and ruser_type is 'c', then name would be fetched from customer table with the JOIN condition customer.user_id = user.id.
For this, I have written a query like this:-
SELECT users.fname as adminFname, customers.fname as customerFname, users.type FROM users
LEFT JOIN customers ON (customers.user_id = users.id AND
(
(users.type = 'r' AND users.ruser_type = 'c')
OR users.type = 'a'
OR users.type = 's'
)
)
WHERE users.id = 1
Is there any possibility to optimize the query more?
Also, how can I write this query using Laravel eloquent?
FWIW, I find this marginally easier to read...
SELECT u.fname adminFname
, c.fname customerFname
, u.type
FROM users u
LEFT
JOIN customers c
ON c.user_id = u.id
WHERE u.id = 1
AND (
(u.type = 'r' AND u.ruser_type = 'c')
OR (u.type IN('a','s'))
)
I have written two sql query hope this will help you
SELECT CASE CU.type WHEN 'a' OR 's' THEN CU.name END AS name,
CASE WHEN CU.type = 'r' AND CU.ruser_type = 'c' THEN CR.name END AS cust_name, CU.type
FROM
user AS CU
LEFT JOIN customer AS CR ON CR.user_id = CU.id
In this you'll get result like this,
name cust_name type
Sam r
Admin a
and i have wrote another query like this,
SELECT CASE WHEN CU.type = 'a' OR 's' THEN CU.name ELSE CR.name END AS name, CU.type
FROM
user AS CU
LEFT JOIN customer AS CR ON CR.user_id = CU.id
In this you'll get result like this
name type
Sam r
Admin a
DB File Link
https://dbfiddle.uk/?rdbms=mysql_5.7&fiddle=b02d931b68a4f70d8b4a84144c60a572
This will give you required result for all users:
SELECT u.fname adminFname
, c.fname customerFname
, u.type
FROM users u
LEFT JOIN customers c
ON (u.type = 'r' AND u.ruser_type = 'c' AND c.user_id = u.id)
Add where condition as required.
You can even simplify it further to get common firstName column in output:
SELECT COALESCE(u.fname, c.fname) firstName, u.type
FROM users u
LEFT JOIN customers c
ON (u.type = 'r' AND u.ruser_type = 'c' AND c.user_id = u.id)

Mix SQL Rows into one

I am using the following SQL Query:
SELECT package_stats.developer
,xf_user.username
,xf_user.email
,xf_user_profile.homepage
,xf_user_profile.location
,xf_user_profile.about
,CASE WHEN field_id ='Github' THEN field_value ELSE '' END AS Github
,CASE WHEN field_id ='Repository' THEN field_value ELSE '' END AS Repository
,CASE WHEN field_id ='twitter' THEN field_value ELSE '' END AS Twitter
FROM package_stats, xf_user, xf_user_field_value, xf_user_profile
WHERE xf_user.username = package_stats.developer AND xf_user_profile.user_id = xf_user.user_id AND xf_user_field_value.user_id = xf_user.user_id AND xf_user_field_value.field_value <> ''
Which outputs the following:
developer username email about Github Repository Twitter
John Doe jdoe j#doe.com It's me https://github....
John Doe jdoe j#doe.com It's me
John Doe jdoe j#doe.com It's me https://link....
John Doe jdoe j#doe.com It's me https://twitter.com/...
How can I mix all those rows into a single row?
I believe you can use conditional aggregation:
SELECT ps.developer, u.username, u.email,
up.homepage, up.location, up.about,
MAX(CASE WHEN ufv.field_id = 'Github' THEN ufv.field_value END) AS Github,
MAX(CASE WHEN ufv.field_id = 'Repository' THEN ufv.field_value END) AS Repository,
MAX(CASE WHEN ufv.field_id = 'twitter' THEN ufv.field_value END) AS Twitter
FROM package_stats ps JOIN
xf_user u
ON u.username = ps.developer JOIN
xf_user_profile up
ON up.user_id = u.user_id JOIN
xf_user_field_value ufv
ON ufv.user_id = u.user_id
WHERE ufv.field_value <> ''
GROUP BY ps.developer, u.username, u.email,
up.homepage, up.location, up.about;
Notes:
Table aliases make the query easier to write and to read.
All columns should be qualified with a table alias, to avoid ambiguity.
There is no reason to have an ELSE clause for this purpose. If there are no matches, then the column is NULL (a reasonable value).
If there could be more than one field for a given category, use GROUP_CONCAT().
You could use a (fake) aggregation function and group by
SELECT package_stats.developer
,xf_user.username
,xf_user.email
,xf_user_profile.homepage
,xf_user_profile.location
,xf_user_profile.about
,min(CASE WHEN field_id ='Github' THEN field_value END) AS Github
,min(CASE WHEN field_id ='Repository' THEN field_value END) AS Repository
,min(CASE WHEN field_id ='twitter' THEN field_value END ) AS Twitter
FROM package_stats
INNER JOIN xf_user ON xf_user.username = package_stats.developer
INNER JOIN xf_user_profile ON xf_user_profile.user_id = xf_user.user_id
INNER JOIN xf_user_field_value v1 ON xf_user_field_value.user_id = xf_user.user_id
WHERE xf_user_field_value.field_value <> ''
GROUP BY package_stats.developer
,xf_user.username
,xf_user.email
,xf_user_profile.homepage
,xf_user_profile.location
,xf_user_profile.about
and don't use implict joins are not clear ..
the implict join is an arcaic sintax

How can I optimize my sql code?

I have following tables
contacts
contact_id | contact_slug | contact_first_name | contact_email | contact_date_added | company_id | contact_is_active | contact_subscribed | contact_last_name | contact_company | contact_twitter
contact_campaigns
contact_campaign_id | contact_id | contact_campaign_created | company_id | contact_campaign_sent
bundle_feedback
bundle_feedback_id | bundle_id, contact_id | company_id | bundle_feedback_rating | bundle_feedback_favorite_track_id | bundle_feedback_supporting | campaign_id
bundles
bundle_id | bundle_name | bundle_created | company_id | bundle_is_active
tracks
track_id | company_id | track_title
I wrote this query, but it works slowly, how can I optimize this query to make it faster ?
SELECT SQL_CALC_FOUND_ROWS c.contact_id,
c.contact_first_name,
c.contact_last_name,
c.contact_email,
c.contact_date_added,
c.contact_company,
c.contact_twitter,
concat(c.contact_first_name," ", c.contact_last_name) AS fullname,
c.contact_subscribed,
ifnull(icc.sendCampaignsCount, 0) AS sendCampaignsCount,
ifnull(round((ibf.countfeedbacks/sendCampaignsCount * 100),2), 0) AS percentFeedback,
ifnull(ibf.bundle_feedback_supporting, 0) AS feedbackSupporting
FROM contacts AS c
LEFT JOIN
(SELECT c.contact_id,
count(cc.contact_campaign_id) AS sendCampaignsCount
FROM contacts AS c
LEFT JOIN contact_campaigns AS cc ON cc.contact_id = c.contact_id
WHERE c.company_id = '876'
AND c.contact_is_active = '1'
AND cc.contact_campaign_sent = '1'
GROUP BY c.contact_id) AS icc ON icc.contact_id = c.contact_id
LEFT JOIN
(SELECT bf.contact_id,
count(*) AS countfeedbacks,
bf.bundle_feedback_supporting
FROM bundle_feedback bf
JOIN bundles b
JOIN contacts c
LEFT JOIN tracks t ON bf.bundle_feedback_favorite_track_id = t.track_id
WHERE bf.bundle_id = b.bundle_id
AND bf.contact_id = c.contact_id
AND bf.company_id='876'
GROUP BY bf.contact_id) AS ibf ON ibf.contact_id = c.contact_id
WHERE c.company_id = '876'
AND contact_is_active = '1'
ORDER BY percentFeedback DESC LIMIT 0, 25;
I have done 2 improvements
1) Removed the contacts which is getting joined unnecessarily twice and put the condition at the final where condition.
2) Removed as per SQL_CALC_FOUND_ROWS
Which is fastest? SELECT SQL_CALC_FOUND_ROWS FROM `table`, or SELECT COUNT(*)
SELECT c.contact_id,
c.contact_first_name,
c.contact_last_name,
c.contact_email,
c.contact_date_added,
c.contact_company,
c.contact_twitter,
concat(c.contact_first_name," ", c.contact_last_name) AS fullname,
c.contact_subscribed,
ifnull(icc.sendCampaignsCount, 0) AS sendCampaignsCount,
ifnull(round((ibf.countfeedbacks/sendCampaignsCount * 100),2), 0) AS percentFeedback,
ifnull(ibf.bundle_feedback_supporting, 0) AS feedbackSupporting
FROM contacts AS c
LEFT JOIN
(SELECT cc.contact_id,
count(cc.contact_campaign_id) AS sendCampaignsCount
FROM contact_campaigns
WHERE cc.contact_campaign_sent = '1'
GROUP BY cc.contact_id) AS icc ON icc.contact_id = c.contact_id
LEFT JOIN
(SELECT bf.contact_id,
count(*) AS countfeedbacks,
bf.bundle_feedback_supporting
FROM bundle_feedback bf
JOIN bundles b
LEFT JOIN tracks t ON bf.bundle_feedback_favorite_track_id = t.track_id
WHERE bf.bundle_id = b.bundle_id
GROUP BY bf.contact_id) AS ibf ON ibf.contact_id = c.contact_id
WHERE c.company_id = '876' and c.contact_is_active = '1'
First, you are not identifying any indexes you have to optimize the query. That said, I would ensure you have at least the following composite / covering indexes.
table index
contacts ( company_id, contact_is_active )
contact_campaigns ( contact_id, contact_campaign_sent )
bundle_feedback ( contact_id, bundle_feedback_supporting )
Next, as noted in other answer, unless you really need how many rows qualified, remove the "SQL_CALC_FOUND_ROWS".
In your first left-join (icc), you do a left-join on contact_campaigns (cc), but then throw into your WHERE clause an "AND cc.contact_campaign_sent = '1'" which turns that into an INNER JOIN. At the outer query level, these would result in no matching record and thus NULL for your percentage calculations.
In your second left-join (ibf), you are doing a join to the tracks table, but not utilizing anything from it. Also, you are joining to the bundles table but not using anything from there either -- unless you are getting multiple rows in the bundles and tracks tables which would result in a Cartesian result and possibly overstate your "CountFeedbacks" value. You also do not need the contacts table as you are not doing anything else with it, and the feedback table has the contact ID basis your are querying for. Since that is only grouped by the contact_id, your "bf.bundle_feedback_supporting" is otherwise wasted. If you want counts of feedback, just count from that table per contact ID and remove the rest. (also, the joins should have the "ON" clauses instead of within the WHERE clause for consistency)
Also, for your supporting feedback, the data type and value are unclear, so I implied as a Yes or No and have a SUM() based on how many are supporting. So, a given contact may have 100 records but only 37 are supporting. This gives you 1 record for the contact having BOTH values 100 and 37 respectively and not lost in a group by based on the first entry found for the contact.
I would try to summarize your query to below:
SELECT
c.contact_id,
c.contact_first_name,
c.contact_last_name,
c.contact_email,
c.contact_date_added,
c.contact_company,
c.contact_twitter,
concat(c.contact_first_name," ", c.contact_last_name) AS fullname,
c.contact_subscribed,
ifnull(icc.sendCampaignsCount, 0) AS sendCampaignsCount,
ifnull(round((ibf.countfeedbacks / icc.sendCampaignsCount * 100),2), 0) AS percentFeedback,
ifnull(ibf.SupportCount, 0) AS feedbackSupporting
FROM
contacts AS c
LEFT JOIN
( SELECT
c.contact_id,
count(*) AS sendCampaignsCount
FROM
contacts AS c
JOIN contact_campaigns AS cc
ON c.contact_id = cc.contact_id
AND cc.contact_campaign_sent = '1'
WHERE
c.company_id = '876'
AND c.contact_is_active = '1'
GROUP BY
c.contact_id) AS icc
ON c.contact_id = icc.contact_id
LEFT JOIN
( SELECT
bf.contact_id,
count(*) AS countfeedbacks,
SUM( case when bf.bundle_feedback_supporting = 'Y'
then 1 else 0 end ) as SupportCount
FROM
contacts AS c
JOIN bundle_feedback bf
ON c.contact_id = bf.contact_id
WHERE
c.company_id = '876'
AND c.contact_is_active = '1'
GROUP BY
bf.contact_id) AS ibf
ON c.contact_id = ibf.contact_id
WHERE
c.company_id = '876'
AND c.contact_is_active = '1'
ORDER BY
percentFeedback DESC LIMIT 0, 25;

MySQL: Select newest record from table for every user I talked to

I have 2 tables.
users schema:
private_messages schema:
time_sent is a datetime. All other fields are varchars and ints.
sender_id and receiver_id both refer to id in users.
What I want:
Given a users id, say 1, select the last sent message for every other user (whether the given user sent it or the other user sent it). It should return all the columns of the private_messages table along with the other user's username, and ordered by the time_sent.
The functionality I'm trying to achieve is that of when you open up all of your text messages, and you can see a preview of the last message sent between you and everyone else. This query is killing me because of the fact that the given users id can appear in either the sender_id or the receiver_id. Please help me out with writing the query.
Here is exported sql code to create the tables and their contents (just in case you want something to test with):
users
private_messages
private_messages 2
This should do the trick
SELECT U.username,
U2.username,
IF(PM.sender_id = U.id, 'Sent', 'Received') AS `action`,
PM.message,
PM.time_sent
FROM users U
INNER JOIN private_messages PM ON (PM.sender_id = U.id OR PM.receiver_id = U.id)
INNER JOIN users U2 ON (U2.id = IF(PM.sender_id = U.id, PM.receiver_id, PM.sender_id))
WHERE NOT EXISTS (
SELECT *
FROM private_messages NXT_PM
WHERE NXT_PM.time_sent > PM.time_sent
AND (
( NXT_PM.sender_id = PM.sender_id AND NXT_PM.receiver_id = PM.receiver_id )
OR
( NXT_PM.receiver_id = PM.sender_id AND NXT_PM.sender_id = PM.receiver_id )
)
)
AND U.username = 'Kacy'
ORDER BY time_sent DESC
This returns:
+----------+----------+----------+----------+---------------------+
| username | username | action | message | time_sent |
+----------+----------+----------+----------+---------------------+
| Kacy | liz | Sent | hi again | 2015-03-08 10:47:26 |
| Kacy | Jamie | Sent | hi | 2015-03-07 23:01:18 |
| Kacy | tracy | Received | hi | 2015-03-06 12:04:34 |
+----------+----------+----------+----------+---------------------+
3 rows in set (0.02 sec)
You can do as below technique for getting max per group
select
u1.username as sender,
u2.username as receiver,
m.message,
m.time_sent
from private_messages m
inner join users u1 on u1.id = m.sender_id
inner join users u2 on u2.id = m.receiver_id
left join private_messages m1 on m1.sender_id = m.sender_id
and m1.receiver_id = m.receiver_id
and m1.time_sent > m.time_sent
where m.sender_id = 1
and m1.id is null ;
You can also use
and m1.id > m.id
instead of
and m1.time_sent > m.time_sent
if id is auto-incremented in private_messages table
If you need to see data as both sender and receiver for a given user the condition would be as
where ( m.sender_id = 1 or m.receiver_id = 1 ) and m1.id is null
instead of
where m.sender_id = 1
and m1.id is null ;
http://dev.mysql.com/doc/refman/5.0/en/example-maximum-column-group-row.html

Mysql COUNT result rows for a related table

I have
users
------------------------
id | name | other_stuff.....
.
engagement
------------------------
user_id | type_code |
type_code is a varchar, but either A, B, C or NULL
[ EDIT for clarity: Users can have many engagements of each type code. SO I want to count how many they have of each. ]
I want to return ALL user rows, but with a count of A, B and C type engagements. E.g.
users_result
------------------------
user_id | user_name | other_stuff..... | count_A | count_B | count_C |
I've done quite a bit of searching, but found the following issues with other solutions:
The "other_stuff..." is actually grouped / concatenated results from a dozen other joins, so it's a bit of a monster already. So I need to be able to just add the additional fields to the pre-existing "SELECT ...... FROM users..." query.
The three additional required bits of data all come from the same engagement table, each with their own condition. I havent found anything to allow me to use the three conditions on the same related table.
Thanks
[edit]
I tried to simplify the question so people didn't have to look through loads of unnecessary stuff, but seems I might not have given enough info. Here is 'most' of the original query. I've taken out a lot of the selected fields as there are loads, but I've left most of the joins in so you can see basically what is actually going on.
SELECT
user.id,
user.first_name,
user.second_name,
GROUP_CONCAT(DISTINCT illness.id ORDER BY illness.id SEPARATOR ',' ) AS reason_for_treatment,
IF(ww_id=1000003, 1,'') as user_refused_program,
Group_CONCAT(DISTINCT physical_activity.name SEPARATOR ', ') AS programme_options,
COUNT(CASE WHEN engagement_item.type_code LIKE 'wm6%' THEN 1 ELSE NULL END) as count_A,
COUNT(CASE WHEN engagement_item.type_code LIKE 'wm12%' THEN 1 ELSE NULL END) as count_B,
COUNT(CASE WHEN engagement_item.type_code LIKE 'wm6%' THEN 1 ELSE NULL END) as count_C
FROM `user`
LEFT JOIN session AS session_induction ON (user.id = session_induction.user_id AND session_induction.session_type_id = 3)
LEFT JOIN stats AS stats_induction ON session_induction.id = stats_induction.session_id
LEFT JOIN session AS session_interim ON (user.id = session_interim.user_id AND session_interim.session_type_id = 4)
LEFT JOIN stats AS stats_interim ON session_interim.id = stats_interim.session_id
LEFT JOIN session AS session_final ON (user.id = session_final.user_id AND session_final.session_type_id = 5)
LEFT JOIN stats AS stats_final ON session_final.id = stats_final.session_id
LEFT JOIN user_has_illness ON user.ID = user_has_illness.user_id
LEFT JOIN illness ON user_has_illness.illness_id = illness.id
LEFT JOIN user_has_physical_activity ON user.ID = user_has_physical_activity.user_id
LEFT JOIN physical_activity ON user_has_physical_activity.physical_activity_id = physical_activity.id
LEFT JOIN engagement_item ON user.ID = engagement_item.user_ID
WHERE (user.INDUCTION_DATE>='2010-06-09' AND user.INDUCTION_DATE<='2011-06-09' AND user.archive!='1' )
GROUP BY user.id, engagement_item.user_id
It's worth mentioning that it works fine - returns all users with all details required. Except for the count_A B and C cols.
[edit added slightly more simplified query below]
Stripped out the unrelated joins and selects.
SELECT
user.id,
user.first_name,
COUNT(CASE WHEN engagement_item.type_code LIKE 'wm6%' THEN 1 ELSE NULL END) as count_A,
COUNT(CASE WHEN engagement_item.type_code LIKE 'wm12%' THEN 1 ELSE NULL END) as count_B,
COUNT(CASE WHEN engagement_item.type_code LIKE 'wm6%' THEN 1 ELSE NULL END) as count_C
FROM `user`
LEFT JOIN engagement_item ON user.ID = engagement_item.user_ID
GROUP BY user.id, engagement_item.user_id
SELECT e.user_id, u.name,
COUNT(CASE type_code WHEN 'A' THEN 1 ELSE NULL END) as count_A,
COUNT(CASE type_code WHEN 'B' THEN 1 ELSE NULL END) as count_B,
COUNT(CASE type_code WHEN 'C' THEN 1 ELSE NULL END) as count_C
FROM engagement e join users u on (e.user_id = u.id)
GROUP BY e.user_id, u.name
I would use COUNT instead of SUM just because that is what it is made for, counting things when not NULL.
SELECT
user.id,
user.first_name,
user.second_name,
GROUP_CONCAT(DISTINCT illness.id ORDER BY illness.id SEPARATOR ',' ) AS reason_for_treatment,
IF(ww_id=1000003, 1,'') as user_refused_program,
Group_CONCAT(DISTINCT physical_activity.name SEPARATOR ', ') AS programme_options,
ei.count_A, ei.count_B, ei.count_C
FROM `user`
LEFT JOIN ( SELECT user_id
, COUNT(CASE WHEN engagement_item.type_code LIKE 'wm6%' THEN 1 ELSE NULL END) as count_A
, COUNT(CASE WHEN engagement_item.type_code LIKE 'wm12%' THEN 1 ELSE NULL END) as count_B
, COUNT(CASE WHEN engagement_item.type_code LIKE 'wm6%' THEN 1 ELSE NULL END) as count_C
FROM engagement_item
GROUP BY userid ) ei
LEFT JOIN session AS session_induction ON (user.id = session_induction.user_id AND session_induction.session_type_id = 3)
LEFT JOIN stats AS stats_induction ON session_induction.id = stats_induction.session_id
LEFT JOIN session AS session_interim ON (user.id = session_interim.user_id AND session_interim.session_type_id = 4)
LEFT JOIN stats AS stats_interim ON session_interim.id = stats_interim.session_id
LEFT JOIN session AS session_final ON (user.id = session_final.user_id AND session_final.session_type_id = 5)
LEFT JOIN stats AS stats_final ON session_final.id = stats_final.session_id
LEFT JOIN user_has_illness ON user.ID = user_has_illness.user_id
LEFT JOIN illness ON user_has_illness.illness_id = illness.id
LEFT JOIN user_has_physical_activity ON user.ID = user_has_physical_activity.user_id
LEFT JOIN physical_activity ON user_has_physical_activity.physical_activity_id = physical_activity.id
LEFT JOIN engagement_item ON user.ID = engagement_item.user_ID
WHERE (user.INDUCTION_DATE>='2010-06-09' AND user.INDUCTION_DATE<='2011-06-09' AND user.archive!='1' )
GROUP BY user.id, engagement_item.user_id, ei.count_A, ei.count_B, ei.count_C
Something like this perhaps?
select e.user_id, u.name,
sum(case e.type_code when 'A' then 1 else 0 end) as count_A,
sum(case e.type_code when 'B' then 1 else 0 end) as count_B,
sum(case e.type_code when 'C' then 1 else 0 end) as count_C
from engagement e join users u on (e.user_id = u.id)
group by e.user_id, u.name
The interesting part is the use of CASE inside the SUM to split the counting into three chunks.