11

我有一组呼叫详细记录,从这些记录中,我应该确定每个系统每小时的平均并发活动呼叫(精度为一分钟)。如果我查询晚上 7 点到晚上 8 点,我应该看到该小时内(对于每个系统)的平均并发呼叫数(平均每分钟的并发呼叫数)。

所以,我需要一种方法来检查 7:00-7:01、7:01-7:02 等的活动呼叫计数,然后平均这些数字。如果呼叫的时间和持续时间在被检查的当前分钟内,则认为呼叫处于活动状态。

使这更加困难的是它需要跨越 SQL 7.0 和 SQL 2000(2000 中的某些函数在 7.0 中不可用,例如 GetUTCTime()),如果我能让 2000 工作我会很高兴。

我可以采取什么方法来解决这个问题?

我考虑在检查的一小时内循环通过分钟 (60) 并添加该分钟之间的呼叫计数,然后以某种方式交叉引用持续时间以确保呼叫从晚上 7:00 开始并且持续时间为300 秒显示在 7:04 处于活动状态,但我无法想象如何解决这个问题。我试图找出一种方法来衡量每个呼叫与特定分钟的权重,这会告诉我呼叫在该分钟内是否处于活动状态,但无法提出有效的解决方案。

这里的数据类型与我必须查询的相同。我对架构没有任何控制权(除了可能转换数据并插入到另一个具有更合适数据类型的表中)。我提供了一些我知道有并发活动调用的示例数据。

CREATE TABLE Records(
  seconds char(10),
  time char(4),
  date char(8),
  dur int,
  system int,
  port int,
)

--seconds is an stime value. It's the difference of seconds from UTC 1/1/1970 00:00:00 to the current UTC time, we use it as an identifier (like epoch).
--time is the time the call was made.
--date is the day the call was made.
--dur is the duration of the call in seconds.
--system is the system number.
--port is the port on the system (not particularly relevant for this question).

INSERT INTO Records(seconds, time, date, dur, system, port) VALUES('1239924228','1923','20090416',105,2,2)
INSERT INTO Records(seconds, time, date, dur, system, port) VALUES('1239923455','1910','20090416',884,1,97)
INSERT INTO Records(seconds, time, date, dur, system, port) VALUES('1239924221','1923','20090416',116,2,15)
INSERT INTO Records(seconds, time, date, dur, system, port) VALUES('1239924259','1924','20090416',90,1,102)
INSERT INTO Records(seconds, time, date, dur, system, port) VALUES('1239923458','1910','20090416',891,2,1)
INSERT INTO Records(seconds, time, date, dur, system, port) VALUES('1239924255','1924','20090416',99,2,42)
INSERT INTO Records(seconds, time, date, dur, system, port) VALUES('1239924336','1925','20090416',20,2,58)
INSERT INTO Records(seconds, time, date, dur, system, port) VALUES('1239924293','1924','20090416',64,2,41)
INSERT INTO Records(seconds, time, date, dur, system, port) VALUES('1239923472','1911','20090416',888,2,27)
INSERT INTO Records(seconds, time, date, dur, system, port) VALUES('1239924347','1925','20090416',25,1,100)
INSERT INTO Records(seconds, time, date, dur, system, port) VALUES('1239924301','1925','20090416',77,2,55)
INSERT INTO Records(seconds, time, date, dur, system, port) VALUES('1239924332','1925','20090416',52,2,43)
INSERT INTO Records(seconds, time, date, dur, system, port) VALUES('1239924240','1924','20090416',151,1,17)
INSERT INTO Records(seconds, time, date, dur, system, port) VALUES('1239924313','1925','20090416',96,2,62)
INSERT INTO Records(seconds, time, date, dur, system, port) VALUES('1239924094','1921','20090416',315,2,16)
INSERT INTO Records(seconds, time, date, dur, system, port) VALUES('1239923643','1914','20090416',788,2,34)
INSERT INTO Records(seconds, time, date, dur, system, port) VALUES('1239924447','1927','20090416',6,2,27)
INSERT INTO Records(seconds, time, date, dur, system, port) VALUES('1239924342','1925','20090416',119,2,15)
INSERT INTO Records(seconds, time, date, dur, system, port) VALUES('1239924397','1926','20090416',76,2,41)
INSERT INTO Records(seconds, time, date, dur, system, port) VALUES('1239924457','1927','20090416',23,2,27)
4

