Skip to content

Elegir la mejor configuración de tabla para timescaleDB

Descripción de la tarea

Existe una tarea bastante común para los mercados de calcular el volumen de algunos productos en un período de 1 día/7 días/30 días en tiempo casi real. TimescaleDB o cualquier otra base de datos de series temporales es perfectamente adecuada para este tipo de tarea. Vamos a probar TimescaleDB, ya que en mi proyecto actual ya se está usando PostgreSQL. Utilizaré un modelo muy simplificado de mi proyecto actual (mercado NFT). Tenemos miles de colecciones para un grupo de blockchains y recopilamos cada lista/oferta o transacción de venta en la tabla. El número total de transacciones es enorme: miles de millones de filas. La idea de cómo podemos hacer que el cálculo sea rápido es dividir nuestra tabla en bloques por tiempo y calcular el número utilizando esos bloques.

Preparación

Esta es un esquema de la tabla y algunos í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));

Creamos algunos índices adicionales:

  • CREATE UNIQUE INDEX ON sale_changes(blockchain, id, time); -- esto es necesario para la consistencia de los datos
  • CREATE INDEX ON sale_changes(collection_id, time); -- esto es para una agregación más rápida por colección

Además, añadimos una dimensión adicional para un mejor rendimiento en consultas específicas de blockchain. 16 -- es el número de bloques, actualmente es suficiente para las pruebas.

Hice un respaldo con los datos del año actual y lo cargué en mi timescaleDB local:

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

Así que tenemos 8102147 (~8m) localmente.

Utilizaremos estas 2 consultas para probar el rendimiento:

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 ?

Pruebas

Vamos a probar el rendimiento ejecutando cada consulta 10 veces y recopilando métricas sobre el tiempo de ejecución. Mi ambiente local:

  • macbook m1 con 32 GB de RAM
  • timescaleDB 2.17.2-pg17 en docker
  • configuraciones de memoria aumentadas para pg: shared_buffers=8GB, work_mem=8GB

Primer intento

Este es el resultado de usar la primera consulta:

PeriodoLímiteTiempo Mínimo (ms)Tiempo Máximo (ms)Tiempo Promedio (ms)Tiempo Mediano (ms)Desv. Est. (ms)
1 hora1007110577.374.59.7
1 día100738176.075.02.4
7 días100161192168.1166.08.5
30 días100515630536.3526.532.2

Este es el resultado de usar la segunda consulta con la blockchain pequeña CELO:

PeriodoLímiteTiempo Mínimo (ms)Tiempo Máximo (ms)Tiempo Promedio (ms)Tiempo Mediano (ms)Desv. Est. (ms)
1 hora1001310123.715.025.8
1 día100139923.414.525.2
7 días100149924.515.524.9
30 días1001610426.117.526.0

Este es el resultado de usar la segunda consulta con la blockchain grande ETHEREUM:

PeriodoLímiteTiempo Mínimo (ms)Tiempo Máximo (ms)Tiempo Promedio (ms)Tiempo Mediano (ms)Desv. Est. (ms)
1 hora1002711437.829.525.5
1 día1002711237.028.025.0
7 días1006617279.668.530.9
30 días100206382251.6220.061.9

Podemos ver que las consultas para intervalos de 30 segundos no son tan rápidas como las demás, excepto para la blockchain CELO. Es absolutamente esperado, ya que la base de datos tiene que procesar más filas. Vamos a probar una configuración diferente y ver si podemos mejorar el rendimiento.

Segundo intento

Vamos a activar la compresión. Utilizaremos collection_id como un segmento de compresión porque tiene sentido mantener estos 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 es el resultado de usar la primera consulta:

PeriodoLímiteTiempo Mínimo (ms)Tiempo Máximo (ms)Tiempo Promedio (ms)Tiempo Mediano (ms)Desv. Est. (ms)
1 hora1002491091355.9261.5247.0
1 día100237286255.4251.015.4
7 días100247278259.4260.58.9
30 días100349412370.2358.521.0

Este es el resultado de usar la segunda consulta con la blockchain pequeña CELO:

PeriodoLímiteTiempo Mínimo (ms)Tiempo Máximo (ms)Tiempo Promedio (ms)Tiempo Mediano (ms)Desv. Est. (ms)
1 hora1001913133.121.032.8
1 día1001812832.921.532.1
7 días1002112933.322.031.9
30 días1002515142.125.036.9

Este es el resultado de usar la segunda consulta con la blockchain grande ETHEREUM:

