Yinelenen sql sorgusu performans sorunu [kapalı]


9

Bu altıncı kez bu soruyu sormaya çalışıyorum ve bu da en kısa olanı. Önceki tüm denemeler, sorunun kendisinden ziyade bir blog yayınına daha benzer bir şeyle sonuçlandı, ancak size sorunumun gerçek olduğunu garanti ediyorum, sadece büyük bir konuyu ilgilendiriyor ve bu sorunun içerdiği tüm ayrıntılar olmadan sorunumun ne olduğu belli değil. İşte gidiyor ...

Öz

Ben bir veritabanı var, bu tür süslü bir şekilde veri depolamak sağlar ve benim iş süreci için gerekli standart dışı birkaç özellik sağlar. Özellikler aşağıdaki gibidir:

  1. Veri kurtarma ve otomatik günlüğe kaydetmeye olanak tanıyan, yalnızca ekleme yaklaşımıyla uygulanan, tahribatsız ve engellemeyen güncellemeler / silmeler (her değişiklik, bu değişikliği yapan kullanıcıya bağlıdır)
  2. Çoklu sürüm verileri (aynı verilerin birkaç sürümü olabilir)
  3. Veritabanı düzeyinde izinler
  4. ASİT spesifikasyonu ve işlem güvenli ile nihai tutarlılık oluşturur / günceller / siler
  5. Mevcut veri görünümünüzü istediğiniz zaman geri sarma veya hızlı ileri sarma yeteneği.

Bahsetmeyi unuttuğum başka özellikler de olabilir.

Veritabanı yapısı

Tüm kullanıcı verileri Itemstabloda JSON kodlu dize ( ntext) olarak depolanır . Tüm veritabanı işlemleri iki saklı yordam aracılığıyla gerçekleştirilir GetLatestve InsertSnashotGIT'in kaynak dosyaları nasıl kullandığına benzer veriler üzerinde çalışmasına izin verir.

Ortaya çıkan veriler, ön uçta tamamen bağlı grafiğe bağlanır (JOINed), bu nedenle çoğu durumda veritabanı sorguları yapmaya gerek yoktur.

Verileri Json kodlu formda saklamak yerine normal SQL sütunlarında saklamak da mümkündür. Ancak bu, genel karmaşıklık suşunu arttırır.

Verileri okuma

GetLatesttalimat şeklinde veriler içeren sonuçlar, açıklama için aşağıdaki diyagramı göz önünde bulundurun :

Yapı şeması

Diyagram, tek bir kayıtta yapılan değişikliklerin evrimini göstermektedir. Diyagramdaki oklar, düzenlemenin gerçekleştiği sürümü gösterir (Kullanıcının, çevrimiçi kullanıcı tarafından yapılan güncellemelere paralel olarak bazı verileri çevrimdışı güncellediğini hayal edin, bu durumda temelde iki veri sürümü olan çakışma ortaya çıkar. yerine).

Dolayısıyla, GetLatestaşağıdaki girdi zaman aralıklarında çağrı yapmak aşağıdaki kayıt sürümleriyle sonuçlanır:

