Разработка базы данных для временных рядов - PullRequest
2 голосов
/ 03 июля 2019

Примерно каждые 10 минут я вставляю ~ 50 записей с одной и той же отметкой времени.
Это означает ~ 600 записей в час или 7.200 записей в день или 2.592.000 записей в год.
Пользователь хочет получить все записи для метки времени, ближайшей к запрашиваемому времени.

Дизайн # 1 - одна таблица с индексом в столбце метки времени:

    CREATE TABLE A (t timestamp, value int);
    CREATE a_idx ON A (t);

Один оператор вставки создает ~ 50 записей с одной и той же отметкой времени:

    INSERT INTO A VALUES (
      (‘2019-01-02 10:00’, 5),
      (‘2019-01-02 10:00’, 12),
      (‘2019-01-02 10:00’, 7),
       ….
    )

Получить все записи, которые ближе всего к запрашиваемому времени
(Я использую функцию great (), доступную в PostgreSQL):

    SELECT * FROM A WHERE t =
(SELECT t FROM A ORDER BY greatest(t - asked_time, asked_time - t) LIMIT 1)

Я думаю, что этот запрос неэффективен, поскольку требует полного сканирования таблицы.
Я планирую разделить таблицу A по меткам времени, чтобы иметь 1 раздел в год, но приблизительное совпадение, приведенное выше, все равно будет медленным.

Дизайн # 2 - создать 2 таблицы:
1-я таблица: для сохранения уникальных временных меток и автоматического увеличения PK,
2-я таблица: хранить данные и внешний ключ на 1-й таблице PK

    CREATE TABLE UNIQ_TIMESTAMP (id SERIAL PRIMARY KEY, t timestamp);
    CREATE TABLE DATA (id INTEGER REFERENCES UNIQ_TIMESTAMP (id), value int);
    CREATE INDEX data_time_idx ON DATA (id);

Получить все записи, которые ближе всего к запрашиваемому времени:

SELECT * FROM DATA WHERE id =
(SELECT id FROM UNIQ_TIMESTAMP ORDER BY greatest(t - asked_time, asked_time - t) LIMIT 1)

Он должен работать быстрее, чем в Design # 1, потому что вложенный выбор сканирует меньшую таблицу.
Недостаток такого подхода:
- Я должен вставить в 2 таблицы вместо одного
- Я потерял возможность разбивать таблицу данных по отметке времени

Что вы могли бы порекомендовать?

Ответы [ 2 ]

0 голосов
/ 03 июля 2019

Вы можете использовать СОЮЗ из двух запросов, чтобы найти все метки времени, наиболее близкие к данной:

(
  select t
  from a
  where t >= timestamp '2019-03-01 17:00:00'
  order by t
  limit 1
)
union all
(
  select t
  from a
  where t <= timestamp '2019-03-01 17:00:00'
  order by t desc
  limit 1
)

Это будет эффективно использовать индекс на t. На таблице с 10 миллионами строк (~ 3 года данных) я получаю следующий план выполнения:

Append  (cost=0.57..1.16 rows=2 width=8) (actual time=0.381..0.407 rows=2 loops=1)
  Buffers: shared hit=6 read=4
  I/O Timings: read=0.050
  ->  Limit  (cost=0.57..0.58 rows=1 width=8) (actual time=0.380..0.381 rows=1 loops=1)
        Output: a.t
        Buffers: shared hit=1 read=4
        I/O Timings: read=0.050
        ->  Index Only Scan using a_t_idx on stuff.a  (cost=0.57..253023.35 rows=30699415 width=8) (actual time=0.380..0.380 rows=1 loops=1)
              Output: a.t
              Index Cond: (a.t >= '2019-03-01 17:00:00'::timestamp without time zone)
              Heap Fetches: 0
              Buffers: shared hit=1 read=4
              I/O Timings: read=0.050
  ->  Limit  (cost=0.57..0.58 rows=1 width=8) (actual time=0.024..0.025 rows=1 loops=1)
        Output: a_1.t
        Buffers: shared hit=5
        ->  Index Only Scan Backward using a_t_idx on stuff.a a_1  (cost=0.57..649469.88 rows=78800603 width=8) (actual time=0.024..0.024 rows=1 loops=1)
              Output: a_1.t
              Index Cond: (a_1.t <= '2019-03-01 17:00:00'::timestamp without time zone)
              Heap Fetches: 0
              Buffers: shared hit=5
Planning Time: 1.823 ms
Execution Time: 0.425 ms

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

Вышеуказанное можно использовать для условия IN:

select *
from a
where t in ( 
  (select t
   from a
   where t >= timestamp '2019-03-01 17:00:00'
   order by t
   limit 1)
  union all
  (select t
   from a
   where t <= timestamp '2019-03-01 17:00:00'
   order by t desc
   limit 1)
);

Если вы знаете, что у вас никогда не будет более 100 значений, близких к запрошенной отметке времени, вы можете полностью удалить запрос IN и просто использовать limit 100 в обеих частях объединения. Это делает запрос немного более эффективным, поскольку нет второго шага для оценки условия IN, но может вернуть больше строк, чем вы хотите.

Если вы всегда будете искать метки времени в одном и том же году, то разделение по годам действительно поможет в этом.

Вы можете поместить это в функцию, если она слишком сложна как запрос:

create or replace function get_closest(p_tocheck timestamp)
  returns timestamp
as
$$
  select *
  from (
     (select t
     from a
     where t >= p_tocheck
     order by t
     limit 1)
    union all
    (select t
     from a
     where t <= p_tocheck
     order by t desc
     limit 1)
  ) x
  order by greatest(t - p_tocheck, p_tocheck - t)
  limit 1;
$$
language sql stable;

Запрос становится таким простым:

select *
from a
where t = get_closest(timestamp '2019-03-01 17:00:00');

Другое решение заключается в использовании расширения btree_gist , которое обеспечивает оператор «расстояния» <->

Затем вы можете создать индекс GiST на отметке времени:

create index on a using gist (t) ;

и используйте следующий запрос:

select *
from a where t in (select t
                  from a
                  order by t <-> timestamp '2019-03-01 17:00:00'
                  limit 1);
0 голосов
/ 03 июля 2019

Я бы использовал подход с использованием одной таблицы, возможно, с разбивкой по годам, чтобы было легче избавиться от старых данных.

Создайте индекс, например

CREATE INDEX ON a (date_trunc('hour', t + INTERVAL '30 minutes'));

Затем используйтеВаш запрос, как вы написали, но добавьте

AND date_trunc('hour', t + INTERVAL '30 minutes')
  = date_trunc('hour', asked_time + INTERVAL '30 minutes')

Дополнительное условие действует как фильтр и может использовать индекс.

...