PeriodoLímiteTiempo Mínimo (ms)Tiempo Máximo (ms)Tiempo Promedio (ms)Tiempo Mediano (ms)Desv. Est. (ms)
1 hora10090203115.799.034.6
1 día10094208110.599.532.8
7 días10095219111.198.536.3
30 días100149272170.7159.534.2

La tabla comprimida nos dio peores resultados... Incluso para consultas para intervalos cortos, sin embargo, para los 30 días en el caso de todas las blockchains mejoró el resultado. Supongo que mantener el número juntos por collection_id ayudó aquí.

Tercer intento

Vamos a intentar aumentar el chunk para nuestra hypertable, dado que tenemos mucha RAM. Siguiendo la recomendación oficial es bueno si el chunk puede ser del 25% de la memoria. Actualmente tengo 24GB de RAM asignados a 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 es el resultado de usar la primera consulta:

PeriodoLímiteTiempo Mínimo (ms)Tiempo Máximo (ms)Tiempo Promedio (ms)Tiempo Mediano (ms)Desv. Est. (ms)
1 hora100106117.111.014.8
1 día100101811.611.02.2
7 días1008711991.588.09.3
30 días100367514390.4373.542.9

Este es el resultado de usar la segunda consulta con la blockchain pequeña CELO:

PeriodoLímiteTiempo Mínimo (ms)Tiempo Máximo (ms)Tiempo Promedio (ms)Tiempo Mediano (ms)Desv. Est. (ms)
1 hora1002145.04.53.2
1 día1002125.04.02.7
7 días1003156.25.03.4
30 días1006157.67.02.6

Este es el resultado de usar la segunda consulta con la blockchain grande ETHEREUM:

PeriodoLímiteTiempo Mínimo (ms)Tiempo Máximo (ms)Tiempo Promedio (ms)Tiempo Mediano (ms)Desv. Est. (ms)
1 hora1003145.95.02.8
1 día1005136.05.02.4
7 días100324737.634.55.4
30 días100129140133.3132.03.2

¡Ahora tenemos resultados mucho mejores! Solo me preocupa la consulta de todas las cadenas para el período de 30 días. Las consultas de CELO/ETHEREUM son realmente rápidas.

Cuarto intento

Probemos usar la compresión para el bloque de 7 días.

Este es el resultado de usar la primera consulta:

PeriodoLímiteTiempo Mínimo (ms)Tiempo Máximo (ms)Tiempo Promedio (ms)Tiempo Mediano (ms)Desv. Est. (ms)
1 hora1004715360.349.531.0
1 día100506254.854.04.4
7 días100627367.167.03.6
30 días100139182150.4145.012.5

Este es el resultado de usar la segunda consulta con la blockchain pequeña CELO:

PeriodoLímiteTiempo Mínimo (ms)Tiempo Máximo (ms)Tiempo Promedio (ms)Tiempo Mediano (ms)Desv. Est. (ms)
1 hora100183922.620.06.1
1 día100203625.123.05.7
7 días1006218.07.04.4
30 días1007219.48.04.0

Este es el resultado de usar la segunda consulta con la blockchain grande ETHEREUM:

PeriodoLímiteTiempo Mínimo (ms)Tiempo Máximo (ms)Tiempo Promedio (ms)Tiempo Mediano (ms)Desv. Est. (ms)
1 hora100213423.823.03.5
1 día100235929.024.510.5
7 días100315838.236.57.4
30 días100597464.563.54.2

Finalmente, nuestras consultas son increíblemente rápidas. La más lenta sigue siendo la de 30 días para todas las cadenas, sin embargo, el tiempo mediano <0.2s, así que diría que es rápida para una agregación entre millones de filas.

Gráficos

Conclusión

Echemos un vistazo a los tamaños de tabla usando la consulta:

sql
select 
    hypertable_name, 
    pg_size_pretty(hypertable_size(format('%I.%I', hypertable_schema, hypertable_name)::regclass))
from timescaledb_information.hypertables;
  • Tabla de chunks de 1 día: 2.4 Gb
  • Tabla de chunks de 1 día con compresión: 2.6 Gb
  • Tabla de chunks de 7 días: 2.2 Gb
  • Tabla de chunks de 7 días con compresión: 0.3 Gb

Es casi una diferencia de 10x entre la primera tabla y la última, lo que explica por qué la tabla de buckets de 7 días con compresión es tan rápida. La configuración con chunks de 7 días y compresión es claramente la ganadora.