Skip to content

为 TimescaleDB 选择最佳表配置

任务描述

对于市场平台来说,一个常见的任务是在近乎实时的情况下,计算某些产品在 1 天/7 天/30 天内的交易量。TimescaleDB 或其他时间序列数据库非常适合这种任务。由于我当前的项目已经在使用 PostgreSQL,所以我们来测试一下 TimescaleDB。我将使用我当前项目(NFT 市场平台)中的一个非常简化的模型。我们针对多个区块链拥有数千个集合,并在表中记录每一次列表/出价或销售交易。交易的总数非常庞大:数十亿行。我们可以通过将表按时间划分成桶并使用这些桶进行计算来加快计算速度。

准备工作

这是表的模式和一些索引:

sql
create table public.sale_changes
(
    time          timestampz not null,
    id            varchar(1024),
    collection_id varchar(512),
    blockchain    varchar(512),
    floor         numeric(78, 10),
    sales         bigint,
    volume        numeric(78, 10)
);
select create_hypertable('sale_changes', 'time', chunk_time_interval = > interval '1 day');
create unique index ON sale_changes(blockchain, id, time);
create index ON sale_changes(collection_id, time);
create * from add_dimension('sale_changes', by_hash('blockchain', 16));

我们创建了一些附加索引:

  • CREATE UNIQUE INDEX ON sale_changes(blockchain, id, time); -- 这是为了数据的一致性
  • CREATE INDEX ON sale_changes(collection_id, time); -- 这是为了加快集合的聚合速度

此外,我们还为提升区块链特定查询的性能添加了额外的维度。“16”是桶的数量,目前对于测试来说已经足够。

我备份了当前年的数据并在本地 TimescaleDB 中加载:

  • POLYGON: 4917155 行
  • ETHEREUM: 3032708 行
  • APTOS: 129760 行
  • CELO: 13692 行
  • ZKSYNC: 8832 行

所以我们本地有 8102147 (~800万) 行。

我们将使用以下两个查询进行性能测试:

