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:
- 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)
- Data multiversion (mungkin ada beberapa versi dari data yang sama)
- Izin tingkat basis data
- Konsistensi akhirnya dengan spesifikasi ACID dan pembuatan / pembaruan / penghapusan aman-transaksi
- 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 Items
tabel sebagai string yang dikodekan JSON ( ntext
). Semua operasi basis data dilakukan melalui dua prosedur tersimpan GetLatest
dan 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
GetLatest
hasil dengan data dalam bentuk instruksi, pertimbangkan diagram berikut untuk penjelasan:
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 GetLatest
dalam 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 GetLatest
mendukung antarmuka efisien seperti setiap record harus berisi atribut layanan khusus BranchId
, RecoveredOn
, CreatedOn
, UpdatedOnPrev
, UpdatedOnCurr
, UpdatedOnNext
, UpdatedOnNextId
yang digunakan oleh GetLatest
untuk mengetahui apakah catatan jatuh memadai ke dalam jangka waktu yang disediakan untuk GetLatest
argumen
Memasukkan data
Untuk mendukung konsistensi, keamanan, dan kinerja transaksi, data dimasukkan ke dalam basis data melalui prosedur multistage khusus.
Data hanya dimasukkan ke dalam basis data tanpa dapat ditanyai oleh
GetLatest
prosedur yang tersimpan.Data tersedia untuk
GetLatest
prosedur tersimpan, data dibuat tersedia dalamdenormalized = 0
keadaan dinormalisasi (yaitu ). Sedangkan data dalam keadaan normal, bidang layananBranchId
,RecoveredOn
,CreatedOn
,UpdatedOnPrev
,UpdatedOnCurr
,UpdatedOnNext
,UpdatedOnNextId
sedang dihitung yang benar-benar lambat.Untuk mempercepat, data didenormalisasi segera setelah tersedia untuk
GetLatest
prosedur 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
InsertSnapshot
panggilan berikut . Kode untuk bagian ini dapat ditemukan di antara langkah 2 dan 3 dariInsertSnapshot
prosedur 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
Masalah
Fitur baru (diperlukan oleh bisnis) memaksa saya untuk menolak Denormalizer
pandangan khusus yang mengikat semua fitur bersama dan digunakan untuk keduanya GetLatest
dan InsertSnapshot
. Setelah itu saya mulai mengalami masalah kinerja. Jika awalnya SELECT * FROM Denormalizer
dieksekusi 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 Denormalizer
tampilan 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:
Melihat ke cadangan asli, apa yang membuat
SELECT * FROM Denormalizer
sangat lambat, saya merasa ada masalah dengan bagian rekursif dari pandangan Denormalizer, saya sudah mencoba membatasidenormalized = 1
tetapi tindakan saya tidak mempengaruhi kinerja.Setelah menjalankan
UPDATE Items SET Denormalized = 0
hal itu akan membuatGetLatest
danSELECT * FROM Denormalizer
lari ke (awalnya dianggap) skenario lambat, apakah ada cara untuk mempercepat hal-hal ketika kita menghitung bidang layananBranchId
,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.
Jawaban:
@ 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.
Saya mengubah permintaan Anda untuk digunakan
OPTION(MERGE JOIN)
dan itu berjalan di bawah 1 detikPerhatikan bahwa Anda tidak dapat menggunakan petunjuk kueri dalam tampilan, jadi Anda harus mencari tahu alternatif membuat tampilan Anda sebagai SP atau solusi apa pun
sumber
CROSS APPLY
bagus, tetapi saya sarankan membaca rencana eksekusi dan cara menganalisisnya sebelum mencoba memanfaatkan petunjuk kueri.