Вычислить количество параллельных событий в SQL

У меня есть таблица, содержащая телефонные звонки со следующими полями:

  • ID
  • STARTTIME
  • EndTime
  • STATUS
  • CALL_FROM
  • CALL_TO

В локальную базу данных PostgreSQL загружено 2,9 миллиона записей. Я добавил индексы по идентификатору (уникальный индекс), времени начала и времени окончания.

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

SELECT T1.sid, count(*) as CountSimultaneous
FROM calls_nov T1, calls_nov T2
WHERE
     T1.StartTime between T2.StartTime and T2.EndTime
     and T1.StartTime between '2011-11-02' and '2011-11-03'
GROUP BY
     T1.sid
ORDER BY CountSimultaneous DESC;

Может кто-нибудь попросит предложить способ исправить запрос/индекс, чтобы он действительно работал или предлагал другой способ вычисления одновременных вызовов?

EDIT:

Объясните план:

Sort  (cost=11796758237.81..11796758679.47 rows=176663 width=35)
  Sort Key: (count(*))
  ->  GroupAggregate  (cost=0.00..11796738007.56 rows=176663 width=35)
        ->  Nested Loop  (cost=0.00..11511290152.45 rows=57089217697 width=35)

Создание таблицы script:

CREATE TABLE calls_nov (
  sid varchar,
  starttime timestamp, 
  endtime timestamp, 
  call_to varchar, 
  call_from varchar, 
  status varchar);

Создание индекса:

CREATE UNIQUE INDEX sid_unique_index on calls_nov (sid);

CREATE INDEX starttime_index on calls_nov (starttime);

CREATE INDEX endtime_index on calls_nov (endtime);

Ответ 1

1.) Ваш запрос не улавливал все перекрытия - это уже было исправлено другими ответами.

2.) Тип данных ваших столбцов starttime и endtime равен timestamp. Поэтому предложение WHERE также немного ошибочно:

BETWEEN '2011-11-02' AND '2011-11-03'

Это будет включать "2011-11-03 00:00". Верхняя граница должна быть исключена.

3.) Удалено синтаксис смешанного случая без двойных кавычек. Идентификаторы без кавычек автоматически преобразуются в нижний регистр. Проще говоря: лучше всего не использовать смешанные идентификаторы случая в PostgreSQL.

4.) Преобразовал запрос, чтобы использовать явный JOIN, который всегда предпочтительнее. На самом деле, я сделал LEFT [OUTER] JOIN, потому что я хочу подсчитывать вызовы, которые перекрываются и без других вызовов.

5.) Упрощенный синтаксис для получения этого базового запроса:

SELECT t1.sid, count(*) AS ct
FROM   calls_nov t1
LEFT   JOIN calls_nov t2 ON t1.starttime <= t2.endtime
                        AND t1.endtime >= t2.starttime
WHERE  t1.starttime >= '2011-11-02 0:0'::timestamp
AND    t1.starttime <  '2011-11-03 0:0'::timestamp
GROUP  BY 1
ORDER  BY 2 DESC;

Этот запрос чрезвычайно медленный для большой таблицы, потому что каждая строка, начинающаяся с '2011-11-02', должна сравниваться с каждой строкой во всей таблице, что приводит к (почти) O (n²).


Быстрее

Мы можем резко сократить затраты на предварительный выбор возможных кандидатов. Выберите только столбцы и строки, которые вам нужны. Я делаю это с двумя CTE.

  • Выбор вызовов, начинающихся в указанный день. → CTE x
  • Рассчитайте последний конец этих вызовов. (подзапрос в CTE y)
  • Выберите только вызовы, которые перекрываются с общим диапазоном CTE x. → CTE y
  • Последний запрос намного быстрее, чем запрос к огромной базовой таблице.

WITH x AS (
    SELECT sid, starttime, endtime
    FROM   calls_nov
    WHERE  starttime >= '2011-11-02 0:0'
    AND    starttime <  '2011-11-03 0:0'
    ), y AS (
    SELECT starttime, endtime
    FROM   calls_nov
    WHERE  endtime >= '2011-11-02 0:0'
    AND    starttime <= (SELECT max(endtime) As max_endtime FROM x)
    )
SELECT x.sid, count(*) AS count_overlaps
FROM   x
LEFT   JOIN y ON x.starttime <= y.endtime
             AND x.endtime >= y.starttime
