【问题标题】:Improving MySQL SELECT performance提高 MySQL SELECT 性能
【发布时间】:2018-09-06 00:10:57
【问题描述】:

设置

我有一个最终将包含 130 亿行的数据库。这些行以 4 个值为键:(asn, cty (country), src (source), time)

asn 大约有 60,000 个不同的值,country 大约有 200 个不同的值,source 大约有 55 个不同的值——尽管并非所有三元组都有效。大约有 500,000 个有效的三元组。

对于每个有效的三元组,我每 5 分钟将数据记录到数据库中一次,time 是记录数据的时间。 90 天后,我们会彻底删除数据。这个产量12 (iterations per hour) * 24 (hours) * 90 (days) = 25920 rows per (asn, country, source) tuple

我的指标表目前如下所示:

create table `metrics` (
  `time` int(10) unsigned NOT NULL,
  `asn` int(10) unsigned NOT NULL,
  `cty` char(2) NOT NULL,
  `src` char(3) NOT NULL,
  `reqs` int(10) unsigned DEFAULT NULL,
  `rtt` float unsigned DEFAULT NULL,
  `rexb` float unsigned DEFAULT NULL,
  `nae` float unsigned DEFAULT NULL,
  `util` float unsigned DEFAULT NULL,
  PRIMARY KEY (`time`, `asn`, `cty`, `src`),
  KEY (`asn`, `cty`, `src`)
) ENGINE=InnoDB DEFAULT CHARACTER SET ascii
partition by range(time) (
  PARTITION start        VALUES LESS THAN (0),
  PARTITION from20171224 VALUES LESS THAN (UNIX_TIMESTAMP('2017-12-31')),
  PARTITION from20171231 VALUES LESS THAN (UNIX_TIMESTAMP('2018-01-07')),
  PARTITION from20180107 VALUES LESS THAN (UNIX_TIMESTAMP('2018-01-14')),
  PARTITION from20180114 VALUES LESS THAN (UNIX_TIMESTAMP('2018-01-21')),
  PARTITION from20180121 VALUES LESS THAN (UNIX_TIMESTAMP('2018-01-28')),
  PARTITION from20180128 VALUES LESS THAN (UNIX_TIMESTAMP('2018-02-04')),
  PARTITION from20180204 VALUES LESS THAN (UNIX_TIMESTAMP('2018-02-11')),
  PARTITION from20180211 VALUES LESS THAN (UNIX_TIMESTAMP('2018-02-18')),
  PARTITION from20180218 VALUES LESS THAN (UNIX_TIMESTAMP('2018-02-25')),
  PARTITION from20180225 VALUES LESS THAN (UNIX_TIMESTAMP('2018-03-04')),
  PARTITION from20180304 VALUES LESS THAN (UNIX_TIMESTAMP('2018-03-11')),
  PARTITION from20180311 VALUES LESS THAN (UNIX_TIMESTAMP('2018-03-18')),
  PARTITION from20180318 VALUES LESS THAN (UNIX_TIMESTAMP('2018-03-25')),
  PARTITION from20180325 VALUES LESS THAN (UNIX_TIMESTAMP('2018-04-01')),
  PARTITION future       VALUES LESS THAN MAXVALUE
);

我还有一个“阈值”表,它记录了在任何给定时间间隔内“好的 RTT”的样子和“坏 RTT”的样子:

