MySQL - Joining tables and turning specific rows into "virtual" columns - mysql

I am doing some database queries on Wordpress. I am joining the usermeta table and users table.
the users table has a single row for each user.
the usermeta table has multiple rows for each user as the "meta_keys" (categories of meta data of each user) can contain all kind of information.
In a query I now would like to create a result set with only one row per user and with additional columns for selected meta_key values defined.
e.g. having an additional columns with the nickname - it should show the content of usermeta.meta_value when usermeta.meta_keys = 'nickname'.
Here's my current query with the unwanted duplication of rows
SELECT
wusers.ID,
wusers.user_login,
wusers.display_name,
wmeta.meta_key,
wmeta.meta_value,
wmeta.user_id
FROM
$wpdb->users wusers
INNER JOIN $wpdb->usermeta wmeta ON wusers.ID = wmeta.user_id
WHERE 1 = 1
AND wmeta.meta_key = 'nickname'
OR wmeta.meta_key = 'description'
OR wmeta.meta_key = 'userphoto_thumb_file'
Is there any MySQL magic I can use to do this and turn data of certain the rows to new "virtual" columns?

What you seek is commonly called a crosstab query:
Select U.Id
, Min( Case When M.meta_key = 'nickname' Then M.meta_value End ) As nickname
, Min( Case When M.meta_key = 'description' Then M.meta_value End ) As description
, Min( Case When M.meta_key = 'userphoto_thumb_file' Then M.meta_value End ) As userphoto_thumb_file
From users As U
Join usermeta As M
On M.user_id = U.id
Group By U.id
It should be noted that you can only do this with static SQL and static columns. The SQL langugage itself was not really designed for dynamic column generation. To dynamically assemble the columns, you will need to dynamically assemble the query (aka dynamic SQL) in your middle-tier code.

Related

Nested MySQL selects issue within WordPress usermeta table

