差距和孤岛:客户端解决方案与T-SQL查询


10

用于间隙和孤岛的T-SQL解决方案能否比在客户端上运行的C#解决方案运行得更快?

具体来说,让我们提供一些测试数据:

CREATE TABLE dbo.Numbers
  (
    n INT NOT NULL
          PRIMARY KEY
  ) ; 
GO 

INSERT  INTO dbo.Numbers
        ( n )
VALUES  ( 1 ) ; 
GO 
DECLARE @i INT ; 
SET @i = 0 ; 
WHILE @i < 21 
  BEGIN 
    INSERT  INTO dbo.Numbers
            ( n 
            )
            SELECT  n + POWER(2, @i)
            FROM    dbo.Numbers ; 
    SET @i = @i + 1 ; 
  END ;  
GO

CREATE TABLE dbo.Tasks
  (
    StartedAt SMALLDATETIME NOT NULL ,
    FinishedAt SMALLDATETIME NOT NULL ,
    CONSTRAINT PK_Tasks PRIMARY KEY ( StartedAt, FinishedAt ) ,
    CONSTRAINT UNQ_Tasks UNIQUE ( FinishedAt, StartedAt )
  ) ;
GO

INSERT  INTO dbo.Tasks
        ( StartedAt ,
          FinishedAt
        )
        SELECT  DATEADD(MINUTE, n, '20100101') AS StartedAt ,
                DATEADD(MINUTE, n + 2, '20100101') AS FinishedAt
        FROM    dbo.Numbers
        WHERE   ( n < 500000
                  OR n > 500005
                )
GO

第一组测试数据恰好有一个差距:

SELECT  StartedAt ,
        FinishedAt
FROM    dbo.Tasks
WHERE   StartedAt BETWEEN DATEADD(MINUTE, 499999, '20100101')
                  AND     DATEADD(MINUTE, 500006, '20100101')

第二组测试数据具有2M -1间隙,即每两个相邻间隔之间的间隙:

TRUNCATE TABLE dbo.Tasks;
GO

INSERT  INTO dbo.Tasks
        ( StartedAt ,
          FinishedAt
        )
        SELECT  DATEADD(MINUTE, 3*n, '20100101') AS StartedAt ,
                DATEADD(MINUTE, 3*n + 2, '20100101') AS FinishedAt
        FROM    dbo.Numbers
        WHERE   ( n < 500000
                  OR n > 500005
                )
GO

目前,我正在运行2008 R2,但非常欢迎使用2012解决方案。我已经发布了我的C#解决方案作为答案。

Answers:


4

还有1秒的解决方案...

;WITH cteSource(StartedAt, FinishedAt)
AS (
    SELECT      s.StartedAt,
            e.FinishedAt
    FROM        (
                SELECT  StartedAt,
                    ROW_NUMBER() OVER (ORDER BY StartedAt) AS rn
                FROM    dbo.Tasks
            ) AS s
    INNER JOIN  (
                SELECT  FinishedAt,
                    ROW_NUMBER() OVER (ORDER BY FinishedAt) + 1 AS rn
                FROM    dbo.Tasks
            ) AS e ON e.rn = s.rn
    WHERE       s.StartedAt > e.FinishedAt

    UNION ALL

    SELECT  MIN(StartedAt),
        MAX(FinishedAt)
    FROM    dbo.Tasks
), cteGrouped(theTime, grp)
AS (
    SELECT  u.theTime,
        (ROW_NUMBER() OVER (ORDER BY u.theTime) - 1) / 2
    FROM    cteSource AS s
    UNPIVOT (
            theTime
            FOR theColumn IN (s.StartedAt, s.FinishedAt)
        ) AS u
)
SELECT      MIN(theTime),
        MAX(theTime)
FROM        cteGrouped
GROUP BY    grp
ORDER BY    grp

这比其他解决方案快30%。1个间隔:(00:00:12.1355011 00:00:11.6406581),2M-1间隔(00:00:12.4526817 00:00:11.7442217)就像在最坏的情况下,这仍然比客户端解决方案慢25%左右,这恰好是Adam Machanic在推特上所预测的。
AK

4

