【问题标题】:Help with Advanced MySQL Optimization高级 MySQL 优化帮助
【发布时间】:2011-08-16 06:38:04
【问题描述】:

我遇到了一个 SQL 查询问题,即当表的记录超过 10 万条时“失败”(耗时太长)。这应该不是问题,我认为我已经涵盖了它,因为它适用于 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,因为如果类别不存在,我不想限制结果。这意味着还会返回未指定类别的视频。

tables结构的思路如下:

  • 'vid​​eos'(id PK,+无关字段)表包含 +100k 记录。
  • 'vid​​eos_categories' (video_id INDEX,category_id INDEX) +600k 记录 - 每个视频多行
  • 'categories'(id PK,+ 无关字段)
  • 'users'(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;

Categories 表有一个 PK id 和不相关的字段。它拥有80条记录。 用户表完全不相关,可以忽略。很抱歉在第一个实例中添加它。

---- 7 月 3 日更新结束----

这是查询的解释结果

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 引擎是“使用文件排序”,因为它使用的是“状态”索引,而不是 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

---- 7 月 3 日更新说明结束​​----

总结一下

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

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

【问题讨论】:

    标签: mysql optimization


    【解决方案1】:

    鉴于您的查询(有些重新格式化):

    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 记录 - 每个视频多行
    • “类别”(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里面的两个video列表应该是不相交的,所以可以用UNION ALL代替UNION;这可能对性能有益(因为它避免了重复消除阶段)。

    如果优化器没有自动为您执行此操作,您可能会将“L.Reported &lt; 10”条件向下推到 UNION 的每一半(它变成V.Reported &lt; 10)。

    我不相信这会比原来的表现更好,但它至少给了你一些思考的想法。

    【讨论】:

    • 谢谢!我编辑了帖子并删除/评论了不相关的字段并添加了更多信息。我已经测试了您的查询,但它返回了类似的结果(约 10 秒)。请参阅更新后的帖子。感谢您分享您的想法,因为它们帮助我更好地测试。
    【解决方案2】:

    Jonathan 提出了一些有趣且有价值的观点。此外,如果这是作为优化器或索引问题而不是查询问题来处理的,那么可能值得询问选择性在 V.status 列上的外观。 (如果需要,有关选择性的更多信息,请参阅here。)如果选择性很差,那么:

    • 将 V 和 VC 表连接起来然后过滤掉与状态限制不匹配的行可能会更有效

    • 对 V.status 进行索引可能没有用。

    其他一些可能有用的检查是:

    • 更新 V 表 (ANALYZE TABLE) 的统计信息,以防糟糕的统计信息误导优化器对状态索引的选择性

      • 如果这是在

      • 视频是 InnoDB 表吗?如果是这样,则状态索引实际上是(PK,状态),因为聚集索引(如果有,则为 PK)包含在状态的非聚集索引中。如果是 MyISAM,你可以测试转换表格,看看是否影响计划。

    顺便说一句,我想尽可能礼貌地指出,我认为您可能对“使用临时”和文件排序的含义有一个小小的误解。 Baron Schwartz 在here 的帖子中谈到了这一点。

    【讨论】:

    • 正如你所说:V.status 索引没有用,但是 mysql Optimizer 正在选择它而不是 V.id。所有表都是 MyISAM(我也使用 FULLTEXT 索引)。谢谢你给我的所有信息。你们都很棒!
    • 由于您在更新中提到
    • 谢谢。我无法删除状态索引,因为它用于其他查询。而且我需要使它与 mysql
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-05
    • 2011-03-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多