MySQL Table Joining With AVG() - mysql

I have a "ratings" table, that contains (as a foreign key) the ID for the thing that it is rating. There are possibly multiple ratings for a thing, or no ratings for a value.
I want to join tables to see the different ratings for all the different IDs, but right now I'm having trouble viewing things that have no ratings. For example:
mysql> select avg(ratings.rating), thing.id from ratings, things where ratings.thingId = thing.id group by thing.id;
+----------------------+----+
| avg(ratings.rating) | id |
+----------------------+----+
| 6.3333 | 1 |
| 6.0000 | 2 |
+----------------------+----+
Is there any way to modify my select query to also include IDs that have no ratings? I tried modifying the statement to say where ratings.thingId = thing.id or thing.id > 0 but that doesn't seem to help.
Thanks and sorry if it's unclear.

SELECT AVG(ratings.rating),
thing.id
FROM things
LEFT OUTER JOIN ratings
ON ratings.thingId = things.id
GROUP BY thing.id

You're currently performing an INNER JOIN, which eliminates things records with no associated ratings. Instead, an OUTER JOIN...
SELECT AVG(COALESCE(ratings.rating, 0)), thing.id
FROM things
LEFT JOIN ratings ON things.id = ratings.thingId
GROUP BY thing.id
Will return ALL things, regardless of whether or not they have ratings. Note the use of COALESCE(), which will return the first non-NULL argument - thus things with no ratings will return 0 as their average.

SELECT p.id,p.title,(select round(avg(pr.rating),1)
from post_rating pr
where pr.postid=p.id)as AVG FROM posts p

Related

SQL: many to many relationships select where multiple criteria

given these tables :
id_article | title
1 | super article
2 | another article
id_tag | title
1 | great
2 | awesome
id_relation | id_article | id_tag
1 | 1 | 1
2 | 1 | 2
3 | 2 | 1
I'd like to be able to select all articles that are "great" AND "awesome" (eventually, I'll probably have to implement OR too)
And basically, if I do a select on articles the relation table joining on id_article: of course, I cant join two different values of id_tag. Only lead I had with concatenating IDs to test as a string, but that seems so lame, there has to be a prettier solution.
Oh and if it matters, I use a MySQL server.
EDIT: for ByWaleed, the typical sql select that would surely fail that I cited in my original question:
SELECT
a.id_article,
a.title
FROM articles a, relations r
WHERE
r.id_article = a.id_article and r.id_tag = 1 and r.id_tag = 2
wouldnt work because r.id_tag cant obviously be 1 and 2 on the same line. I doubt w3schools has an article on that. My search on google didnt yield any result, probably because I searched with the wrong keyword.
If you do all the joins as normal, then aggregate the rows to one group by article, then you can assert that they must have at least two different tags.
(Having already filtered to great and/or awesome, that means they have both.)
SELECT
a.id_article,
a.title
FROM
articles a
INNER JOIN
relations r
ON r.id_article = a.id_article
INNER JOIN
tags t
ON t.id_tag = r.id_tag
WHERE
t.title IN ('great', 'awesome')
GROUP BY
a.id_article,
a.title
HAVING
COUNT(DISTINCT t.id_tag) = 2
(The DISTINCT is to avoid the possibility of one article having 'great' twice, for example.)
To do OR, you just remove the HAVING clause.
One approach is to aggregate by article, and then assert that the article both the "great" and "awesome" tags:
SELECT
a.id_article,
a.title
FROM articles a
INNER JOIN relations r
ON a.id_article = r.id_article
INNER JOIN tags t
ON r.id_tag = t.id_tag
WHERE
t.title IN ('great', 'awesome')
GROUP BY
a.id_article,
a.title
HAVING
MIN(t.title) <> MAX(t.title);
Demo
The logic here is that we first limit records, for each article, to only those of the two targets tags. Then we assert, in the HAVING clause, that both tags appear. I use a MIN/MAX trick here, because if the min and max differ, then it implies that there are two distinct tags.
Step 1: Use a temp table to get all articles with titles.
Step 2: If an article occurs multiple times in your temp table, that means it has great and awesome as titles.
Try:
CREATE TEMPORARY TABLE MyTempTable (
select t1.id_article, t2.title
from table1 t1
inner join table3 t3 on t3.id_article = t1.id_article
inner join table2 t2 on t2.id_tag = t3.id_tag
)
select m.id_article
from MyTempTable m
group by m.id_article
having count(*)>1
Edit: This solution assumes there are two possible tags, great and awesome. If more, please add a "where" clause to the select query for creating the temp table like where t2.title in ('great','awesome')

Reuse body of a mysql query for both count and rows

