Как создать эффективный запрос, который будет подсчитывать записи за определенный интервал времени?

Какую базу данных я использую?

Я использую PostgreSQL 9.5.

Что мне нужно?

Это часть моих data_store таблиц:

  id |          starttime
-----+----------------------------
 185 | 2011-09-12 15:24:03.248+02
 189 | 2011-09-12 15:24:03.256+02    
 312 | 2011-09-12 15:24:06.112+02
 313 | 2011-09-12 15:24:06.119+02
 450 | 2011-09-12 15:24:09.196+02
 451 | 2011-09-12 15:24:09.203+02
 452 | 2011-09-12 15:24:09.21+02
 ... |            ...

Я хотел бы создать запрос, который будет подсчитывать записи за определенный интервал времени. Например, для временного интервала в 4 секунды запрос должен вернуть мне что-то вроде этого:

    starttime-from   |    starttime-to     |  count
---------------------+---------------------+---------
 2011-09-12 15:24:03 | 2011-09-12 15:24:07 |    4
 2011-09-12 15:24:07 | 2011-09-12 15:24:11 |    3
 2011-09-12 15:24:11 | 2011-09-12 15:24:15 |    0
         ...         |         ...         |   ...

Самое важное:

  1. Интервал времени зависит от выбора пользователя. Это может быть 1 second, 37 seconds, 50 minutes или какая-то смесь: 2 month and 30 mintues. Доступные единицы для временного интервала: millisecond, second, minute, hour, day, month, year. Как видите, мне нужен какой-то общий/универсальный запрос для этого, НО я мог бы также создать несколько запросов для каждого модуля - это не проблема.
  2. Запрос должен быть оперативным, т.к. я работаю с большой базой данных (20 миллионов строк и более, но в запросе я использую только часть этой базы, например: 1 миллион).

Вопрос в следующем: Как должен выглядеть запрос, чтобы добиться этого?

Я попытался преобразовать решения, которые нашел в следующих темах, но мне это не удалось:

Что я имею?

Я удалил этот раздел своего поста для большей прозрачности поста. Этот раздел не был необходим, чтобы дать ответ на мой вопрос. Если хотите посмотреть, что здесь было, загляните в историю поста.


person Robert    schedule 05.08.2016    source источник


Ответы (3)


Ваш запрос кажется сложным. Вам нужно только сгенерировать последовательность времен, а затем использовать left join, чтобы объединить их. . . и суммировать:

select g.ts,  g.ts + interval '4 second', count(ds.id)
from (select generate_series(min(starttime), max(strttime), interval '4 second') as ts
      from data_store
     ) g left join
     data_store ds
     on ds.starttime >= g.ts and ds.starttime < g.ts + interval '4 second'
group by g.ts
order by g.ts;

Примечание. Если вы хотите, чтобы интервал начинался с точной секунды (и не имел какого-то странного количества миллисекунд 999 раз из 1000), используйте date_trunc().

РЕДАКТИРОВАТЬ:

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

select gs.ts,
       (select count(*)
        from data_store ds
        where ds.starttime >= g.ts and ds.starttime < g.ts + interval '4 second'
       ) as cnt
from (select generate_series(min(starttime), max(strttime), interval '4 second') as ts
      from data_store
     ) g;
person Gordon Linoff    schedule 05.08.2016
comment
Я был уверен, что мой запрос слишком сложен. Ваш запрос доказывает это. Спасибо за помощь, все работает именно так, как я хотел. Конечно, я приму этот ответ, но перед этим не могли бы вы сказать, можно ли сделать ваш запрос более эффективным? Если я использую какой-то большой интервал, например 60 minutes, он работает даже быстро, но для маленького интервала, такого как 4 seconds, после 30 минут ожидания я сдался. Я знаю, что это зависит от многих вещей. Прежде всего, я создал индекс B-Tree для столбца starttime, но это не помогает. У вас есть идеи, как я могу увеличить скорость вашего запроса? - person Robert; 06.08.2016
comment
Роберт. . . Если вышеуказанное изменение не работает, задайте другой вопрос о производительности — включите запрос, который дает правильный ответ, но не работает хорошо. Могут быть и другие подходы, но я думаю, что они запутывают ответ на этот вопрос. - person Gordon Linoff; 06.08.2016
comment
Второй запрос медленнее первого, поэтому я остановлюсь на первом запросе. - person Robert; 07.08.2016
comment
Кстати: на основе вашего первого запроса и моего запроса, наконец, я нашел более быстрый запрос. Я только что опубликовал ответ на этот запрос, поэтому, если кто-то хочет попробовать, посмотрите здесь - person Robert; 08.08.2016