GROUP  BY 1
ORDER  BY 2 DESC;

Еще быстрее

У меня есть таблица реальной жизни из 350 000 строк с перекрывающимися временными отметками начала и конца, аналогичными вашим. Я использовал это для быстрого теста . PostgreSQL 8.4, скудные ресурсы, потому что это тестовая БД. Индексы на start и end. (Индекс по столбцу ID здесь не имеет значения.) Протестировано с помощью EXPLAIN ANALYZE, лучше всего из 5.

Общая продолжительность выполнения: 476994,774 мс

Вариант CTE:
Общая продолжительность выполнения: 4199.788 мс - это > коэффициент 100.

После добавления многоколоночного индекса формы:

CREATE INDEX start_end_index on calls_nov (starttime, endtime);

Общая продолжительность выполнения: 4159.367 мс


Максимальная скорость

Если этого недостаточно, есть способ ускорить его еще на порядок. Вместо вышеперечисленных CTE материализуйте временные таблицы и - это ключевой момент - создайте индекс на втором. Может выглядеть так:

Выполнить как одну транзакцию:

CREATE TEMP TABLE x ON COMMIT DROP AS   
    SELECT sid, starttime, endtime
    FROM   calls_nov
    WHERE  starttime >= '2011-11-02 0:0'
    AND    starttime <  '2011-11-03 0:0';

CREATE TEMP TABLE y ON COMMIT DROP AS
    SELECT starttime, endtime
    FROM   calls_nov
    WHERE  endtime >= '2011-11-02 0:0'
    AND    starttime <= (SELECT max(endtime) FROM x);

CREATE INDEX y_idx ON y (starttime, endtime); -- this is where the magic happens

SELECT x.sid, count(*) AS ct
FROM   x
LEFT   JOIN y ON x.starttime <= y.endtime
             AND x.endtime >= y.starttime
GROUP  BY 1
ORDER  BY 2 DESC;

Прочитайте временные таблицы в руководстве.


Окончательное решение

  • Создайте функцию plpgsql, которая инкапсулирует магию.

  • Диагностируйте типичный размер ваших временных таблиц. Создайте их автономно и измерьте:

    SELECT pg_size_pretty(pg_total_relation_size('tmp_tbl'));
    
  • Если они больше, чем ваши настройки для temp_buffers, то временно установите их достаточно высокими в своей функции, чтобы удерживать обе ваши временные таблицы в ОЗУ. Это большая скорость, если вам не нужно поменять местами на диск. (Должно быть сначала использовано временные таблицы в сеансе, чтобы иметь эффект.)

CREATE OR REPLACE FUNCTION f_call_overlaps(date)
  RETURNS TABLE (sid varchar, ct integer) AS
$BODY$
DECLARE
    _from timestamp := $1::timestamp;
    _to   timestamp := ($1 +1)::timestamp;
BEGIN

SET temp_buffers = 64MB'; -- example value; more RAM for temp tables;

CREATE TEMP TABLE x ON COMMIT DROP AS   
    SELECT c.sid, starttime, endtime  -- avoid naming conflict with OUT param
    FROM   calls_nov c
    WHERE  starttime >= _from
    AND    starttime <  _to;

CREATE TEMP TABLE y ON COMMIT DROP AS
    SELECT starttime, endtime
    FROM   calls_nov
    WHERE  endtime >= _from
    AND    starttime <= (SELECT max(endtime) FROM x);

CREATE INDEX y_idx ON y (starttime, endtime);

RETURN QUERY
SELECT x.sid, count(*)::int -- AS ct
FROM   x
LEFT   JOIN y ON x.starttime <= y.endtime AND x.endtime >= y.starttime
GROUP  BY 1
ORDER  BY 2 DESC;

END;
$BODY$   LANGUAGE plpgsql;

Вызов:

SELECT * FROM f_call_overlaps('2011-11-02') -- just name your date

Общая продолжительность выполнения: 138,199 мс - этот коэффициент 3000


Что еще вы можете сделать, чтобы ускорить его?

Общая оптимизация производительности.

CLUSTER calls_nov USING starttime_index; -- this also vacuums the table fully

ANALYZE calls_nov;

Ответ 2

Здесь видны возможные перекрытия, где "A" - это "опорный" интервал. Обратите внимание, что запрос ниже (далеко, далеко внизу) не дает того же результата, что и любой из оставшихся ответов.

