2

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

{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 = Start 或 End 两个分配之一。

我想编写一个返回五个字段的 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

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

提前感谢您的帮助!乙

4

4 回答 4

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')
于 2012-07-09T21:55:31.250 回答
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 的一件事是,如果您只返回一行并且外部引用都是真实的表,那么通过将其移动到派生表的 WHERE 子句中,您可以在 SQL 2000 中获得等效项:

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 成本降低了一吨,因此在优化器中的成本更低。

于 2012-07-10T20:14:58.257 回答
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'

编辑:在此链接中找到测试数据生成脚本。

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

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

于 2012-07-10T19:46:26.010 回答
0

让它变得非常简单——为开始事件创建一个子查询,为结束事件创建另一个子查询。对于具有开始和结束的每一行,每个集合中的排名函数。然后,对 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
于 2014-01-28T00:02:13.310 回答