Because I'm working with a framework (Magento) I don't have direct control of the SQL that is actually executed. I can build various parts of the query, but in different contexts its modified in different ways before it goes to the database.
Here is a simplified example of what I'm working with.
students enrolments
-------- ------------------
id| name student_id| class
--+----- ----------+-------
1| paul 1|biology
2|james 1|english
3| jo 2| maths
2|english
2| french
3|physics
3| maths
A query to show all students who are studying English together with all the courses those students are enrolled on, would be:
SELECT name, GROUP_CONCAT(enrolments.class) AS classes
FROM students LEFT JOIN enrolments ON students.id=enrolments.student_id
WHERE students.id IN ( SELECT e.student_id
FROM enrolments AS e
WHERE e.class LIKE "english" )
GROUP BY students.id
This will give the expected results
name| classes
----+----------------------
paul|biology, english
james|maths, english, french
Counting the number of students who study english would be trivial, if it weren't for the fact that Magento automatically uses portions of my first query. For the count, it modifies my original query as follows:
Removes the columns being selected. This would be the name and classes columns.
Adds a count(*) column to the select
Removes any group by clause
After this butchery, my query above becomes
SELECT COUNT(*)
FROM students LEFT JOIN enrolments ON students.id=enrolments.student_id
WHERE students.id IN ( SELECT e.student_id
FROM enrolments AS e
WHERE e.class LIKE "english" )
Which will not give me the number of students enrolled on the English course as I require. Instead it will give me the combined number of enrolments of all students who are enrolled on the English course.
I'm trying to come up with a query which can be used in both contexts, counting and getting the rows. I get to keep any join clauses and and where clauses and that's about it.
The problem with your original query is the GROUP BY clause. Selecting COUNT(*) by keeping the GROUP BY clause would result in two rows with a number of classes for each user:
| COUNT(*) |
|----------|
| 2 |
| 3 |
Removing the GROUP BY clause will just retun the number of all rows from the LEFT JOIN:
| COUNT(*) |
|----------|
| 5 |
The only way I see, magento could solve that problem, is to put the original query into a subquery (derived table) and count the rows of the result. But that might end up in terrible performance. I would also be fine with an exception, complaining that a query with a GROUP BY clause can not be used for pagination (or something like that). Just return an anexpected result is probably the worst what a library can do.
Well, it just so happens I have a solution. :-)
Use a corelated subquery for GROUP_CONCAT in the SELECT clause. This way you will not need a GROUP BY clause.
SELECT name, (SELECT GROUP_CONCAT(enrolments.class)
FROM enrolments
WHERE enrolments.student_id = students.id
) AS classes
FROM students
WHERE students.id IN ( SELECT e.student_id
FROM enrolments AS e
WHERE e.class LIKE "english" )
However, I would rewrite the query to use an INNER JOIN instead of an IN condition:
SELECT s.name, (
SELECT GROUP_CONCAT(e2.class)
FROM enrolments e2
WHERE e2.student_id = s.id
) AS classes
FROM students s
JOIN enrolments e1
ON e1.student_id = s.id
WHERE e1.class = "english";
Both queries will return the same result as your original one.
| name | classes |
|-------|----------------------|
| paul | biology,english |
| james | maths,english,french |
But also return the correct count when modified my magento.
| COUNT(*) |
|----------|
| 2 |
Demo: http://rextester.com/OJRU38109
Additionally - chances are good that it will even perform better, due to MySQLs optimizer, which often creates bad execution plans for queries with JOINs and GROUP BY.

MySQL Joins on n to n relational table keping NULL values