Having an issue with a complex MySql select statement and hoping for some pointers!
So I am using a number of plugins on top of WordPress with some interesting ways of storing data. The bits I'm concerned with are as following: There are some 'parent' accounts which have a number of child accounts. The parent to child relationship is stored in the usermeta table (user_id=user_id of child account, meta_value = parent_id, meta_key='parent'). Each of these child accounts can also complete a number of tasks. This is also stored in the usermeta table (user_id=user_id of child account, meta_value = complete_status, meta_key='task_id_'.task_id).
I'm trying to create a view where I get a list of each of these parent accounts, along with a few bits of information, then a few derived values from their children, including the average number of completed tasks by the children of each parent.
This is my MySQL statement, the part that is having the issue is the nested select:
SELECT
wp_parent_account_info_table.obj_id,
wp_parent_account_info_table.obj_type,
wp_parent_account_info_table.id,
wp_other_custom_table_info.created_at,
wp_other_custom_table_info.product_id,
(SELECT AVG(cc.rcount)
FROM (SELECT DISTINCT COUNT(*) as rcount
FROM wp_usermeta
WHERE meta_key LIKE 'task_id_%'
AND meta_value = 'complete'
AND wp_usermeta.user_id IN (SELECT DISTINCT user_id
FROM wp_usermeta
WHERE meta_key = 'parent'
AND meta_value = wp_parent_account_info_table.id
) AS sc
) AS cc
) AS a
FROM wp_parent_account_info_table
JOIN wp_other_custom_table_info
ON `wp_parent_account_info_table`.`obj_id`=`wp_other_custom_table_info`.`id`
INNER JOIN wp_another_custom_table_info
ON `wp_another_custom_table_info`.`subscription_id`=`wp_parent_account_info_table`.`obj_id` WHERE `wp_parent_account_info_table`.`status` = 'enabled'
AND `wp_parent_account_info_table`.`sub_accounts_available` <> '0'
AND `wp_other_custom_table_info`.`status` = 'active'
AND (`wp_another_custom_table_info`.`expires_at` > CONCAT(CURDATE(), ' 23:59:59')
OR `wp_another_custom_table_info`.`expires_at` = '0000-00-00 00:00:00')
LIMIT 0,30;
I've tried to make this readable, apologies for complexity. I didn't want to remove any parts incase they were relevant.
The statement works fine without the nested select. It also works (has no error) if I replace the most nested select with an array of IDs (so just putting: IN (1,2,3)). Is this something about me trying to get the parent ID from too far down?
This is the error:
#1064 - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'AS sc) AS cc) AS a FROM wp_parent_account_info_table JOIN wp_other_custom_table_info ON'
Any pointers would be much appreciated.
Edit:
Along with the answer below which resolved this error, I also didn't have access to the id variable in the furthest nest (a new error!), so I split them into an extra column. Here is my final code:
SELECT
wp_parent_account_info_table.obj_id,
wp_parent_account_info_table.obj_type,
wp_parent_account_info_table.id,
wp_other_custom_table_info.created_at,
wp_other_custom_table_info.product_id,
(SELECT DISTINCT COUNT(*)
FROM wp_usermeta
WHERE meta_key = 'parent'
AND meta_value = wp_parent_account_info_table.id) as c,
(SELECT DISTINCT COUNT(*) as rcount
FROM wp_usermeta
WHERE meta_key LIKE 'task_id_%'
AND meta_value = 'complete'
AND wp_usermeta.user_id IN (SELECT DISTINCT user_id
FROM wp_usermeta
WHERE meta_key = 'parent'
AND meta_value = wp_parent_account_info_table.id
)
) AS a,
(SELECT a / c)
FROM wp_parent_account_info_table
JOIN wp_other_custom_table_info
ON `wp_parent_account_info_table`.`obj_id`=`wp_other_custom_table_info`.`id`
INNER JOIN wp_another_custom_table_info
ON `wp_another_custom_table_info`.`subscription_id`=`wp_parent_account_info_table`.`obj_id` WHERE `wp_parent_account_info_table`.`status` = 'enabled'
AND `wp_parent_account_info_table`.`sub_accounts_available` <> '0'
AND `wp_other_custom_table_info`.`status` = 'active'
AND (`wp_another_custom_table_info`.`expires_at` > CONCAT(CURDATE(), ' 23:59:59')
OR `wp_another_custom_table_info`.`expires_at` = '0000-00-00 00:00:00')
LIMIT 0,30;
The syntax error is related to your IN cluse ... the IN clause base on a subselect don't require a tablename alias then avoid the sc after the )
SELECT
wp_parent_account_info_table.obj_id,
wp_parent_account_info_table.obj_type,
wp_parent_account_info_table.id,
wp_other_custom_table_info.created_at,
wp_other_custom_table_info.product_id,
(SELECT AVG(cc.rcount)
FROM (SELECT DISTINCT COUNT(*) as rcount
FROM wp_usermeta
WHERE meta_key LIKE 'task_id_%'
AND meta_value = 'complete'
AND wp_usermeta.user_id IN (SELECT DISTINCT user_id
FROM wp_usermeta
WHERE meta_key = 'parent'
AND meta_value = wp_parent_account_info_table.id
)
) AS cc
) AS a
FROM wp_parent_account_info_table
JOIN wp_other_custom_table_info
ON `wp_parent_account_info_table`.`obj_id`=`wp_other_custom_table_info`.`id`
INNER JOIN wp_another_custom_table_info
ON `wp_another_custom_table_info`.`subscription_id`=`wp_parent_account_info_table`.`obj_id` WHERE `wp_parent_account_info_table`.`status` = 'enabled'
AND `wp_parent_account_info_table`.`sub_accounts_available` <> '0'
AND `wp_other_custom_table_info`.`status` = 'active'
AND (`wp_another_custom_table_info`.`expires_at` > CONCAT(CURDATE(), ' 23:59:59')
OR `wp_another_custom_table_info`.`expires_at` = '0000-00-00 00:00:00')
LIMIT 0,30;

MySQL: Do a JOIN based on a regular expression? Is it possible?