GetLatest 0, 15  => 1       <= The data is created upon it's first occurance
GetLatest 0, 25  => 2       <= Inserting another version on top of first one overwrites the existing version
GetLatest 0, 30  => 3       <= The overwrite takes place as soon as the data is inserted
GetLatest 0, 45  => 3, 4    <= This is where the conflict is introduced in the system
GetLatest 0, 55  => 4, 5    <= You can still edit all the versions
GetLatest 0, 65  => 4, 6    <= You can still edit all the versions
GetLatest 0, 75  => 4, 6, 7 <= You can also create additional conflicts
GetLatest 0, 85  => 4, 7, 8 <= You can still edit records
GetLatest 0, 95  => 7, 8, 9 <= You can still edit records
GetLatest 0, 105 => 7, 8    <= Inserting a record with `Json` equal to `NULL` means that the record is deleted
GetLatest 0, 115 => 8       <= Deleting the conflicting versions is the only conflict-resolution scenario
GetLatest 0, 125 => 8, X    <= The conflict can be based on the version that was already deleted.
GetLatest 0, 135 => 8, Y    <= You can delete such version too and both undelete another version on parallel within one Snapshot (or in several Snapshots).
GetLatest 0, 145 => 8       <= You can delete the undeleted versions by inserting NULL.
GetLatest 0, 155 => 8, Z    <= You can again undelete twice-deleted versions
GetLatest 0, 165 => 8       <= You can again delete three-times deleted versions
GetLatest 0, 10000 => 8     <= This means that in order to fast-forward view from moment 0 to moment `10000` you just have to expose record 8 to the user.
GetLatest 55, 115  => 8, [Remove 4], [Remove 5] <= At moment 55 there were two versions [4, 5] so in order to fast-forward to moment 115 the user has to delete versions 4 and 5 and introduce version 8. Please note that version 7 is not present in results since at moment 110 it got deleted.

İçin için GetLatesther kayıt özel servis özelliklerini içermelidir böyle verimli bir arayüz desteklemek için BranchId, RecoveredOn, CreatedOn, UpdatedOnPrev, UpdatedOnCurr, UpdatedOnNext, UpdatedOnNextIdtarafından kullanıldığını GetLatestrekor için sağlanan zaman içine yeterince düşer olmadığını anlamaya GetLatestargümanlar

Veri ekleme

Nihai tutarlılığı, işlem güvenliğini ve performansını desteklemek için veriler özel çok aşamalı prosedürle veritabanına eklenir.

  1. Veriler, GetLatestsaklı yordam tarafından sorgulanamadan veritabanına eklenir .

  2. Veriler GetLatestsaklı yordam için kullanılabilir hale getirilir , veriler normalleştirilmiş (yani denormalized = 0) durumda kullanılabilir hale getirilir . Veri normalize halde iken, hizmet alanları BranchId, RecoveredOn, CreatedOn, UpdatedOnPrev, UpdatedOnCurr, UpdatedOnNext, UpdatedOnNextIdgerçekten yavaş olduğu hesaplanıyor.

  3. İşleri hızlandırmak için, veriler GetLatestsaklı yordam için kullanılabilir hale gelir gelmez normalleştiriliyor .

    • Adım 1,2,3 farklı işlemler içinde gerçekleştirildiğinden, her işlemin ortasında bir donanım arızası meydana gelebilir. Verileri ara durumda bırakma. Bu durum normaldir ve gerçekleşecek olsa bile, veriler aşağıdaki InsertSnapshotçağrıda iyileşir . Bu parçanın kodu, InsertSnapshotsaklı yordamın 2. ve 3. adımları arasında bulunabilir .

Sorun

(İşletme tarafından gerekli) yeni özellikler özel planı ayrı zorladı Denormalizerbağları-up hep birlikte özellikleri ve her ikisi için de kullanılır görünümü GetLatestve InsertSnapshot. Bundan sonra performans sorunları yaşamaya başladım. Başlangıçta SELECT * FROM Denormalizersadece ikinci kesirlerde yürütülürse, şimdi 10000 kayıt işlemek yaklaşık 5 dakika sürer.

Ben bir DB profesyonel değilim ve sadece mevcut veritabanı yapısı ile çıkmak yaklaşık altı ay sürdü. Yeniden düzenleme yapmak için önce iki hafta geçirdim ve ardından performans sorunumun temel nedenini anlamaya çalıştım. Sadece bulamıyorum. Şema (tüm dizinleri ile) SqlFiddle sığacak kadar büyük olduğundan veritabanı yedekleme (burada bulabilirsiniz) veriyorum, veritabanı ayrıca test amaçlı kullandığım eski veri (10000+ kayıt) içerir . Ayrıca Denormalizeryeniden düzenlenmiş ve acı yavaş yavaş görünüm için metin sağlıyorum :

