How to concatenate values into string in MySQL - mysql

I have this SQL table:
user_skills
==========================
user_id | skill_id | value
1 | 4 | 1
1 | 5 | 1
1 | 6 | 1
1 | 7 | 1
1 | 8 | 1
2 | 4 | 1
2 | 6 | 1
4 | 4 | 1
4 | 5 | 1
5 | 8 | 1
Than, I have SQL query which returns me info about user.
$mysqli->query("SELECT u.*,
COUNT(a.user_id) AS jobs,
r.*
FROM users u
LEFT JOIN articles a
ON u.id = a.user_id
LEFT JOIN rating r
ON r.user_id = u.id
LEFT JOIN regions r1
ON r1.id = u.city
WHERE u.active = 1 AND
u.server_id IN (" . $server_id . ")
GROUP BY (CASE WHEN a.user_id IS NULL THEN u.id ELSE a.user_id END)
ORDER BY u.points DESC,
jobs_done DESC,
u.id DESC
LIMIT " . (($page - 1) * $limit) . ", " . ($page * $limit));
Now, I need to select to each user their skills, or better, skill_ids. Ideally as a string separated by commas because we spoke about only 5 skill_id (4-8).
I tried:
$mysqli->query("SELECT u.*,
COUNT(a.user_id) AS jobs,
r.*,
CONCAT_WS(',', us.skill_id) AS skill_ids
FROM users u
LEFT JOIN articles a
ON u.id = a.user_id
LEFT JOIN rating r
ON r.user_id = u.id
LEFT JOIN regions r1
ON r1.id = u.city
LEFT JOIN user_skills us
ON us.user_id = u.id AND skill_id IN (4, 5, 6, 7, 8)
WHERE u.active = 1 AND
u.server_id IN (" . $server_id . ")
GROUP BY (CASE WHEN a.user_id IS NULL THEN u.id ELSE a.user_id END)
ORDER BY u.points DESC,
jobs_done DESC,
u.id DESC
LIMIT " . (($page - 1) * $limit) . ", " . ($page * $limit));
But it returns me only the first skill_id from database, without concatenating.
When I tried to use CONCAT_WS(',', us.skill_id) AS skill_ids, result was comma and the first one skill_id (, 4).
Data in database are okay, SUM(us.skill_id) returns 30 for user with id=4.
Any idea?

You have two major problems...
A) The function you want is GROUP_CONCAT(), which produces a CSV of values for the group. This query give you the skills list for each user:
select user_id, group_concat(skill_id) as skill_ids
from user_skills
group by user_id
B) A more major problem is your incorrect use of GROUP BY: For it to work as expected, you must list every column that is not an aggregate function, either using positional numbers or expressions.
Assuming users table has 6 columns and rating has 4:
SELECT
u.*,
COUNT(a.user_id) AS jobs,
r.*,
GROUP_CONCAT(us.skill_id) AS skill_ids
FROM ...
...
GROUP BY 1,2,3,4,5,6, 8,9,10,11,12 -- note 7 is missing
or you can list column expressions in the GROUP BY:
GROUP BY u.id, u.name, ..., r.id, r.foo, ...
If you omit even one non- aggregate column, it won't work as expected. This peculiar behaviour is mysql only - every other database throws a syntax exception if you left one out.

Related

Select top X records for each group or default

