Multiple SQL joins difficult query - mysql

I have a weird schema but when it was designed it seemed like a good idea at the time. I have one master table, lesson_objects, that has foreign keys linking to the vocabulary, video and quizzes tables.
vocab table:
CREATE TABLE `se_vocab` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`vocab_word` VARCHAR(255) NOT NULL,
`vocab_audio` INT(10) NULL DEFAULT '0',
`vocab_image` INT(10) NULL DEFAULT '0',
PRIMARY KEY (`id`)
)
video table:
CREATE TABLE `se_video` (
`id` INT(10) NOT NULL AUTO_INCREMENT,
`video_name` VARCHAR(255) NOT NULL,
`video_description` MEDIUMTEXT NOT NULL,
`video_file_name` VARCHAR(50) NULL DEFAULT NULL,
`video_url` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id`)
)
quizzes table:
CREATE TABLE `se_quizzes` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`quiz_name` VARCHAR(80) NOT NULL,
`quiz_description` TINYTEXT NULL,
PRIMARY KEY (`id`)
)
lesson objects (contains foreign keys of previous tables)
CREATE TABLE `se_lesson_org` (
`id` INT(10) NOT NULL AUTO_INCREMENT,
`lesson_id` INT(10) NOT NULL,
`section_object_type` ENUM('video','vocabulary','quiz') NOT NULL,
`section_object_id` INT(10) NOT NULL,
PRIMARY KEY (`id`)
)
I'm trying to create a query that returns all the records from lesson_objects but also includes the data in the columns for the type in that record (vocabulary, etc.)
For example:
Only my query returns no rows, while ideally it should turn multiple rows with every record containing SOME empty columns. E.g. if it isn't vocabulary, the columns for quiz and video will be empty.
My attempts are very bad, but here is one for the sake of guidance:
SELECT
lo.id, lo.section_object_type, lo.section_object_id,
vo.id, vo.vocab_text, vo.vocab_image, vo.vocab_audio
vi.id, vi.video_name, vi.video_url,
q.id, q.quiz_name
FROM se_lesson_org lo, se_vocab vo, se_video vi, se_quizzes q
WHERE lo.section_object_id = vo.id
OR lo.section_object_id = vi.id
OR lo.section_object_id = q.id
Any help / comments would be appreciated. Thanks.

Use LEFT JOIN to return all rows from se_lesson_org. Additionally, add a JOIN condition to match for the specific section_object_type
SELECT
lo.*,
vo.*,
vi.*,
q.*
FROM se_lesson_org lo
LEFT JOIN se_vocab vo
ON vo.id = lo.section_object_id
AND lo.section_object_type = 'vocabulary'
LEFT JOIN se_video vi
ON vi.id = lo.section_object_id
AND lo.section_object_type = 'video'
LEFT JOIN se_quizzes q
ON q.id = lo.section_object_id
AND lo.section_object_type = 'quiz'
Note: Avoid using old-style JOIN syntax. Read this article by Aaron Bertrand.

Sounds like you are looking for a LEFT JOIN on each of these tables. That way if the foreign key is valid you will show values and if it isn't valid you will just get NULL for the related columns.
SELECT
lo.id, lo.section_object_type, lo.section_object_id,
vo.id, vo.vocab_text, vo.vocab_image, vo.vocab_audio
vi.id, vi.video_name, vi.video_url,
q.id, q.quiz_name
FROM se_lesson_org lo
LEFT JOIN se_vocab vo ON vo.id = lo.section_object_id
AND lo.section_object_type = 'vocabulary'
LEFT JOIN se_video vi ON vi.id = lo.section_object_id
AND lo.section_object_type = 'video'
LEFT JOIN se_quizzes q ON q.id = lo.section_object_id
AND lo.section_object_type = 'quiz'
Notice also how this syntax makes it clear how each table is being connected into the rest of the query rather than having a whole mess of conditions in the WHERE clause at the end.

Related

select taking 9.+ seconds. how to re-write it better?

I have this select:
select t.id, c.user, t.title, pp.foto, t.data from topics t
inner join cadastro c on t.user = c.id
left join profile_picture pp on t.user = pp.user
left join (
select c.topic, MAX(c.data) cdata from comments c
group by c.topic
)c on t.id = c.topic
where t.community = ?
order by ifnull(cdata, t.data) desc
limit 15
I want to select topics and order them by their date or the date of the topic comments, if it has comments.
Unfortunately, this is taking more than 9 seconds.
I don't think the problem here is indexing, but the way I am writing the select itself.
`topics` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`user` INT(11) UNSIGNED NOT NULL,
`title` varchar(100) NOT NULL,
`description` varchar(1000),
`community` INT(11) UNSIGNED NOT NULL,
`data` datetime NOT NULL,
`ip` varchar(20),
PRIMARY KEY (`id`),
FOREIGN KEY (`user`) REFERENCES cadastro (`id`),
FOREIGN KEY (`community`) REFERENCES discussion (`id`)
)
`comments` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`user` INT(11) UNSIGNED NOT NULL,
`comment` varchar(1000) NOT NULL,
`topic` INT(11) UNSIGNED NOT NULL,
`data` datetime NOT NULL,
`ip` varchar(20),
`delete` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
FOREIGN KEY (`user`) REFERENCES cadastro (`id`),
FOREIGN KEY (`topic`) REFERENCES topics (`id`)
)
Your EXPLAIN gives you a strong hint. The first row in that results says, using temporary, using filesort implying that it's not using a an index.
It might be possible to improve this query by adding indexes and removing some conditionals, but I think in this case a better solution exists.
Why not add a new column to topics that indicates the last time a comment was added? (like a last_modified). Every time a comment gets added, just update that column for that topic as well.
It's effectively denormalizing this. I think this a valid usecase and it's always going to be faster than fixing this messy query.
You are performing a full table scan on the table comments on every query. How many rows does it have? At least create the following index:
comments (topic, data);
to avoid reading the whole table every time.
I know you've said you don't think the problem is indexing, but 9 out of 10 times I've had this problem that's exactly what it's been down to.
Ensure you have an index created on each table that you're using in the query and include the columns specified in the join.
Also, as NiVeR said, don't use the same alias multiple times.
Here's a refactoring of that query, unsure if I've mixed up or missed a column name/alias or two though.
select t.id, c.user, t.title, pp.foto, t.data from topics t
inner join cadastro c on t.user = c.id
left join profile_picture pp on t.user = pp.user
left join (
select com.topic, MAX(com.data) comdata from comments com
group by com.topic
)com1 on t.id = com1.topic
where t.community = ?
order by ifnull(com1.comdata, t.data) desc
limit 15

MySQL Language translation query

I am using multi language supported database. But i don't know how to query translated text from database based on Language.
Here is my DDL for tables. Please ask me if you have any question.
Category Table
CREATE TABLE IF NOT EXISTS `catalog_category` (
`id` int(10) unsigned NOT NULL,
`name_translation_id` int(10) unsigned NOT NULL,
`description_translation_id` int(10) unsigned NOT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
Language Table
CREATE TABLE IF NOT EXISTS `languages` (
`id` int(10) unsigned NOT NULL,
`language_name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`language_code` varchar(2) COLLATE utf8_unicode_ci NOT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
Language Translation table
CREATE TABLE IF NOT EXISTS `language_translation` (
`id` int(10) unsigned NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
Language Translation Entry Table
CREATE TABLE IF NOT EXISTS `language_translation_entry` (
`id` int(10) unsigned NOT NULL,
`translation_id` int(10) unsigned NOT NULL,
`language_id` int(10) unsigned NOT NULL,
`translation_text` text COLLATE utf8_unicode_ci NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
Currently i am using this query to get results but it is giving me from only name. I want description also in results.
SELECT lte.translation_text as name FROM catalog_category AS cc
LEFT JOIN language_translation lt ON lt.id = cc.name_translation_id
LEFT JOIN language_translation_entry AS lte ON lte.translation_id = lt.id
LEFT JOIN languages AS l on l.id = lte.language_id
WHERE l.language_code = "en"
I need to get translated text from Translation Entry Table for category name and description based on language code in a single query.
You can use subselects:
SELECT
(select lte.translation_text
from language_translation_entry AS lte
JOIN languages AS l on l.id = lte.language_id
WHERE lte.translation_id = cc.name_translation_id
and l.language_code = "en"
) as name,
(select lte.translation_text
from language_translation_entry AS lte
JOIN languages AS l on l.id = lte.language_id
WHERE lte.translation_id = cc.description_translation_id
and l.language_code = "en"
) as description
from catalog_category AS cc
This assumes you have max 1 entry for each (translation_id, laguage_id), otherwise this will give you an error, because these subselects can only return one value.
I left out the table language_translation, because you probably only use it to get your autoincrement values, and is not required for the join.
An alternative would be to use another join for each column you need translated, you can e.g. use
SELECT lte.translation_text as name,
lt2.translation_text as description,
FROM catalog_category AS cc
LEFT JOIN language_translation_entry AS lte
ON lte.translation_id = cc.name_translation_id
LEFT JOIN languages AS l on l.id = lte.language_id
LEFT JOIN language_translation_entry AS lte2
ON lte2.translation_id = cc.name_translation_id
and lte2.language_id = l.id
WHERE l.language_code = "en"
where I assume that you want both translation to be the same language.
Btw., left join actually works here like a normal join, since you use where l.language_code = "en", it has to exists. If you want a real left join, you have to move that condition to the on-clause, so it can be missing (if there is a possibility that your translations are missing and you still want to get a result from your query). For description, it already is allowed to be missing to make the code shorter, otherwise you would have to move the condition from on to the where clause, so change the last part to
LEFT JOIN language_translation_entry AS lte2
ON lte2.translation_id = cc.name_translation_id
WHERE l.language_code = "en" and lte2.language_id = l.language_id

LEFT JOIN two tables so that left table data is filtered but right table data is displayed even if left table data does not exist

To allow our customers to store some of their own data along with our data structure I have created two extra tables:
CREATE TABLE external_columns
(
`id` INT(11) PRIMARY KEY NOT NULL,
`column` VARCHAR(30) NOT NULL,
`sid` INT(11) NOT NULL,
`bid` INT(11) NOT NULL,
`label` VARCHAR(30) NOT NULL,
`table` VARCHAR(30) NOT NULL,
`default` TINYTEXT NOT NULL
);
CREATE TABLE external_data
(
`id` INT(11) PRIMARY KEY NOT NULL,
`extcol_id` INT(11) NOT NULL,
`sid` INT(11) NOT NULL,
`bid` INT(11) NOT NULL,
`data` MEDIUMTEXT NOT NULL,
`row_id` INT(11) NOT NULL,
CONSTRAINT `external_data_external_columns_id_fk`
FOREIGN KEY (extcol_id) REFERENCES external_columns (id)
);
CREATE UNIQUE INDEX combinedUniqueIndex
ON external_data (extcol_id, sid, bid, row_id);
sid and bid are system values that identify the customer the data belongs to. row_id refers to the primary key of table referenced in table.
To get data for a certain row I have created this prepared statement:
SELECT `data`.*, `columns`.`column`, `columns`.`default`
FROM `external_columns` as `columns`
LEFT JOIN `external_data` as `data`
ON `columns`.`id` = `data`.`extcol_id`
WHERE (
`columns`.`sid` = :sid
AND `columns`.`bid` = :bid
AND `data`.`row_id` = :row_id
AND `columns`.`table` = :tableName
)
This works fine as long as for each external_column there is an entry in external_data for the given :row_id. But I want to make sure that there is always a row for each column, even if there is no data for the given :row_id. Is there a way to do this with one query?
Very close, by placing AND data.row_id = :row_id in your WHERE, you have effectively written an INNER JOIN as nulled data.row_ids won't match.
You should move this condition to the LEFT JOIN conditions:
SELECT `data`.*, `columns`.`column`, `columns`.`default`
FROM `external_columns` as `columns`
LEFT JOIN `external_data` as `data`
ON `data`.`extcol_id`= `columns`.id
AND `data`.`row_id` = :row_id
WHERE `columns`.`sid` = :sid
AND `columns`.`bid` = :bid
AND `columns`.`table` = :tableName
Personal Preferences:
Don't need the WHERE parentheses and I always tend to put the table conditions for a JOIN in the JOIN conditions where applicable and JOIN table on the LHS to make indexing options more obvious..
No difference for INNER JOINs but essential for certain LEFT JOINs.

How to joint Two customs Queries with two Joins in only One Query in MySQL

The Queries are working perfectly each one separatedly:
SELECT asf.surface_name, am.*
FROM atp_matchs_to_surfaces m2s
LEFT JOIN atp_surfaces asf ON m2s.surfaces_id = asf.surfaces_id
LEFT JOIN atp_matchs am ON am.matchs_id = m2s.matchs_id;
SELECT att.tournament_type_name, am.*
FROM atp_matchs_to_tournament_type m2s
LEFT JOIN atp_tournament_type att ON m2s.tournament_type_id = att.tournament_type_id
LEFT JOIN atp_matchs am ON am.matchs_id = m2s.matchs_id;
The tables 'atp_matchs_to_surfaces' and 'atp_matchs_to_tournament_type' are defined in that way:
CREATE TABLE IF NOT EXISTS `atp_matchs_to_tournament_type` (
`tournament_type_id` int(4) NOT NULL,
`matchs_id` int(6) NOT NULL,
PRIMARY KEY (`tournament_type_id`,`matchs_id`)
CREATE TABLE IF NOT EXISTS `atp_matchs_to_surfaces` (
`surfaces_id` int(4) NOT NULL,
`matchs_id` int(6) NOT NULL,
PRIMARY KEY (`surfaces_id`,`matchs_id`)
And the other Tables with all the data:
CREATE TABLE IF NOT EXISTS `atp_matchs` (
`matchs_id` int(7) NOT NULL AUTO_INCREMENT,
`tournament_name` varchar(36) NOT NULL,
`tournament_year` year NOT NULL,-- DEFAULT '0000',
`tournament_country` varchar(26) NOT NULL,
`match_datetime` datetime NOT NULL,-- DEFAULT '0000-00-00 00:00:00',
`match_link` varchar(85) NOT NULL,
`prize_money` int(12) NOT NULL,
`round` varchar(8) NOT NULL,-- DEFAULT '1R',
`sets` varchar(34) NOT NULL,-- DEFAULT '0-0',
`result` varchar(4) NOT NULL,-- DEFAULT '0-0',
`p1_odd` decimal(4,2) NOT NULL,-- DEFAULT '0.00',
`p2_odd` decimal(4,2) NOT NULL,-- DEFAULT '0.00',
PRIMARY KEY (`matchs_id`)
CREATE TABLE IF NOT EXISTS `atp_surfaces` (
`surfaces_id` int(4) NOT NULL AUTO_INCREMENT,
`surface_name` varchar(24) NOT NULL,
PRIMARY KEY (`surfaces_id`)
CREATE TABLE IF NOT EXISTS `atp_tournament_type` (
`tournament_type_id` int(4) NOT NULL AUTO_INCREMENT,
`tournament_type_name` varchar(22) NOT NULL,
PRIMARY KEY (`tournament_type_id`)
I want in the same Query all the records of match and surface name+tournament type. It's clear? I hope...
I tried to implement this with SubQueries: http://www.w3resource.com/mysql/subqueries/ and How can an SQL query return data from multiple tables but i can't do it to work.
OK, this is your current schema. As you can see, one match can be played on multiple surfaces and one match can be played within multiple tournament types.
If this schema is OK, you can get your result with this query:
SELECT am.*, asu.surface_name, att.tournament_type_name
FROM atp_matchs AS am
LEFT JOIN atp_matchs_to_surfaces AS m2s ON m2s.matchs_id = am.matchs_id
LEFT JOIN atp_surfaces AS asu ON asu.surfaces_id = m2s.surfaces_id
LEFT JOIN atp_matchs_to_tournament_type AS m2t ON m2t.matchs_id = am.matchs_id
LEFT JOIN atp_tournament_type AS att ON att.tournament_type_id = m2t.tournament_type_id
However, if one match can be played on one surface only and within one tournament type only, I would change your schema to:
Tables atp_matchs_to_surfaces and atp_matchs_to_tournament_type are removed and fields surfaces_id and tournament_type_id moved to atp_matchs table. Your query is now:
SELECT am.*, asu.surface_name, att.tournament_type_name
FROM atp_matchs AS am
LEFT JOIN atp_surfaces AS asu ON asu.surfaces_id = am.surfaces_id
LEFT JOIN atp_tournament_type AS att ON att.tournament_type_id = am.tournament_type_id
The LEFT JOIN keyword returns all rows from the left table (table1), with the matching rows in the right table (table2).
SELECT asf.surface_name, am.*
FROM atp_matchs_to_surfaces m2s
LEFT JOIN (SELECT att.tournament_type, am.*
FROM atp_matchs_to_tournament_type m2s) as......
SELECT asf.surface_name, am.*
FROM atp_matchs_to_surfaces m2s
LEFT JOIN atp_surfaces asf ON m2s.surfaces_id = asf.surfaces_id
LEFT JOIN atp_matchs am ON am.matchs_id = m2s.matchs_id
LEFT JOIN(
SELECT att.tournament_type, am.*
FROM atp_matchs_to_tournament_type m2s
LEFT JOIN atp_tournament_type att AS Q1 ON m2s.surfaces_id = att.surfaces_id
LEFT JOIN atp_matchs am AS Q2 ON am.matchs_id = m2s.matchs_id);
I added some "AS" because I had the error: Every derived table must have its own alias. I'm a little lost here!

How to INNER JOIN around a loop of tables

I have four tables as follows:
CREATE TABLE IF NOT EXISTS `categories` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
) ENGINE=MyISAM DEFAULT CHARSET=utf8 ;
CREATE TABLE IF NOT EXISTS `categories_friends` (
`category_id` int(10) unsigned NOT NULL,
`friend_id` int(10) unsigned NOT NULL,
UNIQUE KEY `category_id` (`friend_id`,`category_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `friends` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`friend_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `user_id` (`user_id`,`friend_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 ;
CREATE TABLE IF NOT EXISTS `ratings` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`category_id` int(10) unsigned NOT NULL,
`title` varchar(255) NOT NULL,
`description` text NOT NULL,
`rating` tinyint(2) unsigned NOT NULL,
`public` tinyint(1) NOT NULL DEFAULT '0',
`created` datetime NOT NULL,
PRIMARY KEY (`id`),
) ENGINE=MyISAM DEFAULT CHARSET=utf8 ;
I am trying to perform the following query on those tables:
SELECT *
FROM `favred`.`ratings` AS `Rating`
INNER JOIN `favred`.`friends` AS `JFriend`
ON (`JFriend`.`friend_id` = `Rating`.`user_id`)
INNER JOIN `favred`.`categories_friends` AS `JCategoriesFriend`
ON (`JCategoriesFriend`.`category_id` = `Rating`.`category_id`
AND `JCategoriesFriend`.`friend_id` = `JFriend`.`id`)
INNER JOIN `favred`.`categories` AS `JCategory`
ON (`JCategory`.`id` = `Rating`.`category_id`
AND `JCategory`.`id` = `JCategoriesFriend`.`category_id`)
WHERE `JFriend`.`user_id` = 1
AND `Rating`.`user_id` <> 1
AND `JCategory`.`id` IN (4, 14)
GROUP BY `Rating`.`id`
The query above is not working, as it returns no results (although there is data in the tables that should return), what I'm trying to do is to find all the Ratings that were not authored by me (ID:1), but were authored by my Friends, but only if I've selected to view a specific Category for that Friend, with the resulting set being filtered by a given set of specific Categories.
The INNER JOINs loop around through Rating --> Friend --> CategoriesFreind --> Category --> back to Rating.
If I remove the additional portion of the INNER JOIN's ON clauses as follows:
SELECT *
FROM `favred`.`ratings` AS `Rating`
INNER JOIN `favred`.`friends` AS `JFriend`
ON (`JFriend`.`friend_id` = `Rating`.`user_id`)
INNER JOIN `favred`.`categories_friends` AS `JCategoriesFriend`
ON (`JCategoriesFriend`.`friend_id` = `JFriend`.`id`)
INNER JOIN `favred`.`categories` AS `JCategory`
ON (`JCategory`.`id` = `JCategoriesFriend`.`category_id`)
WHERE `JFriend`.`user_id` = 1
AND `Rating`.`user_id` <> 1
AND `JCategory`.`id` IN (4, 14)
GROUP BY `Rating`.`id`
then the query will return results, but because the INNER JOIN joining the CategoriesFriend to the Rating is not being filtered by the 'JCategory'.'id' IN (4, 14) clause, it returns all Ratings by that friend instead of filtered as it should be.
Any suggestions on how to modify my query to get it to pull the filtered results?
And I'm using CakePHP, so a query that would fit into it's unique query format would be preferred although not required.
first ,why are you use the JFriend.id, does it mean something,or is it as the same as user_id?
try this one,the same logic but it's from top to bottom ,I feel:
SELECT * FROM categories as JCategory
INNER JOIN categories_friends as JCategoriesFriend ON JCategoriesFriend.category_id = JCategory.id
INNER JOIN friends AS JFriend ON JFriend.friend_id = JCategoriesFriend.friend_id
INNER JOIN ratings AS Rating ON Rating.user_id = JFriend.friend_id
WHERE JCategory.id IN (4,14) AND JFriend.user_id = 1 AND Rating.user_id <> 1 GROUP BY Rating.id
I got one result from all the data that I made for the testing.
if it does not work also,try make some correct data,maybe the data is not right...
the testing data below:
categories: id | name (14| 141414)
categories_friends: category_id| friend_id (14| 2)
friends: id | user_id | friend_id (4| 1| 2)
ratings: id | user_id | category_id | title (2| 2| 14 | 'haha')
So I wondered if the INNER JOINs were being a little too limiting and specific in their ON clauses. So I thought that maybe a LEFT JOIN would work better...
SELECT *
FROM `favred`.`ratings` AS `Rating`
INNER JOIN `favred`.`friends` AS `JFriend`
ON (`JFriend`.`friend_id` = `Rating`.`user_id`)
LEFT JOIN `favred`.`categories_friends` AS `JCategoriesFriend`
ON (`JCategoriesFriend`.`friend_id` = `JFriend`.`id`
AND `JCategoriesFriend`.`category_id` = `Rating`.`category_id`)
WHERE `JFriend`.`user_id` = 1
AND `JRatingsUser`.`id` IS NULL
AND `Rating`.`user_id` <> 1
GROUP BY `Rating`.`id`
That query worked for me.
I did away with linking to the categories table directly, and linked indirectly through the categories_friends table which sped up the query a little bit, and everything is working great.