SQL - ordering results from joined table - mysql

Note: This question is related to a WordPress specific question but I wanted to have an "outside look" at this from a pure SQL point of view: https://wordpress.stackexchange.com/questions/55263/order-posts-by-custom-field-and-if-custom-field-is-empty-return-remaining-posts
Let's say we have to tables with the following strucure:
Tabe posts: ID (key), Title
Table post_metadata: post_ID(FKEY), meta_key, meta_value
And I want to retrieve ID and Title of posts that have:
an entry in post_metadata with key = 'meta_1' and meta_value = 'value_1'
AND an entry in post_metadata with key = 'meta_2' and meta_value = 'value_2'
I want to order the results by the value of a third metadata with meta_key = "meta_3".
Now here is the tricky part:
Not all posts have an entry in post_metadata table with 'meta_3' as meta_key. Since Im not filtering posts by meta_3, only ordering, I wanted to keep these posts in my results, as if they had an empty value for this meta.
How can we achieve that?
Thanks
Edit:
There is SQL fiddle now: https://www.db-fiddle.com/f/kBNaaRFB5xfna5MniuTpaG/1

Perhaps:
Use an Left join once to get meta 3 value if it exists ensuring you keep all posts that have meta1 and meta2 with desired values.
and then use an exist and having to ensure you only get records having both meta1 and 2 with desired values.
UNTESTED...
SELECT P.ID, P.Title, PM.Meta_value
FROM Posts P
LEFT JOIN Post_MetaData PM
on P.ID = PM.Post_ID
and PM.key = 'meta_3'
WHERE exists (SELECT 1
FROM post_meta
WHERE ((Key 'meta_1' and meta_value = 'Value_1') OR
(Key 'meta_2' and meta_value = 'Value_2'))
and P.ID = Post_ID --Either here or an AND in the HAVING clause...need to test to know
GROUP BY Post_ID
HAVING count(*) = 2 )
ORDER BY -PM.meta_value desc, P.ID
This does assume that post_metaData has a unique constraint on the key per Post_ID. otherwise we could get meta_1 with value a and meta1 with value a and the count(*) would be 2; and incorrectly return it in the results.
To ensure nulls are last follow this approach;
MySQL Orderby a number, Nulls last
Doing this as an IN.... but would be slower I would think.
SELECT P.ID, P.Title, PM.Meta_value
FROM Posts P
LEFT JOIN Post_MetaData PM
on P.ID = PM.Post_ID
and PM.key = 'meta_3'
WHERE P.ID in (SELECT Post_meta.Post_ID
FROM post_meta
WHERE ((Key 'meta_1' and meta_value = 'Value_1') OR
(Key 'meta_2' and meta_value = 'Value_2'))
GROUP BY Post_ID
HAVING count(*) = 2 )
ORDER BY -PM.meta_value desc, P.ID

Related

SQL - Slow SQL Query

