【问题标题】:How to improve performance of an `INSERT INTO SELECT x FROM y GROUP BY z` MySQL query如何提高`INSERT INTO SELECT x FROM y GROUP BY z` MySQL 查询的性能
【发布时间】:2017-07-18 20:30:19
【问题描述】:

我有以下查询导致 CPU 使用率达到最大值(严重降低服务器上运行的其他所有内容的性能)并且需要几分钟才能运行。

INSERT INTO cache (`time`, name, price, low, high, week, month, season)
SELECT
    MAX(`time`) AS `time`,
    name,
    MIN(CASE WHEN `time` = 1500254967 THEN price ELSE 999999 END) AS price,
    MIN(price) AS low,
    MAX(price) AS high,
    SUM(CASE WHEN `time` > 1499650167 AND price = 1 THEN 1 ELSE 0 END) / SUM(CASE WHEN `time` > 1499650167 THEN 1 ELSE 0 END) AS week,
    SUM(CASE WHEN `time` > 1497835767 AND price = 1 THEN 1 ELSE 0 END) / SUM(CASE WHEN `time` > 1497835767 THEN 1 ELSE 0 END) AS month,
    SUM(CASE WHEN `time` > 1499995767 AND price = 1 THEN 1 ELSE 0 END) / SUM(CASE WHEN `time` > 1499995767 THEN 1 ELSE 0 END) AS season
FROM low_price
GROUP BY name;

解释告诉我:

+----+-------------+-----------+-------+---------------+----------+---------+------+----------+-------+
| id | select_type | table     | type  | possible_keys | key      | key_len | ref  | rows     | Extra |
+----+-------------+-----------+-------+---------------+----------+---------+------+----------+-------+
|  1 | SIMPLE      | low_price | index | idx_name      | idx_name | 603     | NULL | 20875117 | NULL  |
+----+-------------+-----------+-------+---------------+----------+---------+------+----------+-------+

在我的非专家眼中看起来不错。

low_price 表中有 1200 万行可供选择。理想情况下,应该允许这个表从这里开始增长,但可以将它修剪到这个大小作为最大值。使其更小将导致数据丢失。我们可以每天一次而不是每小时一次来获取数据,以使其大小变为原来的 1/24,但如果有其他方法,我不希望这样做。

表定义为:

