如何进一步优化派生表查询,它的性能优于 JOINed 等效项?

Posted

技术标签:

【中文标题】如何进一步优化派生表查询,它的性能优于 JOINed 等效项?【英文标题】:How can I further optimize a derived table query which performs better than the JOINed equivalent? 【发布时间】:2010-11-13 22:07:29 【问题描述】:

更新:我找到了解决方案。请参阅下面的答案。

我的问题

如何优化此查询以最大程度地减少停机时间?我需要更新 50 多个模式,票证数量从 100,000 到 200 万不等。是否建议尝试同时设置 ticket_extra 中的所有字段?我觉得这里有一个我没有看到的解决方案。一天多来,我一直在努力解决这个问题。

另外,我最初尝试不使用子 SELECT,但性能比我目前的性能差很多

背景

我正在尝试针对需要运行的报告优化我的数据库。我需要汇总的字段计算起来非常昂贵,因此我将我的existing schema 非规范化一点以适应此报告。请注意,我通过删除几十个不相关的列来大大简化了工单表。

我的报告将按经理创建时经理解决时汇总工单计数。这种复杂的关系如下图所示:

(来源:mosso.com)

为了避免即时计算所需的六个讨厌的连接,我将下表添加到我的架构中:

mysql> show create table tickets_extra\G
*************************** 1. row ***************************
       Table: tickets_extra