6 回答 6

3

我认为 MarkusQ 有答案,但让我开发一个您可能会发现更容易使用的替代方案。我将使用我惯用的方法来开发它,将其作为一系列简单的视图转换,类似于过程语言中的功能分解

首先,让我们将所有内容放在共同的单位中。回想一下record' 列s是自 1970 年 1 月 1 日午夜以来的秒数我们可以通过仅对一天中的秒数取 s 模数来找到自通话当天午夜以来的秒数:s % (60 * 60 * 24)

select *, 
s % (60 * 60 * 24) as start_secs_from_midnight,
s % (60 * 60 * 24) + dur - 1 as end_secs_from_midnight,
;

我们从中减去 1,s + dur因为从 12:00:00 开始的一秒呼叫也在 12:00:00 结束。

我们可以通过将这些结果除以 60 或仅除以来找到自午夜以来的分钟数floor( s / 60 ) % (60 * 24)

create view record_mins_from_midnight as
select *, 
floor( s / 60 ) % (60 * 24) as start_mins_fm,
floor( ( s + dur - 1) / 60 ) % (60 * 24) as end_mins_fm 
from record
;

现在我们创建一个分钟表。我们需要其中的 1440 个,编号从 0 到 1439。在不支持任意序列的数据库中,我创建了一个人工范围或序列,如下所示:

  create table artificial_range ( 
   id int not null primary key auto_increment, idz int) ;
  insert into artificial_range(idz) values (0);
  -- repeat next line to double rows
  insert into artificial_range(idz) select idz from artificial_range;

所以要创建一个minute表:

  create view minute as 
   select id - 1 as active_minute 
   from artificial_range 
   where id <= 1440
   ;

现在我们只需加入minute我们的记录视图