I have a sql query (see below) for wordpress which is taking around 4-5secs to get results. It gives all order ids which have a product/variation id in it.
I want to make it more fast, any help?
SELECT p.ID order_id
FROM wp_posts p
INNER JOIN wp_woocommerce_order_items i ON p.ID=i.order_id
INNER JOIN wp_woocommerce_order_itemmeta im ON i.order_item_id=im.order_item_id
WHERE im.meta_key IN ('_product_id','_variation_id')
AND im.meta_value IN ('703899','981273','981274','981275')
AND p.post_status IN ('wc-completed')
GROUP BY p.ID HAVING COUNT(p.ID)>1
ORDER BY p.post_date desc
LIMIT 0, 20
Above query EXPLAIN:
Why do you join when you only want to select IDs from wp_posts anyway?
SELECT p.ID order_id
FROM wp_posts p
WHERE p.post_status = 'wc-completed'
AND p.ID IN
(
SELECT i.order_id
FROM wp_woocommerce_order_items i
JOIN wp_woocommerce_order_itemmeta im ON im.order_item_id = i.order_item_id
WHERE im.meta_key IN ('_product_id','_variation_id')
AND im.meta_value IN ('703899','981273','981274','981275')
GROUP BY i.order_id
HAVING COUNT(*) > 1
)
ORDER BY p.post_date DESC
LIMIT 0, 20;
Now let's think about how the DBMS can address this. It can look for posts with status 'wc-completed', if there are only few such rows and then check whether they represent an order with more than one of the desired items. This would ask for these indexes:
create index idx1 on wp_posts(post_status, id, post_date);
create index idx2 on wp_woocommerce_order_items(order_id, order_item_id);
create index idx3 on wp_woocommerce_order_itemmeta(order_item_id, meta_key, meta_value);
Or it could look for the desired products, see whether an order contains more than one of them and then check whther this relates to a post with status = 'wc-completed'. That would ask for these indexes:
create index idx4 on wp_woocommerce_order_itemmeta(meta_key, meta_value, order_item_id);
create index idx5 on wp_woocommerce_order_items(order_item_id, order_id);
create index idx6 on wp_posts(id, post_status, post_date);
We don't know which way the DBMS will prefer, so we create all six indexes. Then we look at the explain plan to see which are being used and remove the others. Maybe the DBMS even sees no advantage in using indexes here at all, but I find this unlikely.
The first thing you can try doing is trimming what data you fetch.
That means:
Not fetching fields that you don't need/check
Implementing our constrains before joining
SELECT
p.ID order_id
FROM
(SELECT id, post_status, post_date FROM wp_posts WHERE post_status = 'wc-completed') p,
(SELECT order_id, order_item_id FROM wp_woocommerce_order_items) i,
(
SELECT
order_item_id,
meta_key,
meta_value
FROM
wp_woocommerce_order_itemmeta
WHERE
meta_key IN ('_product_id','_variation_id')
AND meta_value IN ('703899','981273','981274','981275')
) im
WHERE
p.ID = i.order_id
AND i.order_item_id = im.order_item_id
GROUP BY
p.ID
HAVING
COUNT(p.ID)>1
ORDER BY
p.post_date desc
LIMIT
0, 20
Edit:
If Inner joins are necessary, you can try:
SELECT
p.ID order_id
FROM
(SELECT id, post_status, post_date FROM wp_posts WHERE post_status = 'wc-completed') p
INNER JOIN
(SELECT order_id, order_item_id FROM wp_woocommerce_order_items) i
ON
p.ID = i.order_id
INNER JOIN
(
SELECT
order_item_id,
meta_key,
meta_value
FROM
wp_woocommerce_order_itemmeta
WHERE
meta_key IN ('_product_id','_variation_id')
AND meta_value IN ('703899','981273','981274','981275')
) im
ON
i.order_item_id = im.order_item_id
GROUP BY
p.ID
HAVING
COUNT(p.ID)>1
ORDER BY
p.post_date desc
LIMIT
0, 20
ps* I hope my syntax is correct ˙ my SQL is quite rusty

Select random row per distinct field value while using joins

