用于识别配对项的SQL查询(具有挑战性)

时间:2012-07-09 21:38:11

标签: sql sql-server

假设存在一个包含一个表的关系数据库:

{datetime, tapeID, backupStatus}

2012-07-09 3:00, ID33, Start
2012-07-09 3:05, ID34, Start
2012-07-09 3:10, ID35, Start
2012-07-09 4:05, ID34, End
2012-07-09 4:10, ID33, Start
2012-07-09 5:05, ID33, End
2012-07-09 5:10, ID34, Start
2012-07-09 6:00, ID34, End
2012-07-10 4:00, ID35, Start
2012-07-11 5:00, ID35, End

tapeID = 100个不同磁带中的任何一个,每个磁带都有自己唯一的ID。

backupStatus =两个作业之一,包括开始或结束。

我想编写一个返回五个字段的SQL查询

{startTime,endTime,tapeID,totalBackupDuration,numberOfRestarts}
2012-07-09 3:00,2012-07-09 5:05, ID33, 0days2hours5min,1
2012-07-09 3:05,2012-07-09 4:05, ID34, 0days1hours0min,0
2012-07-09 3:10,2012-07-10 5:00, ID35, 0days0hours50min,1
2012-07-09 5:10,2012-07-09 6:00, ID34, 0days0hours50min,0

我希望将开始日期和结束日期配对,以确定每个备份集何时真正完成。需要注意的是,单个备份集的备份可能会重新启动,因此可能会有多个启动时间在下一个End事件之前被认为是完成的。单个备份集可能每天备份多次,需要将其标识为具有单独的开始和结束时间。

感谢您的帮助! 乙

4 个答案:

答案 0 :(得分:2)

您需要做的是为所有开始分配下一个结束日期。然后计算中间的开始次数。

select tstart.datetime as starttime, min(tend.datetime) as endtime, tstart.tapeid
from (select *
      from t
      where BackupStatus = 'Start'
     ) tstart join
     (select *
      from t
      where BackupStatus = 'End'
     ) tend
     on tstart.tapeid = tend.tapeid and
        tend.datetime >= tstart.datetime

这很接近,但我们每个结束时间都有多行(取决于启动次数)。为了解决这个问题,我们需要按tapeid和结束时间进行分组:

select min(a.starttime) as starttime, a.endtime, a.tapeid,
       datediff(s, min(a.starttime), endtime), -- NOT CORRECT, DATABASE SPECIFIC
       count(*) - 1 as NumRestarts
from (select tstart.dt as starttime, min(tend.dt) as endtime, tstart.tapeid 
      from (select *
            from #t
            where BackupStatus = 'Start'
           ) tstart join
           (select *
            from #t
            where BackupStatus = 'End'
           ) tend
           on tstart.tapeid = tend.tapeid and
              tend.dt >= tstart.dt
     group by tstart.dt, tstart.tapeid
    ) a
group by a.endtime, a.tapeid 

我使用SQL Server语法编写了此版本。要创建测试表,您可以使用:

create table #t (
    dt datetime,
    tapeID varchar(255),
    BackupStatus varchar(255)
)

insert into #t (dt, tapeID, BackupStatus) values ('2012-07-09 3:00', 'ID33', 'Start')
insert into #t (dt, tapeID, BackupStatus) values ('2012-07-09 3:05', 'ID34', 'Start')
insert into #t (dt, tapeID, BackupStatus) values ('2012-07-09 3:10', 'ID35', 'Start')
insert into #t (dt, tapeID, BackupStatus) values ('2012-07-09 4:05', 'ID34', 'End')
insert into #t (dt, tapeID, BackupStatus) values ('2012-07-09 4:10', 'ID33', 'Start')
insert into #t (dt, tapeID, BackupStatus) values ('2012-07-09 5:05', 'ID33', 'End')
insert into #t (dt, tapeID, BackupStatus) values ('2012-07-09 5:10', 'ID34', 'Start')
insert into #t (dt, tapeID, BackupStatus) values ('2012-07-09 6:00', 'ID34', 'End')
insert into #t (dt, tapeID, BackupStatus) values ('2012-07-10 4:00', 'ID35', 'Start')
insert into #t (dt, tapeID, BackupStatus) values ('2012-07-11 5:00', 'ID35', 'End')