create view record_active_minutes as
select * from minutes a 
join record_mins_from_midnight b
on (a.active_minute >= b.start_mins_fm 
and a.active_minute <= b.end_mins_fm 
 ;

这只是交叉乘积/乘以记录行,因此我们在呼叫处于活动状态的每一整分钟都有一个记录行。

请注意,我通过将活动定义为“在一分钟内发生的(部分)呼叫”来做到这一点。也就是说,根据此定义,从 12:00:59 开始并在 12:01:01 结束的两秒通话发生在两个不同的分钟内,但是从 12:00:58 开始并在 12 结束的两秒通话: 00:59 发生在一分钟内。

我这样做是因为您指定了“所以,我需要一种方法来检查 7:00-7:01、7:01-7:02 的活动呼叫计数”。如果您希望只考虑在超过 1 分钟内发生的持续时间超过 60 秒的呼叫,则需要调整加入。

现在,如果我们想找到等于或大于分钟粒度的任何粒度的活动记录数,我们只需在最后一个视图上进行分组。要找到每小时的平均通话次数,我们除以 60 以将分钟变为小时:

 select floor( active_minute / 60 ) as hour, 
 count(*) / 60 as avg_concurent_calls_per_minute_for_hour
 from record_active_minutes
 group by floor( active_minute / 60 ) ;

请注意,这是所有电话的平均每小时,在所有日子里;如果我们想将其限制在特定的日期或日期范围内,我们会添加一个where子句。


但是等等,还有更多!

如果我们创建一个record_active_minutes执行左外连接的版本,我们可以得到一个显示一天中所有小时的平均值的报告:

 create view record_active_minutes_all as
 select * 
 from 
 minutes a 
 left outer join record_mins_from_midnight b
   on (a.active_minute >= b.start_mins_fm 
       and a.active_minute <= b.end_mins_fm) 
 ;

然后我们再次进行选择,但针对的是新视图:

 select floor( active_minute / 60 ) as hour, 
 count(*) / 60 as avg_concurent_calls_per_min
 from record_active_minutes_all
 group by floor( active_minute / 60 ) ;


+------+------------------------------+
| hour | avg_concurrent_calls_per_min |
+------+------------------------------+
|    0 |                       0.0000 |
|    1 |                       0.0000 |
|    2 |                       0.0000 |
|    3 |                       0.0000 |
   etc....

我们也可以用 where 来索引它。record不幸的是,连接意味着我们将在特定小时内不存在调用的基础表中有空值,例如,

 select floor( active_minute / 60 ) as hour, 
 count(*) / 60 as avg_concurent_calls_per_min
 from record_active_minutes_all
 where month(date) = 1 and year(date) = 2008 
 group by floor( active_minute / 60 ) ;

将在未发生呼叫的几个小时内恢复任何行。如果我们仍然想要显示所有时间的“类似报告”视图,我们确保我们还包括那些没有记录的时间:

 select floor( active_minute / 60 ) as hour, 
 count(*) / 60 as avg_concurent_calls_per_minute_for_hour
 from record_active_minutes_all
 where (month(date) = 1 and year(date) = 2008) 
 or date is null 
 group by floor( active_minute / 60 ) ;

请注意,在最后两个示例中,我使用的是 SQL 日期(函数monthyear可以应用到的日期),而不是记录表中的 char(4) 日期。

这带来了另一点:记录表中的日期和时间都是多余的和非规范化的,因为每个都可以从您的 column s 派生。将它们留在表中可能会出现不一致的行,其中date(s) <> datetime(s) <> time. 我宁愿这样做:

   create table record ( id int not null primary key, s, duration) ; 

   create view record_date as 
   select *, dateadd( ss, s, '1970-01-01') as call_date
   from record
  ;

dateadd函数中,ss是一个枚举类型,告诉函数加秒;s是记录中的列。

于 2009-04-18T04:29:03.830 回答
1

如果我理解正确,您想要获取开始时间小于 t+60 秒且开始时间加上持续时间小于或等于 t 的所有记录的计数,对于感兴趣的间隔中的每个 t (例如,t=7:00、7:01、7:02...等)。

然后只需对这些计数进行平均即可。

但什么是平均值?这只是总和除以项目数,对吗?在这种情况下,项目的数量将始终等于以分钟为单位的时间范围,并且总和将等于间隔内的持续时间-分钟的总和,您可以根据给定的数据一次性计算出.

现在听起来不太可能了?在伪 SQL 中:

select sum( 
     ((time+duration rounded up to next minute, capped at end of period)
    - (time rounded down, bottom-capped at start of period) - 1)
     /(1 minute) )
  from Records
  where date is right

然后将其除以感兴趣期间的分钟数。

于 2009-04-17T18:56:31.353 回答
1

我的第一个建议是,如果您发现自己说(在使用 SQL 时)“我可以创建一个循环......”那么您应该立即开始寻找基于集合的方法。使用 SQL 时要摆脱程序化思维模式。

您的逻辑仍有一些模糊的部分。如果通话只是在那一分钟内通话的任何部分,那么通话是否算作在一分钟内?例如,如果一个呼叫从 1923 开始并持续 62 秒,是否认为它与从 1924 开始的所有呼叫重叠?我会假设那个是肯定的,但如果不是这样,你可以调整下面的代码。这应该是一个小调整。

对于以分钟为单位的细分,我通常会使用一个带有时间跨度的表格——我感兴趣的每个切片的开始时间和结束时间。在您的情况下,因为您正在处理确切的分钟数并且您的开始时间以分钟为单位(即使数据类型被搞砸了),我将只使用一个包含确切分钟数的列。

要设置该表:

CREATE TABLE dbo.Minutes (
    start_time  INT NOT NULL,
    CONSTRAINT PK_Minutes PRIMARY KEY CLUSTERED (start_time)
)

DECLARE
    @hour   TINYINT,
    @minute TINYINT

SET @hour = 19
SET @minute = 0

WHILE (@hour <= 20)
BEGIN
    INSERT INTO dbo.Minutes (start_time) VALUES (@hour * 100 + @minute)

    SET @minute = @minute + 1
    IF @minute = 60
    BEGIN
        SET @minute = 0
        SET @hour = @hour + 1
    END
END

现在我们可以选择平均值等。

SELECT
    M.start_time,
    COUNT(R.seconds)
FROM
    dbo.Minutes M
LEFT OUTER JOIN dbo.Records R ON
    M.start_time BETWEEN CAST(R.time AS INT) AND
        (CAST(SUBSTRING(time, 1, 2) AS INT) * 100) +    -- hours
        (FLOOR((CAST(SUBSTRING(time, 3, 2) AS INT) + FLOOR(dur/60))/60)) +  -- carryover to hours
        (CAST(SUBSTRING(time, 3, 2) AS INT) + dur/60) % 60  -- minutes
GROUP BY
    M.start_time

您需要将其用作子查询来获取给定时间内的平均值。由于星期五很晚,我将把这一步留给你;)