I am working on a WordPress site that uses the Advanced Custom Field plugin and the Advanced Custom Field repeater plugin, so I cannot change the database structure.
The custom fields (and their values) for each post are stored in a table called post_meta with the following fields:
meta_id (autoincrement), post_id, meta_key, meta_value
For regular custom fields, it's easy. Let's say post 200 has "John Doe" for field "full_name", then it would be stored like this:
xxx, 200, full_name, John Doe
The problem is when the field has a subset of repeated fields, the way it is stored is a bit more complicated. For the meta_key, the value is {fieldName}_{rowNumber}_{subfieldName}.
For example, in this Wordpress what I am trying to do is to store the players of a team that participatd in a certain match, and how many minutes they played that match (here is an screenshot):
The general field is called planilla (players sheet), so every value is stored like this:
planilla_1_jugador, planilla_1_minutos (player_id and minutes played for the first row)
So, my problem starts when I want to count all the matches played by a player. I could count all the rows in post_meta where meta_value is my player id, but that would count matches where the player played 0 minutes (and I don't want to). This is the query that does that, by the way:
SELECT COUNT(*) as total from $wpdb->postmeta INNER JOIN $wpdb->posts ON ( $wpdb->posts.ID = $wpdb->postmeta.post_id ) WHERE( $wpdb->postmeta.meta_key LIKE 'planilla_%_jugador' AND $wpdb->postmeta.meta_value = {player_id} ) AND $wpdb->posts.post_type = 'partido' AND (($wpdb->posts.post_status = 'publish'))
If I want to check for the played minutes, I would need to add a join that matches with the same position in the players sheet (planilla). If the table would have an extra column called meta_key_index, that would store the number, it would be as easy as this:
SELECT COUNT(*) as total from wp_postmeta pm1 INNER JOIN wp_posts ON ( wp_posts.ID = pm1.post_id )
INNER JOIN wp_postmeta pm2 ON (pm1.post_id = pm2.post_id)
WHERE( pm1.meta_key LIKE 'planilla_%_jugador' AND pm1.meta_value = 420 )
AND (pm2.meta_key LIKE 'planilla_%_minutos_jugados' and pm2.meta_value > 0)
AND (pm1.meta_key_index = pm.meta_key_index)
AND wp_posts.post_type = 'partido' AND ((wp_posts.post_status = 'publish')
But it hasn't, so I need to do the join using a regular expression or something like that. I thought of several things but nothing seem to make it possible to work.
What I am doing right now, I doing 20 queries (max amount of players per sheet) where the key is hardcoded, so instead o doing planilla_%_jugador and I am querying planilla_1_jugador, and so on, and then summing all the results at the end. But the 20 queries are slow and I'd prefer to solve everything doing just one query.
I hope this post makes sense and thanks in advance.

Single SQL to retrieve different information from different tables

I have this query which retrives 10 ( $limited ) queries from MySQL ,
"SELECT content.loc,content.id,content.title,
voting_count.up,voting_count.down
FROM
content,voting_count
WHERE names.id = voting_count.unique_content_id
ORDER BY content.id DESC $limit"
This query did great for posts that were allready in database and had votes , however new posts won't show.
Vote row is "inserted" first time someone votes on post. I guess that the reason why they won't be listed as there is no unique_content_id to connect to.
If i change query into this :
"SELECT content.loc,content.id,content.title
FROM
content
ORDER BY content.id DESC $limit"
it works , but i can't access voting_count.up & voting_count.down rows.
How could i access both information in single query ? Is it doable ?
If some data might not exist in one of the tables, instead of using INNER JOIN you should use LEFT JOIN:
SELECT content.loc,content.id,content.title,
-- USE function COALSESCE will show 0 if there are no
-- related records in table voting_count
COALESCE(voting_count.up, 0) as votes_up,
COALSESCE(voting_count.down, 0) as voted_down
FROM content LEFT JOIN voting_count
ON content.id = voting_count.unique_content_id
ORDER BY content.id DESC
As someone else above mentioned, what is names.id? However, perhaps the following might be of use assuming the join should have been from content.id to voting_count.unique_content_id:
$sql="select
c.`loc`,c.`id`, c.`title`,
case
when v.`up` is null then
0
else
v.`up`
end as 'up',
case
when v.`down` is null then
0
else
v.`down`
end as 'down'
from `content` c
left outer join `voting_count` v on v.`unique_content_id`=c.`id`
order by c.`id` desc {$limit}";

MySQL intersection

I've an existing site, whose DB is not designed correctly and contains lot of records, so we cant change DB structure.
Database for current issue mainly contains 4 tables, users, questions, options and answers. There is standard set of questions and options but for each user, there is one row in answers table for each set of question and options. DB structure and example data is available at SQL fiddle.
Now as a new requirement of advanced search, I need to find users by applying multiple search filters. Example input and expected output is given in comments on SQL Fiddle.
I tried to apply all type of joins, intersection but it always fail somehow. Can someone please help me to write correct query, preferably light weight/optimized joins as DB contain lot of records (10000+ users, 100+ questions, 500+ options and 500000+ records in answers table)?
EDIT: Based on two answers, I used following query
SELECT u.id, u.first_name, u.last_name
FROM users u
JOIN answers a ON a.user_id = u.id
WHERE (a.question_id = 1 AND a.option_id IN (3, 5))
OR (a.question_id = 2 AND a.option_id IN (8))
GROUP BY u.id, u.first_name, u.last_name
HAVING
SUM(CASE WHEN (a.question_id = 1 AND a.option_id IN (3, 5)) THEN 1 ELSE 0 END) >=1
AND SUM(CASE WHEN (a.question_id = 2 AND a.option_id IN (8)) THEN 1 ELSE 0 END) >= 1;
Please note: On real database, columns user_id, question_id and option_id of answers table are indexed.
Running query given on SQL Fiddle.
SQL Fiddle for dnoeth's answer.
SQL Foddle for calcinai's answer.
Add all you n filters into the WHERE using OR and repeat them in a HAVING(SUM(CASE)) using AND:
SELECT u.id, u.first_name, u.last_name
FROM users u JOIN answers a
ON a.user_id = u.id
JOIN questions q
ON a.question_id = q.id
JOIN question_options o
ON a.option_id = o.id
WHERE (q.question = 'Language known' AND o.OPTION IN ('French','Russian'))
OR (q.question = 'height' AND o.OPTION = '1.51 - 1.7')
GROUP BY u.id, u.first_name, u.last_name
HAVING
SUM(CASE WHEN (q.question = 'Language known' AND o.OPTION IN ('French','Russian')) THEN 1 ELSE 0 END) >=1
AND
SUM(CASE WHEN (q.question = 'height' AND o.OPTION = '1.51 - 1.7') THEN 1 ELSE 0 END) >= 1
;
I changed your joins into the more readable Standard SQL syntax.
This will require a bit of fiddling for a dynamic filter, but what you really want to do is search by the IDs, as it'll mean less joins and a faster query.
This produces the results you'd expect. I assume that the search filters are generated based off options in the database, so instead of passing the actual value back in to the query, pass the ID instead.
The multiple inner joins are to support multiple AND criteria and auto-reduce your result set.
SELECT * FROM users u
INNER JOIN answers a ON a.user_id = u.id
AND (a.question_id, a.option_id) IN ((1,3),(1,5)) # q 1: Lang, answer 3/5: En/Ru
INNER JOIN answers a2 ON a2.user_id = u.id
AND (a2.question_id, a2.option_id) = (2,8) # q 2: Height, answer 8: 1.71...
GROUP BY u.id;
I'd suggest making sure there's an index on (user_id, question_id, option_id) for searching:
ALTER TABLE `answers` ADD INDEX idx_search(`user_id`, `question_id`, `option_id`);
Otherwise it should be using primary keys for the joins (if properly defined) so it will be fast.

Mysql Select 1:n

I have two tables that relates 1:n
content
---------
- id
- title
- text
content_meta
-------------
- id
- content_id
- meta_key
- meta_value
A content can have multiple content_meta registers associated to it. Typically content_meta will contain the category, tags, descriptions and all that stuff, so I really don't know the number of registers a content will have.
What I want to accomplish is to take the content register and also all the related registers in content_meta in a single query.
I've tried the subselect approachment but seems that I can only get one register/column (¿?)
SELECT content.*, (
SELECT *
FROM content_meta
WHERE content_id = content.id
)
FROM content
This query complains that "Operand should contain 1 column(s)", so changing the '*' by for example meta_key clears the error, but returns a NULL for this subselect...
SELECT content.*, (
SELECT meta_key
FROM content_meta
WHERE content_id = content.id
)
FROM content
Can anybody show me where to go from here please?
Use:
SELECT c.*,
cm.*
FROM CONTENT c
JOIN CONTENT_META cm ON cm.content_id = c.id
That will only return CONTENT and related when there is a supporting record in the CONTENT_META table. If it's possible for a CONTENT record to not have any CONTENT_META data, use a LEFT JOIN instead:
SELECT c.*,
cm.* --these columns will be null if there is no supporting data
FROM CONTENT c
LEFT JOIN CONTENT_META cm ON cm.content_id = c.id
Followup Question -
it is now possible to group by content.id, but renaming the meta_key column with its own value and the value of this column the content of meta_value?
MySQL doesn't have PIVOT syntax - you have to use CASE statements:
SELECT c.id,
MAX(CASE WHEN cm.meta_key = 'A' THEN cm.meta_value ELSE NULL END) AS 'A',
MAX(CASE WHEN cm.meta_key = 'B' THEN cm.meta_value ELSE NULL END) AS 'B',
MAX(CASE WHEN cm.meta_key = 'C' THEN cm.meta_value ELSE NULL END) AS 'C'
FROM CONTENT c
JOIN CONTENT_META cm ON cm.content_id = c.id
GROUP BY c.id
You'll have to specify the meta_key for each one you want to appear in the resultset.
How about just doing a join on the content id column?
SELECT * FROM content
LEFT JOIN content_meta ON content.id = content_meta.content_id