Masalah kinerja kueri sql rekursif [tertutup]

9

Ini adalah keenam kalinya saya mencoba untuk mengajukan pertanyaan ini dan itu juga yang terpendek. Semua upaya sebelumnya dihasilkan dengan sesuatu yang lebih mirip dengan posting blog daripada pertanyaan itu sendiri, tetapi saya jamin bahwa masalah saya nyata, hanya saja itu menyangkut satu subjek besar dan tanpa semua detail yang berisi pertanyaan ini akan menjadi tidak jelas apa masalah saya. Jadi begini ...

Abstrak

Saya memiliki database, ini memungkinkan menyimpan data dengan cara yang agak mewah dan menyediakan beberapa fitur non-standar yang diperlukan oleh proses bisnis saya. Fitur-fiturnya adalah sebagai berikut:

  1. Pembaruan / penghapusan non-destruktif dan non-pemblokiran diimplementasikan melalui pendekatan sisipkan saja, yang memungkinkan pemulihan data dan pencatatan otomatis (setiap perubahan dikaitkan dengan pengguna yang melakukan perubahan itu)
  2. Data multiversion (mungkin ada beberapa versi dari data yang sama)
  3. Izin tingkat basis data
  4. Konsistensi akhirnya dengan spesifikasi ACID dan pembuatan / pembaruan / penghapusan aman-transaksi
  5. Kemampuan untuk memundurkan atau memajukan tampilan data Anda saat ini ke titik waktu mana pun.

Mungkin ada fitur lain yang saya lupa sebutkan.

Struktur basis data

Semua data pengguna disimpan dalam Itemstabel sebagai string yang dikodekan JSON ( ntext). Semua operasi basis data dilakukan melalui dua prosedur tersimpan GetLatestdan InsertSnashot, mereka memungkinkan untuk beroperasi pada data yang mirip dengan bagaimana GIT mengoperasikan file sumber.

Data yang dihasilkan terhubung (BERGABUNG) di frontend ke dalam grafik yang terhubung penuh sehingga tidak perlu membuat query database dalam banyak kasus.

Dimungkinkan juga untuk menyimpan data dalam kolom SQL biasa alih-alih menyimpannya dalam bentuk penyandian Json. Namun itu meningkatkan ketegangan kompleksitas keseluruhan.

Membaca data

GetLatesthasil dengan data dalam bentuk instruksi, pertimbangkan diagram berikut untuk penjelasan:

Diagram struktur

Diagram menunjukkan evolusi perubahan yang pernah dibuat ke satu rekaman. Panah pada diagram menunjukkan versi berdasarkan di mana pengeditan terjadi (Bayangkan bahwa pengguna memperbarui beberapa data offline, secara paralel dengan pembaruan yang dibuat oleh pengguna online, kasus seperti itu akan menimbulkan konflik, yang pada dasarnya adalah dua versi data bukannya satu).

Jadi memanggil GetLatestdalam rentang waktu input berikut akan menghasilkan dengan versi rekaman berikut:

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.

Agar GetLatestmendukung antarmuka efisien seperti setiap record harus berisi atribut layanan khusus BranchId, RecoveredOn, CreatedOn, UpdatedOnPrev, UpdatedOnCurr, UpdatedOnNext, UpdatedOnNextIdyang digunakan oleh GetLatestuntuk mengetahui apakah catatan jatuh memadai ke dalam jangka waktu yang disediakan untuk GetLatestargumen

Memasukkan data

Untuk mendukung konsistensi, keamanan, dan kinerja transaksi, data dimasukkan ke dalam basis data melalui prosedur multistage khusus.

  1. Data hanya dimasukkan ke dalam basis data tanpa dapat ditanyai oleh GetLatestprosedur yang tersimpan.

  2. Data tersedia untuk GetLatestprosedur tersimpan, data dibuat tersedia dalam denormalized = 0keadaan dinormalisasi (yaitu ). Sedangkan data dalam keadaan normal, bidang layanan BranchId, RecoveredOn, CreatedOn, UpdatedOnPrev, UpdatedOnCurr, UpdatedOnNext, UpdatedOnNextIdsedang dihitung yang benar-benar lambat.

  3. Untuk mempercepat, data didenormalisasi segera setelah tersedia untuk GetLatestprosedur tersimpan.

    • Karena langkah 1,2,3 dilakukan dalam transaksi yang berbeda, mungkin saja terjadi kegagalan perangkat keras di tengah-tengah setiap operasi. Meninggalkan data dalam kondisi perantara. Situasi seperti itu normal dan bahkan jika itu akan terjadi, data akan disembuhkan dalam InsertSnapshotpanggilan berikut . Kode untuk bagian ini dapat ditemukan di antara langkah 2 dan 3 dari InsertSnapshotprosedur tersimpan.

Masalah

Fitur baru (diperlukan oleh bisnis) memaksa saya untuk menolak Denormalizerpandangan khusus yang mengikat semua fitur bersama dan digunakan untuk keduanya GetLatestdan InsertSnapshot. Setelah itu saya mulai mengalami masalah kinerja. Jika awalnya SELECT * FROM Denormalizerdieksekusi hanya dalam sepersekian detik jadi sekarang dibutuhkan hampir 5 menit untuk memproses 10.000 catatan.

Saya bukan pro DB dan saya butuh hampir enam bulan hanya untuk keluar dengan struktur database saat ini. Dan saya telah menghabiskan dua minggu pertama untuk melakukan refactor dan kemudian mencoba mencari tahu apa penyebab utama masalah kinerja saya. Saya tidak bisa menemukannya. Saya menyediakan cadangan basis data (yang dapat Anda temukan di sini) karena skema (dengan semua indeks) agak besar untuk masuk ke SqlFiddle, basis data juga berisi data usang (10.000 catatan) yang saya gunakan untuk tujuan pengujian . Saya juga menyediakan teks untuk Denormalizertampilan yang telah di refactored dan menjadi sangat lambat:

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