编辑:最后一个警告:我没有考虑跨越日界的时间跨度(即,超过午夜)。希望代码为您指明正确的方向。更好的方法可能是创建一个视图,将所有这些讨厌的字符串转换为实际的 DATETIME 值,然后使用 Minutes 表就变得非常简单。

于 2009-04-17T19:40:51.683 回答
1

正如 MarkusQ 所说,您对“并发”的定义允许您缩短数学运算。

  • 呼叫 (A) 开始于“12:00:59”并结束于“12:01:01”
  • 呼叫 (B) 开始于“12:01:59”并结束于“12:02:01”
    => 1 次呼叫在“12:00”间隔
    => 2 次呼叫在“12:01”间隔
    => 1在“12:02”间隔调用

那么平均并发调用为 (1+2+1)/intervalCount

(1+2+1) 可以以不同的方式计算,并且更快/更容易:

  • 通话 (A) 涵盖 2 个不同的分钟间隔(12:00 和 12:01)
  • 通话 (B) 覆盖 2 个不同的分钟间隔(12:01 和 12:02)
    => 总覆盖分钟 = 4

这里的重要事实(以及为什么我在 MarkusQ 发布后打扰回复)是呼叫本身的持续时间不足以计算覆盖了多少分钟间隔。在我的示例中,两个通话仅持续 2 秒...

您需要以下信息:
- “开始时间”,四舍五入到分钟
- “结束时间”,四舍五入到分钟
=> 覆盖的时间间隔 = 分钟差数 + 1

要将“时间”字段四舍五入到分钟,我会使用这个......

DATEADD(minute, DATEDIFF(minute, 0, time), 0)

所以一个电话覆盖的分钟数将是......

DATEDIFF(
   minute,
   DATEADD(minute, DATEDIFF(minute, 0, time), 0),
   DATEADD(second, dur, time)
) + 1

No need to round the "end time" down.
Using DATEDIFF(minute) gives rounding down anyway.

将该值与您正在查看的范围相加,然后除以该范围内的分钟数,您就有了答案。

如果你只是在寻找真正并发的调用,你就不能使用这样的技巧,但它仍然是可能的(我不得不做类似的事情)。但是对于您对并发的定义,应该这样做......

DECLARE
   @date DATETIME, @start DATETIME, @end DATETIME
SELECT
   @date = '2009 Jan 01', @start = '12:00', @end = '13:00'

