【问题标题】:Calculate number of concurrent events in SQL计算 SQL 中的并发事件数
【发布时间】:2025-11-23 19:15:02
【问题描述】:

我有一个保存电话的表格,其中包含以下字段:

  • 身份证
  • 开始时间
  • 结束时间
  • 状态
  • CALL_FROM
  • CALL_TO

有 290 万条记录加载到本地 PostgreSQL 数据库中。我在 ID(唯一索引)、开始时间和结束时间上添加了索引。

在 * 上搜索,我发现了一些有用的 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);

【问题讨论】:

  • T1和T2一样吗??
  • 你能提供解释计划吗? postgresql.org/docs/8.1/static/sql-explain.html 另外,假设“sid”是 ID,将其包含在 select 中并按它进行分组是没有意义的 - “count”始终为 1。
  • @fge - 当然是……这是通话记录。他想知道每个通话期间同时发生了多少个通话。
  • SID 是每个呼叫的唯一 ID。
  • 添加了创建表和索引脚本。谢谢!

标签: sql postgresql performance timestamp query-optimization


【解决方案1】:

以下是可能的重叠部分,其中“A”是“参考”区间。请注意,下面的查询(远,远低于)与尚未发布的任何答案给出的结果不同。

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

“B”根本不与“A”重叠。 “C”紧靠它。 {"D", "E", "F", "G"} 与它重叠。 “H”紧靠它。 “我”根本不重叠。

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

为了获得更好的性能,可以在通用表表达式中使用上面的“表”,并根据that进行计数。

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;

【讨论】:

  • 感谢您提供的非常丰富的答案!但是,当我运行解释计划时,使用表表达式的查询要差几个数量级:“排序(成本=2566228269298.11..2566228269298.61 行=200 宽度=64)”与“排序(成本=11294858654.81..11294859096.47 行= 176663 width=35)" 对于@Eric 的回答。这可能是缺少索引的情况吗?
  • 我有关于开始时间和结束时间的索引。这里的 CTE 速度要快得多,但我没有 290 万行。
  • 是的,我也对此进行了索引,但也许只是我的本地盒子不够强大。
【解决方案2】:

1.) 您的查询没有捕捉到所有重叠 - 这已经被其他答案修复了。

2.) starttimeendtime 列的数据类型是 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. 选择从相关日期开始的通话。 -> CTEx
  2. 计算这些呼叫的最后结束时间。 (CTE 中的子查询y
  3. 仅选择与 CTE x 的总范围重叠的调用。 -> CTEy
  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,资源稀缺,因为它是一个测试数据库。 startend 上的索引。 (ID 列的索引在这里无关紧要。)使用EXPLAIN ANALYZE 进行测试,最好的 5。

总运行时间:476994.774 毫秒

CTE 变体:
总运行时间:4199.788 毫秒——> 100 倍。

添加multicolumn index后的表单:

CREATE INDEX start_end_index on calls_nov (starttime, endtime);

总运行时间:4159.367 毫秒


终极速度

如果这还不够,还有一种方法可以将其加快另一个数量级。而不是上面的 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;

了解temporary tables in the manual


终极解决方案

  • 创建一个封装魔法的plpgsql函数

  • 诊断临时表的典型大小。独立创建并测量:

      SELECT pg_size_pretty(pg_total_relation_size('tmp_tbl'));
    
  • 如果它们大于您为 temp_buffers 设置的值,则在您的函数中临时将它们设置得足够高,以便将您的两个临时表都保存在 RAM 中。如果您不必交换到光盘,这是一个重大的加速。 (必须首先在会话中使用临时表才能生效。)

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 倍


你还能做些什么来加快速度?

General performance optimization.

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

ANALYZE calls_nov;

【讨论】:

  • 谢谢。解释计划仍然没有启发性:ort (cost=4785158982.43..4785158982.93 rows=200 width=32) 但是,它似乎比以前的计划好 10 倍。现在运行查询,希望它会返回。
  • @Sologoub:我在回答中添加了很多内容。
  • 好吧……哇!感谢您提供所有信息。努力吸收知识:)
  • 好吧,得到一个错误:错误:查询结构与函数结果类型 SQL 状态不匹配:42804 详细信息:返回的类型字符变化与第 1 列中的预期类型整数不匹配。上下文:PL/pgSQL RETURN QUERY 处的函数“f_call_overlaps”第 23 行
  • 修改这个似乎允许函数运行:RETURNS TABLE (sid varchar, ct bigint)
【解决方案3】:

我假设您想知道在任何给定时间的活动呼叫量。其他答案会告诉您当前呼叫处于活动状态时还有多少其他呼叫处于活动状态。对于非常长的通话,这会给您带来非常高的数字。有人向我表明,活跃呼叫的数量是您想要从您的一个 cmets 到其他答案的数量(此外,我也在电信部门工作)。不幸的是,我还没有足够的声誉来评论这个答案,因为我创建了我的帐户来回答这个问题。要获取活动呼叫的数量,您可以使用一个变量,该变量在呼叫开始时增加一,在呼叫结束时减少一。我已经在一个有 50+ 百万次调用的 MySQL 数据库上对此进行了测试。对于 MySQL 和 pgsql 之间的任何语法差异,我们深表歉意。

我添加了临时表以提高速度,但只有 2m 行和索引,可能不需要它们。 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 返回两列。 time 列包括原始表中的每个 StartTime 和每个 EndTime(行数的两倍),并且 delta 列是 +1 或 -1,具体取决于将哪一列放在“时间”中。该集合按时间排序,然后我们可以在外部 SELECT 中对其进行迭代。

我将使用额外的外部 SELECT 代替您在查询中使用的“ORDER BY concurrent DESC”,我可以在其中获取 MAX、MIN 等值,还可以按日期、小时等进行分组。这部分查询(ORDER BY concurrent DESC),我其实没有测试。我使用了我自己的建议和一个额外的外部查询,因为当通过在同一个 SELECT 中设置的变量进行排序时,ORDER BY 在 MySQL 中没有按预期执行。它改为按变量的先前值排序。如果您绝对需要通过并发调用进行排序(而 pgsql 也有同样的问题),我相信您可以通过再次使用额外的外部 SELECT 并在那里进行排序来解决这个问题。

我运行的查询非常快!它扫描每个临时表一次,然后将两者组合一次(每行数据较少),对于我自己的带有额外外部查询的版本,它再次扫描组合,然后将其分组。 每个表只扫描一次!如果您的配置和硬件允许,这一切都将在 RAM 中完成。如果没有,其他答案(或问题)会对您有所帮助。

【讨论】:

    【解决方案4】:

    试试这个来代替你的 between 和 cross join:

    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
    

    【讨论】:

    • 这很接近,但需要and t1.sid != t2.sid 以确保同一行不会连接到自身
    • @reff - 我想过,但我没有把它放进去。实际上,你可以把那个条件放进去,把它变成left joincount(t2.sid),它会只需为每个数字少给你 1。或者你可以做count(1)-1。无论哪种方式都可能洗。
    • 没有它应该没问题,因为我正在寻找并发呼叫的数量。数数自己没问题。
    • 查询成本稍微好一点,但总体上仍然在数十亿... 1 行的结果集的成本是 91k。不太清楚这里发生了什么。
    • @Eric - 您的加入条件错误,您正在重复相同的条件