我有一个最终将包含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 |
... ... ... ... ...
打破这个问题,我们:
asn
和cty
)time
汇总这些值 - 计算加权指标time
asn
+ cty
对提供流量),那么使用之前的5分钟间隔值(使用用户定义的变量)*_quality
)我的目标是尽快获得此SELECT
查询。我可以改变:
SELECT
查询我无法改变:
我以前只使用了大约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秒
在一个理想的世界中,我希望这个查询在10秒内以100亿行的完整数据集返回 - 尽管如果不可能,那么在60秒内返回是可以接受的。
我能够计算预聚合值并将它们存储在单独的表中。我已经在很小程度上做到了这一点。我有三个表:metrics_by_asn
,metrics_by_cty
和metrics_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千兆字节。我还没有被告知我允许存储多少数据的严格限制,但为了安全起见,我试图保持在太字节之下。
答案 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, Really或youtube video。
您提到您无法更改MySQL配置选项,但您没有说明选项是什么。其中一个重要选项是InnoDB缓冲池大小。如果没有足够大小的缓冲池,您的查询将强制执行大量I / O操作,因为它会将索引页面交换到RAM中并再次退出。
我没有MariaDB专栏商店的经验,因此我无法评论其功能,或者如何监控或调整它。您可能希望参与MariaDB服务。
我同意James Scheller的回答,即预先汇总部分结果并将其存储很重要,并且可能是解决此问题的唯一方法。我读过的一些列存储会自动执行此操作,预先计算每个分区的各种聚合结果。我不知道MariaDB专栏商店做了什么。