Create Table: CREATE TABLE `tickets_extra` (
  `ticket_id` int(11) NOT NULL,
  `manager_created` int(11) DEFAULT NULL,
  `manager_resolved` int(11) DEFAULT NULL,
  PRIMARY KEY (`ticket_id`),
  KEY `manager_created` (`manager_created`,`manager_resolved`),
  KEY `manager_resolved` (`manager_resolved`,`manager_created`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

现在的问题是,我没有将这些数据存储在任何地方。经理总是动态计算的。我在多个数据库中拥有 数百万 的票证,这些票证具有相同的架构,需要填充此表。我想以尽可能高效的方式执行此操作,但未能成功优化我用来执行此操作的查询:

INSERT INTO tickets_extra (ticket_id, manager_created)
SELECT
  t.id, 
  su.user_id
FROM (
  SELECT 
    t.id, 
    shift_times.shift_id AS shift_id 
  FROM tickets t
  JOIN shifts ON t.shop_id = shifts.shop_id 
  JOIN shift_times ON (shifts.id = shift_times.shift_id
  AND shift_times.dow = DAYOFWEEK(t.created)
  AND TIME(t.created) BETWEEN shift_times.start AND shift_times.end)
) t
LEFT JOIN shifts_users su ON t.shift_id = su.shift_id
LEFT JOIN shift_positions ON su.shift_position_id = shift_positions.id
WHERE shift_positions.level = 1

此查询需要一个多小时才能在具有 > 170 万张票证的架构上运行。这对于我拥有的维护窗口是不可接受的。此外,它甚至不处理计算 manager_resolved 字段,因为尝试将其组合到同一个查询中会将查询时间推向平流层。我目前的倾向是将它们分开,并使用 UPDATE 来填充 manager_resolved 字段,但我不确定。

最后,这里是该查询的 SELECT 部分的 EXPLAIN 输出:

*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: <derived2>
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 167661
        Extra: 
*************************** 2. row ***************************
           id: 1
  select_type: PRIMARY
        table: su
         type: ref
possible_keys: shift_id_fk_idx,shift_position_id_fk_idx
          key: shift_id_fk_idx
      key_len: 4
          ref: t.shift_id
         rows: 5
        Extra: Using where
*************************** 3. row ***************************
           id: 1
  select_type: PRIMARY
        table: shift_positions
         type: ALL
possible_keys: PRIMARY
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 6
        Extra: Using where; Using join buffer
*************************** 4. row ***************************
           id: 2
  select_type: DERIVED
        table: t
         type: ALL
possible_keys: fk_tickets_shop_id
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 173825
        Extra: 
*************************** 5. row ***************************
           id: 2
  select_type: DERIVED
        table: shifts
         type: ref
possible_keys: PRIMARY,shop_id_fk_idx
          key: shop_id_fk_idx
      key_len: 4
          ref: dev_acmc.t.shop_id
         rows: 1
        Extra: 
*************************** 6. row ***************************
           id: 2
  select_type: DERIVED
        table: shift_times
         type: ref
possible_keys: shift_id_fk_idx
          key: shift_id_fk_idx
      key_len: 4
          ref: dev_acmc.shifts.id
         rows: 4
        Extra: Using where
6 rows in set (6.30 sec)

非常感谢您的阅读!

【问题讨论】:

题外话:你用什么工具生成数据库图? 我不知道这是否是您的意图,或者它是否会改善您的查询,但我注意到shift_times 是 InnoDB 类型,而所有其他表都是 MyISAM 类型。也许加入两个表,每个表都有不同的引擎类型,可能会导致一些减速。暂时只能这么说。 +1 准备充分的问题。 另外,看看这个问题的接受答案,它给出了如何优化 BETWEEN 子句的建议,显然 MySQL 优化器没有很好地优化。这里:***.com/questions/763667/mysql-optimize-questions @Ionut:谢谢,请阅读。我从来没有遇到过 MySQL 查询优化器和 BETWEEN 的任何问题。无论如何,这对我来说性能会很糟糕,因为 TIME(created) 不使用我在 created 字段上的索引,所以废话。无论如何,我尝试切换 BETWEEN,但性能没有明显变化。 【参考方案1】:

关于BETWEEN

SELECT * FROM a WHERE a.column BETWEEN x AND y 
是可索引的,对应于索引 a.column 上的范围查找(如果有的话) 100% 等同于 a.column &gt;= x AND a.column &lt;= y

此时:

SELECT * FROM a WHERE somevalue BETWEEN a.column1 AND a.column2
100% 等同于 somevalue &gt;= a.column1 AND somevalue &lt;= a.column2 与上面的第一个完全不同 不能通过范围查找来索引(没有范围,这里有 2 列) 通常会导致糟糕的查询性能

我认为在上面关于“之间”的辩论中对此存在混淆。

OP有第一种,不用担心。

【讨论】:

【参考方案2】:

嗯,我找到了解决办法。这需要大量的实验,我认为有点盲目的运气,但这里是:

CREATE TABLE magic ENGINE=MEMORY
SELECT
  s.shop_id AS shop_id,
  s.id AS shift_id,
  st.dow AS dow,
  st.start AS start,
  st.end AS end,
  su.user_id AS manager_id
FROM shifts s
JOIN shift_times st ON s.id = st.shift_id
JOIN shifts_users su ON s.id = su.shift_id
JOIN shift_positions sp ON su.shift_position_id = sp.id AND sp.level = 1

ALTER TABLE magic ADD INDEX (shop_id, dow);

CREATE TABLE tickets_extra ENGINE=MyISAM
SELECT 
  t.id AS ticket_id,
  (
    SELECT m.manager_id
    FROM magic m
    WHERE DAYOFWEEK(t.created) = m.dow
    AND TIME(t.created) BETWEEN m.start AND m.end
    AND m.shop_id = t.shop_id
  ) AS manager_created,
  (
    SELECT m.manager_id
    FROM magic m
    WHERE DAYOFWEEK(t.resolved) = m.dow
    AND TIME(t.resolved) BETWEEN m.start AND m.end
    AND m.shop_id = t.shop_id
  ) AS manager_resolved
FROM tickets t;
DROP TABLE magic;

冗长的解释

现在,我将解释为什么会这样,以及我的亲戚到这里的过程和步骤。

首先,我知道我正在尝试的查询由于派生表的巨大而受到影响,以及随后的 JOIN 到此。我正在使用索引良好的票证表并将所有 shift_times 数据加入其中,然后让 MySQL 在尝试加入班次和 shift_positions 表时对其进行处理。这个派生的庞然大物将有多达 200 万行未索引的混乱。

现在,我知道这正在发生。我走这条路的原因是因为“正确”的方式来做到这一点,严格使用 JOIN 需要更长的时间。这是由于确定给定班次的经理是谁所需的令人讨厌的混乱。我必须加入 shift_times 以找出正确的班次,同时加入 shift_positions 以确定用户的级别。我认为 MySQL 优化器不能很好地处理这个问题,最终会创建一个巨大的连接临时表的怪物,然后过滤掉不适用的东西。

因此,由于派生表似乎是“要走的路”,我固执地坚持了一段时间。我试着把它放到一个 JOIN 子句中,没有任何改进。我尝试在其中创建一个包含派生表的临时表,但由于临时表未建立索引,它又太慢了。

我开始意识到我必须理智地处理班次、时间、职位的计算。我想,也许 VIEW 将是要走的路。如果我创建了一个包含以下信息的 VIEW:(shop_id, shift_id, dow, start, end, manager_id)。然后,我只需通过 shop_id 和整个 DAYOFWEEK/TIME 计算加入门票表,我就可以开展业务了。当然,我不记得 MySQL 处理 VIEW 的方式相当简单。它根本没有实现它们,它只是运行您用来为您获取视图的查询。因此,通过加入票证,我基本上是在运行我的原始查询 - 没有任何改进。

因此,我决定使用 TEMPORARY TABLE 而不是 VIEW。如果我一次只获取一个管理器(创建或解决),这很有效,但它仍然很慢。另外,我发现使用 MySQL,您不能在同一个查询中两次引用同一个表(我必须加入我的临时表两次才能区分 manager_created 和 manager_resolved)。这是一个很大的 WTF,只要我不指定“TEMPORARY”,我就可以做到 - 这就是 CREATE TABLE 魔法 ENGINE=MEMORY 发挥作用的地方。

有了这个伪临时表,我再次尝试我的 JOIN for just manager_created。它表现良好,但仍然相当缓慢。然而,当我再次加入以在同一个查询中获取 manager_resolved 时,查询时间又回到了平流层。查看 EXPLAIN 显示票证的全表扫描(行约 200 万行),正如预期的那样,魔术表上的 JOIN 每个约 2,087。再一次,我似乎遇到了失败。

我现在开始考虑如何完全避免 JOIN,那时我发现了一些晦涩的古老留言板帖子,其中有人建议使用子选择(在我的历史中找不到链接)。这就是导致上面显示的第二个 SELECT 查询(tickets_extra 创建一个)的原因。在只选择​​一个经理字段的情况下,它表现良好,但同样是垃圾。我查看了 EXPLAIN 并看到了这个:

*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: t
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 173825
        Extra: 
*************************** 2. row ***************************
           id: 3
  select_type: DEPENDENT SUBQUERY
        table: m
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2037
        Extra: Using where
*************************** 3. row ***************************
           id: 2
  select_type: DEPENDENT SUBQUERY
        table: m
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2037
        Extra: Using where
3 rows in set (0.00 sec)

Ack,可怕的依赖子查询。通常建议避免这些,因为 MySQL 通常会以由外向内的方式执行它们,对外部的每一行执行内部查询。我忽略了这一点,并想知道:“嗯......如果我只是索引这个愚蠢的魔法表怎么办?”。于是,ADD 索引 (shop_id, dow) 诞生了。

看看这个:

mysql> CREATE TABLE magic ENGINE=MEMORY
<snip>
Query OK, 3220 rows affected (0.40 sec)

mysql> ALTER TABLE magic ADD INDEX (shop_id, dow);
Query OK, 3220 rows affected (0.02 sec)

mysql> CREATE TABLE tickets_extra ENGINE=MyISAM
<snip>
Query OK, 1933769 rows affected (24.18 sec)

mysql> drop table magic;
Query OK, 0 rows affected (0.00 sec)

现在这就是我在说什么!

结论

这绝对是我第一次动态创建非 TEMPORARY 表,并动态对其进行索引,只是为了高效地执行单个查询。我想我一直认为动态添加索引是一项非常昂贵的操作。 (在我的 200 万行门票表上添加索引可能需要一个多小时)。然而,对于仅仅 3,000 行来说,这简直是小菜一碟。

不要害怕 DEPENDENT SUBQUERIES、创建真正没有的临时表、动态索引或外星人。在适当的情况下,它们都可以成为好事。

感谢 *** 的所有帮助。 :-D

【讨论】:

这就是我喜欢 *** 的原因——人们发布解决方案。尽管如此,mySQL 会把简单的连接弄得那么糟糕,这还是很可怕的。 你保存了我的培根——把东西放到真实的表中,并添加了一个索引来解决 MySQL 的性能不佳——天才! 很棒的开箱即用的想法。 .0005 秒运行一个我之前在运行 20 分钟后放弃的查询。我添加了一个 DROP TABLE IF EXISTS 魔法;一开始是因为我尝试了几次才能使我的顺序正确。【参考方案3】:

您应该使用过 Postgres,哈哈。如果您有足够的 RAM 以避免磁盘抖动,这样的简单查询应该不会超过几十秒。

无论如何。

=> 是 SELECT 还是 INSERT 的问题?

(在测试服务器上单独运行 SELECT 并计时)。

=> 您的查询是磁盘受限还是 CPU 受限?

在测试服务器上启动它并检查 vmstat 输出。 如果它受 CPU 限制,请跳过此操作。 如果它是磁盘绑定的,请检查工作集大小(即数据库的大小)。 如果工作集小于你的 RAM,它不应该是磁盘绑定的。 您可以在执行查询之前强制加载 OS 缓存中的表,方法是启动 SELECT sum( some column ) FROM table 之类的虚拟选择。 如果查询从未缓存在 RAM 中的表中以随机顺序选择许多行,这将很有用......您触发了对表的顺序扫描,将其加载到缓存中,然后随机访问要快得多。通过一些技巧,您还可以缓存索引(或者只是将您的数据库目录 tar 到 >/dev/null,lol)。

当然,添加更多 RAM 会有所帮助(但您需要先检查查询是在杀死磁盘还是 CPU)。或者告诉 MySQL 在配置中使用更多的 RAM(key_buffer 等)。

如果您要进行数百万次随机 HDD 寻道,您会很痛苦。

=> 现在查询好了

首先,分析您的表格。

左加入 shift_positions ON su.shift_position_id = shift_positions.id WHERE shift_positions.level = 1

你为什么要 LEFT JOIN 然后在上面添加一个 WHERE ?左派没有意义。如果 shift_positions 中没有行,LEFT JOIN 会产生一个 NULL,WHERE 会拒绝它。

解决方案:使用 JOIN 代替 LEFT JOIN 并在 JOIN ON() 条件下移动 (level=1)。

当你在做的时候,还要摆脱其他 LEFT JOIN(由 JOIN 代替),除非你真的对所有这些 NULL 感兴趣? (我猜你不是)。

现在您可能可以摆脱子选择了。

下一步。

在 shift_times.start 和 shift_times.end 之间的时间(t.created)

这是不可索引的,因为条件中有一个函数 TIME()(使用 Postgres,哈哈)。 让我们看看:

加入 shift_times ON (shifts.id = shift_times.shift_id AND shift_times.dow = DAYOFWEEK(t.created) AND TIME(t.created) BETWEEN shift_times.start AND shift_times.end)

理想情况下,您希望在 shift_times(shift_id, DAYOFWEEK(t.created),TIME(t.created)) 上有一个多列索引,以便可以索引此 JOIN。

解决方案:将列 'day'、'time' 添加到 shift_times,包含 DAYOFWEEK(t.created)、TIME(t.created),并使用在 INSERT 或 UPDATE 上触发的触发器填充正确的值。

现在在 (shift_id,day,time) 创建多列索引

【讨论】:

不能切换到 Postgres。该查询受 CPU 限制。切换到 JOIN 并没有带来显着的改进。 @peufeu:感谢您的建议。 LEFT JOIN 是无意的,它们只是这个查询在经过许多小时的黑客攻击后所处的当前状态。 嘿,很高兴看到你解决了它;)如果索引创建真的很慢,你需要调整你的 mysql 配置来增加索引创建期间使用的大排序缓冲区的大小。我不记得参数的名称,但它应该在那里。还有key_buffer...请注意,在postgres 中为200 万行表创建索引需要不到5 秒,我认为这很慢,哈哈。 MySQL 总是(以我的经验)在索引创建方面非常慢。【参考方案4】:

这将使您在更改期间拥有只读访问权限:

create table_new (new schema);
insert into table_new select * from table order by primary_key_column;
rename table to table_old;
rename table_new to table;
-- recreate triggers if necessary

向 InnoDB 表插入数据时,按主键顺序执行此操作至关重要(否则对于大型数据集,速度会慢几个数量级)。

【讨论】:

以上是关于如何进一步优化派生表查询,它的性能优于 JOINed 等效项?的主要内容,如果未能解决你的问题,请参考以下文章

优化产品范围查询的性能

mysql的子查询中有统计语句 我该如何优化

MySQL - 添加多个派生表时查询慢 - 优化

如何提高选择查询的性能

如何告诉 MySQL 优化器在派生表上使用索引?

非结构化查询的性能优于集群、散列集群和索引?