以下C#代码解决了该问题:

    var connString =
        "Initial Catalog=MyDb;Data Source=MyServer;Integrated Security=SSPI;Application Name=Benchmarks;";

    var stopWatch = new Stopwatch();
    stopWatch.Start();

    using (var conn = new SqlConnection(connString))
    {
        conn.Open();
        var command = conn.CreateCommand();
        command.CommandText = "dbo.GetAllTaskEvents";
        command.CommandType = CommandType.StoredProcedure;
        var gaps = new List<string>();
        using (var dr = command.ExecuteReader())
        {
            var currentEvents = 0;
            var gapStart = new DateTime();
            var gapStarted = false;
            while (dr.Read())
            {
                var change = dr.GetInt32(1);
                if (change == -1 && currentEvents == 1)
                {
                    gapStart = dr.GetDateTime(0);
                    gapStarted = true;
                }
                else if (change == 1 && currentEvents == 0 && gapStarted)
                {
                    gaps.Add(string.Format("({0},{1})", gapStart, dr.GetDateTime(0)));
                    gapStarted = false;
                }
                currentEvents += change;
            }
        }
        File.WriteAllLines(@"C:\Temp\Gaps.txt", gaps);
    }

    stopWatch.Stop();
    System.Console.WriteLine("Elapsed: " + stopWatch.Elapsed);

此代码调用此存储过程:

CREATE PROCEDURE dbo.GetAllTaskEvents
AS 
  BEGIN ;
    SELECT  EventTime ,
            Change
    FROM    ( SELECT  StartedAt AS EventTime ,
                      1 AS Change
              FROM    dbo.Tasks
              UNION ALL
              SELECT  FinishedAt AS EventTime ,
                      -1 AS Change
              FROM    dbo.Tasks
            ) AS TaskEvents
    ORDER BY EventTime, Change DESC ;
  END ;
GO

它在以下时间以2M的间隔查找并打印一个间隙,即热缓存:

1 gap: Elapsed: 00:00:01.4852029 00:00:01.4444307 00:00:01.4644152

在以下时间,它以2M的间隔查找并打印2M-1间隙,即热缓存:

2M-1 gaps Elapsed: 00:00:08.8576637 00:00:08.9123053 00:00:09.0372344 00:00:08.8545477

这是一个非常简单的解决方案-我花了10分钟来开发。最近应届大学毕业生可以提出。在数据库方面,执行计划是一个琐碎的合并联接,它只使用很少的CPU和内存。

编辑:为了现实,我在单独的盒子上运行客户端和服务器。


是的,但是如果我希望结果集作为数据集而不是文件返回怎么办?
彼得·拉尔森

大多数应用程序都希望使用IEnumerable <SomeClassOrStruct>-在这种情况下,我们只产生return而不是在列表中添加一行。为了使本示例简短,我删除了许多对于衡量原始性能不是必需的事情。
2013年

那是没有cpu的吗?还是会增加您的解决方案时间?
彼得·拉尔森

@PeterLarsson您可以建议一种更好的基准测试方法吗?写入文件模仿了客户端相当慢的数据消耗。
AK

3

我想我已经用尽了我对SQL Server知识的局限性....

