I'm having troubles in making a rather difficult MySQL query work. I've been trying, but creating complex queries has never been my strong side.
This query includes 4 tables, which I'll describe of course.
First, we have song table, which I need to select the needed info from.
+--------------+-----------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+-----------+------+-----+---------+----------------+
| ID | int(6) | NO | PRI | - | auto_increment |
| Anime | char(100) | NO | | - | |
| Title | char(100) | NO | | - | |
| Type | char(20) | NO | | - | |
| Singer | char(50) | NO | | - | |
| Youtube | char(30) | NO | | - | |
| Score | double | NO | | 0 | |
| Ratings | int(8) | NO | | 0 | |
| Favourites | int(7) | NO | | 0 | |
| comments | int(11) | NO | | 0 | |
| release_year | int(4) | NO | | 2019 | |
| season | char(10) | NO | | Spring | |
+--------------+-----------+------+-----+---------+----------------+
Then we have song_ratings, which basically represents the lists of each user, since once you rate a song, it appears on your list.
+------------+----------+------+-----+-------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+----------+------+-----+-------------------+----------------+
| ID | int(11) | NO | PRI | 0 | auto_increment |
| UserID | int(11) | NO | MUL | 0 | |
| SongID | int(11) | NO | MUL | 0 | |
| Rating | double | NO | | 0 | |
| RatedAt | datetime | NO | | CURRENT_TIMESTAMP | |
| Favourited | int(1) | NO | | 0 | |
+------------+----------+------+-----+-------------------+----------------+
Users have the option to create custom lists(playlists), and this is the table which they are stored in. This is table lists.
+------------+-----------+------+-----+-------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+-----------+------+-----+-------------------+----------------+
| ID | int(11) | NO | PRI | 0 | auto_increment |
| userID | int(11) | NO | MUL | 0 | |
| name | char(50) | NO | | - | |
| likes | int(11) | NO | | 0 | |
| favourites | int(11) | NO | | 0 | |
| created_at | datetime | NO | | CURRENT_TIMESTAMP | |
| cover | char(100) | NO | | - | |
| locked | int(1) | NO | | 0 | |
| private | int(1) | NO | | 0 | |
+------------+-----------+------+-----+-------------------+----------------+
And finally, the table which contains all the songs that have been added to any playlists, called list_elements.
+--------+---------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------+---------+------+-----+---------+----------------+
| ID | int(11) | NO | PRI | 0 | auto_increment |
| listID | int(11) | NO | MUL | 0 | |
| songID | int(11) | NO | MUL | 0 | |
+--------+---------+------+-----+---------+----------------+
What my query needs to do is list all the songs that are on the list of a user, basically these are the record in song_ratings where the userID = ?(obviously the ID of the user), but are not on a specific playlist(has no record in list_elements) where the ID/listID = ?(the ID of that playlist).
This is the query I've been using so far, but after a while I had realized this doesn't actually work the way I wanted to.
SELECT DISTINCT
COUNT(*)
FROM
song
INNER JOIN song_ratings ON song_ratings.songID = song.ID
LEFT JOIN list_elements ON song_ratings.songID = list_elements.songID
WHERE
song_ratings.userID = 34 AND list_elements.songID IS NULL
I have also tried something like this, and several variants of it
SELECT DISTINCT
COUNT(*)
FROM
song
INNER JOIN song_ratings ON song_ratings.songID = song.ID
INNER JOIN lists ON lists.userID = song_ratings.userID
LEFT JOIN list_elements ON song_ratings.songID = list_elements.songID
WHERE
song_ratings.userID = 34 AND lists.ID = 1
To make it easier, here's a SQL Fiddle, with all the necessary tables and records in them.
What you need to know. When you check for the playlist with the ID of 1, the query needs to return 23(basically all matches).
When you do the same with the ID 4, it need to return 21, if the query works correctly, because the playlist 1 is empty, thus all of the songs in the table song_ratings can be added to it(at least the ones that exist in song table, which is only half of the overall records now).
But playlist 4 already has 2 songs added to it, so only 21 are left available for adding.
Or in case the number are wrong, playlist 1 needs to return all matches. playlist 4 need to return all matches-2(because 2 songs are already added).
The userID needs to remain the same(34), and there are no records with different ID, so don't change it.
You could try subquery with NOT IN clause
SELECT DISTINCT
COUNT(*)
FROM
song
INNER JOIN song_ratings ON song_ratings.songID = song.ID
WHERE
song_ratings.userID = 34 AND song.ID not in (select songID from list_elements group by songID)
Your original query was almost correct. When you use a column from a joined table with a LEFT JOIN in the WHERE-clause, it causes the LEFT JOIN to turn into an INNER JOIN.
You can put the condition into the ON-clause:
SELECT COUNT(*)
FROM song
INNER JOIN song_ratings ON song_ratings.songID = song.ID
LEFT JOIN list_elements ON song_ratings.songID = list_elements.songID
AND list_elements.songID IS NULL
WHERE song_ratings.userID = 34
Using JOINs in MySQL is faster than using subqueries, this would probably be faster as well.
Btw, you do not need DISTINCT when you only have COUNT(*). The COUNT(*) returns only one row so there is no need to take distinct values from one value.
Related
I need to select and display information from a pair of MySQL tables but the syntax eludes me. Specifically, I need to JOIN the data from the cwd_user table with the data from the cwd_user_attribute table on the field cwd_user.id == cwd_user_attribute.user_id, but I also need to display values from several entries in the cwd_user_attribute table in a single line. It's the latter that eludes me. Here are the gory details:
Given two tables:
mysql (crowd#prod:crowddb)> desc cwd_user;
+---------------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------------+--------------+------+-----+---------+-------+
| id | bigint(20) | NO | PRI | NULL | |
| user_name | varchar(255) | NO | | NULL | |
| active | char(1) | NO | MUL | NULL | |
| created_date | datetime | NO | | NULL | |
| updated_date | datetime | NO | | NULL | |
| display_name | varchar(255) | YES | | NULL | |
| directory_id | bigint(20) | NO | MUL | NULL | |
+---------------------+--------------+------+-----+---------+-------+
mysql (crowd#prod:crowddb)> desc cwd_user_attribute;
+-----------------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------------------+--------------+------+-----+---------+-------+
| id | bigint(20) | NO | PRI | NULL | |
| user_id | bigint(20) | NO | MUL | NULL | |
| directory_id | bigint(20) | NO | MUL | NULL | |
| attribute_name | varchar(255) | NO | | NULL | |
| attribute_value | varchar(255) | YES | | NULL | |
+-----------------------+--------------+------+-----+---------+-------+
Assume that there are up to seven possible values for cwd_user_attribute.attribute_name and I'm interested in four of them: lastAuthenticated, Team, Manager Notes. Example:
mysql (crowd#prod:crowddb)> select * from cwd_user_attribute where user_id = (select id from cwd_user where user_name = 'gspinrad');
+---------+---------+--------------+-------------------------+----------------------------------+
| id | user_id | directory_id | attribute_name | attribute_value |
+---------+---------+--------------+-------------------------+----------------------------------+
| 65788 | 32844 | 1 | invalidPasswordAttempts | 0 |
| 65787 | 32844 | 1 | lastAuthenticated | 1473360428804 |
| 65790 | 32844 | 1 | passwordLastChanged | 1374005378040 |
| 65789 | 32844 | 1 | requiresPasswordChange | false |
| 4292909 | 32844 | 1 | Team | Engineering - DevOps |
| 4292910 | 32844 | 1 | Manager | Matt Karaffa |
| 4292911 | 32844 | 1 | Notes | Desk 32:2:11 |
+---------+---------+--------------+-------------------------+----------------------------------+
5 rows in set (0.00 sec)
I can get a list of the users sorted by lastAuthenticated with this query:
SELECT cwd_user.user_name, cwd_user.id, cwd_user.display_name, from_unixtime(cwd_user_attribute.attribute_value/1000) as last_login FROM cwd_user JOIN cwd_directory ON cwd_user.directory_id = cwd_directory.id JOIN cwd_user_attribute ON cwd_user.id = cwd_user_attribute.user_id AND cwd_user_attribute.attribute_name='lastAuthenticated' WHERE DATEDIFF((NOW()), (from_unixtime(cwd_user_attribute.attribute_value/1000))) > 90 and cwd_user.active='T' order by last_login limit 4;
Result:
+-----------------------+---------+-----------------------+---------------------+
| user_name | id | display_name | last_login |
+-----------------------+---------+-----------------------+---------------------+
| jenkins-administrator | 1605636 | Jenkins Administrator | 2011-10-27 17:28:05 |
| sonar-administrator | 1605635 | Sonar Administrator | 2012-02-06 15:59:59 |
| jfelix | 1605690 | Joey Felix | 2012-02-06 19:15:15 |
| kbitters | 3178497 | Kitty Bitters | 2013-09-03 10:09:59 |
What I need to add to the output is the value of cwd_user_attribute.attribute_value where cwd_user_attribute.attribute_name is Team, Manager, and/or Notes. The output would look something like this:
+-----------------------+---------+-----------------------+-------------------------------------------------------------------+
| user_name | id | display_name | last_login | Team | Manager | Notes |
+-----------------------+---------+-----------------------+-------------------------------------------------------------------+
| jenkins-administrator | 1605636 | Jenkins Administrator | 2011-10-27 17:28:05 | Internal | Internal | |
| sonar-administrator | 1605635 | Sonar Administrator | 2012-02-06 15:59:59 | Internal | Internal | |
| jfelix | 1605690 | Joey Felix | 2012-02-06 19:15:15 | Hardware Eng. | Gary Spinrad | Desk 32:1:51 |
| kbitters | 3178497 | Kitty Bitters | 2013-09-03 10:09:59 | Software QA | Matt Karaffa | Desk 32:2:01 |
+-----------------------+---------+-----------------------+-------------------------------------------------------------------+
You can achieve that result with an additional LEFT JOIN with the attribute table. Then use GROUP BY and aggregated CASE statements to pivot the result (rows to columns).
SELECT
cwd_user.user_name,
cwd_user.id,
cwd_user.display_name,
from_unixtime(cwd_user_attribute.attribute_value/1000) as last_login,
MIN(CASE WHEN attr2.attribute_name = 'TEAM' THEN attr2.attribute_value END) as Team,
MIN(CASE WHEN attr2.attribute_name = 'Manager' THEN attr2.attribute_value END) as Manager,
MIN(CASE WHEN attr2.attribute_name = 'Notes' THEN attr2.attribute_value END) as Notes
FROM
cwd_user
JOIN
cwd_user_attribute ON cwd_user.id = cwd_user_attribute.user_id
AND cwd_user_attribute.attribute_name='lastAuthenticated'
LEFT JOIN
cwd_user_attribute attr2 ON cwd_user.id = attr2.user_id
AND attr2.attribute_name IN ('Team', 'Manager', 'Notes')
WHERE
DATEDIFF((NOW()), (from_unixtime(cwd_user_attribute.attribute_value/1000))) > 90
AND cwd_user.active = 'T'
GROUP BY
cwd_user.id
ORDER BY
last_login
LIMIT 4
With strict mode you would need to list all not aggregated columns in the GROUP BY clause
GROUP BY
cwd_user.user_name,
cwd_user.id,
cwd_user.display_name,
cwd_user_attribute.attribute_value
Another way is just to use three LEFT JOINs (one join per attribute name):
SELECT
cwd_user.user_name,
cwd_user.id,
cwd_user.display_name,
from_unixtime(cwd_user_attribute.attribute_value/1000) as last_login,
attr_team.attribute_value as Team,
attr_manager.attribute_value as Manager,
attr_notes.attribute_value as Notes
FROM cwd_user
JOIN cwd_user_attribute
ON cwd_user.id = cwd_user_attribute.user_id
AND cwd_user_attribute.attribute_name='lastAuthenticated'
LEFT JOIN cwd_user_attribute attr_team
ON cwd_user.id = attr2.user_id
AND attr2.attribute_name = 'Team'
LEFT JOIN cwd_user_attribute attr_manager
ON cwd_user.id = attr2.user_id
AND attr2.attribute_name = 'Manager'
LEFT JOIN cwd_user_attribute attr_notes
ON cwd_user.id = attr2.user_id
AND attr2.attribute_name = 'Notes'
WHERE DATEDIFF((NOW()), (from_unixtime(cwd_user_attribute.attribute_value/1000))) > 90
and cwd_user.active='T'
order by last_login limit 4
Note: I have removed the join with directory table because you seem not to use it. Add it again, if you need it for filtering.
Note 2: Some attributes that you often use for a search (like lastAuthenticated) should be converted to indexed columns in the users table to improve the search performance.
I'm having an issue with a query using INNER JOIN.
I have two tables. I need the department name and all three approvers. If any of the approvers are NULL, I need that displayed also.
mysql> desc department;
+-------------------+----------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------------+----------+------+-----+---------+----------------+
| id | int(8) | NO | PRI | NULL | auto_increment |
| departmentName | tinytext | YES | | NULL | |
| primaryApprover | int(8) | YES | | NULL | |
| secondaryApprover | int(8) | YES | | NULL | |
| tertiaryApprover | int(8) | YES | | NULL | |
+-------------------+----------+------+-----+---------+----------------+
mysql> desc approver;
+------------------+------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------------+------------+------+-----+---------+----------------+
| id | int(8) | NO | PRI | NULL | auto_increment |
| approverName | tinytext | YES | | NULL | |
| approverPosition | tinytext | YES | | NULL | |
| approverLogonId | tinytext | YES | | NULL | |
| approverEmail | tinytext | YES | | NULL | |
| isActive | tinyint(1) | YES | | NULL | |
+------------------+------------+------+-----+---------+----------------+
The following query works, but it does not give me data where the primary or secondary approver are NULL:
SELECT
a.departmentName as DEPARTMENT,
pa.approvername as PRIMARY,
sa.approvername as SECONDARY,
ta.approvername as TERTIARY
FROM
department as a
INNER JOIN
approver pa on a.primaryapprover=pa.id
INNER JOIN
approver sa on a.secondaryapprover = sa.id
INNER JOIN
approver ta on a.tertiaryapprover = ta.id
ORDER BY
a.departmentname;
Using this query, I get this result:
+--------------------------------+---------------------------+---------------------------+------------------------+
| DEPARTMENT | PRIMARY_APPROVER | SECONDARY_APPROVER | TERTIARY_APPROVER |
+--------------------------------+---------------------------+---------------------------+------------------------+
| Facilities | Washburn, Hoban | Cobb, Jayne | Reynolds, Malcomn |
| Personnel / HR | Frye, Kaylee | Serra, Inara | Book, Dariel |
+--------------------------------+---------------------------+---------------------------+------------------------+
2 rows in set (0.00 sec)
but should get this result:
+--------------------------------+---------------------------+---------------------------+------------------------+
| DEPARTMENT | PRIMARY_APPROVER | SECONDARY_APPROVER | TERTIARY_APPROVER |
+--------------------------------+---------------------------+---------------------------+------------------------+
| Business Office | NULL | Rample, Fanty | Niska, Adelei |
| Facilities | Washburn, Hoban | Cobb, Jayne | Reynolds, Malcomn |
| Personnel / HR | Frye, Kaylee | Serra, Inara | Book, Dariel |
| Technical Services | Tam, River | NULL | Tam, Simon |
+--------------------------------+---------------------------+---------------------------+------------------------+
4 rows in set (0.00 sec)
I'm not good at joins to begin with....what am I missing here?
Just use LEFT JOINS
SELECT
a.departmentName as DEPARTMENT,
pa.approvername as PRIMARY,
sa.approvername as SECONDARY,
ta.approvername as TERTIARY
FROM
department as a
LEFT JOIN
approver pa on a.primaryapprover=pa.id
LEFT JOIN
approver sa on a.secondaryapprover = sa.id
LEFT JOIN
approver ta on a.tertiaryapprover = ta.id
ORDER BY
a.departmentname;
INNER JOIN - keeps only records that match from both sides .
LEFT JOIN - keeps all the records from the left table, and only the record matching from the right table.
You can also use COALESCE to replace null values with a default value like '-1' or something.
I'm trying to get a SQL query to give me the results of a count but I need the result to include rows where the count is 0. What I found for solutions to this was to use IFNULL(COUNT(*), 0) in place of COUNT(*) however that had no effect on the result. I also tried using a LEFT JOIN but SQL gave me a syntax error if I tried to put in those. Here's my table setup
User
+-------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+----------------+
| UserID | mediumint(9) | NO | PRI | NULL | auto_increment |
| firstName | varchar(15) | NO | | NULL | |
| lastName | varchar(15) | NO | | NULL | |
| Protocol | varchar(10) | NO | | NULL | |
| Endpoint | varchar(50) | NO | | NULL | |
| UsergroupID | mediumint(9) | NO | MUL | NULL | |
+-------------+--------------+------+-----+---------+----------------+
Subscription
+----------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------------+--------------+------+-----+---------+----------------+
| SubscriptionID | mediumint(9) | NO | PRI | NULL | auto_increment |
| TopicID | mediumint(9) | NO | MUL | NULL | |
| UserID | mediumint(9) | NO | MUL | NULL | |
+----------------+--------------+------+-----+---------+----------------+
Topic
+----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+----------------+
| TopicID | mediumint(9) | NO | PRI | NULL | auto_increment |
| Name | varchar(50) | NO | | NULL | |
| FBName | varchar(30) | YES | | NULL | |
| FBToken | varchar(255) | YES | | NULL | |
| TWName | varchar(10) | YES | | NULL | |
| TWToken | varchar(50) | YES | | NULL | |
| TWSecret | varchar(50) | YES | | NULL | |
+----------+--------------+------+-----+---------+----------------+
My SQL query to try and get the COUNT is :
SELECT Topic.TopicID as ID, Topic.Name AS TopicName, COUNT(*) AS numSubscriptions
FROM User, Topic, Subscription
WHERE Subscription.UserID = User.UserID
AND Subscription.TopicID = Topic.TopicID
GROUP BY Topic.TopicID;
I've tried replacing COUNT(*) with IFNULL(COUNT(*), 0) and I've tried to replace User,Topic,Subscription with User JOIN Subscription JOIN Topic and I also tried User LEFT JOIN Subscription LEFT JOIN Topic but that got a SQL error.
The output I'm getting is:
+----+-----------+------------------+
| ID | TopicName | numSubscriptions |
+----+-----------+------------------+
| 2 | test | 2 |
| 3 | test2 | 1 |
+----+-----------+------------------+
I need to be getting
+----+-----------+------------------+
| ID | TopicName | numSubscriptions |
+----+-----------+------------------+
| 2 | test | 2 |
| 3 | test2 | 1 |
| 4 | test3 | 0 |
+----+-----------+------------------+
By default, outer joins are left to right. So, the trick is to start with Topic:
SELECT Topic.TopicID as ID, Topic.Name AS TopicName,
COUNT(User.UserID) AS numSubscriptions
FROM Topic
LEFT JOIN Subscription
ON Subscription.TopicID = Topic.TopicID
JOIN User
ON User.UserID = Subscription.UserID
GROUP BY Topic.TopicID
This allows for multiple subscriptions per user and requires that the user record exists to be considered in the count.
COUNT(NULL) evaluates to 0, so any topic records without a corresponding subscription and user record will show as 0.
If you're not concerned whether the user record exists, you could simplify it to the following:
SELECT Topic.TopicID as ID, Topic.Name AS TopicName,
COUNT(Subscription.TopicID) AS numSubscriptions
FROM Topic
LEFT JOIN Subscription
ON Subscription.TopicID = Topic.TopicID
GROUP BY Topic.TopicID
The example below should do what you're after. The column in the COUNT() can be any column of the subscription table, but using its ID is a good practice.
Using the left join ensures that all entries of the user table will show up in the results, even if there are no matching subscriptions.
SELECT User.firstName,
User.lastName,
Topic.Name AS TopicName,
COUNT(Subscription.SubscriptionId) AS numSubscriptions
FROM USER
LEFT OUTER JOIN Subscription ON Subscription.UserID=USER.UserID
LEFT OUTER JOIN Topic ON Subscription.TopicID=Topic.TopicID
GROUP BY User.firstName, User.lastName, Topic.Name;
+-----------------------------+
| tables |
+-----------------------------+
| Date |
| IP |
| Location |
| UserAgent |
+-----------------------------+
For simplicty assume that these four tables have only two columns: ID(int), name(VARCHAR)
Then I have this table called access_log, where I store only IDs of items i have in other four tables
+---------+---------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------+---------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| ip_id | int(11) | NO | | NULL | |
| ua_id | int(11) | NO | | NULL | |
| path_id | int(11) | NO | | NULL | |
| date_id | int(11) | NO | | NULL | |
+---------+---------+------+-----+---------+----------------+
Let's say I want to select everything from this access_log table and replace ids with column NAME I have in four other tables.
How can I achive that?
You need to join on all four tables:
SELECT access_log.id,
`date`.name AS date_name,
ip.name AS ip_name,
location.name AS location_name,
useragent.name AS useragent_name
FROM access_log
OUTER JOIN `date` ON `date`.id = access_log.date_id
OUTER JOIN ip ON ip.id = access_log.ip_id
OUTER JOIN location ON location.id = access_log.path_id
OUTER JOIN useragent ON useragent.id = access_log.ua_id
How do i write this query, with left join. since the framework i use doesn't support right join i need to rewrite the query. Can any one suggest me a possible solution.
select Audit.history_id,Audit.field,modifiedtime,operation from Audit right join (select History.history_id from History where refid=2000000020088 order by
modifiedtime limit 5) as Hist on Audit.history_id=Hist.history_id;
desc Audit
+------------------+------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------------+------------+------+-----+---------+-------+
| AUDIT_ID | bigint(19) | | PRI | 0 | |
| HISTORY_ID | bigint(19) | | MUL | 0 | |
| FIELD | varchar(50) | | | |
| OLD_VALUE | varchar(50)| YES | | NULL | |
| NEW_VALUE | varchar(50)| YES | | NULL | |
+------------------+------------+------+-----+---------+-------+
desc History
+---------------+-------------+------+-----+---------------------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+-------------+------+-----+---------------------+-------+
| HISTORY_ID | bigint(19) | | PRI | 0 | |
| REFID | bigint(19) | | MUL | 0 | |
| OPERATION | varchar(50) | YES | | NULL | |
| MODIFIED_TIME | datetime | | | 0000-00-00 00:00:00 | |
+---------------+-------------+------+-----+---------------------+-------+
Simply switch the relations for a left join:
In practice, explicit right outer joins are rarely used, since they can always be replaced with left outer joins (with the table order switched) and provide no additional functionality.
Source:Wikipedia.org
select
Audit.history_id,Audit.field,modifiedtime,operation
from
(
select History.history_id
from History where refid=2000000020088
order by modifiedtime limit 5
) as Hist
left join Audit on (Audit.history_id = Hist.history_id);
Not sure if this will produce exactly same output as the one you have now, but it might give you the right idea:
select Audit.history_id, Audit.field, History.modifiedtime, History.operation
from History
left join Audit on Audit.history_id=History.history_id
where History.refid=2000000020088
order by History.modifiedtime