CREATE TABLE `low_price` (
  `time` int(11) DEFAULT NULL,
  `price` int(11) DEFAULT NULL,
  `name` varchar(150) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

CREATE TABLE `cache` (
  `time` int(11) DEFAULT NULL,
  `name` varchar(150) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `high` int(11) DEFAULT NULL,
  `low` int(11) DEFAULT NULL,
  `price` int(11) DEFAULT NULL,
  `week` double DEFAULT NULL,
  `month` double DEFAULT NULL,
  `season` double DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

此查询不会在用户时间运行。它正在设置在用户时间使用的缓存表。因此,加快速度的最佳选择的替代方法就是告诉它“嘿,不要使用这么多 CPU”。我有点讨厌走那条路,因为它所花费的时间已经很长了,以至于它会开始在每小时运行时自行运行。

你对我有什么建议吗?生成此表的替代样式或改进此特定 INSERT 的注意事项?谢谢!

【问题讨论】:

  • 我会尝试在没有 CASE 语句的情况下运行它,看看你得到了多少改进。
  • 谢谢。由于某种原因,删除 CASE 似乎没有多大帮助。可能是因为 MAX 和 MIN 仍然导致我扫描所有数据?以下答案中建议的索引确实有帮助。
  • @JeffUK - 到目前为止,获取行是任何查询的主要部分。这个查询需要获取所有行。

标签: mysql sql database performance


【解决方案1】:

(name, time, price) 上的复合索引会有所帮助,因为它是一个覆盖索引。

我还将查询写为:

INSERT INTO cache (`time`, name, price, low, high, week, month, season)
    SELECT MAX(`time`) AS `time`,
           name,
           MIN(CASE WHEN `time` = 1500254967 THEN price END) AS price,
           MIN(price) AS low, MAX(price) AS high,
           AVG(CASE WHEN `time` > 1499650167 AND price = 1 THEN 1.0
                    WHEN `time` > 1499650167 THEN 0
               END) AS week,
           AVG(CASE WHEN `time` > 1497835767 AND price = 1 THEN 1.0
                    WHEN `time` > 1497835767 THEN 0.0
               END) AS month,
           AVG(CASE WHEN `time` > 1499995767 AND price = 1 THEN 1.0
                    WHEN `time` > 1499995767 THEN 0.0
               END) AS season
    FROM low_price
    GROUP BY name;

这些变化对性能没有太大帮助;我只是觉得AVG() 比两个SUM()s 简单,而且我不喜欢像999999 这样的任意值。

【讨论】:

  • 完全同意 999999。简洁的省略。
  • 您重写查询以使用 AVG(感谢您指出这一点,doh!)似乎在从索引中获得巨大收益后又节省了大约 15% 的时间。谢谢!
【解决方案2】:

很难准确,但我会尝试使用以下键在 low_price 表上再定义一个(当然不是主键)索引:

name,
time,
price

这可以帮助基于时间和价格的子查询。
提醒一下,索引中的键顺序很重要。

【讨论】:

  • 太棒了! EXPLAIN 告诉我它仍然选择在名称上使用索引,但在我放弃后它的速度提高了大约 5 倍! :D 如果您能解释一下您是如何计算出按照索引的顺序查看字段的,那就太好了?
  • name 首先用于 GROUP BY 子句,然后时间用于每个组内的聚合函数,特别是改进 MAX 和 MIN 函数。只是出于好奇,您可以交换时间和价格,看看什么是最好的。
  • 谢谢!第二个看起来非常接近,但速度稍慢。
  • 生产加速看起来更像是 16 倍!
  • 定时查询时,运行两次——第一次运行可能需要从磁盘获取数据;第二个将尽可能多地缓存。第二次引用。
【解决方案3】:

innodb_buffer_pool_size 应设置为可用 RAM 的大约 70%。在此之下,您可能会受到 I/O 限制(慢 10 倍)。在那之后,你就有被交换的风险(这更糟)。

您可以缩小数据集以节省空间,从而减少 I/O。

  • DOUBLE 占用 8 个字节——矫枉过正。请改用具有 7 位精度的 FLOAT(4 字节)。
  • INT 占用 4 个字节,而且是多余的;没有代码超过 16M 是吗? MEDIUMINT UNSIGNED 是 3 个字节,允许 0..16M。你删掉小数部分了吗?如果你想要分数,那么FLOAT 可能是最好的。
  • 股票代码都是 ascii,当然不是 150 个字符。也许最多 8 个?即使这是全名,也将名称“规范化”到另一个表中,并将 JOIN 放在股票代码上。
  • InnoDB 确实需要一个独特的PRIMARY KEY。对于low_price,它可能应该是PRIMARY KEY(name, time),然后将KEY(name) 作为多余的。 cache 可能也需要。

我对查询中的时间戳感到困扰。它们似乎是像2017-07-09 18:29:27 这样的随机时间。您不想从整点回来吗?

而且,一天真的有 24 小时吗?市场关闭很多。

在你解决了这些问题之后,我们可以讨论如何编写一个汇总表,让你摆脱 cache 并进行实时查询!

【讨论】:

  • 谢谢!市场是 24 小时开放的——万智牌。名称最长可达 LENGTH(“我们的市场研究表明玩家喜欢真正长的卡名,因此我们制作了这张卡以拥有有史以来最长的卡名”),但唯一的子字符串会更小。有些名字有变音符号。时间戳是“上次运行时间”、“一周前”、“一个月前”和“赛季开始”(固定日期 - 每季度更改)。对于 DOUBLE,我几乎不需要任何精度 - 一位小数?我可以在那里节省更多吗?存储为某种小整数?我会尝试主键的事情。再次感谢!
  • 好吧,我做了一个错误的假设。 name 的标准化仍然是明智的。
  • DECIMAL(7,1) 占用 4 个字节;你能拥有的最大数字是多少?也可以使用FLOAT 4 个字节,最大约为 10^38。
  • 太棒了。虽然这些显然没有对获得正确的索引产生巨大的影响,但我确实从实施它们中获得了总共 12.5% 的本地加速。它似乎对目前需要 45 秒的 prod 没有太大影响。汇总表还合适吗?
  • 在挖掘汇总表之前,... 每个卡名每天有多少行?如果每天有很多,则将每天汇总到一个表中。如果每天 1 次,则每周和每月单独汇总。你如何定义“季节”?
猜你喜欢
  • 2020-02-08
  • 2011-12-24
  • 1970-01-01
  • 2019-06-26
  • 1970-01-01
  • 2021-04-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多