Skip to content

Choisir la meilleure configuration de table pour TimescaleDB

Description de la tâche

Il est assez courant pour les marketplaces de calculer le volume de certains produits sur une période de 1 jour/7 jours/30 jours en quasi temps réel. TimescaleDB ou toute autre base de données de séries temporelles est parfaitement adaptée à ce type de tâche. Testons TimescaleDB car mon projet actuel utilise déjà PostgreSQL. Je vais utiliser un modèle très simplifié de mon projet actuel (marketplace NFT). Nous avons des milliers de collections pour un ensemble de blockchains et nous collectons chaque liste/enchère ou transaction de vente dans la table. Le nombre total de transactions est énorme : des milliards de lignes. L'idée de la façon dont nous pouvons rendre le calcul rapide est de diviser notre table en segments temporels et de calculer le nombre en les utilisant.

Préparation

Voici un schéma de table et quelques index :

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

Nous avons créé quelques index supplémentaires :

  • CREATE UNIQUE INDEX ON sale_changes(blockchain, id, time); -- cela est nécessaire pour la cohérence des données
  • CREATE INDEX ON sale_changes(collection_id, time); -- cela permet une agrégation plus rapide par collection

De plus, nous avons ajouté une dimension supplémentaire pour améliorer les performances des requêtes spécifiques à chaque blockchain. 16 -- est le nombre de segments, actuellement suffisant pour les tests.

J'ai effectué une sauvegarde des données pour l'année en cours et je l'ai chargée dans mon TimescaleDB local :

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

Nous avons donc environ 8102147 (~8m) en local.

Utilisons deux requêtes pour le test de performance :

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 ?

Tests

Nous testerons la performance en exécutant chaque requête 10 fois et en collectant des métriques sur le temps d'exécution. Mon environnement local :

  • macbook m1 avec 32 Go de RAM
  • timescaleDB 2.17.2-pg17 dans docker
  • paramètres mémoire augmentés pour pg: shared_buffers=8GB, work_mem=8GB

Première tentative

Voici le résultat de l'utilisation de la première requête :

PériodeLimiteTemps Min (ms)Temps Max (ms)Temps Moy. (ms)Temps Médian (ms)Écart-type (ms)
1 heure1007110577.374.59.7
1 jour100738176.075.02.4
7 jours100161192168.1166.08.5
30 jours100515630536.3526.532.2

Voici le résultat de l'utilisation de la deuxième requête avec la petite blockchain CELO :

PériodeLimiteTemps Min (ms)Temps Max (ms)Temps Moy. (ms)Temps Médian (ms)Écart-type (ms)
1 heure1001310123.715.025.8
1 jour100139923.414.525.2
7 jours100149924.515.524.9
30 jours1001610426.117.526.0

Voici le résultat de l'utilisation de la deuxième requête avec la grande blockchain ETHEREUM :

PériodeLimiteTemps Min (ms)Temps Max (ms)Temps Moy. (ms)Temps Médian (ms)Écart-type (ms)
1 heure1002711437.829.525.5
1 jour1002711237.028.025.0
7 jours1006617279.668.530.9
30 jours100206382251.6220.061.9

Nous pouvons voir que les requêtes pour l'intervalle de 30 secondes ne sont pas aussi rapides que les autres, sauf pour la blockchain CELO. Cela est tout à fait attendu car la base de données doit traiter plus de lignes. Essayons une configuration différente et voyons si nous pouvons améliorer les performances.

Deuxième tentative

Activons la compression. Nous utiliserons collection_id comme segment de compression car il est logique de garder ces nombres ensemble.

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

Voici le résultat de l'utilisation de la première requête :

PériodeLimiteTemps Min (ms)Temps Max (ms)Temps Moy. (ms)Temps Médian (ms)Écart-type (ms)
1 heure1002491091355.9261.5247.0
1 jour100237286255.4251.015.4
7 jours100247278259.4260.58.9
30 jours100349412370.2358.521.0

Voici le résultat de l'utilisation de la deuxième requête avec la petite blockchain CELO :

PériodeLimiteTemps Min (ms)Temps Max (ms)Temps Moy. (ms)Temps Médian (ms)Écart-type (ms)
1 heure1001913133.121.032.8
1 jour1001812832.921.532.1
7 jours1002112933.322.031.9
30 jours1002515142.125.036.9

Voici le résultat de l'utilisation de la deuxième requête avec la grande blockchain ETHEREUM :