-- A            |------|
-- B |-|
-- C        |---|
-- D          |---|
-- E             |---|
-- F               |---|
-- G                 |---|
-- H                   |---|
-- I                       |---|

"B" не перекрывает "A" вообще. "С" примыкает к нему. { "D", "E", "F", "G" } перекрывает его. "Н" примыкает к нему. "Я" вообще не перекрывает его.

create table calls_nov (
  sid varchar(5) primary key,
  starttime timestamp not null,
  endtime timestamp not null
);  

insert into calls_nov values
('A', '2012-01-04 08:00:00', '2012-01-04 08:00:10'),
('B', '2012-01-04 07:50:00', '2012-01-04 07:50:03'),
('C', '2012-01-04 07:59:57', '2012-01-04 08:00:00'),
('D', '2012-01-04 07:59:57', '2012-01-04 08:00:03'),
('E', '2012-01-04 08:00:01', '2012-01-04 08:00:04'),
('F', '2012-01-04 08:00:07', '2012-01-04 08:00:10'),
('G', '2012-01-04 08:00:07', '2012-01-04 08:00:13'),
('H', '2012-01-04 08:00:10', '2012-01-04 08:00:13'),
('I', '2012-01-04 08:00:15', '2012-01-04 08:00:18');

Вы можете увидеть все перекрывающиеся интервалы, подобные этому. (Я просто использовал to_char(), чтобы было легко увидеть все данные. Его можно опустить в процессе производства.)

select t1.sid, to_char(t1.starttime, 'HH12:MI:SS'), 
               to_char(t1.endtime,   'HH12:MI:SS'), 
       t2.sid, to_char(t2.starttime, 'HH12:MI:SS'), 
               to_char(t2.endtime,   'HH12:MI:SS')
from calls_nov t1
inner join calls_nov t2 on (t2.starttime, t2.endtime) 
                  overlaps (t1.starttime, t1.endtime) 
order by t1.sid, t2.sid;

A   08:00:00   08:00:10   A   08:00:00   08:00:10
A   08:00:00   08:00:10   D   07:59:57   08:00:03
A   08:00:00   08:00:10   E   08:00:01   08:00:04
A   08:00:00   08:00:10   F   08:00:07   08:00:10
A   08:00:00   08:00:10   G   08:00:07   08:00:13
B   07:50:00   07:50:03   B   07:50:00   07:50:03
C   07:59:57   08:00:00   C   07:59:57   08:00:00
C   07:59:57   08:00:00   D   07:59:57   08:00:03
D   07:59:57   08:00:03   A   08:00:00   08:00:10
D   07:59:57   08:00:03   C   07:59:57   08:00:00
D   07:59:57   08:00:03   D   07:59:57   08:00:03
D   07:59:57   08:00:03   E   08:00:01   08:00:04
E   08:00:01   08:00:04   A   08:00:00   08:00:10
E   08:00:01   08:00:04   D   07:59:57   08:00:03
E   08:00:01   08:00:04   E   08:00:01   08:00:04
F   08:00:07   08:00:10   A   08:00:00   08:00:10
F   08:00:07   08:00:10   F   08:00:07   08:00:10
F   08:00:07   08:00:10   G   08:00:07   08:00:13
G   08:00:07   08:00:13   A   08:00:00   08:00:10
G   08:00:07   08:00:13   F   08:00:07   08:00:10
G   08:00:07   08:00:13   G   08:00:07   08:00:13
G   08:00:07   08:00:13   H   08:00:10   08:00:13
H   08:00:10   08:00:13   G   08:00:07   08:00:13
H   08:00:10   08:00:13   H   08:00:10   08:00:13
I   08:00:15   08:00:18   I   08:00:15   08:00:18

Из этой таблицы видно, что "A" должно считаться 5, включая себя. "B" должен считаться 1; он перекрывает себя, но никакие другие интервалы не перекрывают его. Это кажется правильным.

Подсчет прост, но работает как разорванная черепаха. Это потому, что для оценки перекрытия требуется много работы.

select t1.sid, count(t2.sid) as num_concurrent
from calls_nov t1
inner join calls_nov t2 on (t2.starttime, t2.endtime) 
                  overlaps (t1.starttime, t1.endtime) 
group by t1.sid
order by num_concurrent desc;