SELECT
   system,
   SUM(
       DATEDIFF(
          minute,
          CASE WHEN
             CAST(LEFT(time,2) + ':' + RIGHT(time,2) AS DATETIME) < @start
          THEN
             @start
          ELSE
             CAST(LEFT(time,2) + ':' + RIGHT(time,2) AS DATETIME)
          END,
          CASE WHEN
             DATEADD(second, dur, CAST(LEFT(time,2) + ':' + RIGHT(time,2) AS DATETIME)) > @end
          THEN
             @end
          ELSE
             DATEADD(second, dur, CAST(LEFT(time,2) + ':' + RIGHT(time,2) AS DATETIME))
          END
       ) + 1
   )
   /
   CAST(DATEDIFF(minute, @start, @end) AS FLOAT)
FROM
   records
WHERE
   date = @date
   AND CAST(LEFT(time,2) + ':' + RIGHT(time,2) AS DATETIME) >= @start
   AND DATEADD(second, dur, CAST(LEFT(time,2) + ':' + RIGHT(time,2) AS DATETIME)) < @end
GROUP BY
   system


这将故意不包括间隔 13:00->13:01
仅包括 12:00->12:01 到 12:59->13:00 的 60 个“1 分钟长间隔”


编辑:

我刚刚注意到您的时间和日期存储为字符串,您需要将它们转换为 DATETIME 才能使我的代码正常工作。

编辑2:

错误已更正。如果呼叫开始于“11:59:01”并结束于“12:00:01”,则不应计算“11:59”间隔。添加 CASE 语句以进行补偿。

各种布局编辑

于 2009-04-17T19:22:48.823 回答
0

我只能看到一种从通话记录中提取数据的方法:

创建事件列表,其中事件定义为通话开始或通话结束。(因此每个通话记录将生成两个事件。)每个事件项应包含:系统、日期时间和布尔值开始/结束。日期时间应向下舍入到最接近的分钟。

按(系统、日期时间)对该列表进行排序并扫描它。对于每个调用开始,将 CURCNT 加一。对于每个呼叫结束,将 CURCNT 减 1。

如果日期时间值与之前的记录不同,请将 CURCNT 添加到 HOURSUM。如果 datetime 值指示新小时的开始,则将 HOURSUM 除以 60,写入新的结果记录(系统、日期、小时、平均值),并将 HOURSUM 重置为零。

应该很明显什么时候初始化CURCNT和HOURSUM,当系统值与之前的记录不同时怎么办等等。

-阿尔。

于 2009-04-17T21:10:23.667 回答
0

我通过将数据转换为更简单的格式来解决这个问题。我创建了一个表格,其中每一行代表一分钟的通话。一旦你知道了每小时每分钟的平均值就很简单了。那里有多个选择来显示中间结果。只要被查询的时间范围和时长不是很大,应该没问题...?

CREATE TABLE #Records(
  seconds char(10),
  [time] char(4),
  date char(8),
  dur int,
  system int,
  port int
)

/*
seconds is an s[time] value. It's the difference of seconds from UTC 1/1/1970 00:00:00 to the current UTC [time], we use it as an identifier (like epoch).
[time] is the [time] the call was made.
date is the day the call was made.
dur is the duration of the call in seconds.
system is the system number.
port is the port on the system (not particularly relevant for this question).
*/