ALTER VIEW [dbo].[Denormalizer]
AS
WITH Computed AS
(
    SELECT  currItem.Id,
            nextOperation.id AS NextId,
            prevOperation.FinishedOn AS PrevComputed,
            currOperation.FinishedOn AS CurrComputed,
            nextOperation.FinishedOn AS NextComputed

    FROM Items currItem 
    INNER JOIN dbo.Operations AS currOperation ON currItem.OperationId = currOperation.Id

    LEFT OUTER JOIN dbo.Items AS prevItem ON currItem.PreviousId = prevItem.Id
    LEFT OUTER JOIN dbo.Operations AS prevOperation ON prevItem.OperationId = prevOperation.Id 
    LEFT OUTER JOIN
    (
        SELECT MIN(I.id) as id, S.PreviousId, S.FinishedOn
        FROM Items I
        INNER JOIN
        (
            SELECT I.PreviousId, MIN(nxt.FinishedOn) AS FinishedOn
            FROM dbo.Items I
            LEFT OUTER JOIN dbo.Operations AS nxt ON I.OperationId = nxt.Id
            GROUP BY I.PreviousId
        ) AS S ON I.PreviousId = S.PreviousId 
        GROUP BY S.PreviousId, S.FinishedOn
    ) AS nextOperation ON nextOperation.PreviousId = currItem.Id

    WHERE currOperation.Finished = 1 AND currItem.Denormalized = 0
),

RecursionInitialization AS
(
    SELECT  currItem.Id,
            currItem.PreviousId,
            currItem.UUID,
            currItem.Json,
            currItem.TableName,
            currItem.OperationId,
            currItem.PermissionId,
            currItem.Denormalized,
            currItem.Id AS BranchID,
            COALESCE (C.PrevComputed, C.CurrComputed) AS CreatedOn,
            COALESCE (C.PrevComputed, CAST(0 AS BIGINT)) AS RecoveredOn,
            COALESCE (C.PrevComputed, CAST(0 AS BIGINT)) AS UpdatedOnPrev,
            C.CurrComputed AS UpdatedOnCurr,
            COALESCE (C.NextComputed, CAST(8640000000000000 AS BIGINT)) AS UpdatedOnNext,
            C.NextId AS UpdatedOnNextId,

            0 AS RecursionLevel

    FROM Items AS currItem
    INNER JOIN Computed AS C ON currItem.Id = C.Id
    WHERE currItem.Denormalized = 0

    UNION ALL

    SELECT  currItem.Id,
            currItem.PreviousId,
            currItem.UUID,
            currItem.Json,
            currItem.TableName,
            currItem.OperationId,
            currItem.PermissionId,
            currItem.Denormalized,
            currItem.BranchId,
            currItem.CreatedOn,
            currItem.RecoveredOn,
            currItem.UpdatedOnPrev,
            currItem.UpdatedOnCurr,
            currItem.UpdatedOnNext,
            currItem.UpdatedOnNextId,

            0 AS RecursionLevel

    FROM Items AS currItem
    WHERE currItem.Denormalized = 1
),
Recursion AS
(
    SELECT *
    FROM RecursionInitialization AS currItem

    UNION ALL

    SELECT  currItem.Id,
            currItem.PreviousId,
            currItem.UUID,
            currItem.Json,
            currItem.TableName,
            currItem.OperationId,
            currItem.PermissionId,
            currItem.Denormalized,

            CASE
                WHEN prevItem.UpdatedOnNextId = currItem.Id
                THEN prevItem.BranchID
                ELSE currItem.Id
            END AS BranchID,

            prevItem.CreatedOn AS CreatedOn,

            CASE
                WHEN prevItem.Json IS NULL
                THEN CASE
                            WHEN currItem.Json IS NULL
                            THEN prevItem.RecoveredOn
                            ELSE C.CurrComputed
                        END
                ELSE prevItem.RecoveredOn
            END AS RecoveredOn,

            prevItem.UpdatedOnCurr AS UpdatedOnPrev,

            C.CurrComputed AS UpdatedOnCurr,

            COALESCE (C.NextComputed, CAST(8640000000000000 AS BIGINT)) AS UpdatedOnNext,

            C.NextId,

            prevItem.RecursionLevel + 1 AS RecursionLevel
    FROM Items currItem
    INNER JOIN Computed C ON currItem.Id = C.Id
    INNER JOIN Recursion AS prevItem ON currItem.PreviousId = prevItem.Id
    WHERE currItem.Denormalized = 0
)
SELECT  item.Id,
        item.PreviousId,
        item.UUID,
        item.Json,
        item.TableName,
        item.OperationId,
        item.PermissionId,
        item.Denormalized,
        item.BranchID,
        item.CreatedOn,
        item.RecoveredOn,
        item.UpdatedOnPrev,
        item.UpdatedOnCurr,
        item.UpdatedOnNext,
        item.UpdatedOnNextId

