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

hob*_*ave 21 mysql sql optimization derived-table query-optimization

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

我的问题

如何优化此查询以最大限度地减少停机时间?我需要更新50多个模式,门票数量从100,000到200万不等.是否可以尝试同时在tickets_extra中设置所有字段?我觉得这里有一个解决方案,我只是没有看到.我一直在打击这个问题超过一天.

另外,我最初尝试不使用子SELECT,但表现得太多比我现在有更坏.

背景

我正在尝试优化我的数据库以获取需要运行的报告.我需要聚合的字段计算起来非常昂贵,因此我对现有模式进行了非规范化以适应此报告.请注意,通过删除几十个不相关的列,我简化了故障单表.

我的报告将按创建时管理器解析后管理器聚合票证计数.这个复杂的关系如下图所示:

EAV http://cdn.cloudfiles.mosso.com/c163801/eav.png

为了避免在运行中计算这个需要的六个令人讨厌的连接,我已经将以下表添加到我的模式中:

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)
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

此查询需要一个多小时才能在具有> 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)
Run Code Online (Sandbox Code Playgroud)

非常感谢您的阅读!

hob*_*ave 13

好吧,我找到了解决方案.这需要大量的实验,我认为这是一个很好的盲目运气,但这里是:

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;
Run Code Online (Sandbox Code Playgroud)

冗长的解释

现在,我将解释为什么这样做,以及我的相对过程和步骤来到这里.

首先,我知道我正在尝试的查询因为巨大的派生表而受到影响,以及随后的JOIN到此.我正在使用我的索引良好的票证表并将所有shift_times数据加入其中,然后让MySQL在尝试加入shift和shift_positions表时咀嚼它.这个衍生出来的庞然大物将达到200万行无法估量的混乱.

现在,我知道这种情况正在发生.我之所以走这条路,是因为采用"JOINs"的"正确"方式,需要花费更长的时间.这是由于确定给定班次的经理是谁所需的一些混乱.我必须加入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 magic ENGINE = MEMORY的用武之地.

有了这个伪临时表,我尝试了另一个manager_created的JOIN.它表现不错,但仍然很慢.然而,当我再次加入以在同一查询中获得manager_resolved时,查询时间又回到了平流层.查看EXPLAIN显示了票据的全表扫描(行~2mln),如预期的那样,以及每个~2087的魔术表上的JOIN.再一次,我似乎陷入了失败.

我现在开始考虑如何完全避免JOINs,当我发现一些不起眼的古老留言板帖子时,有人建议使用子选择(在我的历史记录中找不到链接).这导致了上面显示的第二个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)
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

现在THAT'S我对你说一下!

结论

这绝对是我第一次动态创建非TEMPORARY表,并在运行中对其进行索引,只是为了有效地进行单个查询.我想我一直认为在运行中添加索引是一项非常昂贵的操作.(在我的票证表上添加2mln行的索引可能需要一个多小时).然而,仅仅3000行,这就是一个小路.

不要害怕相关的SUBQUERIES,创建真正没有的TEMPORARY表,即时索引或外星人.在适当的情况下,它们都可以成为好事.

感谢StackOverflow的所有帮助.:-D