INSERT INTO #Records(seconds, [time], date, dur, system, port) VALUES('1239924228','1923','20090416',105,2,2)
INSERT INTO #Records(seconds, [time], date, dur, system, port) VALUES('1239923455','1910','20090416',884,1,97)
INSERT INTO #Records(seconds, [time], date, dur, system, port) VALUES('1239924221','1923','20090416',116,2,15)
INSERT INTO #Records(seconds, [time], date, dur, system, port) VALUES('1239924259','1924','20090416',90,1,102)
INSERT INTO #Records(seconds, [time], date, dur, system, port) VALUES('1239923458','1910','20090416',891,2,1)
INSERT INTO #Records(seconds, [time], date, dur, system, port) VALUES('1239924255','1924','20090416',99,2,42)
INSERT INTO #Records(seconds, [time], date, dur, system, port) VALUES('1239924336','1925','20090416',20,2,58)
INSERT INTO #Records(seconds, [time], date, dur, system, port) VALUES('1239924293','1924','20090416',64,2,41)
INSERT INTO #Records(seconds, [time], date, dur, system, port) VALUES('1239923472','1911','20090416',888,2,27)
INSERT INTO #Records(seconds, [time], date, dur, system, port) VALUES('1239924347','1925','20090416',25,1,100)
INSERT INTO #Records(seconds, [time], date, dur, system, port) VALUES('1239924301','1925','20090416',77,2,55)
INSERT INTO #Records(seconds, [time], date, dur, system, port) VALUES('1239924332','1925','20090416',52,2,43)
INSERT INTO #Records(seconds, [time], date, dur, system, port) VALUES('1239924240','1924','20090416',151,1,17)
INSERT INTO #Records(seconds, [time], date, dur, system, port) VALUES('1239924313','1925','20090416',96,2,62)
INSERT INTO #Records(seconds, [time], date, dur, system, port) VALUES('1239924094','1921','20090416',315,2,16)
INSERT INTO #Records(seconds, [time], date, dur, system, port) VALUES('1239923643','1914','20090416',788,2,34)
INSERT INTO #Records(seconds, [time], date, dur, system, port) VALUES('1239924447','1927','20090416',6,2,27)
INSERT INTO #Records(seconds, [time], date, dur, system, port) VALUES('1239924342','1925','20090416',119,2,15)
INSERT INTO #Records(seconds, [time], date, dur, system, port) VALUES('1239924397','1926','20090416',76,2,41)
INSERT INTO #Records(seconds, [time], date, dur, system, port) VALUES('1239924457','1927','20090416',23,2,27)

/* convert date + [time] into datetimes */
select 
    seconds,
    system,
    cast(date + ' ' + left([time], 2) + ':' + right([time], 2) as datetime) as start_date,
    /* end date to the minute */
    dateadd(mi, datediff(mi, 0, dateadd(s, dur, cast(date + ' ' + left([time], 2) + ':' + right([time], 2) as datetime))), 0) as end_date
into 
    #r
from
    #Records

select * from #r order by system, seconds, start_date, end_date;

/* create a row for each minute of each call */
create table #r_min(rnd int, seconds char(10), system int, minute datetime)

declare @maxrnd int;
select @maxrnd = max(datediff(mi, r.start_date, r.end_date)) from #r r
declare @i int;
set @i = 0;

while @i < @maxrnd begin

    insert into #r_min
    select @i, r.seconds, r.system, dateadd(mi, @i, r.start_date)
    from #r r
    where dateadd(mi, @i, r.start_date) <= r.end_date

set @i = @i + 1
end

select * from #r_min order by system, seconds, minute

/* concurrent per minute */
select  
    system, minute, count(*) as cnt
from 
    #r_min 
group by
    system, minute
order by 
    system, minute

/* avg concurrent per minute by hour */
select
    m.system,
    dateadd(hh, datediff(hh, 0, m.minute), 0) as hour,
    avg(m.cnt) as average_concurrent_per_minute
from
    (select  
        system, minute, count(*) as cnt
    from 
        #r_min 
    group by
        system, minute
    ) m
group by
    m.system,
    dateadd(hh, datediff(hh, 0, m.minute), 0)


drop table #Records
drop table #r
drop table #r_min

最后的选择产生...

system  hour    average_concurrent_per_minute
1   2009-04-16 19:00:00.000 1
2   2009-04-16 19:00:00.000 3
于 2009-04-17T20:37:19.143 回答