Рассчитать количество одновременных событий в SQL - PullRequest
11 голосов
/ 05 января 2012

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

  • ID
  • STARTTIME
  • ENDTIME
  • STATUS
  • CALL_FROM
  • CALL_TO

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

Выполняя поиск по стеку, я нашел несколько полезных 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;

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

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

План объяснения:

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)

Сценарий создания таблицы:

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);

Ответы [ 4 ]

8 голосов
/ 05 января 2012

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

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

«Б» не перекрывает «А» вообще. «С» упирается в это. {"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

Из этой таблицы видно, что «А» должно насчитать 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;
6 голосов
/ 06 января 2012

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.

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

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 мс


UltimateСкорость

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

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

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,169 мс - это фактор 3000


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

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

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

ANALYZE calls_nov;
2 голосов
/ 23 сентября 2016

Я предполагаю, что вы хотите знать количество активных вызовов в любой момент времени.В других ответах указано, сколько других вызовов было активным, когда был активен текущий вызов.Для очень длинных звонков это может дать вам очень высокие номера.Мне было указано, что количество активных звонков - это то, что вы хотели от одного из ваших комментариев к другим ответам (кроме того, я также работаю в сфере телекоммуникаций).К сожалению, у меня недостаточно репутации, чтобы комментировать этот ответ, так как я создал свой аккаунт, чтобы ответить на этот вопрос.Чтобы узнать количество активных вызовов, вы можете использовать переменную, которая увеличивается на единицу при начале вызова и уменьшается на единицу при его завершении.Я проверил это на базе данных 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 в зависимости от того, какой столбец был помещен в «time».Этот набор упорядочен по времени, после чего мы можем выполнить итерацию во внешнем SELECT.

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

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

1 голос
/ 05 января 2012

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

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
...