FROM Recursion AS item
INNER JOIN
(
    SELECT Id, MAX(RecursionLevel) AS Recursion
    FROM Recursion AS item
    GROUP BY Id
) AS nested ON item.Id = nested.Id AND item.RecursionLevel = nested.Recursion
GO

Sorular)

Göz önünde bulundurulması gereken iki senaryo vardır: denormalize ve normalleştirilmiş vakalar:

  1. Orijinal yedeklemeye bakarak, bu SELECT * FROM Denormalizerkadar acı verici olanı yavaşlatan, Denormalizer görünümünün özyinelemeli kısmında bir sorun var gibi hissediyorum, kısıtlamayı denedim denormalized = 1ama eylemlerimin hiçbiri performansı etkilemedi.

  2. Çalıştırdıktan sonra UPDATE Items SET Denormalized = 0onu yapar GetLatestve SELECT * FROM Denormalizerçalışma içine yavaş senaryo (başlangıçta olduğu düşünülen), hız şeyler kadar biz hizmet alanlarını hesaplıyoruz için bir yol var BranchId, RecoveredOn, CreatedOn, UpdatedOnPrev, UpdatedOnCurr, UpdatedOnNext,UpdatedOnNextId

Şimdiden teşekkür ederim

PS

Sorgu gelecek için MySQL / Oracle / SQLite gibi diğer veritabanlarına kolayca taşınabilir hale getirmek için standart SQL sopa çalışıyorum, ama veritabanına özgü yapılara yapışmasını Tamam ben yardımcı olabilir standart sql varsa.


1
Standart SQL ve listelediğiniz DB'lerle ilgili olarak: burada bir CTE kullanıyorsunuz ve bunlar mySQL tarafından desteklenmiyor ve ana uygulamalar arasında bazı sözdizimi varyasyonları var. Ayrıca şu anda postgreslerde büyük bir performans endişesi olabilecek bir optimizasyon çiti. Bunların hiçbiri onları kullanmayı durdurmamalıdır, "bitişik liste" formundaki bir ağaçla genellikle iş için doğru araçtır, ancak uyumluluk bir endişe kaynağı olduğunda, bu noktalar sizi ısırmadan önce dikkat edilmesi gereken şeylerdir. diğer DBMS'lere geçiş gerçeğe dönüştüğünde ihtiyaç duyulan ekstra işler için hazırlanmıştır.
David Spillett

Teşekkür ederim, mümkün olduğunca standart SQL'e sadık kalmaya çalışıyorum. Bunun nedeni, mevcut kodun diğer Veritabanlarına taşınması gerektiğinde gelecekte sorun miktarını azaltması gerektiğine inanmam. Her zaman mümkün değildir. Denklemin bir parçası olan zaman faktörü de vardır. Mevcut veritabanı yapısını yerleştirmek için yarım yıl harcadım ... Sadece standart yapıları içermesini istiyorum, ancak 10 yıl daha gerektirecekse, gitmenin yolu değil ... Yani daha fazla standart yeniden düzenleme mümkün kabul etmekten mutluluk duyacağım ...
Lu4

