21

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

我的问题

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

此外,我最初尝试不使用子 SELECT,但性能比我目前拥有的要差得多。

背景

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

我的报告将按Manager When CreatedManager When Resolved汇总票数。这种复杂的关系如下图所示:

EAV
(来源: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)

非常感谢您的阅读!

4

4 回答 4

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;

冗长的解释

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

首先,我知道我正在尝试的查询因为巨大的派生表而受到影响,以及随后的 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 的方式相当简单。它根本没有实现它们,它只是运行您用来为您获取视图的查询。因此,通过加入票证,我基本上是在运行我的原始查询 - 没有任何改进。

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

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

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

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

确认,可怕的依赖子查询。通常建议避免这些,因为 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)

这就是我要说的

结论

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

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

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

于 2009-07-26T01:19:45.733 回答
2

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

反正。

=> 是 SELECT 还是 INSERT 的问题?

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

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

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

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

如果您要进行数百万次随机 HDD 寻道,您就处于痛苦之中。

=> OK 现在查询

首先,分析您的表格。

左连接 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) 创建多列索引

于 2009-07-25T10:42:00.800 回答
0

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

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 表时,按主键顺序执行此操作至关重要(否则对于大型数据集,速度会慢几个数量级)。

于 2009-07-25T02:11:29.123 回答
0

关于 BETWEEN

SELECT * FROM a WHERE a.column BETWEEN x AND y 
  • 是可索引的,并且对应于索引 a.column 上的范围查找(如果有的话)
  • 100% 相当于a.column >= x AND a.column <= y

虽然这样:

SELECT * FROM a WHERE somevalue BETWEEN a.column1 AND a.column2
  • 100% 相当于somevalue >= a.column1 AND somevalue <= a.column2
  • 与上面的第一个非常不同
  • 不能通过范围查找来索引(没有范围,这里有 2 列)
  • 通常会导致糟糕的查询性能

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

OP有第一种,所以不用担心。

于 2009-07-25T21:57:39.740 回答