Skip to content

Выбор лучшей конфигурации таблицы для TimescaleDB

Описание задачи

Существует довольно распространенная задача для маркетплейсов - рассчитывать объем продаж для некоторых продуктов в течение 1 дня/7 дней/30 дней в режиме почти реального времени. TimescaleDB или любая другая база данных временных рядов идеально подходит для такой задачи. Давайте протестируем TimescaleDB, так как в моем текущем проекте уже используется PostgreSQL. Я буду использовать очень упрощенную модель из моего текущего проекта (рынок 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 (~8 миллионов).

Давайте используем 2 запроса для тестирования производительности:

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 ГБ ОЗУ
  • TimescaleDB 2.17.2-pg17 в Docker
  • увеличенные настройки памяти для pg: shared_buffers=8GB, work_mem=8GB

Первая попытка

Результаты использования первого запроса:

ПериодЛимитМин. Время (мс)Макс. Время (мс)Средн. Время (мс)Медиан. Время (мс)Стд. Отклонение (мс)
1 час1007110577.374.59.7
1 день100738176.075.02.4
7 дней100161192168.1166.08.5
30 дней100515630536.3526.532.2

Результаты использования второго запроса с небольшим блокчейном CELO:

ПериодЛимитМин. Время (мс)Макс. Время (мс)Средн. Время (мс)Медиан. Время (мс)Стд. Отклонение (мс)
1 час1001310123.715.025.8
1 день100139923.414.525.2
7 дней100149924.515.524.9
30 дней1001610426.117.526.0

Результаты использования второго запроса с крупным блокчейном ETHEREUM:

ПериодЛимитМин. Время (мс)Макс. Время (мс)Средн. Время (мс)Медиан. Время (мс)Стд. Отклонение (мс)
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;

Результаты использования первого запроса:

ПериодЛимитМин. Время (мс)Макс. Время (мс)Средн. Время (мс)Медиан. Время (мс)Стд. Отклонение (мс)
1 час1002491091355.9261.5247.0
1 день100237286255.4251.015.4
7 дней100247278259.4260.58.9
30 дней100349412370.2358.521.0

Результаты использования второго запроса с небольшим блокчейном CELO:

ПериодЛимитМин. Время (мс)Макс. Время (мс)Средн. Время (мс)Медиан. Время (мс)Стд. Отклонение (мс)
1 час1001913133.121.032.8
1 день1001812832.921.532.1
7 дней1002112933.322.031.9
30 дней1002515142.125.036.9

Результаты использования второго запроса с крупным блокчейном ETHEREUM:

ПериодЛимитМин. Время (мс)Макс. Время (мс)Средн. Время (мс)Медиан. Время (мс)Стд. Отклонение (мс)
1 час10090203115.799.034.6
1 день10094208110.599.532.8
7 дней10095219111.198.536.3
30 дней100149272170.7159.534.2

Сжатая таблица дала худшие результаты... Даже для запросов на малый интервал, однако для 30 дней в случае всех блокчейнов это улучшило результат. Полагаю, хранение чисел вместе по collection_id помогло здесь.

Третья попытка

Попробуем увеличить сегмент для нашей гипертаблицы, учитывая, что у нас достаточно ОЗУ. Следуя официальной рекомендации, хорошо, если сегмент может занимать 25% памяти. В настоящее время у меня выделено 24 ГБ ОЗУ для 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));

Результаты использования первого запроса:

ПериодЛимитМин. Время (мс)Макс. Время (мс)Средн. Время (мс)Медиан. Время (мс)Стд. Отклонение (мс)
1 час100106117.111.014.8
1 день100101811.611.02.2
7 дней1008711991.588.09.3
30 дней100367514390.4373.542.9

Результаты использования второго запроса с небольшим блокчейном CELO:

ПериодЛимитМин. Время (мс)Макс. Время (мс)Средн. Время (мс)Медиан. Время (мс)Стд. Отклонение (мс)
1 час1002145.04.53.2
1 день1002125.04.02.7
7 дней1003156.25.03.4
30 дней1006157.67.02.6

Результаты использования второго запроса с крупным блокчейном ETHEREUM:

ПериодЛимитМин. Время (мс)Макс. Время (мс)Средн. Время (мс)Медиан. Время (мс)Стд. Отклонение (мс)
1 час1003145.95.02.8
1 день1005136.05.02.4
7 дней100324737.634.55.4
30 дней100129140133.3132.03.2

Теперь у нас намного лучшие результаты! Мне не нравится только запрос всех цепочек за 30 дней. Запросы CELO/ETHEREUM действительно быстрые.

Четвертая попытка

Попробуем использовать сжатие для 7-дневного сегмента.

Результаты использования первого запроса:

ПериодЛимитМин. Время (мс)Макс. Время (мс)Средн. Время (мс)Медиан. Время (мс)Стд. Отклонение (мс)
1 час1004715360.349.531.0
1 день100506254.854.04.4
7 дней100627367.167.03.6
30 дней100139182150.4145.012.5

Результаты использования второго запроса с небольшим блокчейном CELO:

ПериодЛимитМин. Время (мс)Макс. Время (мс)Средн. Время (мс)Медиан. Время (мс)Стд. Отклонение (мс)
1 час100183922.620.06.1
1 день100203625.123.05.7
7 дней1006218.07.04.4
30 дней1007219.48.04.0

Результаты использования второго запроса с крупным блокчейном ETHEREUM:

ПериодЛимитМин. Время (мс)Макс. Время (мс)Средн. Время (мс)Медиан. Время (мс)Стд. Отклонение (мс)
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 ГБ
  • 1дневная таблица сегментов с компрессией: 2.6 ГБ
  • 7дневная таблица сегментов: 2.2 ГБ
  • 7дневная таблица сегментов с компрессией: 0.3 ГБ

Это почти в 10 раз больше между первой таблицей и последней, это объясняет, почему таблица сегментов с 7дн с компрессией работает так быстро. Конфигурация с 7дн сегментом и компрессией - явный победитель.