1
Hayır, bu gibi durumlarda farklı öncelikler ve komplikasyonlarla başa çıkmak için pragmatik bir yol gibi geliyor. Sadece beyinle henüz karşılaşmamış olmanız durumunda akla getirilen sorunları döktüm (şu anda bu konuda bir şey yapmak mümkün / pratik olmasa bile, üretimde sürprizle ısırılmamaktan daha iyi! ).
David Spillett

Yanıtlar:


9

@ Lu4 .. Bu soruyu "Buzdağı İpucu" olarak kapatmak için oy kullandım ama sorgu ipucunu kullanarak 1 saniye altında çalıştırabileceksiniz. Bu sorgu yeniden düzenlenebilir ve kullanılabilir CROSS APPLY, ancak bir soru-cevap sitesinde yanıt olarak değil, bir danışmanlık işi olacaktır.

Sorgunuz olduğu gibi 4 CPU ve 16GB RAM ile sunucumda 13+ dakika çalışacak.

resim açıklamasını buraya girin

Kullanmak için sorgunuzu değiştirdim OPTION(MERGE JOIN)ve 1 saniyenin altında koştu

set nocount on 
set statistics io on
set statistics time on
;WITH Computed AS
(
    SELECT  currItem.Id,
            nextOperation.id AS NextId,
            prevOperation.FinishedOn AS PrevComputed,
            currOperation.FinishedOn AS CurrComputed,
            nextOperation.FinishedOn AS NextComputed

    FROM Items currItem 
    INNER JOIN dbo.Operations AS currOperation ON currItem.OperationId = currOperation.Id

    LEFT OUTER JOIN dbo.Items AS prevItem ON currItem.PreviousId = prevItem.Id
    LEFT OUTER JOIN dbo.Operations AS prevOperation ON prevItem.OperationId = prevOperation.Id 
    LEFT OUTER JOIN
    (
        SELECT MIN(I.id) as id, S.PreviousId, S.FinishedOn
        FROM Items I
        INNER JOIN
        (
            SELECT I.PreviousId, MIN(nxt.FinishedOn) AS FinishedOn
            FROM dbo.Items I
            LEFT OUTER JOIN dbo.Operations AS nxt ON I.OperationId = nxt.Id
            GROUP BY I.PreviousId
        ) AS S ON I.PreviousId = S.PreviousId 
        GROUP BY S.PreviousId, S.FinishedOn
    ) AS nextOperation ON nextOperation.PreviousId = currItem.Id

    WHERE currOperation.Finished = 1 AND currItem.Denormalized = 0
),

