SQL query on a JOINED table with multiple conditions - mysql

I have two sql tables: The wall table and the tag table. Each of them is linked with has_and_belongs_to_many relationship. Also the tag table has unique names.
Here are the tables in sql
mysql> describe tags;
+------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+----------------+
| id | bigint(20) | NO | PRI | NULL | auto_increment |
| name | varchar(255) | NO | UNI | NULL | |
| count | int(11) | NO | | NULL | |
| created_at | datetime | NO | | NULL | |
| updated_at | datetime | NO | | NULL | |
+------------+--------------+------+-----+---------+----------------+
mysql> describe tags_walls;
+---------+------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------+------------+------+-----+---------+-------+
| tag_id | bigint(20) | NO | MUL | NULL | |
| wall_id | bigint(20) | NO | | NULL | |
+---------+------------+------+-----+---------+-------+
mysql> describe walls;
+------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+----------------+
| id | bigint(20) | NO | PRI | NULL | auto_increment |
| name | varchar(255) | NO | | NULL | |
| created_at | datetime | NO | | NULL | |
| updated_at | datetime | NO | | NULL | |
+------------+--------------+------+-----+---------+----------------+
I am in rails 5 and i want to query a wall that has multiple tags.
I'm trying to do
result = Wall.all.includes(:tags).where(tags: {name: 'TAG1'})
result = result.where(tags: {name: 'TAG2'})
and the query that is constructed by rails is
SELECT DISTINCT `walls`.`id`
FROM `walls`
LEFT OUTER JOIN `tags_walls` ON `tags_walls`.`wall_id` = `walls`.`id`
LEFT OUTER JOIN `tags` ON `tags`.`id` = `tags_walls`.`tag_id`
WHERE `tags`.`name` = 'TAG1' AND `tags`.`name` = 'TAG2'
It should give me multiple walls as a results but the return is #<ActiveRecord::Relation []>
I want to build a custom sql query and just do a
Wall.includes(:tags).where query
How can i do a WHERE query on a joined table with multiple conditions linked by an AND ?

I would write this as:
SELECT tw.id
FROM tags_walls tw JOIN
tags t
ON t.id = tw.tag_id
WHERE t.name IN ('TAG1', 'TAG2')
GROUP BY tw.id
HAVING COUNT(*) = 2;
This assumes that tags are not duplicated on a wall. If that is possible, then use COUNT(DISTINCT t.name) = 2.
Notes:
walls is not needed, so that JOIN is removed.
You are looking for matches, so INNER JOIN is more appropriate than LEFT JOIN.
Table aliases make the query easier to write and to read.
Unnecessary backticks make the query harder to write and to read.

SELECT w.id
FROM walls w
JOIN tags_walls tw
ON tw.wall_id = w.id
JOIN tags t
ON t.id = tw.tag_id
AND t.name IN('TAG1','TAG2')
GROUP
BY w.id
HAVING COUNT(DISTINCT t.name) = 2 -- where '2' equals the number of arguments in IN()

WHERE tags.name = 'TAG1' OR tags.name = 'TAG2'
or
WHERE tags.name IN ('TAG1','TAG2')

Related

Cannot update table by counting fields in related table

I'd like to update followers in profile table by counting the followed_id on follow table.
mysql> explain follow;
+-------------+---------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------------+---------+------+-----+---------+-------+
| id | int(11) | NO | | NULL | |
| followed_id | int(11) | NO | | NULL | |
| follower_id | int(11) | NO | | NULL | |
+-------------+---------+------+-----+---------+-------+
And
mysql> explain profile;
+----------------+---------------+------+-----+-------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------------+---------------+------+-----+-------------------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| user_id | int(10) | NO | MUL | 0 | |
| followers | int(7) | NO | | 0 | |
| following | int(7) | NO | | 0 | |
+----------------+---------------+------+-----+-------------------+----------------+
Here is the query that I came up with:
UPDATE profile A
INNER JOIN (SELECT id,COUNT(*) idcount FROM follow GROUP BY id) as B
ON B.id = A.user_id
SET A.followers = B.idcount
But the query does not work as it should. It adds only 1 when profile has followers.
How can I fix this?
You are currently counting the number of rows for each id value in follow, which is always going to be 1. What you need to do is count the number of follower_id values for each followed_id. Also, as #juergend pointed out, you should use a LEFT JOIN so that you can get 0 values for users with no followers. Change your query to this:
UPDATE profile A
LEFT JOIN (SELECT followed_id, COUNT(DISTINCT follower_id) AS idcount
FROM follow
GROUP BY followed_id) as B ON B.followed_id = A.user_id
SET A.followers = COALESCE(B.idcount, 0)
You can use a similar query to update following:
UPDATE profile A
LEFT JOIN (SELECT follower_id, COUNT(DISTINCT followed_id) AS idcount
FROM follow
GROUP BY follower_id) as B ON B.follower_id = A.user_id
SET A.following = COALESCE(B.idcount, 0)