Улучшение запроса в выбранном ответе.

Я только что улучшил запрос, который вы можете найти в выбранном ответе.

Окончательный запрос выглядит следующим образом:

SELECT gp.tp AS starttime_from, gp.tp + interval '4 second' AS starttime_to, count(ds.id)
FROM (SELECT generate_series(min(starttime),max(starttime), interval '4 second') as tp
      FROM data_store
      WHERE id_user_table=1 and sip='147.32.84.138'
      ORDER BY 1
     ) gp 
     LEFT JOIN data_store ds 
     ON ds.id_user_table=1 and ds.sip='147.32.84.138' 
        and ds.starttime >= gp.tp and ds.starttime < gp.tp + interval '4 second'
GROUP BY starttime_from

Я переместил ORDER BY в подзапрос. Теперь немного быстрее. Я также добавил обязательные столбцы в предложение WHERE. Наконец, я создал многоколоночный индекс для столбцов, которые я всегда использую в запросе:

CREATE INDEX my_index ON data_store (id_user_table, sip, starttime);

На данный момент запрос выполняется очень быстро. Обратите внимание: для очень коротких интервалов времени результат запроса включает много строк с нулевым количеством. Эти строки съедают место. В этом случае запрос должен включать ограничение HAVING count(ds.id) > 0, но тогда вам придется обрабатывать эти 0 на стороне клиента.

Другое решение

Это решение не такое быстрое, как предыдущее, но в приведенном ниже запросе не используется многоколоночный индекс, и оно по-прежнему быстрое.

Две важные вещи в запросе, которые вы можете найти в конце этого ответа:

  • 'second' - это точность, до которой усекается входное значение. Вы также можете выбрать другую точность, например: millisecond,minute,day и т. д.

  • '4 second' - это временной интервал. Временной интервал может иметь другие единицы измерения, такие как millisecond, minute, day и т. д.

Здесь вы можете найти объяснение запроса:

  • Запрос generate_period генерирует интервалы, начиная с указанной даты и времени до определенной даты и времени. Вы можете указать эту конкретную дату и время вручную или с помощью своего столбца таблицы (как в моем случае). Для временного интервала в 4 секунды запрос возвращает:

              tp
    ---------------------
     2011-09-12 15:24:03
     2011-09-12 15:24:07
     2011-09-12 15:24:11
             ...
    
  • Запрос data_series подсчитывает записи для определенной точности даты и времени: for 1 second time interval, for 1 day time interval и т. д. В моем случае конкретная точность равна 'second', поэтому for 1 second time interval, но результат операции выбора не включает значение 0 для даты и времени, которое не т происходит. В моем случае запрос data_series возвращает:

           starttime     |    ct
    ---------------------+-----------
     2011-09-12 15:24:03 |     2
     2011-09-12 15:24:06 |     2
     2011-09-12 15:24:09 |     3     
             ...         |    ...
    
  • Наконец, последняя часть запроса суммирует столбец ct за определенные периоды времени. Запрос возвращает это:

        starttime-from   |    starttime-to     |   ct
    ---------------------+---------------------+---------
     2011-09-12 15:24:03 | 2011-09-12 15:24:07 |    4
     2011-09-12 15:24:07 | 2011-09-12 15:24:11 |    3
     2011-09-12 15:24:11 | 2011-09-12 15:24:15 |    0
             ...         |         ...         |   ...
    

Вот запрос:

WITH generate_period AS(

    SELECT generate_series(date_trunc('second',min(starttime)), 
                           date_trunc('second',max(starttime)), 
                           interval '4 second') as tp
    FROM data_store 
    WHERE id_user_table=1 --other restrictions

), data_series AS(

    SELECT date_trunc('second', starttime) AS starttime, count(*) AS ct
    FROM data_store  
    WHERE id_user_table=1 --other restrictions
    GROUP  BY 1

)

SELECT gp.tp AS starttime-from, 
       gp.tp + interval '4 second' AS starttime-to, 
       COALESCE(sum(ds.ct),0) AS ct
FROM  generate_period gp
LEFT JOIN data_series ds ON date_trunc('second',ds.starttime) >= gp.tp 
                        and date_trunc('second',ds.starttime) < gp.tp + interval '4 second'
GROUP BY 1
ORDER BY 1;
person Robert    schedule 07.08.2016

person    schedule
comment
Спасибо за вашу помощь. Может быть, я попробую это. - person Robert; 06.08.2016