答案 1 :(得分:2)

这是我的版本。如果您向表中添加INSERT #T SELECT '2012-07-11 12:00', 'ID35', 'Start',您还会在此查询中看到未完成的备份。 OUTER APPLY是解决问题的自然方式。

SELECT
   Min(T.dt) StartTime,
   Max(E.dt) EndTime,
   T.tapeID,
   Datediff(Minute, Min(T.dt), Max(E.dt)) TotalBackupDuration,
   Count(*) - 1 NumberOfRestarts
FROM
   #T T
   OUTER APPLY (
      SELECT TOP 1 E.dt
      FROM #T E
      WHERE
         T.tapeID = E.tapeID
         AND E.BackupStatus = 'End'
         AND E.dt > T.dt
      ORDER BY E.dt
   ) E
WHERE
   T.BackupStatus = 'Start'
GROUP BY
   T.tapeID,
   IsNull(E.dt, T.dt)

关于CROSS APPLY的一件事是,如果你只返回一行并且外部引用都是真实的表,那么在SQL 2000中将它移动到派生表的WHERE子句中就具有等价:

SELECT
   Min(T.dt) StartTime,
   Max(T.EndTime) EndTime,
   T.tapeID,
   Datediff(Minute, Min(T.dt), Max(T.EndTime)) TotalBackupDuration,
   Count(*) - 1 NumberOfRestarts
FROM (
      SELECT
         T.*,
         (SELECT TOP 1 E.dt
            FROM #T E
            WHERE
               T.tapeID = E.tapeID
               AND E.BackupStatus = 'End'
               AND E.dt > T.dt
            ORDER BY E.dt
         ) EndTime
      FROM #T T
      WHERE T.BackupStatus = 'Start'
   ) T
GROUP BY
   T.tapeID,
   IsNull(T.EndTime, T.dt)

对于不是所有实际表的外部引用(您希望从另一个子查询的外部引用中获取计算值),您必须添加嵌套的派生表来完成此操作。

我终于咬紧牙关,做了一些真正的测试。我使用SPFiredrake的表填充脚本来查看大量数据的实际性能。我是以编程方式完成的,因此没有输入错误。我分别执行了10次执行,并为每列输出了最差和最好的值,然后平均了该统计数据的剩余8列值。

在填充表格后创建索引,填充因子为100%。只有聚集索引存在时,“索引”列显示1。当添加BackupStatus上的非聚集索引时,它显示2。

为了从测试中排除客户端网络数据传输,我将每个查询选择为如下变量:

DECLARE
   @StartTime datetime,
   @EndTime datetime,
   @TapeID varchar(5),
   @Duration int,
   @Restarts int;


WITH A AS (
-- The query here
)
SELECT
   @StartTime = StartTime,
   @EndTime = EndTime,
   @TapeID = TapeID,
   @Duration = TotalBackupDuration,
   @Restarts = NumberOfRestarts
FROM A;

我还将表列长度修剪为更合理的值:tapeID varchar(5),BackupStatus varchar(5)。实际上,BackupStatus应该是一个列,并且tapeID应该是一个整数。但是我们暂时会坚持使用varchar。

   Server  Indexes       UserName   Reads  Writes    CPU  Duration
---------  -------  -------------  ------  ------  -----  --------
   x86 VM        1          ErikE   97219       0    599       325
   x86 VM        1  Gordon Linoff     606       0  63980     54638
   x86 VM        1    SPFiredrake  344927     260  23621     13105

   x86 VM        2          ErikE   96388       0    579       324
   x86 VM        2  Gordon Linoff  251443       0  22775     11830
   x86 VM        2    SPFiredrake  197845       0  11602      5986

x64 Beefy        1          ErikE   96745       0    919        61
x64 Beefy        1  Gordon Linoff  320012      70  62372     13400
x64 Beefy        1    SPFiredrake  362545     288  20154      1686

x64 Beefy        2          ErikE   96545       0    685       164
x64 Beefy        2  Gordon Linoff  343952      72  65092     17391
x64 Beefy        2    SPFiredrake  198288       0  10477       924

注意:

  • x86 VM:几乎空闲的虚拟机,Microsoft SQL Server 2008(RTM) - 10.0.1600.22(Intel X86)
  • x64 Beefy:非常强大且可能非常繁忙的Microsoft SQL Server 2008 R2(RTM) - 10.50.1765.0(X64)