RecursionInitialization AS
(
    SELECT  currItem.Id,
            currItem.PreviousId,
            currItem.UUID,
            currItem.Json,
            currItem.TableName,
            currItem.OperationId,
            currItem.PermissionId,
            currItem.Denormalized,
            currItem.Id AS BranchID,
            COALESCE (C.PrevComputed, C.CurrComputed) AS CreatedOn,
            COALESCE (C.PrevComputed, CAST(0 AS BIGINT)) AS RecoveredOn,
            COALESCE (C.PrevComputed, CAST(0 AS BIGINT)) AS UpdatedOnPrev,
            C.CurrComputed AS UpdatedOnCurr,
            COALESCE (C.NextComputed, CAST(8640000000000000 AS BIGINT)) AS UpdatedOnNext,
            C.NextId AS UpdatedOnNextId,

            0 AS RecursionLevel

    FROM Items AS currItem
    INNER JOIN Computed AS C ON currItem.Id = C.Id
    WHERE currItem.Denormalized = 0

    UNION ALL

    SELECT  currItem.Id,
            currItem.PreviousId,
            currItem.UUID,
            currItem.Json,
            currItem.TableName,
            currItem.OperationId,
            currItem.PermissionId,
            currItem.Denormalized,
            currItem.BranchId,
            currItem.CreatedOn,
            currItem.RecoveredOn,
            currItem.UpdatedOnPrev,
            currItem.UpdatedOnCurr,
            currItem.UpdatedOnNext,
            currItem.UpdatedOnNextId,

            0 AS RecursionLevel

    FROM Items AS currItem
    WHERE currItem.Denormalized = 1
),
Recursion AS
(
    SELECT *
    FROM RecursionInitialization AS currItem

    UNION ALL

    SELECT  currItem.Id,
            currItem.PreviousId,
            currItem.UUID,
            currItem.Json,
            currItem.TableName,
            currItem.OperationId,
            currItem.PermissionId,
            currItem.Denormalized,

            CASE
                WHEN prevItem.UpdatedOnNextId = currItem.Id
                THEN prevItem.BranchID
                ELSE currItem.Id
            END AS BranchID,

            prevItem.CreatedOn AS CreatedOn,

            CASE
                WHEN prevItem.Json IS NULL
                THEN CASE
                            WHEN currItem.Json IS NULL
                            THEN prevItem.RecoveredOn
                            ELSE C.CurrComputed
                        END
                ELSE prevItem.RecoveredOn
            END AS RecoveredOn,

            prevItem.UpdatedOnCurr AS UpdatedOnPrev,

            C.CurrComputed AS UpdatedOnCurr,

            COALESCE (C.NextComputed, CAST(8640000000000000 AS BIGINT)) AS UpdatedOnNext,

            C.NextId,

            prevItem.RecursionLevel + 1 AS RecursionLevel
    FROM Items currItem
    INNER JOIN Computed C ON currItem.Id = C.Id
    INNER JOIN Recursion AS prevItem ON currItem.PreviousId = prevItem.Id
    WHERE currItem.Denormalized = 0
)
SELECT  item.Id,
        item.PreviousId,
        item.UUID,
        item.Json,
        item.TableName,
        item.OperationId,
        item.PermissionId,
        item.Denormalized,
        item.BranchID,
        item.CreatedOn,
        item.RecoveredOn,
        item.UpdatedOnPrev,
        item.UpdatedOnCurr,
        item.UpdatedOnNext,
        item.UpdatedOnNextId

FROM Recursion AS item
INNER JOIN
(
    SELECT Id, MAX(RecursionLevel) AS Recursion
    FROM Recursion AS item
    GROUP BY Id
) AS nested ON item.Id = nested.Id AND item.RecursionLevel = nested.Recursion
OPTION (MERGE JOIN)

set nocount oFF 
set statistics io OFF
set statistics time OFF

resim açıklamasını buraya girin

Bir görünümde sorgu ipuçlarını kullanamayacağınızı unutmayın, bu nedenle görünümünüzü SP veya herhangi bir geçici çözüm haline getirmenin bir alternatifini bulmanız gerekir


1
Bu konuda çok teşekkür ederim, sorunun yığın akışı standartlarından uzak olduğunu dikkate alarak, çabanızı benim için iki kez değerli kılıyor. Ödevimi ÇAPRAZ UYGULAMA üzerine yapacağım ve SEÇENEK'yi (MERGE JOIN) anlamaya çalışacağım. Bu soru ile ilgili sorun ne görünüyor şimdi belli değil, ancak ben bunu anlayacağım eminim, bir kez daha teşekkür ederim
Lu4

@ Lu4 Sorun, sorgu iyileştiricinin en iyi yürütme planını seçmemesi (veya üretmemesi). Bu durumda sorgu ipucu, optimize ediciyi birleştirmeyi uygulamak için belirli bir strateji kullanmaya 'teşvik eder'. Daha fazla ayrıntı için bkz. Join İpuçları (Transact-SQL) .
Kenny Evitt

CROSS APPLYharika ama sorgu ipuçlarını kullanmaya çalışmadan önce yürütme planları ve bunları analiz etmeyi öneririm.
Kenny Evitt
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.