create table `thresholds` (
  `time` int(10) unsigned NOT NULL,
  `rtt_good` float NOT NULL DEFAULT 0,
  `rtt_bad` float NOT NULL DEFAULT 100,
  `rexb_good` float NOT NULL DEFAULT 0,
  `rexb_bad` float NOT NULL DEFAULT 100,
  `nae_good` float NOT NULL DEFAULT 0,
  `nae_bad` float NOT NULL DEFAULT 100,
  `util_good` float NOT NULL DEFAULT 0,
  `util_bad` float NOT NULL DEFAULT 100,
  PRIMARY KEY (`time`)
) ENGINE=InnoDB
partition by range(time) (
  PARTITION start        VALUES LESS THAN (0),
  PARTITION from20171224 VALUES LESS THAN (UNIX_TIMESTAMP('2017-12-31')),
  PARTITION from20171231 VALUES LESS THAN (UNIX_TIMESTAMP('2018-01-07')),
  PARTITION from20180107 VALUES LESS THAN (UNIX_TIMESTAMP('2018-01-14')),
  PARTITION from20180114 VALUES LESS THAN (UNIX_TIMESTAMP('2018-01-21')),
  PARTITION from20180121 VALUES LESS THAN (UNIX_TIMESTAMP('2018-01-28')),
  PARTITION from20180128 VALUES LESS THAN (UNIX_TIMESTAMP('2018-02-04')),
  PARTITION from20180204 VALUES LESS THAN (UNIX_TIMESTAMP('2018-02-11')),
  PARTITION from20180211 VALUES LESS THAN (UNIX_TIMESTAMP('2018-02-18')),
  PARTITION from20180218 VALUES LESS THAN (UNIX_TIMESTAMP('2018-02-25')),
  PARTITION from20180225 VALUES LESS THAN (UNIX_TIMESTAMP('2018-03-04')),
  PARTITION from20180304 VALUES LESS THAN (UNIX_TIMESTAMP('2018-03-11')),
  PARTITION from20180311 VALUES LESS THAN (UNIX_TIMESTAMP('2018-03-18')),
  PARTITION from20180318 VALUES LESS THAN (UNIX_TIMESTAMP('2018-03-25')),
  PARTITION from20180325 VALUES LESS THAN (UNIX_TIMESTAMP('2018-04-01')),
  PARTITION future       VALUES LESS THAN MAXVALUE
);

查询

现在,我对此数据执行的最常见查询之一是返回给定 asn、国家或 asn+国​​家对的每次加权平均值。它看起来像这样:

SELECT
    t.time * 1000 as time,
    @rtt := coalesce(m_sum.weighted_rtt, @rtt) as rtt,
    floor(least(100, greatest(0,
        100 * (coalesce(m_sum.weighted_rtt, @rtt) - t.rtt_bad) / (t.rtt_good - t.rtt_bad)
    ))) as rtt_quality,
    @util := coalesce(m_sum.weighted_util, @util) as util,
    floor(least(100, greatest(0,
        100 * (coalesce(m_sum.weighted_util, @util) - t.util_bad) / (t.util_good - t.util_bad)
    ))) as util_quality
FROM
    thresholds as t
LEFT JOIN
    (
        SELECT
            m.time,
            sum(m.rtt*m.reqs)/sum(m.reqs) AS weighted_rtt,
            sum(m.util*m.reqs)/sum(m.reqs) AS weighted_util
        FROM metrics AS m
        WHERE m.asn = '7018' and m.cty = 'us'
        GROUP BY m.time
    ) AS m_sum ON t.time = m_sum.time
ORDER BY t.time asc;

它返回如下内容:

+---------------+---------+-------------+----------+--------------+
| time          | rtt     | rtt_quality | util     | util_quality |
+---------------+---------+-------------+----------+--------------+
| 1521234900000 | NULL    | NULL        | NULL     | NULL         |
| 1521235200000 | 45      | 80          | 3000     | 40           |
| 1521235500000 | 45      | 80          | 3000     | 40           |
| 1521235800000 | 65      | 70          | 2000     | 60           |
| 1521236100000 | 65      | 70          | 2000     | 60           |
| 1521236400000 | 65      | 70          | 2000     | 60           |
| 1521236700000 | 65      | 70          | 2000     | 60           |
| 1521237000000 | 120     | 50          | 4500     | 10           |
      ...           ...         ...         ...           ...

