Boşluklar ve Adalar: İstemci çözümü ve T-SQL sorgusu


10

Boşluklar ve adalar için bir T-SQL çözümü istemcide çalışan bir C # çözümünden daha hızlı çalışabilir mi?

Spesifik olarak, bazı test verileri verelim:

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

Bu ilk test verisi kümesinin tam olarak bir boşluğu vardır:

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

İkinci test verisi seti, her iki bitişik aralık arasında bir boşluk olan 2M-1 boşluklara sahiptir:

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

Şu anda 2008 R2 kullanıyorum, ancak 2012 çözümleri çok hoş karşılanıyor. C # çözümümü bir cevap olarak gönderdim.

Yanıtlar:


4

Ve 1 saniyelik bir çözüm ...

;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

Bu, diğer çözümünüzden yaklaşık% 30 daha hızlıdır. 1 boşluk: (00: 00: 12.1355011 00: 00: 11.6406581), 2M-1 boşlukları (00: 00: 12.4526817 00: 00: 11.7442217). Yine de, en kötü durumda, tam olarak Adam Machanic'in Twitter'da tahmin ettiği gibi, müşteri tarafı çözümünden yaklaşık% 25 daha yavaş.
AK

4

Aşağıdaki C # kodu sorunu çözer:

    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);

Bu kod bu saklı yordamı çağırır:

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

Aşağıdaki zamanlarda 2M aralıklarla bir boşluk bulur ve yazdırır, sıcak önbellek:

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

Aşağıdaki zamanlarda 2M aralıklarla 2M-1 boşlukları bulur ve yazdırır, sıcak önbellek:

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

Bu çok basit bir çözüm - gelişmem 10 dakika sürdü. Yeni bir üniversite mezunu bu konuyu gündeme getirebilir. Veritabanı tarafında, yürütme planı çok az CPU ve bellek kullanan önemsiz bir birleştirme birleşimidir.

Düzenleme: gerçekçi olmak için, istemci ve sunucuyu ayrı kutularda çalıştırıyorum.


Evet, ancak sonuç kümesini dosya olarak değil, veri kümesi olarak geri istiyorsam ne olur?
Peter Larsson

Çoğu uygulama IEnumerable <SomeClassOrStruct> kullanmak ister - bu durumda bir listeye satır eklemek yerine yalnızca getiri veririz. Bu örneği kısa tutmak için, ham performansı ölçmek için gerekli olmayan birçok şeyi kaldırdım.
AK

Ve bu cpu içermiyor mu? Yoksa çözümünüze zaman ekliyor mu?
Peter Larsson

@PeterLarsson kıyaslama için daha iyi bir yol önerebilir misiniz? Bir dosyaya yazmak, istemci tarafından oldukça yavaş veri tüketimini taklit eder.
AK

3

Ben bu bir SQL Server bilgimin sınırlarını tükenmiş düşünüyorum.