I have a Wordpress instance showing some posts. Each post is defined in a specific language and has a property _post_year set. So we can have several posts with the same language and referring to the same year.
MySQL tables:
wp-posts
Contains all posts.
ID | post_author | post_date | ...
==================================
1 | ...
2 | ...
...
wp_term_relationships
Contains information about a language of a post (amongst other things).
object_id | term_taxonomy_id | term_order |
===========================================
1 | ...
1 | ...
2 | ...
...
wp_postmeta
Contains post meta information (like an additional property "_post_year").
meta_id | post_id | meta_key | meta_value |
===========================================
1 | 1 | ...
2 | 1 | ...
...
I once was able to load one random post per year (for all years available) like this:
SELECT DISTINCT
wp_posts.*,
postmeta.meta_value as post_meta_year
FROM (
SELECT * FROM wp_posts
JOIN wp_term_relationships as term_relationships
ON term_relationships.object_id = wp_posts.ID
AND term_relationships.term_taxonomy_id IN ({LANGUAGE_ID})
ORDER BY RAND()
) as wp_posts
JOIN wp_postmeta as postmeta
ON postmeta.post_id = wp_posts.ID
AND postmeta.meta_key = '_post_year'
AND post_status = 'publish'
GROUP BY post_meta_year DESC
ORDER BY post_meta_year DESC
Since i upgraded MySQL to version 5.7 this doesn't work anymore:
Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'wp_posts.ID' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by
How can i achieve to get a random post per year sorted descendingly?
One method you can try: From a derived table with the distinct years select the year and, in a correlated subquery, a random post ID with that year using ORDER BY rand() and LIMIT 1. Join the result of that second derived table with the posts.
SELECT po1.*,
ppmo1.meta_value
FROM (SELECT pmo1.meta_value,
(SELECT pi1.id
FROM wp_posts pi1
INNER JOIN wp_postmeta pmi2
ON pmi2.post_id = pi1.id
INNER JOIN wp_term_relationships tri1
ON tri1.object_id = pi1.id
WHERE tri1.term_taxonomy_id = {LANGUAGE_ID}
AND pmi2.meta_key = '_post_year'
AND pmi2.meta_value = pmo1.meta_value
ORDER BY rand()
LIMIT 1) id
FROM (SELECT DISTINCT
pmi1.meta_value
FROM wp_postmeta pmi1
WHERE pmi1.meta_key = '_post_year') pmo1) ppmo1
INNER JOIN wp_posts po1
ON po1.id = ppmo1.id
ORDER BY ppmo1.meta_value DESC;
(Untested because schema and sample data weren't given by consumable DDL and DML.)
In MySQL 5.7, where mode ONLY_FULL_GROUP_BY is (happily) enabled by default, I would recommend a correlated subquery for filtering:
select * -- better enumerate the actual column names here
from wp_posts p
inner join wp_postmeta pm on pm.post_id = p.id
where pm.meta_key = '_post_year' and p.id = (
select pm1.post_id
from wp_post p1
inner join wp_postmeta pm1 on pm1.post_id = p1.id
where p1.status = 'publish' and pm1.meta_key = '_post_year' and pm1.meta_value = pm.meta_value
order by rand() limit 1
)
Basically the subquery selects one random post id per group of records having the same '_post_year', which is used to filter the query.
Note that with this technique there is no need to filter again in the outer query on the post status, since the subquery does it already and returns a primary key column.

Get 2 meta values from meta key column

I need to use some library to export product names, SKU and prices to one CSV file. This library connects using PDO and needs an SQL query.
I want to select 'name', 'SKU' and 'price' from 2 WordPress tables, namely wp_posts and wp_postmeta.
I don't know how to get data from 'meta_value' column twice for 'meta_key'='_price' and 'meta_key'='_sku', ex.
My current query:
"SELECT a.post_title, m1.meta_value, m2.meta_value FROM wp_posts a, wp_postmeta m1, wp_postmeta m2
WHERE a.post_type='product' AND m1.post_id = a.ID
AND m1.meta_key='_sku'
AND m2.meta_key='_price'"
It sounds like you could do with a join so you're relating the meta information to the right posts.
SELECT
post.post_title,
meta.meta_value
FROM wp_posts AS post
LEFT JOIN wp_postmeta AS meta
ON post.post_id = meta.post_id
WHERE post.post_type = 'product'
AND meta.meta_key IN ('_sku', '_price')
Example results:
post_title | meta_value
--------------|-----------
Cheddar | CHE001
Cheddar | 2.45
Red Leicester | CHE002
...
This assumes that the id column in wp_posts is post_id.
It's important to note that this will return up to two rows for each post, depending on whether it has a meta row for _sku and _price). If you need the data all on the same row (as you might for your export) you might need something like this instead:
SELECT
post.post_title,
metaSku.meta_value AS sku,
metaPrice.meta_value AS price
FROM wp_posts AS post
LEFT JOIN (
SELECT
*
FROM wp_postmeta
WHERE meta_key = '_sku'
) AS metaSku
ON post.post_id = metaSku.post_id
LEFT JOIN (
SELECT
*
FROM wp_postmeta
WHERE meta_key = '_price'
) AS metaPrice
ON post.post_id = metaPrice.post_id
WHERE post.post_type = 'product'
Example results:
post_title | sku | price
--------------|--------|------
Cheddar | CHE001 | 2.45
Red Leicester | CHE002 |
...
I hope this helps.
This will works for me
SELECT post.post_title, metaSku.meta_value AS sku, metaPrice.meta_value AS price FROM wp_posts AS post LEFT JOIN ( SELECT * FROM wp_postmeta WHERE meta_key = '_sku' ) AS metaSku ON post.ID = metaSku.post_id LEFT JOIN ( SELECT * FROM wp_postmeta WHERE meta_key = '_price' ) AS metaPrice ON post.ID = metaPrice.post_id WHERE post.post_type = 'product';

Update MySql table from complicated Select

I have complex SELECT request:
select meta_value
from wp_posts v
left join wp_postmeta pm on (pm.post_id = v.id)
left join wp_posts p on (v.post_parent = p.id)
where meta_key in ('_price','_regular_price')
and v.post_type = 'product_variation'
and p.id = '1743'
limit 0,100
It returns me 4 (four) needed fields with values like
400
500
300
350
I need to update these values and set their values equal, for example 1000.
Can I, based on my SELECT, run an UPDATE query?
Any condition used in a Select query could be used in Update query, of course it might need a little modification. What you can do in your case is the following:
meta_value
Update wp_posts
set meta_value = 1000
where id IN (select v.id
from wp_posts v
left join wp_postmeta pm on (pm.post_id = v.id)
left join wp_posts p on (v.post_parent = p.id)
where v.meta_key in ('_price','_regular_price')
and v.post_type = 'product_variation'
and p.id = '1743')
Please note that I didn't test this, so I am not totally sure it will work. Make sure to backup your database before any queries like this. Hopefully it will give you the desired results.
Edit: I have assumed that id in wp_posts is a unique value.

MySQL: order by value from second table, use default if value not set

This takes place inside WordPress, but it's a general MySQL question.
There are two tables, one of which contains posts, the other metadata, linked by ID.
post_title | ID post_id | meta_key | meta_value
-----------+--- --------+----------+-----------
title | 1 1 | key_1 | aaa
-----------+--- --------+----------+-----------
title | 2 1 | key_2 | bbb
--------+----------+-----------
1 | mykey | 1
--------+----------+-----------
2 | key_n | ccc ddd
I'm trying to order results on some column value, which might not be set for all rows. Basically, I want to see rows with this column/value pair set first, followed by all the others. Each post might have some metadata associated with it, based on meta_key and meta_value pairs. There may be more keys for a single post and they need not include the one I want to sort by.
The problem is that using a MySQL query with a WHERE meta_key = mykey will exclude all the posts where this key doesn't exist. So what I need is a way to display a default value for all those posts, where this meta key doesn't exist.
First step: It's easy to select all rows with a certain meta_key:
SELECT
p.ID, p.post_title, p.post_type, p.post_date, m.meta_value
FROM wp_posts AS p
LEFT JOIN wp_postmeta AS m ON p.ID = m.post_id
WHERE
m.meta_key = 'mykey'
Second step: how do I select all the rows where this meta_key doesn't exist?
Here's what I mean, but this is probably a bad solution:
SELECT
p.ID, p.post_title, p.post_type, p.post_date, "some_default"
FROM wp_posts AS p
WHERE
p.ID NOT IN (
SELECT
p.ID
FROM wp_posts AS p
LEFT JOIN wp_postmeta AS m ON p.ID = m.post_id
WHERE
m.meta_key = 'mykey'
)
Third step: show combined results. This could be a UNION of both queries above.
I'm sure there must be a better sulution. What's more important, I don't know how to specify additional paramaters – e. g., first find all posts with some given meta key, or title, or category etc. and then order by said mykey as layed out above.
FINAL EDIT
If anyone's interested, here's the final solution in context. RedFilter's answer made it possible, thanks again.
SELECT p1.ID, p1.post_title, p1.post_type, p1.post_date, m1.meta_value AS meta1, meta2
FROM wp_posts AS p1
LEFT JOIN wp_postmeta AS m1 ON m1.post_id = p1.ID
LEFT JOIN wp_term_relationships AS tr0 ON tr0.object_id = p1.ID
LEFT JOIN wp_term_taxonomy AS tt0 ON tr0.term_taxonomy_id = tt0.term_taxonomy_id
LEFT JOIN wp_terms AS t0 ON tt0.term_id = t0.term_id
LEFT JOIN
(
SELECT
p.ID, IF (m.meta_value = 'on', 1, 0) AS meta2
FROM wp_posts AS p
LEFT JOIN wp_postmeta AS m
ON p.ID = m.post_id
and m.meta_key = 'mykey'
) as extra
ON extra.ID = p1.ID
WHERE 1 = 1
AND m1.meta_key = 'some-other-meta-key'
AND p1.post_type IN ('post', 'some-custom-post-type')
AND tt0.taxonomy = 'some-taxonomy'
AND t0.term_id = 'some-id'
ORDER BY meta2 DESC, meta1 ASC, p1.post_date DESC
SELECT p.ID, p.post_title, p.post_type, p.post_date,
ifnull(m.meta_value, 'default val') as meta_value
FROM wp_posts AS p
LEFT JOIN wp_postmeta AS m ON p.ID = m.post_id
and m.meta_key = 'mykey'