I have the following schema:
users:
id email
1 'user.one#test.com'
2 'user.two#test.com'
video_group:
id title
1 'Group 1'
2 'Group 2'
videos:
id group_id rank title
1 1 1 'Group 1 - Video 1'
2 1 2 'Group 1 - Video 2'
3 2 1 'Group 2 - Video 1'
user_video_play_times:
video_id user_id time last_update
2 1 12 01-02-2018
1 1 120 01-01-2018
I need to get the time, user_id, video_id, and group_id of the last video played by a user in specific groups, but if there's no records on user_video_play_times for a group, the video with the lowest rank should be returned. For example:
user_id group_id video_id time
1 1 2 12 -- user.one + group 1
1 2 3 0 -- user one + group 2
This is the query I have so far:
SELECT
pt.user_id user_id,
v.id video_id,
g.id group_id,
pt.time time
FROM
videos v
INNER JOIN video_groups g ON g.id = v.group_id
LEFT JOIN user_video_play_times pt ON
pt.video_id = v.id AND
pt.user_id = 1
LEFT JOIN (
SELECT
g.id AS g_id,
MAX(pt.last_update) AS pt_last_update
FROM
user_video_play_times pt
INNER JOIN videos v ON v.id = pt.video_id
INNER JOIN video_groups g ON g.id = v.group_id
WHERE
pt.user_id = 1 AND
g.id IN (1, 2)
GROUP BY
g.id
) lpt ON lpt.g_id = g.id AND lpt.pt_last_update = pt.last_update
WHERE
g.id IN (1, 2)
GROUP BY
g.id
It is sort of working, but...
Adding v.title to the column selection messes the results for some reason, making everything return only videos with rank 1. Any idea why?
Could this query be optimized, or is there another slicker way to achieve the same results?
Any help with this is really appreciated!
DB fiddle here
Update 1:
This issue seems to only happen when the column os of type text.
Since your db<>fiddle is for MariaDB version 10.3; I am presuming that you have Window Functions available.
We can use Row_number() function over a partition of group_id to get row number values, as per the defined rules. Video with latest last_update value will have Row number of 1 and so on. If there is no video played, then the one with least value of Rank will have Row number = 1.
We can use this result-set as a Derived Table, and consider only those rows where Row number = 1.
SELECT
dt.user_id,
dt.group_id,
dt.video_id,
dt.video_title,
dt.time
FROM
(
SELECT
pt.user_id AS user_id,
g.id AS group_id,
v.id AS video_id,
v.title AS video_title,
pt.time AS time,
ROW_NUMBER() OVER(PARTITION BY v.group_id
ORDER BY pt.last_update DESC,
v.`rank` ASC) AS row_num
FROM videos AS v
INNER JOIN video_groups AS g
ON g.id = v.group_id AND
g.id IN (1,2)
LEFT JOIN user_video_play_times AS pt
ON pt.video_id = v.id AND
pt.user_id = 1
) AS dt
WHERE dt.row_num = 1
View on DB Fiddle
Result:
| user_id | group_id | video_id | video_title | time |
| ------- | -------- | -------- | ----------------- | ---- |
| 1 | 1 | 2 | Group 1 - Video 2 | 12 |
| | 2 | 3 | Group 2 - Video 1 | |
PS: Note that Rank is a Reserved Keyword, and you should really avoid using it as column/table name.

Get top 5 records for user, average records and rank by averages

I have a table for user records that hold scores (and need to get only users that are members). I need to get the top 5 scores for each user during this year, average the scores and return the top 10 users.
NOTE: Also the user must have a minimum of 5 entries.
SCORE TABLE:
user_id | score | date_submitted
1 99 2017-11-07 22:00:00
2 55 2017-10-33 11:33:35
1 12 2017-09-33 11:33:35
USER TABLE
id | is_member
1 1
2 1
3 0
Here is what I have so far:
SELECT s.user_id,
(SELECT AVG(s.score) FROM score s2 WHERE s2.user_id = s.user_id ORDER BY score DESC LIMIT 5) gr
FROM score s, users u
WHERE u.id = s.user_id
AND u.is_member = 1
AND YEAR(s.date_submitted) = YEAR(CURDATE())
GROUP BY s.user_id
HAVING COUNT(*) >= 5
ORDER BY gr DESC LIMIT 10
This returns:
1242 - Subquery returns more than 1 row
I understand that its the limit in the subquery, I am trying to figure out how to get the top 5 records for that user.
You don't need the subquery. I would also suggest using the join syntax:
SELECT s.user_id, AVG(s.score) gr
FROM score s
INNER JOIN users u
ON u.id = s.user_id
AND u.is_member = 1
WHERE YEAR(s.date_submitted) = YEAR(CURDATE())
GROUP BY s.user_id
HAVING COUNT(*) >= 5
ORDER BY gr DESC
LIMIT 10
Following query will calculate avg of current year's top(highest) 5 scores of each user who is member and display top 10 users:
SELECT s.user_id, AVG(s.score) gr
FROM (select * from score s
where YEAR(date_submitted) = YEAR(CURDATE())
and
(select count(*)
from score
where user_id = s.user_id
and score>=s.score
)<=5
) s
INNER JOIN users u
ON u.id = s.user_id
AND u.is_member = 1
GROUP BY s.user_id
HAVING COUNT(*) >= 5
ORDER BY gr DESC
LIMIT 10;
Hope it helps.

MySQL multiple SubQueries on same table