SQL sunucusunda bir boşluk bulmak için (C # kodunun yaptığı şey) ve boşlukları (ilk başlatmadan önce veya son bitişten sonra olanlar) başlatmayı veya sonlandırmayı umursamıyorsanız, aşağıdaki sorgu (veya değişkenler) bulabildiğim en hızlı:

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

Her bir başlangıç-bitiş seti için, başlangıç ​​ve bitişe ayrı diziler olarak davranabilir, bitişi bir dengeleyebilir ve boşluklar gösterilebilir.

örneğin (S1, F1), (S2, F2), (S3, F3) 'i alın ve şu şekilde sıralayın: {S1, S2, S3, null} ve {null, F1, F2, F3}. her sette ve boşluklar F set değerinin S set değerinden daha az olduğu yerlerdir ... sorun bence SQL sunucusunda iki ayrı seti birleştirmek veya sadece değerlerin sırasına göre karşılaştırmak mümkün değildir. küme ... dolayısıyla satır_sayısı işlevinin tamamen satır numarasına dayalı olarak birleştirmemizi sağlamak için kullanılması ... ancak SQL sunucusuna bu değerlerin benzersiz olduğunu söylemenin bir yolu yoktur (bunları bir indekse sahip bir tabloya eklemeden) üzerinde - hangisi daha uzun sürer - denedim), bu yüzden birleştirme birleşimi en iyi olduğunu düşünüyorum? (yapabileceğim her şeyden daha hızlı olduğunu kanıtlamak zor olsa da)

LAG / LEAD işlevlerini kullanarak çözümler elde edebildim:

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

(bu arada, sonuçları garanti etmiyorum - işe yarıyor gibi görünüyor, ancak sanırım Görevler tablosunda sıralı olmaya başladım ... ve daha yavaştı)

Toplam değişikliğini kullanma:

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

(sürpriz değil, aynı zamanda daha yavaş)

Hatta bir CLR toplama işlevi denedim (toplamı değiştirmek için - toplamdan daha yavaştı ve verilerin sırasını korumak için row_number () kullanıldı) ve CLR (iki sonuç kümesini açmak ve değerleri tamamen temel alan değerleri karşılaştırmak için) ve daha yavaştı. SQL ve CLR sınırlamalarında kafamı birçok kez denedim, birçok yöntemi denedim

Ve ne için?

Aynı makinede çalışan ve hem C # verilerini hem de SQL filtrelenmiş verileri bir dosyaya tükürmek (orijinal C # koduna göre), zamanlar neredeyse aynıdır ... 1 boşluk verileri için yaklaşık 2 saniye (C # genellikle daha hızlı ), Çoklu boşluk veri kümesi için 8-10 saniye (SQL genellikle daha hızlıdır).

NOT : SQL Server Geliştirme Ortamı zamanlama karşılaştırması için kullanmayın, çünkü ızgaraya gösterilmesi zaman alır. SQL 2012, VS2010, .net 4.0 İstemci profili ile test edildiği gibi

Her iki çözümün de SQL sunucusundaki verilerin aynı sıralama işlemini gerçekleştirdiğine dikkat çekeceğim, böylece getirme sıralaması için sunucu yükü benzer olacaktır, hangi çözümü kullanırsanız kullanın, tek fark istemcideki (sunucu yerine) işlemdir. ve ağ üzerinden aktarım.

Farklı personel üyeleri tarafından bölümleme yaparken veya boşluk bilgileriyle ekstra verilere ihtiyaç duyabildiğinizde (ne bir personel kimliği dışında başka bir şey düşünemesem de) ne fark olabilir, ya da tabii ki orada bir olduğunu yavaş SQL sunucusu ve istemci makine (veya arasındaki veri bağlantısı yavaş istemci) ... Ne de birden fazla kullanıcı için kilit zamanlarda veya çekişme sorunları veya CPU / AĞ konularda bir karşılaştırmasını yaptık ... Bu yüzden hangisinin bu durumda bir darboğaz olması daha muhtemel olduğunu bilmiyorum.

Ne biliyorum, evet, SQL Server bu tür küme karşılaştırmaları iyi değil ve sorguyu doğru yazmazsanız bunun için ödeyecek.

C # sürümünü yazmaktan daha mı kolay ya da zor mu? Tamamen emin değilim, Değişim +/- 1, toplam çözümü çalıştıran da tamamen sezgisel değil ve ben ama ortalama bir mezunun geleceği ilk çözüm değil ... bir kez yapıldığında kopyalamak yeterince kolay, ama ilk etapta yazmak içgörü gerektirir ... aynı SQL versiyonu için de söylenebilir. Hangisi daha zor? Hangisi haydut verilere daha dayanıklı? Hangisi paralel işlemler için daha fazla potansiyele sahiptir? Programlama çabasına kıyasla farkın ne kadar küçük olduğu gerçekten önemli mi?

Son bir not; veriler üzerinde belirtilmemiş bir kısıtlama vardır - StartedAt, FinishedAt değerinden daha az olmalıdır , aksi takdirde kötü sonuçlar alırsınız.


3

İşte 4 saniyede çalışan bir çözüm.

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, bir boşluğu olan bir veri setinde bu 10 kattan daha yavaştır: (00: 00: 18.1016745 - 00: 00: 17.8190959) 2M-1 boşluklu verilerde, 2 kat daha yavaştır: (00:00 : 17.2409640 00: 00: 17.6068879)
AK
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.