Joining different tables based on column value - mysql

I have a table called notifications:
CREATE TABLE `notifications` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL,
`type` varchar(20) NOT NULL DEFAULT '',
`parent_id` int(11) DEFAULT NULL,
`parent_type` varchar(15) DEFAULT NULL,
`type_id` int(11) DEFAULT NULL,
`etc` NULL
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=utf8;
Each notification is related to a different table, the value of parent_type field specifies the name of the table that I want to * join the table with. All target tables have several similar columns:
CREATE TABLE `tablename` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`is_visible` tinyint(1) NOT NULL,
`etc` NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
Currently I'm using this query for selecting notifcations that their related row in the target table exists and their is_visible field is 1:
SELECT n.id,
FROM notifications n
LEFT JOIN books b ON n.parent_id = b.id AND n.parent_type = 'book' AND b.is_visible = 1
LEFT JOIN interviews i ON n.parent_id = i.id AND n.parent_type = 'interview' AND i.is_visible = 1
LEFT JOIN other tables...
WHERE n.user_id = 1
GROUP BY n.id
But since it is a LEFT JOIN it returns the notification if it matches any table or not, how can I rewrite it so it doesn't return notifications that don't match with any row in the target table? I have also tried the CASE statement unsuccessfully.

I'm not 100% sure the syntax is right and I have no chance to test it right now, but the idea should be clear.
SELECT DISTINCT n.id
FROM notifications n
JOIN (
(SELECT b.id, 'book' AS type FROM books b WHERE b.is_visible = 1)
UNION
(SELECT i.id, 'interview' AS type FROM interviews i WHERE i.is_visible = 1)
) ids ON n.parent_id = ids.id AND n.parent_type = ids.type
WHERE n.user_id = 1

Related

mySQL Left Join 5 Tables?

