私がこの質問をするのはこれで6回目ですが、これも最短の質問です。これまでのすべての試みは、質問自体ではなくブログ投稿に類似したものでしたが、私の問題が本当であることを保証します。それは、1つの大きな主題に関するものであり、この質問に含まれるすべての詳細なしでは、私の問題が何であるか明確ではありません。だからここに行く...
概要
私はデータベースを持っています。これは、データをちょっと豪華な方法で格納することを可能にし、私のビジネスプロセスに必要ないくつかの非標準機能を提供します。機能は次のとおりです。
- 挿入のみのアプローチを介して実装された非破壊的で非ブロック的な更新/削除により、データの回復と自動ロギングが可能になります(各変更は、その変更を行ったユーザーに関連付けられます)
- マルチバージョンデータ(同じデータの複数のバージョンが存在する場合があります)
- データベースレベルの権限
- ACID仕様およびトランザクションセーフな作成/更新/削除との最終的な整合性
- データの現在のビューを任意の時点まで巻き戻しまたは早送りする機能。
私が言及し忘れていた他の機能があるかもしれません。
データベースの構造
すべてのユーザーデータはItems
、JSONエンコードされた文字列(ntext
)としてテーブルに保存されます。すべてのデータベース操作は2つのストアドプロシージャGetLatest
を介して行われInsertSnashot
、GITがソースファイルを操作する方法と同様にデータを操作できます。
結果のデータは、フロントエンドで完全にリンクされたグラフにリンク(結合)されるため、ほとんどの場合、データベースクエリを実行する必要はありません。
データをJsonエンコード形式で保存する代わりに、通常のSQL列に保存することもできます。ただし、全体的な複雑性の負担が増大します。
データの読み取り
GetLatest
命令形式のデータを使用した結果については、説明のために次の図を検討してください。
この図は、単一のレコードに対して行われた変更の進化を示しています。図の矢印は、編集が行われたベースのバージョンを示しています(ユーザーがオンラインユーザーによって行われた更新と並行して、一部のデータをオフラインで更新していると想像してください。 1つではなく)。
したがって、GetLatest
次の入力タイムスパン内で呼び出すと、次のレコードバージョンが生成されます。
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.
ために、GetLatest
各レコードは特別なサービス属性が含まれている必要があり、このような効率的なインターフェースをサポートするためにBranchId
、RecoveredOn
、CreatedOn
、UpdatedOnPrev
、UpdatedOnCurr
、UpdatedOnNext
、UpdatedOnNextId
で使用されるGetLatest
レコードをするために設けられタイムスパン内に十分収まっているか否かを把握するGetLatest
引数
データの挿入
結果の一貫性、トランザクションの安全性、およびパフォーマンスをサポートするために、特別なマルチステージプロシージャを介してデータがデータベースに挿入されます。
データはデータベースに挿入されるだけで、
GetLatest
ストアドプロシージャによるクエリは実行できません。データは
GetLatest
ストアドプロシージャで使用できるようになり、データは正規化された(つまりdenormalized = 0
)状態で使用できるようになります。データは正規化された状態にある間、サービス分野ではBranchId
、RecoveredOn
、CreatedOn
、UpdatedOnPrev
、UpdatedOnCurr
、UpdatedOnNext
、UpdatedOnNextId
本当に遅いである計算されています。処理速度を上げるために、データは
GetLatest
ストアドプロシージャで使用できるようになり次第、非正規化されています。- ステップ1、2、3は異なるトランザクション内で実行されるため、各操作の途中でハードウェア障害が発生する可能性があります。データを中間状態のままにします。このような状況は正常であり、たとえそれが発生したとしても、次の
InsertSnapshot
呼び出しでデータが修復されます。この部分のコードは、InsertSnapshot
ストアドプロシージャのステップ2と3の間にあります。
- ステップ1、2、3は異なるトランザクション内で実行されるため、各操作の途中でハードウェア障害が発生する可能性があります。データを中間状態のままにします。このような状況は正常であり、たとえそれが発生したとしても、次の
問題
新しい機能(ビジネスに必要)によりDenormalizer
、すべての機能を結び付け、およびの両方に使用される特別なビューをリファクタリングする必要がGetLatest
ありましたInsertSnapshot
。その後、パフォーマンスの問題が発生し始めました。最初はSELECT * FROM Denormalizer
ほんの一瞬で実行された場合、10000レコードを処理するのに5分近くかかります。
私はDBプロではありません。現在のデータベース構造を思いついただけで、半年近くかかりました。そして、最初に2週間を費やしてリファクタリングを行い、次にパフォーマンスの問題の根本的な原因を突き止めようとしました。私はそれを見つけることができません。スキーマ(すべてのインデックスを含む)がSqlFiddleに収まるにはかなり大きいため、データベースのバックアップ(ここで見つけることができます)を提供しています。データベースには、テスト目的で使用している古いデータ(10000以上のレコード)も含まれています。また、Denormalizer
リファクタリングされて非常に遅くなったビューのテキストを提供しています。
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
質問)
考慮される2つのシナリオ、非正規化ケースと正規化ケースがあります。
元のバックアップを見ると、
SELECT * FROM Denormalizer
非常に遅くなりますが、非正規化子ビューの再帰的な部分に問題があるように感じdenormalized = 1
ます。実行した後
UPDATE Items SET Denormalized = 0
、それはなるだろうGetLatest
とSELECT * FROM Denormalizer
(もともとあると考えられて)遅いのシナリオに遭遇し、我々はサービスのフィールドを計算する際に、最大スピード、物事への道がありますBranchId
、RecoveredOn
、CreatedOn
、UpdatedOnPrev
、UpdatedOnCurr
、UpdatedOnNext
、UpdatedOnNextId
前もって感謝します
PS
標準SQLを使用して、将来のMySQL / Oracle / SQLiteなどの他のデータベースにクエリを簡単に移植できるようにしていますが、標準SQLがない場合は、データベース固有の構造を使用しても問題ありません。