Pertanyaan-pertanyaan)

Ada dua skenario yang dipertimbangkan, kasus terdenormalisasi dan normal:

  1. Melihat ke cadangan asli, apa yang membuat SELECT * FROM Denormalizersangat lambat, saya merasa ada masalah dengan bagian rekursif dari pandangan Denormalizer, saya sudah mencoba membatasi denormalized = 1tetapi tindakan saya tidak mempengaruhi kinerja.

  2. Setelah menjalankan UPDATE Items SET Denormalized = 0hal itu akan membuat GetLatestdan SELECT * FROM Denormalizerlari ke (awalnya dianggap) skenario lambat, apakah ada cara untuk mempercepat hal-hal ketika kita menghitung bidang layanan BranchId, RecoveredOn, CreatedOn, UpdatedOnPrev, UpdatedOnCurr, UpdatedOnNext,UpdatedOnNextId

Terima kasih sebelumnya

PS

Saya mencoba untuk tetap berpegang pada SQL standar untuk membuat kueri mudah dibawa-bawa ke database lain seperti MySQL / Oracle / SQLite untuk masa depan, tetapi jika tidak ada sql standar yang mungkin membantu saya baik-baik saja dengan tetap berpegang pada konstruksi spesifik-basis data.

Lu4
sumber
1
Berkenaan dengan SQL standar dan DB yang Anda daftarkan: Anda menggunakan CTE di sini dan mereka tidak didukung oleh mySQL dan ada beberapa variasi sintaksis antara implementasi utama. Juga mereka adalah pagar optimasi di postgres saat ini yang bisa menjadi masalah kinerja yang besar. Tidak ada yang dapat menghentikan Anda menggunakannya, dengan pohon di "daftar adjacency" bentuk mereka biasanya alat yang tepat untuk pekerjaan itu, tetapi di mana kompatibilitas menjadi perhatian poin-poin ini adalah hal-hal yang harus diperhatikan sebelum mereka menggigit Anda sehingga Anda dapat siap untuk pekerjaan tambahan yang dibutuhkan saat migrasi ke DBMS lain menjadi kenyataan.
David Spillett
Terima kasih, saya berusaha untuk tetap menggunakan SQL standar sebanyak mungkin. Alasan untuk itu adalah bahwa saya percaya itu harus mengurangi jumlah masalah di masa depan, ketika akan diminta untuk memigrasi kode yang ada ke Database lain. Itu tidak selalu mungkin. Ada juga faktor waktu yang merupakan bagian dari persamaan. Saya telah menghabiskan setengah tahun menyelesaikan struktur basis data saat ini ... Saya ingin ini mengandung konstruksi standar saja, tetapi jika itu akan memerlukan 10 tahun lagi, itu bukan cara untuk pergi ... Jadi, jika Anda melihat bahwa ada lebih banyak standar refactoring mungkin saya akan senang menerimanya ...
Lu4
1
Tidak, itu terdengar seperti cara pragmatis untuk menangani prioritas dan komplikasi yang berbeda dalam kasus-kasus seperti ini. Saya hanya otak membuang masalah yang muncul dalam pikiran jika Anda belum menemukan mereka (lebih baik untuk tahu sekarang, bahkan jika itu tidak mungkin / praktis untuk melakukan sesuatu tentang hal itu sekarang, daripada digigit oleh kejutan dalam produksi! ).
David Spillett

Jawaban:

9

@ Lu4 .. Saya memilih untuk menutup pertanyaan ini sebagai "Tip of Iceberg" tetapi menggunakan petunjuk permintaan Anda akan dapat menjalankannya di bawah 1 detik. Kueri ini dapat di-refactored dan dapat digunakan CROSS APPLY, tetapi itu akan menjadi pertunjukan konsultasi dan bukan sebagai jawaban di situs tanya jawab.

Permintaan Anda sebagaimana adanya akan berjalan selama 13+ menit di server saya dengan 4 CPU dan 16GB RAM.

masukkan deskripsi gambar di sini

Saya mengubah permintaan Anda untuk digunakan OPTION(MERGE JOIN)dan itu berjalan di bawah 1 detik

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

masukkan deskripsi gambar di sini

Perhatikan bahwa Anda tidak dapat menggunakan petunjuk kueri dalam tampilan, jadi Anda harus mencari tahu alternatif membuat tampilan Anda sebagai SP atau solusi apa pun

Kin Shah
sumber
1
terima kasih banyak atas ini, dengan mempertimbangkan bahwa pertanyaannya jauh dari standar stackoverflow itu membuat usaha Anda dua kali berharga bagi saya. Saya akan mengerjakan pekerjaan rumah saya di CROSS APPLY dan mencoba mencari tahu OPSI (GABUNG GABUNG). Tidak jelas sekarang apa yang tampaknya menjadi masalah dengan permintaan itu, tetapi saya cukup yakin bahwa saya akan mengetahuinya, terima kasih sekali lagi
Lu4
@ Lu4 Masalahnya adalah pengoptimal kueri tidak memilih (atau menghasilkan) rencana eksekusi terbaik. Petunjuk kueri dalam kasus ini 'mendorong' pengoptimal untuk menggunakan strategi khusus untuk mengimplementasikan penggabungan. Lihat Bergabung Petunjuk (Transact-SQL) untuk lebih jelasnya.
Kenny Evitt
CROSS APPLYbagus, tetapi saya sarankan membaca rencana eksekusi dan cara menganalisisnya sebelum mencoba memanfaatkan petunjuk kueri.
Kenny Evitt