Skip to content

Escolhendo a Melhor Configuração de Tabela para TimescaleDB

Descrição da Tarefa

É comum em marketplaces a necessidade de calcular o volume de alguns produtos em um período de 1 dia/7 dias/30 dias em quase tempo real. TimescaleDB ou qualquer outro banco de dados de séries temporais é perfeitamente adequado para esse tipo de tarefa. Vamos testar o TimescaleDB, pois em meu projeto atual já está sendo utilizado PostgreSQL. Vou usar um modelo muito simplificado do meu projeto atual (marketplace de NFTs). Temos milhares de coleções para vários blockchains e coletamos cada lista/lance ou transação de venda na tabela. O número total de transações é enorme: bilhões de linhas. A ideia de como podemos tornar esse cálculo rápido é dividir nossa tabela em buckets por tempo e calcular os números usando-os.

Preparação

Aqui está o esquema da tabela e alguns índices:

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));

Criamos alguns índices adicionais:

  • CREATE UNIQUE INDEX ON sale_changes(blockchain, id, time); -- isso é necessário para a consistência dos dados
  • CREATE INDEX ON sale_changes(collection_id, time); -- isso é para uma agregação mais rápida por coleção

Além disso, adicionamos uma dimensão adicional para melhorar o desempenho das consultas específicas de blockchain. 16 -- é o número de buckets, atualmente é suficiente para os testes.

Eu fiz um backup com dados do ano atual e carreguei no meu TimescaleDB local:

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

Então, temos 8102147 (~8m) localmente.

Vamos usar duas consultas para testes de desempenho:

sql
-- consulta geral
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 ?

-- Consulta de chain específica:
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 ?

Testes

Vamos testar o desempenho executando cada consulta 10 vezes e coletando métricas sobre o tempo de execução. Meu ambiente local:

  • macbook m1 com 32 GB RAM
  • timescaleDB 2.17.2-pg17 em docker
  • configurações de memória aumentadas para pg: shared_buffers=8GB, work_mem=8GB

Primeira Tentativa

Este é o resultado usando a primeira consulta:

PeríodoLimiteTempo Mín (ms)Tempo Máx (ms)Tempo Méd (ms)Mediana (ms)Desvio Padrão (ms)
1 hora1007110577.374.59.7
1 dia100738176.075.02.4
7 dias100161192168.1166.08.5
30 dias100515630536.3526.532.2

Este é o resultado usando a segunda consulta com o blockchain pequeno CELO:

PeríodoLimiteTempo Mín (ms)Tempo Máx (ms)Tempo Méd (ms)Mediana (ms)Desvio Padrão (ms)
1 hora1001310123.715.025.8
1 dia100139923.414.525.2
7 dias100149924.515.524.9
30 dias1001610426.117.526.0

Este é o resultado usando a segunda consulta com o grande blockchain ETHEREUM:

PeríodoLimiteTempo Mín (ms)Tempo Máx (ms)Tempo Méd (ms)Mediana (ms)Desvio Padrão (ms)
1 hora1002711437.829.525.5
1 dia1002711237.028.025.0
7 dias1006617279.668.530.9
30 dias100206382251.6220.061.9

Podemos ver que as consultas para intervalos de 30 segundos não são tão rápidas quanto em outros, exceto no blockchain CELO. Isso é absolutamente esperado porque o banco de dados precisa processar mais linhas. Vamos tentar uma configuração diferente para ver se podemos melhorar o desempenho.

Segunda Tentativa

Vamos ativar a compressão. Usaremos collection_id como um segmento de compressão porque faz sentido manter esses números juntos.

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

Este é o resultado usando a primeira consulta:

PeríodoLimiteTempo Mín (ms)Tempo Máx (ms)Tempo Méd (ms)Mediana (ms)Desvio Padrão (ms)
1 hora1002491091355.9261.5247.0
1 dia100237286255.4251.015.4
7 dias100247278259.4260.58.9
30 dias100349412370.2358.521.0

Este é o resultado usando a segunda consulta com o blockchain pequeno CELO:

PeríodoLimiteTempo Mín (ms)Tempo Máx (ms)Tempo Méd (ms)Mediana (ms)Desvio Padrão (ms)
1 hora1001913133.121.032.8
1 dia1001812832.921.532.1
7 dias1002112933.322.031.9
30 dias1002515142.125.036.9

