Counting many to many association records - mysql

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

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)

SQL query on a JOINED table with multiple conditions

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')

Need help using local variables to subtract previous row value having several groups

I have 3 tables as follows:
+-----------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------+--------------+------+-----+---------+-------+
| id | int(11) | NO | PRI | 0 | |
| full_name | varchar(200) | YES | | NULL | |
| gender | varchar(1) | YES | | NULL | |
+-----------+--------------+------+-----+---------+-------+
+----------+---------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------+---------+------+-----+---------+-------+
| actor_id | int(11) | YES | MUL | NULL | |
| movie_id | int(11) | YES | MUL | NULL | |
| salary | int(11) | YES | | NULL | |
+----------+---------+------+-----+---------+-------+
+-------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| id | int(11) | NO | PRI | 0 | |
| title | varchar(100) | YES | | NULL | |
| year | int(11) | YES | | NULL | |
| genre | varchar(100) | YES | | NULL | |
+-------+--------------+------+-----+---------+-------+
I'm trying to figure out which actor has the longest time between movies.
I'm trying to use local variables to reference the previous row value in the case where the actor was the same (or else it defaults to 0). However for some reason the #previousName variable returns a random list of names.
Here is the code:
SELECT
a.full_name,
m.year,
m.title,
#PreviousName,
#PreviousYear,
if(#PreviousName = a.full_name, m.year - #PreviousYear, 0) AS Delta,
#PreviousName := a.full_name,
#PreviousYear := m.year
FROM
actors AS a
INNER JOIN
cast AS c ON a.id = c.actor_id
INNER JOIN
movies AS m ON c.movie_id = m.id,
(SELECT #PreviousName := null, #PreviousYear := 999) as SQLVars
ORDER BY full_name;
And here is what I get:
Picture of result table
Note I am using mySQL V5.7 so window functions are not an option.
You can do this with a correlated subquery:
select mc.*, (year - prev_year) as diff
from (select c.*, m.year,
(select m2.year
from movies m2 join
cast c2
on c2.movie_id = m2.id
where c2.actor_id = c.actor_id and
m2.year < m.year
order by m2.year desc
limit 1
) prev_year
from movies m join
cast c
on c.movie_id = m.id
) mc
order by diff desc;
This assumes that an actor is not in two movies in the same year. If you had a release date or something, that would be more effective for ordering the times.

Get records from third table

I have three tables, and duplicate column names also :) I want to join albums to products and images to albums. Images are many. Trying such query, it gives me duplicate products. Is there a chance to grab everything in one query?
SELECT
*, p.name as nazwa, a.name as nazwa_al, i.name as obrazek
FROM products p
JOIN
albums a on p.album_id=a.id
JOIN
(SELECT *, images.name AS nazwa_im FROM images ORDER BY images.order ASC) i
ON i.album_id=a.id
ORDER BY p.order ASC
Products
+-------------+---------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+---------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | text | NO | | NULL | |
| description | text | NO | | NULL | |
| album_id | int(11) | YES | | NULL | |
| order | int(11) | NO | | NULL | |
+-------------+---------+------+-----+---------+----------------+
Albums
+-------+---------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+---------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | text | NO | | NULL | |
+-------+---------+------+-----+---------+----------------+
Images
+----------+---------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+---------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | text | NO | | NULL | |
| alt | text | NO | | NULL | |
| album_id | int(11) | NO | | NULL | |
| order | int(11) | NO | | NULL | |
+----------+---------+------+-----+---------+----------------+
For the sake of simplicity, I don't want to modify structure of db. The easiest solution for me would be: one product=>one album=>many images
Use joins and use aliases to solve duplicate name error.
You can use distint or group by have results aligned as per same product id.
SELECT
*, p.name as nazwa, a.name as nazwa_al, i.name as obrazek
FROM
products p
JOIN
albums a on p.album_id = a.id
JOIN
images i ON i.album_id = a.id
GROUP BY p.id
ORDER BY p.order ASC
You need to use group_concat if multiple rows on right side.
SELECT
*, p.name as nazwa, a.name as nazwa_al, group_concat(i.name) as obrazek
FROM
products p
JOIN
albums a on p.album_id = a.id
JOIN
images i ON i.album_id = a.id
GROUP BY p.id
ORDER BY p.order ASC

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);