How to return NULL values when joining multiple tables

here are the contents of the tables.
mysql> desc student;
+------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+---------+----------------+
| name | varchar(20) | NO | | NULL | |
| sex | enum('F','M') | NO | | NULL | |
| student_id | int(10) unsigned | NO | PRI | NULL | auto_increment |
+------------+------------------+------+-----+---------+----------------+
mysql> desc grade_event;
+----------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+------------------+------+-----+---------+----------------+
| date | date | NO | | NULL | |
| category | enum('T','Q') | NO | | NULL | |
| event_id | int(10) unsigned | NO | PRI | NULL | auto_increment |
+----------+------------------+------+-----+---------+----------------+
mysql> desc score;
+------------+------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+---------+-------+
| student_id | int(10) unsigned | NO | PRI | NULL | |
| event_id | int(10) unsigned | NO | PRI | NULL | |
| score | int(11) | NO | | NULL | |
+------------+------------------+------+-----+---------+-------+
What I'm trying to accomplish is to display which students missed a quiz/test, found under 'category' in the grade_event table.
Here's what I've come up with, but am not generating any results;
select name, category, sc.event_id
from student s
join score sc on s.student_id=sc.student_id
join grade_event ge on sc.event_id=ge.event_id
where score is NULL
group by name, event_id;
I've also gone the route of attempting a subquery;
select name, category, sc.event_id
from student s
join score sc on s.student_id=sc.student_id
join grade_event ge on sc.event_id=ge.event_id
where score not in (select score from score)
group by name, event_id;
Any help would be appreciated.
I think you should just replace your join by left join, join is an inner join in MySQL : http://dev.mysql.com/doc/refman/5.7/en/join.html
And take care with group by event_id, it can be useful to precise group by sc.event_id. I don't know in MySQL but in sql server it wouldn't work.
Your second query is necessarily empty as you ask a column to have its values not in its values :)
You need to use an outer join instead of an inner join to get list of students that do not have a corresponding record in the events after creating a carthesian join of students and events:
select name, category, ge.event_id
from (student s
join grade_event ge) --no join condition creates a carthesian join
left join score sc on s.student_id=sc.student_id and sc.event_id=ge.event_id
where sc.score is NULL

How do i write this simple sql statement involving joins?