I got a table votes that indicates me if a user voted for a specific movie. It also shows me how many movies a user has voted for.
id_film | id_user | voting
----------------------------
1 | 1 | 7
1 | 33 | 5
3 | 1 | 9
4 | 7 | 7
4 | 2 | 8
4 | 1 | 6
6 | 1 | 6
... | ... | ...
I want to get a list of id_film's which are related to id_user's in this way:
Get all id_film's from a specific id_user like
SELECT id_film FROM votes WHERE id_user = 1
Grab every id_user which is related
SELECT DISTINCT v.user FROM votes v WHERE id_film IN ( id_film's )
Then SELECT id_film's FROM votes v WHERE user IN ( "user list from previous query" ) except id_film's from first query.
This was my first attempt:
SELECT id_film, film.title, film.originaltitle, COUNT(*)
FROM votes v
INNER JOIN film ON v.id_film = film.id
WHERE user IN
(
SELECT DISTINCT v.user
FROM votes v
WHERE id_film IN
(
SELECT id_film
FROM votes v
WHERE user = 1
)
)
AND
id_film NOT IN
(
SELECT id_film
FROM votes v
WHERE user = 1
)
GROUP BY id_film
It doesn't work. MySQL took too long for a result and I restarted XAMPP.
So I tried another SELECT, this time with JOINS:
SELECT DISTINCT v.id_film AS vFilm, v1.user AS v1User, v2.id_film AS v2Film
FROM votes v
LEFT OUTER JOIN votes v1 ON v1.id_film = v.id_film
LEFT OUTER JOIN votes v2 ON v1.user = v2.user
WHERE v.user = 1
AND v1.user != 1
AND v2.id_film NOT
IN
(
SELECT id_film
FROM votes
WHERE user = 1
)
GROUP BY v2.id_film
Also doesn't work, but when I tried it without the NOT IN condition in the end it works! (It took appr. 13 sec.) :-(
Here is the working query.
SELECT DISTINCT v2.id_film AS v2Film
FROM votes v
LEFT OUTER JOIN votes v1 ON v1.id_film = v.id_film
LEFT OUTER JOIN votes v2 ON v1.user = v2.user
WHERE v.user = 1
AND v1.user != 1
With Output
v2Film
---------
1
13
14
58
4
...
But this query doesn't except id_film's from first query.
Because I know that user 1 already voted for id_film 1.
So, am I totally wrong with my logic or is my code too complex for this?

MySQL INNER JOIN from second table (TOP10)

$stmt = $conn->prepare('SELECT a.*, c.*, SUM(a.money+b.RESULT) AS ARESULT
FROM users a
INNER JOIN bankaccounts c
ON a.id = c.owner
INNER JOIN
(
SELECT owner, SUM(amount) AS RESULT
FROM bankaccounts
GROUP BY owner
) b ON a.id = b.owner
ORDER BY ARESULT DESC LIMIT 10');
What's problem, it show wrong only one record? I want list max 10 records - like TOP 10 richest who has [money+(all his bankaccounts amount)]
Lets say.. I have 2 tables.
Table: users
ID | username | money
1 | richman | 500
2 | richman2 | 600
Table: bankaccounts
ID | owner | amount
65 | 1 | 50
68 | 1 | 50
29 | 2 | 400
So it would list:
richman2 1000$
richman 600$
Try using a subqueries...
$stmt = $conn->prepare('SELECT a.*,
IFNULL((SELECT SUM(amount) FROM bankaccounts b WHERE b.owner=a.id),0) AS BANK_MONEY,
(IFNULL(a.money,0) + IFNULL((SELECT SUM(amount) FROM bankaccounts c WHERE c.owner=a.id),0)) AS ARESULT
FROM users a
ORDER BY ARESULT DESC LIMIT 0, 10');
EDIT: Added a field for bank account totals
EDIT2: Added IFNULL to SQL statement in case user is not in BankAccounts table
Try this:
SELECT a.*, (a.money + b.RESULT) AS ARESULT
FROM users a
INNER JOIN (SELECT owner, SUM(amount) AS RESULT
FROM bankaccounts
GROUP BY owner
) b ON a.id = b.owner
ORDER BY ARESULT DESC
LIMIT 10

Calculate average message quality per user in MySQL

Consider the following tables:
users messages
------------------- ----------------------
user_id avg_quality msg_id user_id quality
------------------- ----------------------
1 1 1 1
2 2 1 0
3 3 1 0
4 1 1
5 1 1
6 2 0
7 2 0
8 3 1
messages.quality is either 0 or 1. I need to calculate the average message quality per user and update users.avg_quality accordingly. So the desired output would be modified users table like so:
users
-------------------
user_id avg_quality <-- DECIMAL (8,2)
-------------------
1 0.60 <-- (3x1 + 2x0) / 5
2 0.00 <-- (2x0) / 2
3 1.00 <-- (1x1) / 1
I've begun my query like this, I know the syntax is incorrect but have no better idea. Do you?
UPDATE messages m, users u
SET avg_quality = (SELECT COUNT(m.msg_id) / SUM(m.quality))
WHERE m.user_id = u.user_id
This should work:
UPDATE users u
INNER JOIN (SELECT a.user_id, AVG(quality) avg_quality
FROM messages a
INNER JOIN users b
ON a.user_id = b.user_id
GROUP BY a.user_id
) tmp
ON u.user_id = tmp.user_id
SET u.avg_quality = tmp.avg_quality;
See the average function:
http://dev.mysql.com/doc/refman/5.0/en/group-by-functions.html#function_avg
Your select should be something like this:
select user_id, AVG(quality) from messages group by user_id
If you begin with an empty users table you could run a query like this one to update it all:
insert into users (user_id, avg_quality)
select m.user_id, coalesce(AVG(m.quality),0) from messages m group by m.user_id
If you need continuous results Luc's proposal will work for you:
update users u left join (
select m.user_id, AVG(m.quality) as average from messages m group by m.user_id
) as average_result_t on u.user_id = average_result_t.user_id
set u.average = coalesce(average_result_t.average,0)