This is a vote system, where candidate can be voted from different(limited) places. and I want the number of vote per place of each candidate.
I have 3 tables
TABLE candidate
------------------
id
name
TABLE place
------------------
id
label
TABLE vote
------------------
id
id_candidate
id_vote
no_votes // represents the amount of votes in this place for that particular candidate
Suppose I have 10 candidates and 15 different places, I'm trying to make a query that will return 10*15 = 150 rows even if there is no votes, keeping NULL value for ids that are not present in the relational table(which i can replace by 0).
But i'm not making the correct query
Here is the query i made so far (i've tried many modification, inner, outer joins... but nothing worked)
SELECT *
FROM votes
RIGHT JOIN candidate ON candidate.id = candidate_id
LEFT JOIN palce ON place.id = place_id
First, if you want the number of votes per candidate, then you should be thinking "aggregation".
Second, don't mix left and right joins in a query. It is just confusing. Start with the table where you want to keep all the rows, and then just use left join.
So, something like this:
SELECT c.*,
SUM(p.place_name = 'place1') as place1,
SUM(p.place_name = 'place2') as place2,
SUM(p.place_name = 'place3') as place3
FROM candidate c LEFT JOIN
votes v
ON c.id = v.candidate_id LEFT JOIN
place p
ON p.id = v.place_id
GROUP BY c.id;
Considering:
TABLE vote
------------------
id PK
id_candidate FK to candidate
id_vote FK to place
no_votes
-
SELECT CA.name,
PL.label,
SUM(VO.no_votes) as votes
FROM candidate CA
LEFT JOIN vote VO ON CA.id = VO.id_candidate
LEFT JOIN place PL ON PL.id = VO.id_vote
GROUP BY CA.id, PL.id

SQL query using outer join and limiting child records for each parent

I'm having trouble figuring out how to structure a SQL query. Let's say we have a User table and a Pet table. Each user can have many pets and Pet has a breed column.
User:
id | name
______|________________
1 | Foo
2 | Bar
Pet:
id | owner_id | name | breed |
______|________________|____________|_____________|
1 | 1 | Fido | poodle |
2 | 2 | Fluffy | siamese |
The end goal is to provide a query that will give me all the pets for each user that match the given where clause while allowing sort and limit parameters to be used. So the ability to limit each user's pets to say 5 and sorted by name.
I'm working on building these queries dynamically for an ORM so I need a solution that works in MySQL and Postgresql (though it can be two different queries).
I've tried something like this which doesn't work:
SELECT "user"."id", "user"."name", "pet"."id", "pet"."owner_id", "pet"."name",
"pet"."breed"
FROM "user"
LEFT JOIN "pet" ON "user"."id" = "pet"."owner_id"
WHERE "pet"."id" IN
(SELECT "pet"."id" FROM "pet" WHERE "pet"."breed" = 'poodle' LIMIT 5)
In Postgres (8.4 or later), use the window function row_number() in a subquery:
SELECT user_id, user_name, pet_id, owner_id, pet_name, breed
FROM (
SELECT u.id AS user_id, u.name AS user_name
, p.id AS pet_id, owner_id, p.name AS pet_name, breed
, row_number() OVER (PARTITION BY u.id ORDER BY p.name, pet_id) AS rn
FROM "user" u
LEFT JOIN pet p ON p.owner_id = u.id
AND p.breed = 'poodle'
) sub
WHERE rn <= 5
ORDER BY user_name, user_id, pet_name, pet_id;
When using a LEFT JOIN, you can't combine that with WHERE conditions on the left table. That forcibly converts the LEFT JOIN to a plain [INNER] JOIN (and possibly removes rows from the result you did not want removed). Pull such conditions up into the join clause.
The way I have it, users without pets are included in the result - as opposed to your query stub.
The additional id columns in the ORDER BY clauses are supposed to break possible ties between non-unique names.
Never use a reserved word like user as identifier.
Work on your naming convention. id or name are terrible, non-descriptive choices, even if some ORMs suggest this nonsense. As you can see in the query, it leads to complications when joining a couple of tables, which is what you do in SQL.
Should be something like pet_id, pet, user_id, username etc. to begin with.
With a proper naming convention we could just SELECT * in the subquery.
MySQL does not support window functions, there are fidgety substitutes ...
SELECT user.id, user.name, pet.id, pet.name, pet.breed, pet.owner_id,
SUBSTRING_INDEX(group_concat(pet.owner_id order by pet.owner_id DESC), ',', 5)
FROM user
LEFT JOIN pet on user.id = pet.owner_id GROUP BY user.id
Above is rough/untested, but this source has exactly what you need, see step 4. also you don't need any of those " 's.

COUNT(*) FROM different tables with LEFT JOIN

I have 1 table of users, and 10 tables (articles, news, ...) where I save user's publications. I want to show how many publications has each user, in one query:
| ID_USER | COUNT(id_article) | COUNT(id_news) | etc...
-------------------------------------------------
| 1 | 0 | 3 |
| 2 | 2 | 9 |
| 3 | 14 | 5 |
| 4 | 0 | 0 |
If I use this query to show the number of articles...
SELECT id_user,COUNT(articles.id_article) FROM users
LEFT JOIN articles ON articles.id_user_article=users.id_user
GROUP BY users.id_user
... it shows the information correctly. But if I start to add the second table...
SELECT id_user,COUNT(articles.id_article),COUNT(news.id_news) FROM users
LEFT JOIN articles ON articles.id_user_article=users.id_user
LEFT JOIN news ON news.id_user_news=users.id_user
GROUP BY users.id_user
... it doesn't show the correct information.. and if I join all the rest tables, if shows really strange result (thousands of articles for first user, and NULL for the rest).
Which is the correct way of show this information using only one query? Thank you!
You can use a subselect instead of a left join for each table. The final result will be the same but maybe in that way is clearer.
SELECT u.id_user,
(SELECT COUNT(a.id_article)
FROM articles a
WHERE a.id_user_article = u.id_user) AS articles,
(SELECT COUNT(n.news)
FROM news n
WHERE n.id_user_news = u.id_user) AS news
FROM users u
Also if you only uses one column of each table, the subselect is a better option than multiple left joins.
Your problem is that you are joining along different dimensions, which creates cartesian products for each user. The solution by #rafa is actually a fine solution in MySQL. The use of count(distinct) works okay, but only when the counts are not very large. Another approach is to pre-aggregate the results along each dimension:
SELECT u.id_user, a.articles, n.news
FROM users u left outer join
(select id_user_article, count(*) as articles
from articles
group by id_user_article
) a
on u.id_user = a.id_user_article left outer join
(select id_user_news, count(*) as news
from news
group by id_user_news
) n
on u.id_user = n.id_user_news;
EDIT:
If you are using the count(distinct) approach, then you are generating a cross product. If every user had 3 articles and 4 news items, then the users would be multiplied by 12. Probably feasible.
If every user had 300 articles and 400 news items, then every user would be multiplied by 120,000. Probably not feasible.