sql
-- all query
select csc.*,
       cs.floor,
       cs.owners from (select collection_id,
       SUM(volume),
       SUM(sales)  AS total_sales,
       CASE
           WHEN first(floor, time) = 0 THEN 0
           ELSE ROUND(((last(floor, time) - first(floor, time)) / first(floor, time)) * 100, 2)
           END         AS floor_change
from sale_changes
where time >= NOW() - INTERVAL '$period'
group by collection_id
order by floor_change DESC
limit ?

-- Specific chain query:
select csc.*,
       cs.floor,
       cs.owners
from (select collection_id,
             SUM(volume),
             SUM(sales) AS total_sales,
             CASE
                 WHEN first(floor, time) = 0 THEN 0
          ELSE ROUND(((last (floor, time) - first (floor, time)) / first (floor, time)) * 100, 2)
          END AS floor_change
                           from $table
WHERE blockchain = ? and time >= NOW() - INTERVAL '$period'
group by collection_id
order by floor_change DESC
limit ?

测试

我们将通过每个查询执行 10 次,并收集有关执行时间的指标来测试性能。 我的本地环境:

  • MacBook M1,具有32 GB RAM
  • TimescaleDB 2.17.2-pg17,在 Docker 中运行
  • 为 PostgreSQL 增加了内存设置:shared_buffers=8GB, work_mem=8GB

第一尝试

以下是使用第一个查询的结果:

时间段限制最短时间 (ms)最长时间 (ms)平均时间 (ms)中位数时间 (ms)标准差 (ms)
1 小时1007110577.374.59.7
1 天100738176.075.02.4
7 天100161192168.1166.08.5
30 天100515630536.3526.532.2

以下是使用第二个查询的小区块链 CELO 结果:

时间段限制最短时间 (ms)最长时间 (ms)平均时间 (ms)中位数时间 (ms)标准差 (ms)
1 小时1001310123.715.025.8
1 天100139923.414.525.2
7 天100149924.515.524.9
30 天1001610426.117.526.0

以下是使用第二个查询的大区块链 ETHEREUM 结果:

时间段限制最短时间 (ms)最长时间 (ms)平均时间 (ms)中位数时间 (ms)标准差 (ms)
1 小时1002711437.829.525.5
1 天1002711237.028.025.0
7 天1006617279.668.530.9
30 天100206382251.6220.061.9

可以看到 30 秒间隔的查询没有其他查询快,除了 CELO 区块链外。这是意料之中的,因为数据库需要处理更多行。我们尝试不同的配置,看看能否改善性能。

第二尝试

让我们启用压缩。我们将使用 collection_id 作为压缩段,因为将这些数字保存在一起是有意义的。

sql
ALTER TABLE sale_changes SET (
    timescaledb.compress,
    timescaledb.compress_segmentby = 'collection_id'
    );
SELECT compress_chunk(c) from show_chunks('sale_changes') c;

这是使用第一个查询的结果:

时间段限制最短时间 (ms)最长时间 (ms)平均时间 (ms)中位数时间 (ms)标准差 (ms)
1 小时1002491091355.9261.5247.0
1 天100237286255.4251.015.4
7 天100247278259.4260.58.9
30 天100349412370.2358.521.0

这是使用第二个查询的小区块链 CELO 结果:

时间段限制最短时间 (ms)最长时间 (ms)平均时间 (ms)中位数时间 (ms)标准差 (ms)
1 小时1001913133.121.032.8
1 天1001812832.921.532.1
7 天1002112933.322.031.9
30 天1002515142.125.036.9

这是使用第二个查询的大区块链 ETHEREUM 结果:

时间段限制最短时间 (ms)最长时间 (ms)平均时间 (ms)中位数时间 (ms)标准差 (ms)
1 小时10090203115.799.034.6
1 天10094208110.599.532.8
7 天10095219111.198.536.3
30 天100149272170.7159.534.2

压缩表给我们带来了更糟糕的结果... 即使对于时间较短的查询也是如此,不过对于所有区块链的30天查询结果有所改善。我猜保持数字按 collection_id 整合在一起发挥了作用。

第三尝试

让我们尝试增大我们的超表的块大小,因为有充足的 RAM。按照官方推荐,块占内存的 25% 是个好选择。我目前为 Docker 分配了 24GB RAM。

sql
select create_hypertable('sale_changes', 'time', chunk_time_interval => interval '7 day');
CREATE UNIQUE INDEX ON sale_changes(blockchain, id, time);
CREATE INDEX ON sale_changes(collection_id, time);
SELECT * FROM add_dimension('sale_changes', by_hash('blockchain', 16));

这是使用第一个查询的结果:

时间段限制最短时间 (ms)最长时间 (ms)平均时间 (ms)中位数时间 (ms)标准差 (ms)
1 小时100106117.111.014.8
1 天100101811.611.02.2
7 天1008711991.588.09.3
30 天100367514390.4373.542.9

这是使用第二个查询的小区块链 CELO 结果:

时间段限制最短时间 (ms)最长时间 (ms)平均时间 (ms)中位数时间 (ms)标准差 (ms)
1 小时1002145.04.53.2
1 天1002125.04.02.7
7 天1003156.25.03.4
30 天1006157.67.02.6

这是使用第二个查询的大区块链 ETHEREUM 结果:

时间段限制最短时间 (ms)最长时间 (ms)平均时间 (ms)中位数时间 (ms)标准差 (ms)
1 小时1003145.95.02.8
1 天1005136.05.02.4
7 天100324737.634.55.4
30 天100129140133.3132.03.2

现在我们有了更好的结果!我不太喜欢对于30天期间的所有链查询。不过 CELO/ETHEREUM 查询非常快。

第四尝试

让我们尝试为7天块启用压缩。

这是使用第一个查询的结果:

时间段限制最短时间 (ms)最长时间 (ms)平均时间 (ms)中位数时间 (ms)标准差 (ms)
1 小时1004715360.349.531.0
1 天100506254.854.04.4
7 天100627367.167.03.6
30 天100139182150.4145.012.5

这是使用第二个查询的小区块链 CELO 结果:

时间段限制最短时间 (ms)最长时间 (ms)平均时间 (ms)中位数时间 (ms)标准差 (ms)
1 小时100183922.620.06.1
1 天100203625.123.05.7
7 天1006218.07.04.4
30 天1007219.48.04.0

这是使用第二个查询的大区块链 ETHEREUM 结果:

时间段限制最短时间 (ms)最长时间 (ms)平均时间 (ms)中位数时间 (ms)标准差 (ms)
1 小时100213423.823.03.5
1 天100235929.024.510.5
7 天100315838.236.57.4
30 天100597464.563.54.2

终于,我们的查询速度非常快。最慢的是 30 天全链查询,不过中位时间 <0.2s,所以我认为在数百万行的聚合查询中已经很快了。

图表

结论

让我们使用以下查询查看表大小:

sql
select 
    hypertable_name, 
    pg_size_pretty(hypertable_size(format('%I.%I', hypertable_schema, hypertable_name)::regclass))
from timescaledb_information.hypertables;
  • 1 天块表:2.4 Gb
  • 1 天块表(压缩):2.6 Gb
  • 7 天块表:2.2 Gb
  • 7 天块表(压缩):0.3 Gb

第一张表和最后一张表之间几乎有10倍的差异,这解释了为什么采用压缩的7天块表如此快速。 7 天块加压缩的配置明显是胜利者。