Este é o resultado usando a segunda consulta com o grande blockchain ETHEREUM:

PeríodoLimiteTempo Mín (ms)Tempo Máx (ms)Tempo Méd (ms)Mediana (ms)Desvio Padrão (ms)
1 hora10090203115.799.034.6
1 dia10094208110.599.532.8
7 dias10095219111.198.536.3
30 dias100149272170.7159.534.2

A tabela comprimida nos deu resultados piores... Mesmo para consultas de pequeno intervalo, no entanto, para 30 dias, no caso de todos os blockchains, melhorou o resultado. Acho que manter os números juntos por coleção ajudou aqui.

Terceira Tentativa

Vamos tentar aumentar o chunk para nossa hypertable, já que temos bastante RAM. Seguindo a recomendação oficial, é bom se o chunk puder representar 25% da memória. Atualmente, tenho 24GB de RAM alocados para o docker.

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));

Este é o resultado usando a primeira consulta:

PeríodoLimiteTempo Mín (ms)Tempo Máx (ms)Tempo Méd (ms)Mediana (ms)Desvio Padrão (ms)
1 hora100106117.111.014.8
1 dia100101811.611.02.2
7 dias1008711991.588.09.3
30 dias100367514390.4373.542.9

Este é o resultado usando a segunda consulta com o blockchain pequeno CELO:

PeríodoLimiteTempo Mín (ms)Tempo Máx (ms)Tempo Méd (ms)Mediana (ms)Desvio Padrão (ms)
1 hora1002145.04.53.2
1 dia1002125.04.02.7
7 dias1003156.25.03.4
30 dias1006157.67.02.6

Este é o resultado usando a segunda consulta com o grande blockchain ETHEREUM:

PeríodoLimiteTempo Mín (ms)Tempo Máx (ms)Tempo Méd (ms)Mediana (ms)Desvio Padrão (ms)
1 hora1003145.95.02.8
1 dia1005136.05.02.4
7 dias100324737.634.55.4
30 dias100129140133.3132.03.2

Agora temos resultados muito melhores! Não gosto apenas da consulta para todos os chains para o período de 30 dias. As consultas CELO/ETHEREUM são realmente rápidas.

Quarta Tentativa

Vamos tentar usar compressão para bucket de 7 dias.

Este é o resultado usando a primeira consulta:

PeríodoLimiteTempo Mín (ms)Tempo Máx (ms)Tempo Méd (ms)Mediana (ms)Desvio Padrão (ms)
1 hora1004715360.349.531.0
1 dia100506254.854.04.4
7 dias100627367.167.03.6
30 dias100139182150.4145.012.5

Este é o resultado usando a segunda consulta com o blockchain pequeno CELO:

PeríodoLimiteTempo Mín (ms)Tempo Máx (ms)Tempo Méd (ms)Mediana (ms)Desvio Padrão (ms)
1 hora100183922.620.06.1
1 dia100203625.123.05.7
7 dias1006218.07.04.4
30 dias1007219.48.04.0

Este é o resultado usando a segunda consulta com o grande blockchain ETHEREUM:

PeríodoLimiteTempo Mín (ms)Tempo Máx (ms)Tempo Méd (ms)Mediana (ms)Desvio Padrão (ms)
1 hora100213423.823.03.5
1 dia100235929.024.510.5
7 dias100315838.236.57.4
30 dias100597464.563.54.2

Finalmente, nossas consultas estão extremamente rápidas. A mais lenta ainda é a de 30 dias para todos os chains, porém o tempo mediano é <0,2s, então eu diria que é rápida para agregação entre milhões de linhas.

Gráficos

Conclusão

Vamos analisar o tamanho das tabelas usando a consulta:

sql
select 
    hypertable_name, 
    pg_size_pretty(hypertable_size(format('%I.%I', hypertable_schema, hypertable_name)::regclass))
from timescaledb_information.hypertables;
  • tabela de chunk de 1 dia: 2.4 Gb
  • tabela de chunk de 1 dia com compressão: 2.6 Gb
  • tabela de chunk de 7 dias: 2.2 Gb
  • tabela de chunk de 7 dias com compressão: 0.3 Gb

Há uma diferença de quase 10 vezes entre a primeira tabela e a última, o que explica por que a tabela de bucket de 7 dias com compressão é tão rápida. A configuração com chunk de 7 dias e compressão é a vencedora clara.