Refactoring multiple mysql queries into 1 - php

I am currently developing a system which generates various reports for a client and i need pointing in the direction in-order to achieve what i need.
The current situation is that i need to produce an overview of tasks which are outstanding (and some details about what is remaining).
So far i have created a list of all tasks so far (including information about the customer), this was achieved in the following query:
SELECT `fusion_repairs`.*, `fusion_customers`.*
FROM `fusion_repairs`
RIGHT JOIN `fusion_customers`
ON `fusion_repairs`.`customer_id` = `fusion_customers`.`customer_id`
ORDER BY `fusion_repairs`.`repair_id`
DESC LIMIT 20
Ideally i need to be able to check to see if the customer has been emailed about the task (fusion_mail table). If there are no mail records found for this task then i still want to show it (so another right join is out of the question.
I also need to do the same with the fusion_response table to check to see if the customer has sent in a response. If they haven't then i still want to display the task anyway.
Obviously i know i can achieve this by running a query within the main select query, but this doesn't sit well in my head. Is there a way i can do this in 1 query instead?
Thank you.

Change your query structure to use left joins only - essentially the same as a right join but then not having to swap between left and right join.
The fact you right-joined customers onto repairs tells me customers is the main table so now we can say get me everything from customers, join in repairs if a record exists.
SELECT `fusion_repairs`.*, `fusion_customers`.*
FROM `fusion_customers`
LEFT OUTER JOIN `fusion_repairs` ON `fusion_repairs`.`customer_id` = `fusion_customers`.`customer_id`
ORDER BY `fusion_repairs`.`repair_id`
DESC LIMIT 20
Here we can use left outer join to see if we have any mail, if not, do not remove the row and the same with response
SELECT `fusion_repairs`.*, `fusion_customers`.*
FROM `fusion_customers`
LEFT OUTER JOIN `fusion_repairs` ON `fusion_repairs`.`customer_id` = `fusion_customers`.`customer_id`
LEFT OUTER JOIN `fusion_mail` ON `fusion_mail`.`repair_id` = `fusion_repairs`.`repair_id`
LEFT OUTER JOIN `fusion_response` ON `fusion_response`.`repair_id` = `fusion_repairs`.`repair_id`
ORDER BY `fusion_repairs`.`repair_id`
DESC LIMIT 20
Now my question is.. What do you want to be returned? If there are 5 mail records then there will be 5 records for the 1 repair record. Do you just want a yes/no tick just to say they have then maybe a correlated subquery in the main select is actually what you want or a group by and count the number of mail inside a case to tick.
Let me know what you want as your output to finish the query up.
EDIT:
I've made an update to the query to take into account the information you gave me. Please note I have no idea how you want to output the final result so I just made a guess! Let me know if this solves it otherwise back to the drawing board for me!
SELECT fusion_repairs.*,
fusion_customers.*,
CONCAT( CASE WHEN fusionMail.mail_type_A > 0 THEN 'displayA' ELSE '' END,
CASE WHEN fusionMail.mail_type_B > 0 THEN 'displayB' ELSE '' END,
CASE WHEN fusionMail.mail_type_C > 0 THEN 'displayC' ELSE '' END) as email_font_awesome_icon,
CONCAT( CASE WHEN fusionResponse.response_status_A > 0 THEN 'displayA' ELSE '' END,
CASE WHEN fusionResponse.response_status_B > 0 THEN 'displayB' ELSE '' END,
CASE WHEN fusionResponse.response_status_C > 0 THEN 'displayC' ELSE '' END) as response_font_awesome_icon
FROM fusion_customers
LEFT OUTER JOIN fusion_repairs ON fusion_repairs.customer_id = fusion_customers.customer_id
LEFT OUTER JOIN (
SELECT repair_id,
SUM(CASE WHEN mail_type = 1 THEN 1 else 0 END) AS mail_type_A,
SUM(CASE WHEN mail_type = 2 THEN 1 else 0 END) AS mail_type_B,
SUM(CASE WHEN mail_type = 3 THEN 1 else 0 END) AS mail_type_C
FROM fusion_mail
GROUP BY repair_id
) fusionMail ON fusionMail.repair_id = fusion_repairs.repair_id
LEFT OUTER JOIN (
SELECT repair_id,
SUM(CASE WHEN response_status = 1 THEN 1 else 0 END) AS response_status_A,
SUM(CASE WHEN response_status = 2 THEN 1 else 0 END) AS response_status_B,
SUM(CASE WHEN response_status = 3 THEN 1 else 0 END) AS response_status_C
FROM fusion_response
GROUP BY repair_id
) fusionResponse ON fusionResponse.repair_id = fusion_repairs.repair_id

Related

How to count all values of columns where two conditions are satisfied using mysql?

I have two tables one is "invoices" and other is "invoice_items".so i want to generate report using these two tables.
This should do the trick...please have a look on CASE WHEN
select i.Date, i.No,sum(CASE WHEN t.VAT<>'no' THEN
amount ELSE 0 END) as Excluding_VAT,
sum(t.amt_vat)as vatamount,
sum(CASE WHEN t.VAT='no' THEN amount ELSE 0 END) as NonVat,
sum(t.amt_vat+t.amount)as totamt
from a i join b t on i.ID=t.ID
where i.Date between '1991-11-18' and '1995-11-19'
group by i.ID,i.No,i.Date
this is for Sale_Value_Excluding_VAT
SELECT SUM(amount) FROM (select amount from invoice_items join invoices on invoices.invoiceid=invoice_items.invoiceid
where includevat=TRUE) AS T

how can i select data and sub query group by

When i select data only show first lot data . but i need all lot data.
Here is my query:
SELECT lot,
(select count(pass) FROM pass_fail_result where pass=0) toatl_fail,
(select count(pass) FROM pass_fail_result where pass=1) toatl_pass FROM pass_fail_result group by lot;
I want to show all pass result like pass=10 and fail=2
The easiest way to do this is via conditional aggregation, where we count or sum CASE expressions which target the failing or passing records:
SELECT
lot,
COUNT(CASE WHEN pass = 0 THEN 1 END) AS toatl_fail,
COUNT(CASE WHEN pass = 1 THEN 1 END) AS toatl_pass
FROM pass_fail_result
GROUP BY
lot;

Relational division SQL

I programmed a filter which generates a Query to show special employees.
I have table employees and a lot of 1:1, 1:n and n:m relationships e.g. for skills and languages for the employees like this:
Employees
id name
1 John
2 Mike
Skills
id skill experience
1 PHP 3
2 SQL 1
Employee_Skills
eid sid
1 1
1 2
Now I want to filter employees which have at least 2 years experience in using PHP and 1 year SQL.
My filter always generates a correct working Query for every table, relationship and field.
But now my problem is when I would like to filter the same field in a related table multiple times with a and it does not work.
e.g.
John PHP 3
John SQL 1
PHP and SQL are different rows so AND can not work.
I tried using group_concat and find_in_set but I have the problem that I can not filter experience over 2 years with find_in_set and find_in_set does not know PHP is 3 and SQL is 1.
I also tried
WHERE emp.id IN (SELECT eid FROM Employee_Skills WHERE sid IN (SELECT id FROM Skills WHERE skill = 'PHP' AND experience > 1)) AND emp.id IN (SELECT eid FROM Employee_Skills WHERE sid IN (SELECT id FROM Skills WHERE skill = 'SQL' AND experience > 0))
which works for this example, but it only works for n:m and it too complex to know the relationship type.
I have the final Query with
ski.skill = 'PHP' AND ski.experience > 1 AND ski.skill = 'SQL' AND ski.experience > 0
and I would like to manipulate the Query to make it work.
How does a Query have to look like to deal with relational division.
you can try next approach:
select * from Employees
where id in (
select eid
from Employee_Skills as a
inner join
Skills as ski
on (a.sid = ski.id)
where
(ski.skill = 'PHP' AND a.experience > 2) OR
(ski.skill = 'SQL' AND a.experience > 1)
group by eid
having count(*) = 2
)
so, for every filter you will add OR statement, having will filter employees with all filters passed, just pass appropriate number
You could make a kind of pivot query, where you put the experience in each of all of the known skills in columns. This could be a long query, but you could build it dynamically in php, so it would add all skills as columns to the final query, which would look like this:
SELECT e.*, php_exp, sql_exp
FROM Employee e
INNER JOIN (
SELECT es.eid,
SUM(CASE s.skill WHEN 'PHP' THEN s.experience END) php_exp,
SUM(CASE s.skill WHEN 'SQL' THEN s.experience END) sql_exp,
SUM(CASE s.skill WHEN 'JS' THEN s.experience END) js_exp
-- do the same for other skills here --
FROM Employee_Skills es
INNER JOIN Skills s ON es.sid = s.id
GROUP BY es.eid
) pivot ON pivot.eid = e.id
WHERE php_exp > 2 AND sql_exp > 0;
The WHERE clause is then very concise and intuitive: you use the logical operators like in other circumstances.
If the set of skills is rather static, you could even create a view for the sub-query. Then the final SQL is quite concise.
Here is a fiddle.
Alternative
Using the same principle, but using the SUM in the HAVING clause, you can avoid gathering all skill's experiences:
SELECT e.*
FROM Employee e
INNER JOIN (
SELECT es.eid
FROM Employee_Skills es
INNER JOIN Skills s ON es.sid = s.id
GROUP BY es.eid
HAVING SUM(CASE s.skill WHEN 'PHP' THEN s.experience END) > 2
AND SUM(CASE s.skill WHEN 'SQL' THEN s.experience END) > 0
) pivot ON pivot.eid = e.id;
Here is a fiddle.
You can also replace the CASE construct by the IF function, like this:
HAVING SUM(IF(s.skill='PHP', s.experience, 0)) > 2
... etc.
But it comes down to the same.
The straightforward way would be to repeatedly JOIN the skills:
SELECT e.*
FROM Employees AS e
JOIN Employee_Skills AS j1 ON (e.id = j1.eid)
JOIN Skills AS s1 ON (j1.sid = s1.id AND s1.skill = 'PHP' AND s1.experience > 3)
JOIN Employee_Skills AS j2 ON (e.id = j2.eid)
JOIN Skills AS s2 ON (j2.sid = s2.id AND s2.skill = 'SQL' AND s2.experience > 1)
...
Since all the clauses are required this translated to a straight JOIN.
You will need to add two JOINs for each clause, but they're quite fast joins.
A more hackish way would be to compress the skills into a code in a 1:1 relation with the employees. If experience never exceeds, say, 30, then you can multiply the first condition's experience by 1, the second by 30, the third by 30*30, the fourth by 30*30*30... and never get an overflow.
SELECT eid, SUM(CASE skill
WHEN 'PHP' THEN 30*experience
WHEN 'SQL' THEN 1*experience) AS code
FROM Employees_Skills JOIN Skills ON (Skills.id = Employees_Skills.sid)
GROUP BY eid HAVING code > 0;
Actually since you want 3 years PHP, you can HAVE code > 91. If you had three conditions with experiences 2, 3 and 5, you would request more than x = 2*30*30 + 3*30 + 5. This only serves to whittle the results, since 3*30*30 + 2*30 + 4 still passes the filter but is of no use to you. But since you want a restriction on code, and "> x" costs the same as "> 0" and gives better results... (if you needed more complex filtering than a series of AND, > 0 is safer, though).
The table above you join with Employees, then on the result you perform the true filtering, requiring
((code / 30*30) % 30) > 7 // for instance :-)
AND
((code / 30) % 30) > 3 // for PHP
AND
((code / 1) % 30) > 1 // for SQL
(the *1 and /1 are superfluous, and only inserted to clarify)
This solution requires a full table scan on Skills, with no real possibility of automatic optimizations. So it is slower than the other solution. On the other hand, its cost grows much more slowly, so if you have complex queries, or need OR operators or conditional expressions instead of ANDs, it may be more convenient to implement the "hackish" solution.

Make SQL query faster

Can anyone tell me how to make this query faster?
$session_id = '000000000015';
$start = 0;
$finish = 30;
try {
$stmt = $conn->prepare("SELECT TOPUSERS.ID, TOPUSERS.USERNAME, TOPUSERS.NAME, TOPUSERS.NAME2, TOPUSERS.PHOTO, TOPUSERS.FB_USERID, TOPUSERS.IMAGE_TYPE, TOPUSERS.TW_USERID, TOPUSERS.TW_PHOTO,
COALESCE((SELECT COUNT(USERS_BUCKETS.ID) FROM USERS_BUCKETS WHERE USERS_BUCKETS.USERID=TOPUSERS.ID),0) AS NUM_ALL,
COALESCE((SELECT SUM(CASE WHEN USERS_BUCKETS.STATUS='Completed' THEN 1 ELSE 0 END) FROM USERS_BUCKETS WHERE USERS_BUCKETS.USERID=TOPUSERS.ID),0) AS NUM_DONE,
COALESCE((SELECT COUNT(USERS_LIKES.ID) FROM USERS_LIKES WHERE USERS_LIKES.USERID=TOPUSERS.ID),0) AS NUM_LIKES,
(SELECT USERS_BUCKETS.BUCKETID FROM USERS_BUCKETS WHERE USERS_BUCKETS.USERID=TOPUSERS.ID ORDER BY USERS_BUCKETS.DATE_MODIFIED DESC LIMIT 1) AS RECENT_BUCKET,
(SELECT BUCKETS_NEW.BUCKET_NAME FROM BUCKETS_NEW WHERE BUCKETS_NEW.ID=RECENT_BUCKET) AS REC,
COALESCE((SELECT COUNT(ID) FROM FOLLOW WHERE FOLLOW.USER_ID=TOPUSERS.ID),0) AS FOLLOWING,
COALESCE((SELECT COUNT(ID) FROM FOLLOW WHERE FOLLOW.FOLLOW_ID=TOPUSERS.ID),0) AS FOLLOWERS,
(SELECT IF(TOPUSERS.NAME = '',0,1) + IF(TOPUSERS.BIO = '',0,1) + IF(TOPUSERS.LOCATION = '',0,1) + IF(TOPUSERS.BIRTHDAY = '0000-00-00',0,1) + IF(TOPUSERS.GENDER = '',0,1)) as COMPLETENESS,
CASE WHEN ? IN (SELECT USER_ID FROM FOLLOW WHERE FOLLOW_ID = TOPUSERS.ID) THEN 'Yes' ELSE 'No' END AS DO_I_FOLLOW_HIM
FROM TOPUSERS
LEFT JOIN FOLLOW ON TOPUSERS.ID = FOLLOW.FOLLOW_ID
LEFT JOIN USERS_BUCKETS ON USERS_BUCKETS.USERID=TOPUSERS.ID
LEFT JOIN BUCKETS_NEW ON BUCKETS_NEW.ID=USERS_BUCKETS.BUCKETID
WHERE NOT TOPUSERS.ID = ?
GROUP BY TOPUSERS.ID ORDER BY TOPUSERS.RANDOM, TOPUSERS.USERNAME LIMIT $start, $finish");
When I run this in a browser it takes about 7 seconds to load. Without a few lines (the COALESCE in the middle, the two SELECTS above and the line below them) the time is reduced to 3-4 seconds.
The result of the query is a list of people with names, profile picture and some data.
TL,DR: you need to rewrite the query.
You need to rewrite your query to make it more efficient. I had to rewrite a similar query at work last week and here is what I have done.
The structure of your query should look like this to be efficient:
select ...
...
from ...
join ...
where ...
what you have now is something like:
select ...
inner select
inner select
from ...
join ...
where ...
That's the inner selects that kill your query. You need to find a way to move the inner select into the from section. Especially that you already query the tables.
What you need to understand is that your inner selects run for every records you have. So if you have 10 records, it would be alright (speed wise). But with hundred or thousand of records, it would be very slow.
If you want more information on your query run it with the explain keyword in from of it.

What is the error in my query?

in below query it returns 12 record while query in from clause (as t) returns 18 record, can anyone help what is the issue in this query?
SELECT count(abc.id) as total_this_month,t.*
FROM email_details abc
JOIN
(SELECT count(email_details.id) as total_emails,MAX(`email_details`.email_date) as email_date1, `email_details`.* FROM (`cld_users` join email_details on email_details.fk_user_id = cld_users.id) GROUP BY `email_details`.`email_title` ORDER BY `email_details`.`email_date` DESC) as t
ON abc.email_title = t.email_title
where (MONTH(abc.email_date)=MONTH(NOW()) AND YEAR(abc.email_date)=YEAR(NOW()))
group by t.email_title
ORDER BY t.`email_date` DESC
In your query, you specify
where (MONTH(abc.email_date)=MONTH(NOW()) AND YEAR(abc.email_date)=YEAR(NOW()))
But in the subquery (the one returning 18 results), you have 6 emails with a month that is not december 2014. There's no way that those emails can be returned by a query that explicitly excludes them.
You want those emails as well so you get 18 results ? Remove the WHERE clause excluding them:
SELECT Count(abc.id) AS total_this_month,
t.*
FROM email_details abc
JOIN (SELECT Count(email_details.id) AS total_emails,
Max(`email_details`.email_date) AS email_date1,
`email_details`.*
FROM (`cld_users`
JOIN email_details
ON email_details.fk_user_id = cld_users.id)
GROUP BY `email_details`.`email_title`
ORDER BY `email_details`.`email_date` DESC) AS t
ON abc.email_title = t.email_title
GROUP BY t.email_title
ORDER BY t.`email_date` DESC
Starting from there, if you want to count the emails from the current month, simply replace:
SELECT Count(abc.id) AS total_this_month,
with
SELECT SUM(CASE WHEN MONTH(abc.email_date)=MONTH(NOW()) AND YEAR(abc.email_date)=YEAR(NOW()) THEN 1 ELSE 0 END) AS total_this_month,

Categories