如何在 MySQL 中优化此查询

Posted

技术标签:

【中文标题】如何在 MySQL 中优化此查询【英文标题】:How to optimize this query in MySQL 【发布时间】:2015-10-27 18:08:21 【问题描述】:

我有这两张表(Moodle 2.8):

CREATE TABLE `mdl_course` (
  `id` bigint(10) NOT NULL AUTO_INCREMENT,
  `category` bigint(10) NOT NULL DEFAULT '0',
  `sortorder` bigint(10) NOT NULL DEFAULT '0',
  `fullname` varchar(254) NOT NULL DEFAULT '',
  `shortname` varchar(255) NOT NULL DEFAULT '',
  `idnumber` varchar(100) NOT NULL DEFAULT '',
  `summary` longtext,
  `summaryformat` tinyint(2) NOT NULL DEFAULT '0',
  `format` varchar(21) NOT NULL DEFAULT 'topics',
  `showgrades` tinyint(2) NOT NULL DEFAULT '1',
  `newsitems` mediumint(5) NOT NULL DEFAULT '1',
  `startdate` bigint(10) NOT NULL DEFAULT '0',
  `marker` bigint(10) NOT NULL DEFAULT '0',
  `maxbytes` bigint(10) NOT NULL DEFAULT '0',
  `legacyfiles` smallint(4) NOT NULL DEFAULT '0',
  `showreports` smallint(4) NOT NULL DEFAULT '0',
  `visible` tinyint(1) NOT NULL DEFAULT '1',
  `visibleold` tinyint(1) NOT NULL DEFAULT '1',
  `groupmode` smallint(4) NOT NULL DEFAULT '0',
  `groupmodeforce` smallint(4) NOT NULL DEFAULT '0',
  `defaultgroupingid` bigint(10) NOT NULL DEFAULT '0',
  `lang` varchar(30) NOT NULL DEFAULT '',
  `theme` varchar(50) NOT NULL DEFAULT '',
  `timecreated` bigint(10) NOT NULL DEFAULT '0',
  `timemodified` bigint(10) NOT NULL DEFAULT '0',
  `requested` tinyint(1) NOT NULL DEFAULT '0',
  `enablecompletion` tinyint(1) NOT NULL DEFAULT '0',
  `completionnotify` tinyint(1) NOT NULL DEFAULT '0',
  `cacherev` bigint(10) NOT NULL DEFAULT '0',
  `calendartype` varchar(30) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  KEY `mdl_cour_cat_ix` (`category`),
  KEY `mdl_cour_idn_ix` (`idnumber`),
  KEY `mdl_cour_sho_ix` (`shortname`),
  KEY `mdl_cour_sor_ix` (`sortorder`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `mdl_log` (
  `id` bigint(10) NOT NULL AUTO_INCREMENT,
  `time` bigint(10) NOT NULL DEFAULT '0',
  `userid` bigint(10) NOT NULL DEFAULT '0',
  `ip` varchar(45) NOT NULL DEFAULT '',
  `course` bigint(10) NOT NULL DEFAULT '0',
  `module` varchar(20) NOT NULL DEFAULT '',
  `cmid` bigint(10) NOT NULL DEFAULT '0',
  `action` varchar(40) NOT NULL DEFAULT '',
  `url` varchar(100) NOT NULL DEFAULT '',
  `info` varchar(255) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  KEY `mdl_log_coumodact_ix` (`course`,`module`,`action`),
  KEY `mdl_log_tim_ix` (`time`),
  KEY `mdl_log_act_ix` (`action`),
  KEY `mdl_log_usecou_ix` (`userid`,`course`),
  KEY `mdl_log_cmi_ix` (`cmid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

这个查询:

SELECT l.id,
       l.userid AS participantid,
       l.course AS courseid,
       l.time,
       l.ip,
       l.action,
       l.info,
       l.module,
       l.url
FROM   mdl_log l
INNER JOIN mdl_course c ON l.course = c.id AND c.category <> 0      
WHERE 
      l.id > [some large id]
      AND
      l.time > [some unix timestamp]
ORDER BY l.id ASC
LIMIT 0,200

mdl_log 表有超过 2 亿条记录,我需要使用 php 将其导出到文件中,而不是故意死掉。这里的主要问题是执行太慢了。这里的主要杀手是连接到 mdl_course 表。如果我删除它,一切都会很快。

这里是解释:

+----+-------------+-------+-------+---------------------------------------------+----------------------+---------+----------------+------+-----------------------------------------------------------+
| id | select_type | table | type  | possible_keys                               | key                  | key_len | ref            | rows | Extra                                                     |
+----+-------------+-------+-------+---------------------------------------------+----------------------+---------+----------------+------+-----------------------------------------------------------+
|  1 | SIMPLE      | c     | range | PRIMARY,mdl_cour_cat_ix                     | mdl_cour_cat_ix      | 8       | NULL           | 3152 | Using where; Using index; Using temporary; Using filesort |
|  1 | SIMPLE      | l     | ref   | PRIMARY,mdl_log_coumodact_ix,mdl_log_tim_ix | mdl_log_coumodact_ix | 8       | xray2qasb.c.id |  618 | Using index condition; Using where                        |
+----+-------------+-------+-------+---------------------------------------------+----------------------+---------+----------------+------+-----------------------------------------------------------+

有什么方法可以消除临时文件和文件排序的使用吗?你在这里有什么建议?

【问题讨论】:

尝试添加包含 (l.time, l.course) 的索引,因为这些是您查询我们使用的过滤器。您可能也考虑将category 添加到log 表中;即使它没有标准化,它也可能会提高性能,足以值得麻烦。如果你这样做,你也会将l.category 添加到索引中。 您没有在您的选择中使用来自mdl_course 的任何字段。您可以将其作为exists 语句移动到您的where 也许值得努力重构这些表,以便将对分析无用的文本字段推送到它们自己的表中,并通过类似于日志的 id 进行引用。这可以减少对本质上更具参考性的查询的一些拖累。后果是,如果有其他资源依赖于该结构,则必须对它们进行重新设计以支持新结构。 不幸的是,这不会发生。我必须按原样处理结构......我只能在需要的地方添加索引或新表...... 【参考方案1】:

经过一些测试,此查询按预期快速运行:

SELECT l.id,
       l.userid AS participantid,
       l.course AS courseid,
       l.time,
       l.ip,
       l.action,
       l.info,
       l.module,
       l.url
FROM   mdl_log l
WHERE 
      l.id > 123456
      AND
      l.time > 1234
      AND
      EXISTS (SELECT * FROM mdl_course c WHERE l.course = c.id AND c.category <> 0  )
ORDER BY l.id ASC
LIMIT 0,200

感谢 JamieD77 的建议!

执行计划:

+----+--------------------+-------+--------+-------------------------+---------+---------+--------------------+----------+-------------+
| id | select_type        | table | type   | possible_keys           | key     | key_len | ref                | rows     | Extra       |
+----+--------------------+-------+--------+-------------------------+---------+---------+--------------------+----------+-------------+
|  1 | PRIMARY            | l     | range  | PRIMARY,mdl_log_tim_ix  | PRIMARY | 8       | NULL               | 99962199 | Using where |
|  2 | DEPENDENT SUBQUERY | c     | eq_ref | PRIMARY,mdl_cour_cat_ix | PRIMARY | 8       | xray2qasb.l.course |        1 | Using where |
+----+--------------------+-------+--------+-------------------------+---------+---------+--------------------+----------+-------------+

【讨论】:

您可以将此查询的解释计划添加到您的答案帖子中吗?【参考方案2】:

尝试将类别选择移到JOIN 之外。在这里,我将它放在 IN() 中,引擎将在连续运行时对其进行缓存。我没有 200M 行要测试,所以 YMMV。

DESCRIBE 

SELECT l.id,
   l.userid AS participantid,
   l.course AS courseid,
   l.time,
   l.ip,
   l.action,
   l.info,
   l.module,
   l.url
FROM   mdl_log l   
WHERE 
  l.id > 1234567890
  AND
  l.time > 1234567890
  AND 
  l.course IN (SELECT c.id FROM mdl_course c WHERE c.category > 0)      
ORDER BY l.id ASC
LIMIT 0,200;

【讨论】:

不幸的是,这个查询在我的数据库上太慢了。等了几分钟后我停止了它。 mdl_course 表中有多少条记录?而且,其中有多少人拥有category=0 6219 条记录,只有一条属于类别 0 尝试使用l.course &lt;&gt; WHATEVER_THAT_COURSE_ID_IS 而不是IN() 子句 这对我不起作用。日志表中可能存在课程表中不再存在的课程条目。我必须同时检查两者,一般来说 Exists 比大型数据集更快。【参考方案3】:

(除了使用EXISTS...)

  l.id > 123456 AND l.time > 1234

似乎乞求二维索引。

99962199 -- 桌子很大,对吗?

考虑PARTITION BY RANGE on mdl_log on time。但是……

不要超过大约 50 个分区;其他效率低下的问题也随之而来。 分区可能无济于事 idtime 有点步调一致。典型情况:idAUTO_INCREMENTtime大约是INSERT的时间。

如果适用,请考虑:

PRIMARY KEY(time, id)  -- see below
INDEX(id)              -- Yes, this is sufficient for `id AUTO_INCREMENT`.

有了这些索引,你可以高效地做

WHERE time > ...
ORDER BY time, id

这可能是你真正想要的。

【讨论】:

我会检查它,但真诚地怀疑它可能比现有查询更有效。需要 (id = something) 条件来避免将光标缓慢定位到第 n 条记录。我真的不需要 ORDER BY 时间,我只关心 ORDER BY id 以便我可以尽可能快地进行分页导出。

以上是关于如何在 MySQL 中优化此查询的主要内容,如果未能解决你的问题,请参考以下文章

MySQL - 如何优化此查询?

如何优化这个 MySql 查询 - 连接 3 个表?

mysql慢查询

如何在 Postgres 中优化此查询

如何在 Firebird 2.1 中优化此查询?

我如何在mysql中优化这个查询?