为了找到SQL Server中的间隙(C#代码可以做什么),并且您不必担心间隙的开始或结束(那些间隙在第一次开始之前或在最后一个结束之后),则以下查询(或变体)是我最快能找到:

SELECT e.FinishedAt as GapStart, s.StartedAt as GapEnd
FROM 
(
    SELECT StartedAt, ROW_NUMBER() OVER (ORDER BY StartedAt) AS rn
    FROM dbo.Tasks
) AS s
INNER JOIN  
(
    SELECT  FinishedAt, ROW_NUMBER() OVER (ORDER BY FinishedAt) + 1 AS rn
    FROM    dbo.Tasks
) AS e ON e.rn = s.rn and s.StartedAt > e.FinishedAt

对于每个开始完成集来说,虽然有点费力,但您可以将开始和结束视为单独的序列,将结束偏移一个,并显示间隙。

例如,取(S1,F1),(S2,F2),(S3,F3),顺序为:{S1,S2,S3,null}和{null,F1,F2,F3}然后将第n行与第n行进行比较在每个集合中,间隙是F集合值小于S集合值的地方...我认为问题是,在SQL Server中,无法纯粹按照值的顺序连接或比较两个单独的集合set ...因此使用row_number函数允许我们仅基于行号进行合并...但是没有办法告诉SQL Server这些值是唯一的(无需将它们插入具有索引的表var中(它花了更长的时间-我尝试了),所以我认为合并联接的效果不佳?(尽管很难证明它何时比我能做的更快)

我能够使用LAG / LEAD函数获得解决方案:

select * from
(
    SELECT top (100) percent StartedAt, FinishedAt, LEAD(StartedAt, 1, null) OVER (Order by FinishedAt) as NextStart
    FROM dbo.Tasks
) as x
where NextStart > FinishedAt

(顺便说一句,我不保证结果-似乎可以工作,但是我认为它依赖于StartedAt在Tasks表中的顺序...而且速度较慢)

使用总和更改:

select * from
(
    SELECT EventTime, Change, SUM(Change) OVER (ORDER BY EventTime, Change desc ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as RunTotal --, x.*
    FROM    
    ( 
        SELECT StartedAt AS EventTime, 1 AS Change
        FROM dbo.Tasks
    UNION ALL
        SELECT  FinishedAt AS EventTime, -1 AS Change
        FROM dbo.Tasks
    ) AS TaskEvents
) as x
where x.RunTotal = 0 or (x.RunTotal = 1 and x.Change = 1)
ORDER BY EventTime, Change DESC

(不足为奇,也慢一些)

我什至尝试了CLR聚合函数(替换总和-它比总和慢,并依靠row_number()保持数据的顺序),以及CLR一个表值函数(打开两个结果集并完全基于比较值)按顺序)...也太慢了。我在SQL和CLR限制方面屡试不爽,尝试了许多其他方法...

为了什么

在同一台机器上运行,然后将C#数据和SQL过滤后的数据分散到一个文件中(按照原始C#代码),时间实际上是相同的.... 1个间隙数据大约需要2秒(C#通常更快),则需要8到10秒的多间隙数据集(SQL通常更快)。

注意:不要将SQL Server开发环境用于时序比较,因为它显示到网格需要时间。经过SQL 2012,VS2010,.net 4.0客户端配置文件的测试

我将指出,这两种解决方案在SQL Server上执行的数据排序几乎相同,因此无论您使用哪种解决方案,fetch-sort的服务器负载都将相似,唯一的区别是客户端(而不是服务器)上的处理,并通过网络进行传输。

我不知道当由不同的工作人员进行分区时,或者当您可能需要带有间隔信息的额外数据时(尽管除了工作人员id,我无法想到的其他东西),或者如果有一个缓慢的SQL服务器和客户机(或之间的数据连接客户端)......我也没有做的锁定时间,或争用问题,或CPU /网络问题,为多个用户的比较。所以我不知道在这种情况下哪一个更可能成为瓶颈。

我所知道的是,是的,SQL Server不擅长这种集合比较,如果您不正确地编写查询,则将付出昂贵的代价。

比编写C#版本容易还是困难?我不确定,Change +/- 1,运行总解决方案也不是很直观,我也不是普通毕业生会想到的第一个解决方案。一旦完成,它就很容易复制,但是首先要花很多精力才能写出来……对于SQL版本也可以这样说。哪个更难?哪个对流氓数据更健壮?哪个具有更大的并行操作潜力?与编程工作相比,差异如此之小真的重要吗?

最后一点;数据存在未声明的约束-StartedAt 必须小于FinishedAt,否则您将得到不良结果。


3

这是一个在4秒钟内运行的解决方案。

WITH cteRaw(ts, type, e, s)
AS (
    SELECT  StartedAt,
        1 AS type,
        NULL,
        ROW_NUMBER() OVER (ORDER BY StartedAt)
    FROM    dbo.Tasks

    UNION ALL

    SELECT  FinishedAt,
        -1 AS type, 
        ROW_NUMBER() OVER (ORDER BY FinishedAt),
        NULL
    FROM    dbo.Tasks
), cteCombined(ts, e, s, se)
AS (
    SELECT  ts,
        e,
        s,
        ROW_NUMBER() OVER (ORDER BY ts, type DESC)
    FROM    cteRaw
), cteFiltered(ts, grpnum)
AS (
    SELECT  ts, 
        (ROW_NUMBER() OVER (ORDER BY ts) - 1) / 2 AS grpnum
    FROM    cteCombined
    WHERE   COALESCE(s + s - se - 1, se - e - e) = 0
)
SELECT      MIN(ts) AS starttime,
        MAX(ts) AS endtime
FROM        cteFiltered
GROUP BY    grpnum;

彼得(Peter),在有一个缺口的数据集上,它的速度要慢10倍以上:(00:00:18.1016745-00:00:17.8190959)在有2M-1缺口的数据上,它要慢2倍:(00:00 :17.2409640 00:00:17.6068879)
AK
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.