Optimize Query - Select COUNT and SUM from the same subquery - mysql

I tried to find the solution to this on Stackoverflow, maybe my wording is wrong.
I have a query which takes to long to execute. I am sure there are simple ways to improve it. For example, I use the same sub-query twice for displaying two different columns (sum and count) but encountered several errors while trying to solve it on my own.
SELECT u.ID,
u.user_email AS mail,
u.user_login AS userName,
u.user_registered AS signUpDate,
(select meta_value from wp_usermeta where user_id = u.ID and meta_key = 'first_name' limit 1) as firstName,
(select meta_value from wp_usermeta where user_id = u.ID and meta_key = 'last_name' limit 1) as lastName,
(select meta_value from wp_usermeta where user_id = u.ID and meta_key = 'billing_phone' limit 1) as billingPhone,
(select meta_value from wp_usermeta where user_id = u.ID and meta_key = 'shipping_phone' limit 1) as shippingPhone,
(SELECT COUNT(meta_value) from wp_postmeta WHERE meta_key = '_order_total' and post_id IN (select post_id from wp_postmeta where meta_value = u.ID and meta_key = '_customer_user')) as orderCount,
(SELECT SUM(meta_value) from wp_postmeta WHERE meta_key = '_order_total' and post_id IN (select post_id from wp_postmeta where meta_value = u.ID and meta_key = '_customer_user')) as moneySpent
FROM wp_users u;

The problem with your query is that you have nested correlated sub queries. Correlated sub queries are a huge hit to SQL performance. In this case you want to try use joins with aggregation (group by).
I don't understand the intricacies of the schema your database, so I'm not quite sure why you're limiting the sub queries for firstName etc to 1 record. I can edit the answer to accommodate that if you give more detail.
I would suggest you try something like this:
SELECT u.ID,
u.user_email AS mail,
u.user_login AS userName,
u.user_registered AS signUpDate,
mfn.meta_value as firstName,
mln.meta_value as lastName,
mbp.meta_value as billingPhone,
msp.meta_value as shippingPhone,
COUNT(pval.meta_value) as orderCount,
SUM(pval.meta_value) as moneySpent
FROM wp_users u
JOIN wp_usermeta as mfn ON u.ID = mfn.user_id AND mfn.meta_key = 'first_name'
JOIN wp_usermeta as mln ON u.ID = mln.user_id AND mln.meta_key = 'last_name'
JOIN wp_usermeta as mbp ON u.ID = mbp.user_id AND mbp.meta_key = 'billing_phone'
JOIN wp_usermeta as msp ON u.ID = msp.user_id AND msp.meta_key = 'shipping_phone'
JOIN wp_postmeta as p ON p.meta_value = u.ID AND p.meta_key = '_customer_user'
JOIN wp_postmeta as pval ON pval.post_id = p.post_id AND pval.meta_key = '_order_total'
GROUP BY u.ID,
u.user_email,
u.user_login,
u.user_registered,
mfn.meta_value,
mln.meta_value,
mbp.meta_value,
msp.meta_value
As per #O.Jones's comment, consider using LEFT JOINS instead of INNER JOINS so that the query still returns results where a meta_value is missing.

#Timur, as per your question in the comments of the first answer; you'd have to do something like this. You still need joins but you don't need as many for the wp_usermeta table. You will still need to join to wp_postmeta twice, because you can't do a join on the '_customer_user' meta_key and simultaneously retrieve the '_order_total' meta_key in a single join.
Just a note on the MAX(CASE WHEN um.meta_key = '...' THEN um.meta_value END) logic; it's the equivalent to MAX(CASE WHEN um.meta_key = '...' THEN um.meta_value ELSE NULL END).
SELECT u.ID,
u.user_email AS mail,
u.user_login AS userName,
u.user_registered AS signUpDate,
MAX(CASE WHEN um.meta_key = 'first_name' THEN um.meta_value END) AS firstName,
MAX(CASE WHEN um.meta_key = 'last_name' THEN um.meta_value END) AS lastName,
MAX(CASE WHEN um.meta_key = 'billing_phone' THEN um.meta_value END) AS billingPhone,
MAX(CASE WHEN um.meta_key = 'shipping_phone' THEN um.meta_value END) AS shippingPhone,
COUNT(pval.meta_value) AS orderCount,
SUM(pval.meta_value) AS moneySpent
FROM wp_users AS u
LEFT JOIN wp_usermeta AS um ON u.ID = um.user_id
LEFT JOIN wp_postmeta AS pm ON u.ID = pm.meta_value AND pm.meta_key = '_customer_user'
LEFT JOIN wp_postmeta AS pval ON pm.post_id = pval.post_id AND pval.meta_key = '_order_total'
WHERE (um.meta_key IN ('first_name', 'last_name', 'billing_phone', 'shipping_phone')
OR um.meta_key IS NULL)
GROUP BY u.ID,
u.user_email,
u.user_login,
u.user_registered;
I'm not 100% sure on the logic. Let me know if it needs tweaking