第二个索引帮助了所有查询,至少是我的。

有趣的是,Gordon最初在一台服务器上读取的数量很少,但是它的持续时间较短,因此显然选择了不同的执行计划可能是因为有更多资源来搜索可能的计划空间更快(做一个更强大的服务器)。但是,该索引提高了读取次数,因为该计划将CPU成本降低了一吨,因此优化器中的成本降低了。

答案 2 :(得分:1)

以为我会捅它。测试了Gordon Linoff的解决方案,并且在他自己的例子中没有完全正确计算tapeID 33(匹配到下一个开始,而不是相应的结束)。

我的尝试假设您正在使用SQL Server 2005+,因为它使用CROSS / OUTER APPLY。如果你需要它用于服务器2000我可能会摆动它,但这对我来说似乎是最干净的解决方案(因为你从所有的终端元素开始并匹配第一个启动元素来获得结果)。我也会注释,所以你可以理解我在做什么。

SELECT 
    startTime, endT.dt endTime, endT.tapeID, DATEDIFF(s, startTime, endT.dt), restarts
FROM 
    #t endT -- Main source, getting all 'End' records so we can match.
    OUTER APPLY ( -- Match possible previous 'End' records for the tapeID
        SELECT TOP 1 dt 
        FROM #t 
        WHERE dt < endT.dt AND tapeID = endT.tapeID 
        AND BackupStatus = 'End') g
    CROSS APPLY (SELECT ISNULL(g.dt, CAST(0 AS DATETIME)) dt) t 
    CROSS APPLY ( 
        -- Match 'Start' records between our 'End' record
        -- and our possible previous 'End' record.
        SELECT MIN(dt) startTime, 
            COUNT(*) - 1 restarts -- Restarts, so -1 for the first 'Start'
        FROM #t 
        WHERE tapeID = endT.tapeID AND BackupStatus = 'Start' 
                -- This is where our previous possible 'End' record is considered
            AND dt > t.dt AND dt < endt.dt) starts
WHERE 
    endT.BackupStatus = 'End'

编辑:测试数据生成脚本found at this link.

因此决定针对这三种方法运行一些数据,并发现ErikE的解决方案是最快的,我的是非常接近的第二,而Gordon对于任何相当大的集合都是低效的(即使使用1000条记录,它开始显示缓慢)。对于较小的集合(大约5k记录),我的方法胜过Erik,但不是很多。老实说,我喜欢我的方法,因为它不需要任何额外的聚合函数来获取数据,但ErikE在效率/速度战中获胜。

编辑2:对于表格中的55k记录(和12k匹配的开始/结束对),Erik需要~0.307s,我的需要~0.157s(平均超过50次尝试)。我对此感到有些惊讶,因为我会假设单个运行会转换为整体,但我想我的查询会更好地利用索引缓存,因此后续命中更便宜。看看执行计划,ErikE在主路径上只有1个分支,所以他最终在大多数查询中使用更大的集合。我有3个分支,它们更接近输出,因此我在任何给定时刻都会减少数据并在最后结合。

答案 3 :(得分:0)

让它变得非常简单 - 为start事件创建一个子查询,为End事件创建另一个子查询。为具有开始和结束的每一行的每个集合中的排名函数。然后,使用左连接进行2个子查询:

-- QUERY
WITH CTE as 
(
SELECT  dt
        , ID
        , status  
        --, RANK () OVER (PARTITION BY ID ORDER BY DT) as rnk1
        --,  RANK () OVER (PARTITION BY status ORDER BY DT) as rnk2
FROM INT_backup
)
SELECT * 
FROM CTE 
ORDER BY id, rnk2


select * FROM INT_backup order by id, dt

SELECT * 
FROM 
(
    SELECT  dt
            , ID
            , status  
            , rank () over (PARTITION by ID ORDER BY dt) as rnk
    FROM INT_backup
    WHERE status = 'start' 
) START_SET
LEFT JOIN 
(
    SELECT  dt
            , ID
            , status  
            , rank () over (PARTITION by ID ORDER BY dt) as rnk
    FROM INT_backup
    where status = 'END'
) END_SET
ON Start_Set.ID = End_SET.ID 
AND Start_Set.Rnk = End_Set.rnk