提高MySQL SELECT性能

时间:2018-03-27 14:17:40

标签: mysql query-optimization

设置

我有一个最终将包含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,country或asn + country对返回每次的加权平均值。它看起来像这样:

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. 使用包含" threshold"的表格加入这些汇总结果每个给定时间的值(也许在凌晨5点我们认为100毫秒是非常糟糕的RTT,但在下午5点,当每个人都在观看Netflix我们认为100毫秒相当不错)
  4. time
  5. 排序
  6. 如果我们在给定时间内没有记录的指标(可能我们在5分钟的时间间隔内没有为asn + cty对提供流量),那么使用之前的5分钟间隔值(使用用户定义的变量)
  7. 计算相对善良"每个指标的值(*_quality
  8. 变量

    我的目标是尽快获得此SELECT查询。我可以改变:

    • 我的SELECT查询
    • 表架构
    • 表引擎(我可以访问MyISAM,InnoDB和MariaDB&#39的Columnstore)
    • 表索引
    • 分区

    我无法改变:

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

    以前的测试

    我以前只使用了大约1.5亿行进行了一些测试(最终数据集的1% - 包括300个不同的time值而不是完整的25920)并且看起来InnoDB是最快的 - 表现优异的Columnstore 3-4倍(InnoDB在大约0.7秒内返回数据,Columnstore大约需要2.5秒)。

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

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

    至少,我第一次针对特定asn + country运行查询时速度更快。 InnoDB似乎有中间缓存,因为我可以使用相同的asn + country运行其他查询,并在1秒内完成,但即使在完全相同的查询中运行Columnstore又花了30秒

    问题

    1. 为什么Columnstore比InnoDB更快,即使Columnstore不支持索引(因此我们必须扫描整个表)?
    2. 我在我的表定义或查询中做错了什么可能会减慢它的速度?感谢InnoDB的索引,我只需要从磁盘中读取少量行。我不知道为什么需要一分钟才能这样做
    3. 是否有人可以提供额外的性能提示,与前两个问题无关?
    4. 在一个理想的世界中,我希望这个查询在10秒内以100亿行的完整数据集返回 - 尽管如果不可能,那么在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 千兆字节。添加预先聚合的表和其他表,我们转储用于计算这些指标的原始数据,并且我们在磁盘上占用大约850千兆字节。我还没有被告知我允许存储多少数据的严格限制,但为了安全起见,我试图保持在太字节之下。

2 个答案:

答案 0 :(得分:1)

我曾经在一个系统上工作,这个系统汇总了电话的计费数据,每天有数亿个电话,所以我看到了类似于你所描述的内容。

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

总有一个甜蜜点。如果表格非常小​​或非常大,索引不一定很容易解决。

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

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

答案 1 :(得分: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

temp table / filesort消失了。这是因为一旦我将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, Reallyyoutube video

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

我没有MariaDB专栏商店的经验,因此我无法评论其功能,或者如何监控或调整它。您可能希望参与MariaDB服务。

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