Here subquery has been introduced for minimizing multiple times using a single table and removing few column from GROUP BY clause for optimization purpose. Use WHERE clause in every subquery if need a particular user/ few users or any searching condition. If subquery returns no data but needed user info then use LEFT JOIN instead of INNER JOIN.
-- MySQL
SELECT u.ID
, u.user_email AS mail
, u.user_login AS userName
, u.user_registered AS signUpDate
, t.firstName
, t.lastName
, t.billingPhone
, t.shippingPhone
, r.orderCount
, r.moneySpent
FROM wp_users u
INNER JOIN (SELECT user_id
, MAX(CASE WHEN meta_key = 'first_name' THEN meta_value END) firstName
, MAX(CASE WHEN meta_key = 'last_name' THEN meta_value END) lastName
, MAX(CASE WHEN meta_key = 'billing_phone' THEN meta_value END) billingPhone
, MAX(CASE WHEN meta_key = 'shipping_phone' THEN meta_value END) shippingPhone
FROM wp_usermeta
WHERE meta_key IN ('first_name', 'last_name', 'billing_phone', 'shipping_phone')
GROUP BY user_id) t
ON u.ID = t.user_id
INNER JOIN (SELECT p.meta_value
, COUNT(pval.meta_value) as orderCount
, SUM(pval.meta_value) as moneySpent
FROM wp_postmeta as p
INNER JOIN wp_postmeta as pval
ON p.post_id = pval.post_id
AND p.meta_key = '_customer_user'
AND pval.meta_key = '_order_total'
GROUP BY p.meta_value) r
ON u.ID = r.meta_value;

Related

Select record from table where user is the owner of the company

Hi I have this stupid problem but I cannot find a solution.
I want to select a user from the database that is the owner of a provided company.
So it needs to have meta_key = 'owner' and meta_value = '1' but it needs also to have meta_key = 'company' and meta_value = 'company_name'
This is what I came up with and obviously it is not working:
SELECT user_id FROM wp_usermeta
WHERE meta_key = 'owner'
AND meta_value = '1'
AND (meta_key = 'company' AND meta_value = 'company_name');
This is a screen of the table:
I can't write a comment, but the meta_key is most likely the column name so this might work:
SELECT
user_id
FROM wp_usermeta
WHERE owner = '1'
AND company = 'company_name';
How about query like this
SELECT
a.user.id, b.meta_value as company, c.meta_value as owner
FROM
wp_usermeta a
JOIN wp_usermeta b ON a.user_id = b.user_id where meta_key=company
JOIN wp_usermeta c on a.user_id = c.user_id WHERE meta_key=owner
WHERE
company = 'company_name' AND owner='1'
I suspect that you want:
SELECT uo.user_id
FROM wp_usermeta uo JOIN
wp_usermeta uc
ON uo.user_id = uc.user_id
WHERE uo.meta_key = 'owner' AND uo.meta_value = '1' AND
uc.meta_key = 'company' AND uc.meta_value = 'company_name';

Filter users using by two different user meta