A   5
D   4
G   4
E   3
F   3
H   2
C   2
I   1
B   1

Чтобы повысить производительность, вы можете использовать "таблицу" выше в общем выражении таблицы и рассчитывать на основе этого.

with interval_table as (
select t1.sid as sid_1, t1.starttime, t1.endtime,
       t2.sid as sid_2, t2.starttime, t2.endtime
from calls_nov t1
inner join calls_nov t2 on (t2.starttime, t2.endtime) 
                  overlaps (t1.starttime, t1.endtime) 
order by t1.sid, t2.sid
) 
select sid_1, count(sid_2) as num_concurrent
from interval_table
group by sid_1
order by num_concurrent desc;

Ответ 3

Попробуйте это вместо вашего между крестиком и крестом:

select
    t1.sid,
    count(1) as CountSimultaneous
from
   calls_nov t1
   inner join nov t2 on
       t1.starttime <= t2.endtime
       and t1.endtime >= t2.starttime
where
    t1.starttime between '2011-11-02' and '2011-11-03'
group by
    t1.sid
order by CountSimultaneous desc

Ответ 4

Я предполагаю, что вы хотите узнать количество активных вызовов в любой момент времени. Другие ответы дают вам, сколько других вызовов было активным, пока текущий вызов был активным. Для очень длинных звонков это может дать вам очень высокие номера. Мне было указано, что количество активных вызовов - это то, что вы хотели от одного из ваших комментариев к другим ответам (кроме того, я также работаю в телекоммуникационном секторе). К сожалению, у меня недостаточно репутации, чтобы прокомментировать этот ответ, так как я создал свою учетную запись, чтобы ответить на этот вопрос. Чтобы получить количество активных вызовов, вы можете использовать переменную, которая увеличивается на единицу при запуске вызова и уменьшается на единицу, когда она заканчивается. Я тестировал это в базе данных MySQL с более чем 50 миллионами звонков. Извините за любые различия в синтаксисе между MySQL и pgsql.

Я добавил временные таблицы для скорости, но только с 2-мя строками и индексами они могут не понадобиться. MySQL не может ссылаться на одну и ту же временную таблицу дважды, поэтому мне пришлось создать два.

CREATE TEMPORARY TABLE a
SELECT sid, StartTime, EndTime 
FROM calls_nov
WHERE StartTime between '2011-11-02' and '2011-11-03';

CREATE TEMPORARY TABLE b
SELECT *
FROM a;

SET @i := 0;

SELECT *, @i := @i + c.delta AS concurrent
FROM (
  SELECT StartTime AS time, 1 AS delta
  FROM a
  UNION ALL
  SELECT EndTime AS time, -1 AS delta
  FROM b
  ORDER BY time
) AS c
ORDER BY concurrent DESC
;

Внутренний SELECT возвращает два столбца. Столбец времени включает каждый StartTime и каждый EndTime из исходной таблицы (в два раза больше количества строк), а столбец дельта - +1 или -1, в зависимости от того, какой столбец был помещен в "время". Этот набор упорядочен по времени, который затем можно перебирать во внешнем SELECT.

Вместо "ORDER BY concurrent DESC", как и в вашем запросе, я бы использовал дополнительный внешний SELECT, где я мог бы получить MAX, MIN и т.д., и я мог бы также указать дату GROUP BY, час и т.д. Эта часть query (ORDER BY concurrent DESC), я на самом деле не тестировал. Я использовал свое собственное предложение с дополнительным внешним запросом, поскольку ORDER BY не выполняет так, как ожидалось, в MySQL при заказе переменной, которая была установлена ​​в том же SELECT. Вместо этого он заказывает предыдущее значение переменной. Если вам абсолютно необходимо заказать одновременные вызовы (и pgsql имеет ту же проблему), я считаю, что вы можете обойти это снова, используя дополнительный внешний SELECT и упорядочив там.

Запрос, который я запускал, был очень быстрым! Он просматривает каждую временную таблицу один раз, а затем комбинацию из двух раз (с меньшим количеством данных в строке), а для моей собственной версии с дополнительным внешним запросом она снова просматривает комбинацию, а затем группирует ее. Каждая таблица проверяется только один раз! Это все будет сделано в ОЗУ, если это позволит ваша конфигурация и аппаратное обеспечение. Другие ответы (или вопросы) помогут вам, если это не так.