PériodeLimiteTemps Min (ms)Temps Max (ms)Temps Moy. (ms)Temps Médian (ms)Écart-type (ms)
1 heure10090203115.799.034.6
1 jour10094208110.599.532.8
7 jours10095219111.198.536.3
30 jours100149272170.7159.534.2

Les tables compressées nous ont donné des résultats moins bons... Même pour les requêtes avec de petits intervalles, cependant pour 30 jours dans le cas de toutes les blockchains, le résultat s'est amélioré. Je suppose que garder les nombres ensemble par collection_id a aidé ici.

Troisième tentative

Essayons d'augmenter le segment pour notre hypertable, car nous avons beaucoup de RAM. Suivant la recommandation officielle, il est bon si le segment peut être de 25% de la mémoire. J'ai actuellement 24 Go de RAM alloués à 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));

Voici le résultat de l'utilisation de la première requête :

PériodeLimiteTemps Min (ms)Temps Max (ms)Temps Moy. (ms)Temps Médian (ms)Écart-type (ms)
1 heure100106117.111.014.8
1 jour100101811.611.02.2
7 jours1008711991.588.09.3
30 jours100367514390.4373.542.9

Voici le résultat de l'utilisation de la deuxième requête avec la petite blockchain CELO :

PériodeLimiteTemps Min (ms)Temps Max (ms)Temps Moy. (ms)Temps Médian (ms)Écart-type (ms)
1 heure1002145.04.53.2
1 jour1002125.04.02.7
7 jours1003156.25.03.4
30 jours1006157.67.02.6

Voici le résultat de l'utilisation de la deuxième requête avec la grande blockchain ETHEREUM :

PériodeLimiteTemps Min (ms)Temps Max (ms)Temps Moy. (ms)Temps Médian (ms)Écart-type (ms)
1 heure1003145.95.02.8
1 jour1005136.05.02.4
7 jours100324737.634.55.4
30 jours100129140133.3132.03.2

Nous avons maintenant de bien meilleurs résultats ! Seule la requête pour toutes les chaînes pendant une période de 30 jours ne me plaît pas. Les requêtes CELO/ETHEREUM sont vraiment rapides.

Quatrième tentative

Essayons d'utiliser la compression pour le segment de 7 jours.

Voici le résultat de l'utilisation de la première requête :

PériodeLimiteTemps Min (ms)Temps Max (ms)Temps Moy. (ms)Temps Médian (ms)Écart-type (ms)
1 heure1004715360.349.531.0
1 jour100506254.854.04.4
7 jours100627367.167.03.6
30 jours100139182150.4145.012.5

Voici le résultat de l'utilisation de la deuxième requête avec la petite blockchain CELO :

PériodeLimiteTemps Min (ms)Temps Max (ms)Temps Moy. (ms)Temps Médian (ms)Écart-type (ms)
1 heure100183922.620.06.1
1 jour100203625.123.05.7
7 jours1006218.07.04.4
30 jours1007219.48.04.0

Voici le résultat de l'utilisation de la deuxième requête avec la grande blockchain ETHEREUM :

PériodeLimiteTemps Min (ms)Temps Max (ms)Temps Moy. (ms)Temps Médian (ms)Écart-type (ms)
1 heure100213423.823.03.5
1 jour100235929.024.510.5
7 jours100315838.236.57.4
30 jours100597464.563.54.2

Enfin, nos requêtes sont extrêmement rapides. La plus lente est toujours celle de 30 jours pour toutes les chaînes, cependant le temps médian est <0.2s donc je dirais que c'est rapide pour l'agrégation parmi des millions de lignes.

Graphiques

Conclusion

Jetons un coup d'œil aux tailles des tables à l'aide de la requête :

sql
select 
    hypertable_name, 
    pg_size_pretty(hypertable_size(format('%I.%I', hypertable_schema, hypertable_name)::regclass))
from timescaledb_information.hypertables;
  • Table chunk 1 jour : 2.4 Gb
  • Table chunk 1 jour avec compression : 2.6 Gb
  • Table chunk 7 jours : 2.2 Gb
  • Table chunk 7 jours avec compression : 0.3 Gb

Il y a presque 10x de différence entre la première table et la dernière, cela explique pourquoi la table avec segment de 7 jours et compression est si rapide. La configuration avec un segment de 7 jours et compression est le grand gagnant.