I have a user meta field called country (string) and another one called is_blocked (binary). What I need is returns the number of non-blocked users per country selecting the user registered date too.
With this query, I managed to get the values without considering the is_blocked field:
SELECT meta_value AS 'country', COUNT(meta_value) AS 'total'
FROM wp_usermeta INNER JOIN
wp_users
ON wp_usermeta.user_id = wp_users.ID
WHERE meta_key = 'user_country' AND
meta_value <> '' AND
DATE_FORMAT(wp_users.user_registered, '%Y-%m') = '2021-01'
GROUP BY meta_value
ORDER BY total DESC
But I can't just select only non-bloked users. What I try:
SELECT meta_value AS 'country', COUNT(meta_value) AS 'total'
FROM wp_usermeta INNER JOIN
wp_users
ON wp_usermeta.user_id = wp_users.ID
WHERE meta_key = 'user_country' AND
meta_value <> '' AND
DATE_FORMAT(wp_users.user_registered, '%Y-%m') = '2021-01' AND
(wp_usermeta.meta_key = 'user_blocked' AND
CAST(wp_usermeta.meta_value AS BINARY) != '1'
)
GROUP BY meta_value
ORDER BY total DESC
You need another join or exists:
SELECT umc.meta_value AS country, COUNT(umb.user_id) AS total
FROM wp_users u INNER JOIN
wp_usermeta umc
ON umc.user_id = u.ID AND
umc.meta_key = 'user_country' AND
umc.meta_value <> '' LEFT JOIN
wp_usermeta umb
ON umb.user_id = u.ID AND
umb.meta_key = 'user_blocked' AND
CAST(umb.meta_value AS BINARY) <> '1'
WHERE DATE_FORMAT(u.user_registered, '%Y-%m') = '2021-01'
GROUP BY umc.meta_value
ORDER BY total DESC;
This should give you 0 values, if you have countries with no unblocked users. This also assumes that the two meta keys are not repeated for a single user.
You can use conditional aggregation in the table wp_usermeta to get each user and country which meet your conditions:
SELECT user_id,
MAX(CASE WHEN meta_key = 'user_country' THEN meta_value END) country
FROM wp_usermeta
GROUP BY user_id
HAVING country <> ''
AND MAX(CASE WHEN meta_key = 'user_blocked' THEN meta_value END) + 0 <> 1
and then join it to the table wp_users:
SELECT m.country, COUNT(*) total
FROM wp_users u
INNER JOIN (
SELECT user_id,
MAX(CASE WHEN meta_key = 'user_country' THEN meta_value END) country
FROM wp_usermeta
GROUP BY user_id
HAVING country <> ''
AND MAX(CASE WHEN meta_key = 'user_blocked' THEN meta_value END) + 0 <> 1
) m ON m.user_id = u.ID
WHERE DATE_FORMAT(u.user_registered, '%Y-%m') = '2021-01'
GROUP BY m.country
ORDER BY total DESC

SELECT with SELECT within IF statement with saving state

I try to found some solution to avoid third SELECT, but I don't even know, how google it.
My try (working, but looks ugly):
SELECT
u.*,
IF(
(SELECT meta_value from usermeta WHERE user_id = u.ID AND meta_key = 'first_name') = '',
'John',
(SELECT meta_value from usermeta WHERE user_id = u.ID AND meta_key = 'first_name')
) as first_name,
(SELECT meta_value from usermeta WHERE user_id = u.ID AND meta_key = 'reputation') as reputation
FROM wp_users AS u
And I'd like something like this:
SELECT
u.*,
IF(
(SELECT meta_value from usermeta WHERE user_id = u.ID AND meta_key = 'first_name') = '',
'John',
SOME_STATE0
) as first_name,
(SELECT meta_value from usermeta WHERE user_id = u.ID AND meta_key = 'reputation') as reputation
FROM wp_users AS u
Join the tables rather than using a subquery.
SELECT u.*, IF(meta_value = '' OR meta_value IS NULL, 'John', meta_value) AS first_name
FROM wp_users AS u
LEFT JOIN usermeta AS m ON m.user_id = u.id AND m.meta_key = 'first_name'

Wordpress - fetch data from multiple tables