Thanks in advance for any help. I am working with 5 tables in a mySQL database. The system is such that I have a top level table called "owners" (clients) that have local business (shops). These owners go out and create accounts at websites like yelp (citation_sources) and as such have login credential (citation_login). Once they have an account at a citation source, they add shops to the directory.
I am hoping to create one query that would select ALL of the citation sources, regardless of if an owner has an account or not, and loop through the recordset, showing login for each citation source they have an account with, as well as any shop listings.
My question pertains to doing a left join on 5 tables. I left out most fo the fields but have set up primary and foreign keys Is the sequence of the join important, ie. start with one particular table, ending with another?
I tried this command but it only brings back 33 rows when in fact there are 96 citation_sources.
I think I figured it out. I created a new table called "citation_shop" with a composite primary key - citation - shop. I then ran a query and it got me the results I was after. I ended up putting a condition in the first left join.
SELECT citation_sources.name, citation_shop.shop from citation_sources left join citation_shop on citation_sources.id = citation_shop.citation and citation_shop.shop in (6,7) left join shops on citation_shop.shop = shops.id group by citation_sources.name, citation_shop.shop limit 100
CREATE TABLE `citation_shop` (
`shop` smallint(5) UNSIGNED NOT NULL,
`citation` smallint(6) UNSIGNED NOT NULL,
`url` text NOT NULL,
`count` smallint(3) UNSIGNED NOT NULL,
`status` tinyint(1) UNSIGNED NOT NULL,
`sort` tinyint(3) UNSIGNED NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
--
-- Indexes for dumped tables
--
--
-- Indexes for table `citation_shop`
--
ALTER TABLE `citation_shop`
ADD PRIMARY KEY (`citation`,`shop`);
select owners.id as owner_id, shops.id as shop_id, citation_sources.name, citation_shop_urls.url, citation_logins.password
from owners
inner join shops on owners.id = shops.owner_id
left join citation_logins on owners.id = citation_logins.owner
left join citation_sources on citation_logins.c_source = citation_sources.id
left join citation_shop_urls on citation_sources.id = citation_shop_urls.citation_id
where owners.id = 3
group by citation_sources.name
Here are my tables in order of what I think is relevlance:
CREATE TABLE `owners` (
`id` smallint(6) UNSIGNED NOT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;
CREATE TABLE `shops` (
`id` smallint(5) UNSIGNED NOT NULL,
`title` varchar(50) DEFAULT '',
`owner_id` smallint(5) UNSIGNED NOT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `citation_sources` (
`id` smallint(6) UNSIGNED NOT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `citation_shop_urls` (
`shop` smallint(5) UNSIGNED NOT NULL DEFAULT '0',
`citation_id` tinyint(5) UNSIGNED NOT NULL DEFAULT '0',
`owner` smallint(6) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `citation_logins` (
`c_source` smallint(5) UNSIGNED NOT NULL DEFAULT '0',
`owner` smallint(6) NOT NULL,
`user_name` text NOT NULL,
`password` text NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
In a LEFT JOIN, the first table is the one where you get all the rows, even if they don't have a match in other tables. So if you want all citation_sources, even those not associated with any owner, then citation_sources should be the table on the left of the LEFT JOIN.
To filter the owner information only to id = 3, put o.id = 3 in the ON clause that joins with owners. Then use a WHERE clause to remove all the other rows.
SELECT o.id as owner_id, s.id as shop_id, cs.name, u.url, cl.password
FROM citation_sources AS cs
LEFT JOIN citation_shop_urls AS u ON u.citation_id = cs.id
LEFT JOIN citation_logins AS cl ON cs.id = cl.c_source
LEFT JOIN owners AS o ON o.id = cl.owner AND o.id = 3
LEFT JOIN shops AS s ON s.owner_id = o.id
WHERE o.id IS NULL OR o.id = 3

MySQL returning all matches from a table and indicating if an id is on another table

How can I return, on a select, a field that indicates that an id was found?
My goal is to return all songs(song) from a specific source(source) checking if an user(user) has it or not (user_song).
The query I made almost works. If I remove 'hasSong' (which Im trying to indicate that an user has a song or not), I can see all songs.
If I keep 'hasSong', I see all songs repeating the song for each user.
QUERY:
SELECT DISTINCT(song.id) AS id_song, CONCAT(song.article, ' ', song.name) AS name
FROM `song`
LEFT JOIN `user_song` ON `song`.`id` = `user_song`.`id_song`
LEFT JOIN `user` ON `user`.`id` = `user_song`.`id_user`
JOIN `song_source` ON `song`.`id` = `song_source`.`id_song`
WHERE `song_source`.`id_source` = '1'
AND ( `user_song`.`id_user` = '3' OR song.id = song_source.id_song )
ORDER BY `song`.`name` ASC
DB:
CREATE TABLE `song` (
`id` int(11) NOT NULL,
`article` varchar(10) NOT NULL,
`name` varchar(150) NOT NULL,
`shortname` varchar(150) NOT NULL,
`year` int(11) NOT NULL,
`artist` int(11) NOT NULL,
`duration` int(11) NOT NULL,
`genre` int(11) NOT NULL,
`updated` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `song_source` (
`id_song` int(11) NOT NULL,
`id_source` int(11) NOT NULL
)
CREATE TABLE `source` (
`id` int(11) NOT NULL,
`article` varchar(10) NOT NULL,
`name` varchar(150) NOT NULL,
`updated` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `user` (
`id` int(11) NOT NULL,
`email` varchar(100) NOT NULL,
`password` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `user_song` (
`id_user` int(11) NOT NULL,
`id_song` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
The specification isn't entirely clear, ...
To return all songs (with no repeated values of song.id) that are from a particular source (id_source='1'),
along with an indicator, a value of 0 or 1, that tells us if there's a row in user_song that matches on id_song and is related to a particular user,(id_user = '3')
something like this:
SELECT s.id AS id_song
, MAX( CONCAT(s.article,' ',s.name) ) AS name
, MAX( IF(us.id_user = '3' ,1,0) ) AS has_song
FROM `song` s
JOIN `song_source` ss
ON ss.id_song = s.id
AND ss.id_source = '1'
LEFT
JOIN `user_song` us
ON us.id_song = s.id
AND us.id_user = '3'
GROUP BY s.id
ORDER BY MAX(s.name)
There are a couple of other query patterns that will return an equivalent result. For example, we could use a correlated subquery in the SELECT list.
SELECT s.id AS id_song
, MAX( CONCAT(s.article,' ',s.name) ) AS name
, ( SELECT IF( COUNT(us.id_user) >0,1,0)
FROM `user_song` us
WHERE us.id_song = s.id
AND us.id_user = '3'
) AS has_song
FROM `song` s
JOIN `song_source` ss
ON ss.id_song = s.id
AND ss.id_source = '1'
GROUP BY s.id
ORDER BY MAX(s.name)
These queries are complicated by the fact that there are no guarantees of uniqueness in any of the tables. If we had guarantees, we could eliminate the need for a GROUP BY and aggregate functions.
Please consider adding PRIMARY and/or UNIQUE KEY constraints on the tables, to prevent duplication. The way the tables are defined, we could add multiple rows to song with the same id value. (And those could have different name values.)
(And the queries would be much simpler if we had some guarantees of uniqueness.)

Mysql query with inner subquery and where,order,group not use indexes

all.
I have 2 mysql tables type myisam
CREATE TABLE `users` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
CREATE TABLE `users_ratings` (
`id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT,
`created_date` varchar(255) DEFAULT NULL,
`user_id` mediumint(9) DEFAULT NULL,
`rating1` mediumint(9) DEFAULT NULL,
`rating2` mediumint(9) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Dump can be downloaded here
I want all users to select and sort the list by rating2 and rating2 must be > 1000
SELECT * FROM users as u
INNER JOIN (SELECT rating2, user_id,MAX(created_date) as maxdate FROM `users_ratings` GROUP BY user_id) as x ON x.user_id = u.id
INNER JOIN users_ratings as ur ON(ur.created_date = x.maxdate and ur.user_id = u.id)
WHERE x.rating2 > 1000
ORDER by ur.rating2
Without such an index the query is executed for 9 seconds.
All possible codes, which I thought was allowed to reduce the time of the request to 1-2 seconds.
Help the right to place indexes
I suspect you want the following query. Assuming you do, and performance is still an issue, provide the EXPLAIN for same...
(this assumes a Unique Key on user_id,[rating2,]created_date, which hasn't actually been specified)
SELECT u.*
, x.*
FROM users u
JOIN users_ratings x
ON x.user_id = u.id
JOIN
( SELECT user_id
, MAX(created_date) maxdate
FROM users_ratings
WHERE rating2 > 1000
GROUP
BY user_id
) y
ON y.user_id = x.user_id
AND y.maxdate = x.created_date
ORDER
BY x.rating2;

Get information about all rows from one table, when in join table that row is not

How obtain in case that in table userItems is not record joined with moduleItems just null?
SELECT `users`.*, `useritems`.*, `moduleitems`.*, `modulesubitems`.* FROM `users`
LEFT JOIN `useritems` ON useritems.f_user_id = users.user_id
LEFT JOIN `moduleitems` ON moduleitems.moduleItem_id = useritems.f_moduleItem_id
LEFT JOIN `modulesubitems` ON modulesubitems.modulesubitem_id = useritems.userItem_value
Edit: My DB structure
CREATE TABLE IF NOT EXISTS `moduleitems` (
`moduleItem_id` int(11) NOT NULL AUTO_INCREMENT,
`f_module_id` int(11) NOT NULL,
PRIMARY KEY (`moduleItem_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=63 ;
CREATE TABLE IF NOT EXISTS `modules` (
`module_id` int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`module_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=2 ;
CREATE TABLE IF NOT EXISTS `modulesubitems` (
`moduleSubitem_id` int(11) NOT NULL AUTO_INCREMENT,
`f_moduleItem_id` int(11) NOT NULL,
PRIMARY KEY (`moduleSubitem_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=19 ;
CREATE TABLE IF NOT EXISTS `useritems` (
`f_user_id` int(11) NOT NULL,
`f_moduleItem_id` int(11) NOT NULL,
`userItem_value` varchar(255) DEFAULT NULL,
PRIMARY KEY (`f_user_id`,`f_moduleItem_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `users` (
`user_id` int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=35 ;
If you are would like to see every moduleitem for each user, regardless if they have a matching record in the useritems table, this should work:
SELECT `users`.*, `useritems`.*, `moduleitems`.*, `modulesubitems`.*
FROM `users`
CROSS JOIN `moduleitems`
LEFT JOIN `useritems`
ON useritems.f_user_id = users.user_id
AND moduleitems.moduleItem_id = useritems.f_moduleItem_id
LEFT JOIN `modulesubitems`
ON modulesubitems.modulesubitem_id = useritems.userItem_value
If you are trying to find all records that are not in a joined table (when using a LEFT JOIN), add to your WHERE clause where the field you are joining in that table is NULL. All the fields in the joining table will be NULL if there is no match.
For example this would work if you are trying to find cases where there is no match in the moduleitems table.:
SELECT `users`.*, `useritems`.*, `moduleitems`.*, `modulesubitems`.*
FROM `users`
LEFT JOIN `useritems` ON useritems.f_user_id = users.user_id
LEFT JOIN `moduleitems` ON moduleitems.moduleItem_id = useritems.f_moduleItem_id
LEFT JOIN `modulesubitems` ON modulesubitems.modulesubitem_id = useritems.userItem_value
WHERE moduleitems.moduleItem_id IS NULL

Return records after group by with a specific MAX(field)

I search on SO many topics like this, but I can't apply to my query.
This is :
SELECT forum_categories.title, COUNT(DISTINCT forum_topics.id) AS total_topics,
SUM(CASE WHEN forum_messages.original=0 THEN 1 ELSE 0 END) AS total_replies, forum_messages.author,
MAX(forum_messages.date) AS last_message, SUM(CASE WHEN r.user IS NULL THEN 1 ELSE 0 END) to_view
FROM forum_categories
JOIN forum_topics ON forum_topics.category_id=forum_categories.id
LEFT OUTER JOIN (SELECT topic, user FROM forum_visits WHERE user='userA') r ON forum_topics.id=r.topic
JOIN forum_messages ON forum_messages.topic_id=forum_topics.id
GROUP BY forum_categories.id
ORDER BY forum_categories.date
It works: the only problem is that return only the field forum_messages.date with MAX data; intead, I'd like to return the whole row with that MAX field (so the corrispondent author, for example).
So what I should return, in less words, is :
the title for each category; *(at the moment this works)
the number of the topics for that category; (at the moment this works)
the number of replies for all topics for that category; (here there is another condition as you can see, the counter of that replies is get by the message with the filed original=0) (at the moment this works)
the author/data for the last message for that category (HERE there is the problem : it return correctly only the date, not the author);
a flag that indicate if there is any topic that userA haven't been checked yet; (also at the moment works: if SUM return somethings more high than 0, there is a topic not viewed)
this query is supposted to be as faster as possible, because the tables could be very big;
For details, these are my actual tables :
CREATE TABLE IF NOT EXISTS `forum_categories` (
`id` int(11) unsigned NOT NULL auto_increment,
`title` varchar(255) NOT NULL,
`description` varchar(255) NOT NULL,
`date` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=12 DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `forum_topics` (
`id` int(11) unsigned NOT NULL auto_increment,
`category_id` int(11) unsigned NOT NULL,
`title` varchar(255) NOT NULL,
`author` varchar(255) NOT NULL,
`date` datetime NOT NULL,
`view` int(11) unsigned NOT NULL default '0',
`sticky` tinyint(11) unsigned NOT NULL default '0',
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=25 DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `forum_messages` (
`id` int(11) unsigned NOT NULL auto_increment,
`topic_id` int(11) unsigned NOT NULL,
`author` varchar(255) NOT NULL,
`message` mediumtext NOT NULL,
`date` datetime NOT NULL,
`original` tinyint(11) unsigned NOT NULL default '0',
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=29 DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `forum_visits` (
`id` int(11) unsigned NOT NULL auto_increment,
`topic` int(11) unsigned NOT NULL,
`user` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `forum_visits_unique_idx` (`topic`,`user`)
) ENGINE=MyISAM AUTO_INCREMENT=131 DEFAULT CHARSET=utf8;
Hope that someone can help me!
We can use the query from your previous question to get the message author and the message date, and the query above to get the counters (topics and replies), and join them together:
EDIT: This query works (tested). BUT it got a little complex and has 2 subqueries in it, so I hope someone else posts a better simpler one. If the DB gets very big, this one may get sluggish.
SELECT forum_categories.title,
COUNT(DISTINCT forum_topics.id) AS total_topics,
SUM(CASE WHEN forum_messages.original=0 THEN 1 ELSE 0 END) AS total_replies,
t2.author, t2.last_message
-- first get the counters per category
FROM forum_categories
JOIN forum_topics ON forum_topics.category_id=forum_categories.id
JOIN forum_messages ON forum_messages.topic_id=forum_topics.id
-- Then join a query to get last message per category
JOIN (SELECT forum_categories.id, forum_messages.author,
forum_messages.date AS last_message
FROM forum_categories
JOIN forum_topics ON forum_topics.category_id=forum_categories.id
JOIN forum_messages ON forum_messages.topic_id=forum_topics.id
JOIN (SELECT MAX(m.date) as date, top.category_id
FROM forum_messages m
JOIN forum_topics top ON m.topic_id = top.id
GROUP BY top.category_id) as t
ON t.category_id = forum_topics.category_id AND t.date = forum_messages.date
GROUP BY forum_categories.id) t2
ON t2.id = forum_categories.id
GROUP BY forum_categories.id
To supplement the current output with the missing data, I would probably go like this:
SELECT
forum_stats.*, /* just repeat the already pulled columns (expand it if needed) */
forum_messages.* /* and here you may actually want to be more specific as to
what else you would like to pull from forum_messages */
FROM (
SELECT
forum_categories.id AS category_id,
forum_categories.title,
COUNT(DISTINCT forum_topics.id) AS total_topics,
SUM(CASE WHEN forum_messages.original=0 THEN 1 ELSE 0 END) AS total_replies,
MAX(forum_messages.date) AS last_message,
SUM(CASE WHEN r.user IS NULL THEN 1 ELSE 0 END) AS to_view,
forum_categories.date
FROM forum_categories
JOIN forum_topics ON forum_topics.category_id=forum_categories.id
LEFT OUTER JOIN (
SELECT topic, user FROM forum_visits WHERE user='userA'
) r ON forum_topics.id=r.topic
JOIN forum_messages ON forum_messages.topic_id=forum_topics.id
GROUP BY forum_categories.id
) forum_stats
JOIN forum_topics ON forum_topics.category_id=forum_stats.category_id
JOIN forum_messages ON forum_messages.topic_id=forum_topics.id
AND forum_messages.date=forum_stats.last_message
ORDER BY forum_stats.date
Of course, this assumes that forum_messages.date is not just a date, but a timestamp and that no two messages can share absolutely the same timestamp.