帮助进行高级MySQL优化

时间:2011-07-03 01:37:54

标签: mysql optimization

我遇到了一个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,因为如果某个类别不存在,我不想限制结果。这意味着还会返回未分配类别的视频。

表结构的想法是下一个:

  • '视频'(id PK,+无关字段)表保留了+ 100k记录。
  • 'vid​​eos_categories'(video_id INDEX,category_id INDEX)+ 600k记录 - 每个视频多行
  • '类别'(id PK,+无关字段)
  • '用户'(id PK,+无关字段)不是问题。

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

  • 问题1:为什么优化器不使用V.id作为索引来进行排序和过滤?我正在使用ORDER BY和LIMIT。

重要说明:如果我从WHERE子句中删除'V.status = 1'过滤器,查询需要0.01秒,并使用V.id(PRIMARY)作为索引,解决它所有

  • 问题2:有没有办法在mysql上强制使用索引< 5.0?

---- 7月3日更新说明----

总结一下

假设我已涵盖所有相关索引:如何优化查询,因为它需要0.1秒?

我很确定这对高级SQL管理员和程序员来说是一个很好的挑战。

2 个答案:

答案 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

您对表格的描述不正确。你说:

  • 'vid​​eos'(id PK,+无关字段)表保存+ 100k记录。
  • 'vid​​eos_categories'(video_id INDEX,category_id INDEX)+ 600k记录 - 每个视频多行
  • 'categories'(id PK,+无关字段)
  • 'users'(id PK,+无关字段)不是问题。

类别和用户的基数(行数)将提供信息。但更严重的是,查询引用了:

  • videos.status
  • videos.reported
  • videos.user_id
  • categories.status
  • users.status

这些字段应与不相关的字段分开提及,并且应标识这些列上的任何索引。最好提供可用于回答查询的表模式,并在每个表的末尾添加注释“-- 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的帖子中谈到了这一点。