分解这个查询,我们:

  1. 仅过滤我们关心的行(在本例中基于 asncty
  2. 为每个 time 聚合这些值 - 计算加权指标
  3. 将这些汇总结果与包含每个给定时间的“阈值”值的表格相结合(也许在凌晨 5 点,我们认为 100 毫秒的 RTT 非常差,但在下午 5 点,当每个人都在观看 Netflix 时,我们认为 100 毫秒非常好)
  4. time排序
  5. 如果我们没有记录给定时间的指标(也许我们没有在该 5 分钟间隔内将流量传送到该 asn+cty 对)然后使用前 5 分钟间隔的值(使用用户定义的变量)
  6. 计算每个指标的“相对优度”值 (*_quality)

变量

我的目标是尽快得到这个SELECT 查询。我可以改变:

  • 我的SELECT查询
  • 表架构
  • 表引擎(我可以访问 MyISAM、InnoDB 和 MariaDB 的列存储)
  • 表索引
  • 分区

我无法改变:

  • 数据库服务器
  • 数据库配置

以前的测试

我之前只使用大约 1.5 亿行(最终数据集的 1% - 包括 300 个不同的 time 值而不是完整的 25920)进行了一些测试,看起来 InnoDB 是最快的 - 比列存储高 3 -4x(InnoDB 大约 0.7 秒返回数据,Columnstore 大约需要 2.5 秒)。

我相信这是真的,因为我们所做的第一件事是在完成任何聚合或其他工作之前过滤掉这 1.5 亿行中的大部分。 InnoDB 支持索引,这让我可以快速找到我想要过滤的行并且只使用这些行——从不从磁盘读取其他数据。

不过,这里有一个问题:我现在有 50 亿行(大约是最终数据集的 40%),并且我进行了相同的性能比较。这一次,Columnstore 似乎比 InnoDB 快 2 倍! (InnoDB 30 秒 vs 60 秒)

至少,当我第一次运行特定 asn+country 的查询时,它更快。 InnoDB 似乎有中间缓存,因为然后我可以使用相同的 asn+country 运行其他查询,它们在 1 秒内完成,但即使在 Columnstore 中运行 exact same 查询也需要另外 30秒

问题

  1. 为什么 Columnstore 比 InnoDB 更快,即使 Columnstore 不支持索引(因此我们必须扫描整个表)?
  2. 我是否在表定义或查询中做错了可能会减慢速度的问题?感谢 InnoDB 的索引,我应该只需要从磁盘读取少数行。我不知道为什么这样做需要一整分钟
  3. 是否有人可以提供与前两个问题无关的其他性能提示?

在理想情况下,我希望这个查询在 10 秒内返回完整的 130 亿行数据集 - 尽管如果这不可能,那么在 60 秒内返回是可以接受的。

补充说明

我有能力计算预聚合值并将它们存储在单独的表中。我已经在很小的程度上做到了这一点。我有三个表:metrics_by_asnmetrics_by_ctymetrics_by_time。前两个存储指标的加权平均值,并且仅在 (asn, time)(cty, time) 上键入。这有效地减少了这个查询:

SELECT
    m.time,
    sum(m.rtt*m.reqs)/sum(m.reqs) AS weighted_rtt,
    sum(m.util*m.reqs)/sum(m.reqs) AS weighted_util
FROM metrics AS m
WHERE m.asn = '7018'
GROUP BY m.time

到这个:

SELECT
    m.time,
    weighted_rtt,
    weighted_util
FROM metrics_by_asn AS m
WHERE m.asn = '7018'

第三张表metrics_by_time 返回汇总统计信息,如最大 RTT、平均 RTT、行数等。

我没有创建metrics_by_asn_and_cty 表有两个原因。首先,我没想到会看到令人难以置信的性能提升。平均而言,特定的 asn+cty 对仅从 1.3 个不同的来源提供服务。因此,大多数情况下,预先聚合它不会减少我们需要选择的行数。其次,我们已经达到了一些主要的磁盘使用限制。仅查看我们的指标表,我们就有 130 亿行乘以大约每行 35 个字节。这个数据库有 455 GB。添加预聚合表和其他表,我们在其中转储用于计算这些指标的原始数据,我们在磁盘上大约有 850 GB。我没有被告知我可以存储多少数据的硬性限制,但为了安全起见,我试图保持在 TB 以下。

【问题讨论】:

  • 错误:1521234900000 不适合 INT??
  • time 总是正好在 5 分钟标记上吗?
  • @RickJames 1521234900000 是存储值乘以 1000 的结果(如 select 语句所示)。除以 1000 得到实际存储在 int 中的内容:1521234900 - 一个有效的 unix 时间戳。
  • @RickJames time 总是精确到 5 分钟。将此数据放入数据库的代码在保存之前四舍五入到最接近的 300 倍数
  • 只要你在玩 1000 的因子,你还不如再扔 300。这会让你把列缩小到 3 个字节 -- MEDIUMINT UNSIGNED

标签: mysql query-optimization


【解决方案1】:

您在帖子中显示了 CREATE TABLE,这很好,但您没有提及任何其他查询分析。在调查查询优化时,您应该考虑:

我尝试至少为您的子查询测试 EXPLAIN。顺便说一句,pop 列在您的索引中被提及但未出现在您的表中,因此您还没有发布真正的 CREATE TABLE。

我知道了:

mysql> EXPLAIN SELECT m.time, sum(m.rtt*m.reqs)/sum(m.reqs) AS weighted_rtt, 
sum(m.util*m.reqs)/sum(m.reqs) AS weighted_util FROM metrics AS m 
WHERE m.asn = '7018' and m.cty = 'us' GROUP BY m.time\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: m
         type: ref
possible_keys: PRIMARY,asn,bk1
          key: asn
      key_len: 6
          ref: const,const
         rows: 1
        Extra: Using index condition; Using where; Using temporary; Using filesort

请注意,仅使用 asn 索引的前两列,如 const,const 所示。此外,Using temporary; Using filesort 通常表示查询的开销很大。

添加索引后我变得更好了:

mysql> alter table metrics add index bk1 (asn,cty,time);

我不得不使用索引提示来说服 MySQL 优化器使用我的索引。这可能只是因为我的表中没有数据行,所以优化器无法分析哪个索引更好。

mysql> EXPLAIN SELECT m.time, sum(m.rtt*m.reqs)/sum(m.reqs) AS weighted_rtt, 
sum(m.util*m.reqs)/sum(m.reqs) AS weighted_util FROM metrics AS m use index(bk1) 
WHERE m.asn = '7018' and m.cty = 'us' GROUP BY m.time\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: m
         type: ref
possible_keys: PRIMARY,asn,bk1
          key: bk1
      key_len: 6
          ref: const,const
         rows: 1
        Extra: Using index condition; Using where

临时表/文件排序不见了。这是因为一旦我将time 列放在用于过滤的两列之后,GROUP BY 就可以按索引顺序执行。

最后我尝试创建一个包含子查询中引用的所有列的索引:

mysql> alter table metrics add index bk2 (asn,cty,time,rtt,reqs,util);

mysql> EXPLAIN SELECT m.time, sum(m.rtt*m.reqs)/sum(m.reqs) AS weighted_rtt, 
sum(m.util*m.reqs)/sum(m.reqs) AS weighted_util FROM metrics AS m use index(bk2) 
WHERE m.asn = '7018' and m.cty = 'us' GROUP BY m.time\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: m
         type: ref
possible_keys: PRIMARY,asn,bk1,bk2
          key: bk2
      key_len: 6
          ref: const,const
         rows: 1
        Extra: Using where; Using index

Using index 是个好兆头。这称为“覆盖索引”,这意味着查询只需读取索引即可获得所需的所有列,而根本无需读取表。这是一个有用的技术。

您可能会喜欢我的演示文稿How to Design Indexes, Really,或youtube video

您提到您无法更改 MySQL 配置选项,但您没有说明选项是什么。重要的选项之一是 InnoDB 缓冲池大小。如果没有足够大小的缓冲池,您的查询将强制执行大量 I/O,因为它会将索引页面交换到 RAM 中并再次退出。

我没有使用 MariaDB 列存储的经验,所以我无法评论它的功能,或者如何监控或调整它。您可能想使用 MariaDB 服务。

我同意 James Scheller 的回答,即预先汇总部分结果并将其存储很重要,并且可能是解决此问题的唯一方法。我读过的一些列存储会自动执行此操作,预先计算每个分区的各种聚合结果。我不知道 MariaDB 列存储是做什么的。

【讨论】:

  • 关于“pop”列,这是我的复制/粘贴错误。 “pop”曾一度重命名为“src”(pop = 存在点,我们的数据中心之一)——我的桌面上有无数文本文件,其中包含创建表查询和选择查询,因为我一直在试验和开发而且它们还没有全部更新为新的列名
  • 感谢您提供的所有重要提示。我现在正在努力尝试实现它们,看看我得到了什么样的性能改进。不幸的是,有 50 亿行数据几乎不可能(也许实际上不可能?)删除索引并创建一个新索引。所以我可能不得不制作一个全新的表,开始在那里转储数据一周,然后在一周后看看它的表现
  • 不是不可能,除非你没有足够的磁盘空间。我支持一些超过 2TB 的 MySQL 数据库。
  • 我已经更新了脚本上的配置文件以开始将数据转储到新表(使用新索引),并开始运行create index 查询以更新旧表。现在已经持续了 12 分钟 - 不知道需要多长时间。我们将在我被踢出 VPN 或我的连接中断之前查看它是否完成。在最坏的情况下,我将数据过滤到新表中,因此几天后我可以测量性能。我还将监控写入性能,看看更大的索引是否在这方面花费了我太多。
  • 事实证明,尽管已断开连接,但该语句仍在后台运行。我今天注意到新索引已经到位。我测试了我的 select 语句,使用新的覆盖索引它从 60 秒下降到 1.3!
【解决方案2】:

我曾经在一个系统上工作,该系统可以汇总每天数亿次电话通话的计费数据,所以我看到了与您所描述的类似的情况。

基于树的索引的部分问题在于,当您在表中获得大量行时,索引本身会变得非常大和深。即使您的索引键相当紧凑,您也可以创建一个非常深(并且可以量化的大)节点集,必须遍历这些节点才能导航索引以查找表行。这可能涉及比您预期更多的磁盘和内存带宽,并且如果索引本身比实际数据大得多,那么它的性能可能会比盲目读取表的性能更差。

总有一个甜蜜点。如果一个表非常小或非常大,索引不一定是一个简单的修复。

对于这个电信计费应用程序,我们绝对必须预先汇总数据。事实上,我们在具有不同标准的多层中有效地进行了处理,以便应用程序中的报告层可以根据不同业务案例所需的任何标准(按地理、业务合作伙伴等)有效地获取数据。这些表足够小(数十万行),传统索引非常有效。

但是,在那个业务案例中,我们进行了大量的批量更新,因此我们将处理数千行,并且可以在该过程中在内存中聚合大量数据,然后只对表进行相对少量的更新跟踪聚合。它非常有效,但非常适合这种用法。

【讨论】:

  • 这个问题是基于树的索引独有的吗?如果 InnoDB 支持哈希索引(我不确定它是否支持,但我现在会查找它)我可以更改我的索引类型。我没有真正的理由需要使用 B 树。我没有按 ASN 或任何东西排序。
  • @stevendesu,我不确定 InnoDB 作为引擎会是什么样子。那个电信的东西用的是甲骨文。可能是其他一些索引架构足够高效,但您的测试数据听起来令人难以忘怀......
猜你喜欢
  • 2016-08-25
  • 2016-04-19
  • 1970-01-01
  • 1970-01-01
  • 2014-06-11
  • 1970-01-01
  • 2016-10-07
  • 2012-04-11
相关资源
最近更新 更多