Categories table:
+-----------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+--------------+------+-----+---------+----------------+
| cat_id | int(8) | NO | PRI | NULL | auto_increment |
| cat_name | varchar(255) | NO | UNI | NULL | |
| cat_description | varchar(255) | NO | | NULL | |
| cat_order | int(8) | YES | | NULL | |
+-----------------+--------------+------+-----+---------+----------------+
Topics table:
+---------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------------+------+-----+---------+----------------+
| topic_id | int(8) | NO | PRI | NULL | auto_increment |
| topic_subject | varchar(255) | NO | | NULL | |
| topic_date | datetime | NO | | NULL | |
| topic_cat | int(8) | NO | MUL | NULL | |
| topic_by | int(8) | NO | MUL | NULL | |
+---------------+--------------+------+-----+---------+----------------+
topic_cat corresponds to cat_id in a foreign key relationship.
How can i write an sql statement which returns: The NEWEST topic from every category plus the category name???
Heres what i have so far:
SELECT * FROM categories
LEFT JOIN
(SELECT topic_id, topic_subject, max(topic_date) AS MaxDate, topic_cat, topic_by
FROM topics
GROUP BY topic_subject) AS mt
ON categories.cat_id = mt.topic_cat
ORDER BY cat_order;
It returns multiple topics from the same category, where i only want one topic per category.
When you try to get the greatest-n-per-group, you need to join back to the original table (topics) to pick the row that has the max date. Because just mentioning MAX(topic_date) in your subquery doesn't make the other columns come from the row where that max date is found. What if you also mentioned MIN(topic_date)?
This should be a solution (though I haven't tested it):
SELECT *
FROM topics AS t
JOIN (SELECT topic_cat, MAX(topic_date) AS topic_date
FROM topics
GROUP BY topic_cat) AS maxt USING (topic_cat, topic_date)
RIGHT JOIN categories AS c ON t.topic_cat = c.cat_id
The first, easy, but not so powerfull solution would be to GROUP BY topic_cat and ORDER BY topic_date in topics select:
SELECT * FROM categories
LEFT JOIN
(SELECT topic_id, topic_subject, topic_date, topic_cat, topic_by
FROM topics
ORDER BY topic_date DESC
GROUP BY topic_cat) AS mt
ON categories.cat_id = mt.topic_cat
ORDER BY cat_order;
I think MySQL should be able to handle this, even it's not really universal solution for other database engines, where the rules for grouping are more strict.

SQL Left Inner Join (mysql)

I have been using the following query:
I am using two tables: (there are some others mentioned but not needed for this question)
assessment_criteria
+-------------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------------+--------------+------+-----+---------+----------------+
| id | mediumint(9) | NO | PRI | NULL | auto_increment |
| scheme_of_work_id | mediumint(9) | NO | | NULL | |
| level | char(255) | YES | | NULL | |
| criteria | char(255) | NO | | NULL | |
+-------------------+--------------+------+-----+---------+----------------+
criteria_completed
+------------------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------------------+--------------+------+-----+---------+----------------+
| id | mediumint(9) | NO | PRI | NULL | auto_increment |
| student_ID | mediumint(9) | NO | | NULL | |
| assessment_criteria_id | mediumint(9) | NO | | NULL | |
| date_marked | date | NO | | NULL | |
| notes | varchar(255) | YES | | NULL | |
| attainment | varchar(15) | YES | | NULL | |
| effort | varchar(15) | YES | | NULL | |
| marked_by | varchar(20) | NO | | NULL | |
+------------------------+--------------+------+-----+---------+----------------+
I was using a query like this to display a list of assessment criteria that a student HAS NOT completed:
SELECT DISTINCT assessment_criteria.id, assessment_criteria.level, assessment_criteria.criteria FROM assessment_criteria, criteria_completed
WHERE (assessment_criteria.scheme_of_work_id = '17')
AND (assessment_criteria.id NOT IN (SELECT criteria_completed.assessment_criteria_id FROM criteria_completed WHERE (student_ID = '403')))
ORDER BY level;
This query has become incredibly slow to run, I have been trying to make it faster using LEFT JOIN.
SELECT DISTINCT a.id, a.level, a.criteria
FROM assessment_criteria a
LEFT JOIN criteria_completed b
ON a.id = b.assessment_criteria_id
WHERE b.assessment_criteria_id IS NULL
But I am having no success when I try to add in clauses for project and student; ie.
SELECT DISTINCT a.id, a.level, a.criteria
FROM assessment_criteria a
LEFT JOIN criteria_completed b
ON a.id = b.assessment_criteria_id
WHERE b.assessment_criteria_id IS NULL
AND (b.student_ID = '403')
AND (a.scheme_of_work_id = '17');
mysql reports "empty set". I suspect I am referencing these foreign keys incorrectly?
(Just to confirm, you are using b.assessment_criteria_id IS NULL to detect failed joins)
Applying the filters on table b to the WHERE clause will filter out any records where the join has failed, which I believe is the cause of the problem.
You can try moving the b filters into the JOIN condition:
SELECT DISTINCT a.id, a.level, a.criteria
FROM assessment_criteria a
LEFT JOIN criteria_completed b
ON a.id = b.assessment_criteria_id
AND (b.student_ID = 403)
WHERE b.assessment_criteria_id IS NULL
AND (a.scheme_of_work_id = 17);
Although personally, I dislike filtering like this in a JOIN. The alternative would be:
SELECT DISTINCT a.id, a.level, a.criteria
FROM assessment_criteria a
LEFT JOIN criteria_completed b
ON a.id = b.assessment_criteria_id
WHERE (a.scheme_of_work_id = 17)
AND (b.assessment_criteria_id IS NULL OR b.student_ID = 403);

Counting many to many association records

I have many to many association between words and definitions.
words:
+-----------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
+-----------------+--------------+------+-----+---------+----------------+
definitions:
+-------------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| language_id | int(11) | YES | MUL | NULL | |
+-------------------+--------------+------+-----+---------+----------------+
definitions_words:
+---------------+---------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+---------+------+-----+---------+-------+
| definition_id | int(11) | NO | PRI | NULL | |
| word_id | int(11) | NO | PRI | NULL | |
+---------------+---------+------+-----+---------+-------+
I would like to get all word records which have exactly one definition with language_id = 1.
I think the simplest way to express this in SQL is using in:
select *
from words
where id in (select word_id
from word_definitions
where language_id = 1
having count(*) = 1
)
However, in with a subquery does not always work efficiently in MySQL. It can be replaced with an exists clause:
select *
from words w
where exists (select 1
from word_definitions wd
where language_id = 1
having count(*) = 1 and wd.word_id = w.id
)
SELECT a.ID, COUNT(*) totalRecordCount
FROM words a
INNER JOIN definition_words b
ON a.ID = b.word_ID
INNER JOIN definitions c
ON b.definition_id = c.ID
INNER JOIN
(
SELECT id,
SUM(language = 1) totalCount
FROM definitions
GROUP BY id
) d ON c.ID = d.ID AND
d.TotalCount = 1
GROUP BY a.ID