I have these two different tables (wp_usermeta & my cutom table-sia_licence):
wp_usermeta table:-
sia_licence table(custom):-
and I want final data table to be like this:-
We tried using
$data = $wpdb->get_results("SELECT * FROM wp_usermeta, sia_licence WHERE wp_usermeta.user_id = sia_user_id
AND wp_usermeta.meta_key = 'first_name'
AND wp_usermeta.meta_key = 'last_name'
AND wp_usermeta.meta_key = 'phone_number'
AND wp_usermeta.meta_key = 'email'
AND wp_usermeta.meta_key = 'address'");
but getting blank array..
You need to perform conditional aggregation on wp_usermeta in order to pivot all attributes (first name, last name, ...) per user_id and then JOIN with sia_licence to produce desired output
SELECT q.user_id, q.first_name, q.last_name, q.email, q.phone_number, q.address,
l.select_licence, l.visa_expiry, l.licence_number, l.sia_licence, l.driving_licence
FROM
(
SELECT user_id,
MAX(CASE WHEN meta_key = 'first_name' THEN meta_value END) first_name,
MAX(CASE WHEN meta_key = 'last_name' THEN meta_value END) last_name,
MAX(CASE WHEN meta_key = 'phone_number' THEN meta_value END) phone_number,
MAX(CASE WHEN meta_key = 'email' THEN meta_value END) email,
MAX(CASE WHEN meta_key = 'address' THEN meta_value END) address
FROM wp_usermeta m
WHERE EXISTS
(
SELECT *
FROM sia_licence
WHERE sia_user_id = m.user_id
)
GROUP BY user_id
) q JOIN sia_licence l
ON q.user_id = l.sia_user_id
or join first and then aggregate
SELECT q.user_id,
MAX(CASE WHEN meta_key = 'first_name' THEN meta_value END) first_name,
MAX(CASE WHEN meta_key = 'last_name' THEN meta_value END) last_name,
MAX(CASE WHEN meta_key = 'phone_number' THEN meta_value END) phone_number,
MAX(CASE WHEN meta_key = 'email' THEN meta_value END) email,
MAX(CASE WHEN meta_key = 'address' THEN meta_value END) address,
MAX(select_licence) select_licence,
MAX(visa_expiry) visa_expiry,
MAX(licence_number) licence_number,
MAX(sia_licence) sia_licence,
MAX(driving_licence) driving_licence
FROM wp_usermeta q JOIN sia_licence l
ON q.user_id = l.sia_user_id
GROUP BY q.user_id
try this:
SELECT * FROM wp_usermeta wu
INNER JOIN sia_licence sl ON wu.user_id = sl.sia_user_id
WHERE <you can specify your filter here>;
Does sla_user_id reference user_id?
if so you want to use a JOIN like this...
SELECT um.*,sl.select_licence,sl.visa_expiry,sl.licence_number,sl.sia_licence,sl.driving_licence FROM `wp_usermeta`um JOIN `sia_licence`sl ON um.`user_id`=sl.`sia_user_id`;
You can also add a where clause like this
SELECT um.*,sl.select_licence,sl.visa_expiry,sl.licence_number,sl.sia_licence,sl.driving_licence FROM `wp_usermeta`um JOIN `sia_licence`sl ON um.`user_id`=sl.`sia_user_id` WHERE um.`user_id`=101;
if neeeded. IN the above example it would just return user ID = 101.
You can also just use SELECT *, in this case, but generally it's a bad idea because if there are any ambigious columns (columns with the same names) between the two tables it'll give you an error, but in your case it looks like you should be fine with:
SELECT * FROM `wp_usermeta`um JOIN `sia_licence`sl ON um.`user_id`=sl.`sia_user_id`;

MySQL subquery in where clause

I have a MySQL query which goes like this:
select
*,
(select meta_value
from nord_usermeta m
where meta_key = 'firm' and user_id = s.user_id
limit 1)
as firm,
(select meta_value f
rom nord_usermeta m
where meta_key = 'first_name' and user_id = s.user_id
limit 1)
as first_name,
(select meta_value
from nord_usermeta m
where meta_key = 'last_name' and user_id = s.user_id
limit 1)
as last_name
from nord_submissions s
order by created_at desc
Now I need to narrow it down with a where clause like this:
select
*,
(select meta_value
from nord_usermeta m
where meta_key = 'firm' and user_id = s.user_id
limit 1)
as firm,
(select meta_value
from nord_usermeta m
where meta_key = 'first_name' and user_id = s.user_id
limit 1)
as first_name,
(select meta_value
from nord_usermeta m
where meta_key = 'last_name' and user_id = s.user_id
limit 1)
as last_name
from nord_submissions s
where firm like '%DG%'
order by created_at desc
The problem obviously is in way i use subquery but i cant find a way to reference it in where clause.
Please help.
Why go so Long...try with SELF JOIN ( joining same tables in query )..see below (not tested)
SELECT ns.*,
um1.meta_value AS firmName,
um2.meta_value AS firstName,
um3.meta_value AS lastName
FROM nord_submission ns
INNER JOIN nord_usermeta um1 USING(id) AND meta_key = 'firm'
INNER JOIN nord_usermeta um2 USING(id) AND meta_key = 'first_name'
INNER JOIN nord_usermeta um3 USING(id) AND meta_key = 'last_name'
WHERE um1.meta_value LIKE '%DG%'
ORDER BY ns.created_at
Note: if both table have different column name , by which u refrence each other, then you can replace USING(id) with ns.user_id=um1.user_id and so on
If you're going to use keys rather than columns you'll have to let the application sort it out. Using so many sub-queries you'll see a performance hit.
SELECT *
FROM nord_submissions
INNER JOIN nord_usermeta
USING (user_id)
WHERE (meta_key='last_name' or meta_key='first_name' or (meta_key='firm' and meta_value LIKE '%DG%') )
ORDER BY created_at desc