我遇到了一个SQL查询问题,即当表有超过100k的记录时,“失败”(耗时太长)。这不应该是一个问题,我认为我已经覆盖了它,因为它对50k记录非常好。
我会尽量简短明了,所以我将从查询开始:
SELECT
V.id
FROM
videos V
LEFT JOIN videos_categories VC ON V.id = VC.video_id
LEFT JOIN categories C ON VC.category_id = C.id
LEFT JOIN users U ON V.user_id = U.id -- irrelevant table. Don't pay attention
WHERE
V.status = 1
AND (C.status = 1 OR C.id IS NULL)
AND (U.status = 1 OR U.id IS NULL) -- irrelevant
GROUP BY V.id
ORDER BY V.id DESC
LIMIT 0, 12
---------------------------------------------
**Query took 10.8771 sec** (very bad! this would take 0.1 max)
我正在使用所有LEFT JOIN,因为如果某个类别不存在,我不想限制结果。这意味着还会返回未分配类别的视频。
表结构的想法是下一个:
---- 7月3日更新----
表格的结构:
CREATE TABLE `videos` ( -- Holding +100k records
`id` int(10) unsigned NOT NULL auto_increment,
`user_id` int(10) unsigned NOT NULL default '0', -- irrelevant for this example
`status` tinyint(1) NOT NULL default '0',
PRIMARY KEY (`id`),
KEY `status` (`status`)
-- ... -- Irrelevant Keys
) ENGINE=MyISAM DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC AUTO_INCREMENT=113339 ;
CREATE TABLE `videos_categories` ( -- Holding +600k records (several categories per video)
`video_id` int(10) unsigned NOT NULL default '0',
`category_id` int(10) unsigned NOT NULL default '0',
KEY `video_id` (`video_id`),
KEY `category_id` (`category_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
类别表有一个PK id和不相关的字段。它拥有80条记录。 用户表完全不相关,可能会被忽略。很抱歉在第一时间添加它。
---- 7月3日更新结束----
这是查询的EXPLAINED结果
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE V range status status 1 NULL 112895 Using where; Using temporary; Using filesort
1 SIMPLE VC ref video_id video_id 4 V.id 2
1 SIMPLE C eq_ref PRIMARY PRIMARY 4 VC.category_id 1 Using where
1 SIMPLE U eq_ref PRIMARY PRIMARY 4 V.user_id 1 Using where
我认为问题在于SQL引擎是“使用filesort”,因为它使用的是'status'索引,而不是 V.id 。
此外,它是“使用临时”,因为引擎必须写入的记录数和内存表是不够的。
更新(7月3日):经过一些测试后我得出的结论是,这个特定查询的问题是使用V.status作为索引根本没有帮助(98%视频的状态= 1)
重要说明:如果我从WHERE子句中删除'V.status = 1'过滤器,查询需要0.01秒,并使用V.id(PRIMARY)作为索引,解决它所有
---- 7月3日更新说明----
假设我已涵盖所有相关索引:如何优化查询,因为它需要0.1秒?
我很确定这对高级SQL管理员和程序员来说是一个很好的挑战。
答案 0 :(得分:2)
鉴于您的查询(稍微重新格式化):
SELECT V.id
FROM videos V
LEFT JOIN videos_categories VC ON V.id = VC.video_id
LEFT JOIN categories C ON VC.category_id = C.id
LEFT JOIN users U ON V.user_id = U.id
WHERE V.status = 1
AND V.reported < 10
AND (C.status = 1 OR C.id IS NULL)
AND (U.status = 1 OR U.id IS NULL)
GROUP BY V.id
ORDER BY V.id DESC
LIMIT 0, 12
您对表格的描述不正确。你说:
类别和用户的基数(行数)将提供信息。但更严重的是,查询引用了:
这些字段应与不相关的字段分开提及,并且应标识这些列上的任何索引。最好提供可用于回答查询的表模式,并在每个表的末尾添加注释“-- and other irrelevant columns
”。
Video_Categories表是否对组合(Video_ID,Category_ID)列有唯一约束?为什么不呢?
目前还不清楚为什么Videos表有User_ID列;它看起来更像应该有一个带有(Video_ID,User_ID)列的Video_Users表。但是,这是一个单独的讨论。此外,还不清楚为什么你会有没有用户ID值的视频,因此左边的外部联接也令人费解。但是,你勇敢地断言这不是问题的一部分,所以我们会接受你的意思。
LEFT OUTER JOIN可能是一种严重的性能抑制剂。您可能会从UNION获得更好的结果(或者您可能不会 - UNION也可以成为性能抑制器!):
SELECT V.ID
FROM (SELECT V.ID, V.User_ID, V.Status, V.Reported
FROM videos AS V
JOIN videos_categories AS VC ON V.id = VC.Video_ID
JOIN categories AS C ON VC.category_id = C.ID
WHERE C.Status = 1
UNION
SELECT V.ID, V.User_ID, V.Status, V.Reported
FROM videos V
WHERE V.ID NOT IN (SELECT Video_ID FROM Video_Categories)
) AS L
LEFT JOIN Users AS U ON L.User_ID = U.ID
WHERE L.Reported < 10
AND (U.status = 1 OR U.ID IS NULL)
GROUP BY L.ID
ORDER BY L.ID DESC
LIMIT 0, 12
(别名'L'代表'视频列表'。)这里的想法是UNION的前半部分处理内部联接,下半部分处理未分类的视频。但是,NOT IN条件可能是性能问题,如果有的话。想想看,我认为UNION中的两个视频列表应该是不相交的,所以你可以使用UNION ALL代替UNION;这可能对性能有益(因为它避免了重复的消除阶段)。
如果优化器不会自动为您执行此操作,您可以将'L.Reported < 10
'条件下推到UNION的每一半(它变为V.Reported < 10
)。
我并不相信这会比原版表现更好,但它至少会给你一些想法来考虑。
答案 1 :(得分:1)
Jonathan提出了一些有趣和有价值的观点。此外,如果将其作为优化程序或索引问题而非查询问题,则可能值得询问选择性在V.status列上的显示方式。 (如果需要,请参阅here了解有关选择性的更多信息。)如果选择性差,则:
进行V和VC表的连接然后过滤掉与状态限制不匹配的行可能更有效
对V.status进行索引可能没用。
其他一些可能有用的东西是:
更新V表(ANALYZE TABLE)的统计信息,以防不良统计数据误导优化器有关状态索引的选择性
如果打开&lt; 5.5,请检查EXPLAIN是否在5.5上显示相同的计划。在以后的版本中对优化器进行了重大改进。
视频是InnoDB表吗?如果是这样,状态索引实际上是(PK,状态),因为聚簇索引(如果存在PK,则包含在状态的非聚簇索引中)。如果是MyISAM,您可以测试转换表以查看是否会影响计划。
在某种程度上,我想尽可能礼貌地指出,我认为你可能对“使用临时”和“文件”意味着什么有一点误解。 Baron Schwartz在here的帖子中谈到了这一点。