再帰的なSQLクエリのパフォーマンスの問題[終了]


9

私がこの質問をするのはこれで6回目ですが、これも最短の質問です。これまでのすべての試みは、質問自体ではなくブログ投稿に類似したものでしたが、私の問題が本当であることを保証します。それは、1つの大きな主題に関するものであり、この質問に含まれるすべての詳細なしでは、私の問題が何であるか明確ではありません。だからここに行く...

概要

私はデータベースを持っています。これは、データをちょっと豪華な方法で格納することを可能にし、私のビジネスプロセスに必要ないくつかの非標準機能を提供します。機能は次のとおりです。

  1. 挿入のみのアプローチを介して実装された非破壊的で非ブロック的な更新/削除により、データの回復と自動ロギングが可能になります(各変更は、その変更を行ったユーザーに関連付けられます)
  2. マルチバージョンデータ(同じデータの複数のバージョンが存在する場合があります)
  3. データベースレベルの権限
  4. ACID仕様およびトランザクションセーフな作成/更新/削除との最終的な整合性
  5. データの現在のビューを任意の時点まで巻き戻しまたは早送りする機能。

私が言及し忘れていた他の機能があるかもしれません。

データベースの構造

すべてのユーザーデータは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各レコードは特別なサービス属性が含まれている必要があり、このような効率的なインターフェースをサポートするためにBranchIdRecoveredOnCreatedOnUpdatedOnPrevUpdatedOnCurrUpdatedOnNextUpdatedOnNextIdで使用されるGetLatestレコードをするために設けられタイムスパン内に十分収まっているか否かを把握するGetLatest引数

データの挿入

結果の一貫性、トランザクションの安全性、およびパフォーマンスをサポートするために、特別なマルチステージプロシージャを介してデータがデータベースに挿入されます。

  1. データはデータベースに挿入されるだけで、GetLatestストアドプロシージャによるクエリは実行できません。

  2. データはGetLatestストアドプロシージャで使用できるようになり、データは正規化された(つまりdenormalized = 0)状態で使用できるようになります。データは正規化された状態にある間、サービス分野ではBranchIdRecoveredOnCreatedOnUpdatedOnPrevUpdatedOnCurrUpdatedOnNextUpdatedOnNextId本当に遅いである計算されています。

  3. 処理速度を上げるために、データはGetLatestストアドプロシージャで使用できるようになり次第、非正規化されています。

    • ステップ1、2、3は異なるトランザクション内で実行されるため、各操作の途中でハードウェア障害が発生する可能性があります。データを中間状態のままにします。このような状況は正常であり、たとえそれが発生したとしても、次のInsertSnapshot呼び出しでデータが修復されます。この部分のコードは、InsertSnapshotストアドプロシージャのステップ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つのシナリオ、非正規化ケースと正規化ケースがあります。

  1. 元のバックアップを見ると、SELECT * FROM Denormalizer非常に遅くなりますが、非正規化子ビューの再帰的な部分に問題があるように感じdenormalized = 1ます。

  2. 実行した後UPDATE Items SET Denormalized = 0、それはなるだろうGetLatestSELECT * FROM Denormalizer(もともとあると考えられて)遅いのシナリオに遭遇し、我々はサービスのフィールドを計算する際に、最大スピード、物事への道がありますBranchIdRecoveredOnCreatedOnUpdatedOnPrevUpdatedOnCurrUpdatedOnNextUpdatedOnNextId

前もって感謝します

PS

標準SQLを使用して、将来のMySQL / Oracle / SQLiteなどの他のデータベースにクエリを簡単に移植できるようにしていますが、標準SQLがない場合は、データベース固有の構造を使用しても問題ありません。


1
標準SQLとリストするDBに関しては、ここではCTEを使用しており、それらはmySQLではサポートされておらず、主要な実装間で構文のバリエーションがいくつかあります。また、それらは現在postgresの最適化フェンスであり、パフォーマンスの大きな問題になる可能性があります。「隣接リスト」形式のツリーを使用すると、それらの使用が停止することはありません。通常、それらはジョブに適したツールですが、互換性が懸念される場合、これらの点は、噛み付く前に知っておくべきことです。他のDBMSへの移行が現実になるときに必要な追加の作業に備えます。
David Spillett、2015

ありがとう、できる限り標準SQLを使用するようにしています。その理由は、既存のコードを他のデータベースに移行する必要があるときに、将来的に問題の量を減らすはずだと私は信じているからです。常に可能であるとは限りません。方程式の一部である時間係数もあります。私は現在のデータベース構造の解決に半年を費やしてきました...標準のみの構成を含めたいのですが、さらに10年を必要とする場合、それは進むべき道ではありません...したがって、標準のリファクタリングが可能です。それを受け入れ
させていただき

1
いいえ、これは、このような場合の異なる優先順位と複雑化に対処するための実用的な方法のように聞こえます。あなたがまだそれらに遭遇していなかった場合に備えて、頭に浮かんだ問題を頭の中で投げ出しました(今、それについて何かをすることが不可能/実用的ではない場合でも、今知っておくとよいでしょう) )。
David Spillett、2015

回答:


9

@ Lu4 ..私はこの質問を「Tip of Iceberg」として閉じることに投票しましたが、クエリヒントを使用すると、1秒未満で実行できます。このクエリはリファクタリングしてを使用できますCROSS APPLYが、これはコンサルティングギグであり、Q&Aサイトでの回答ではありません。

クエリは、CPUが4、RAMが16 GBのサーバーで13分以上実行されます。

ここに画像の説明を入力してください

私が使用するようにクエリを変更OPTION(MERGE JOIN)し、それが1秒で実行されていました

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

ここに画像の説明を入力してください

ビューではクエリヒントを使用できないため、ビューをSPまたは任意の回避策として作成する別の方法を理解する必要があることに注意してください。


1
質問がスタックオーバーフローの標準とはかけ離れていることを考慮に入れて、これに感謝します。CROSS APPLYで宿題をして、OPTION(MERGE JOIN)を理解しようとします。今のところ、そのクエリの問題のように見えるのは明らかではありませんが、解決できると確信しています。もう一度ありがとう
Lu4

@ Lu4問題は、クエリオプティマイザが最適な実行プランを選択(または生成)していないことです。この場合のクエリヒントは、特定の戦略を使用して結合を実装するようオプティマイザに「推奨」します。詳細については、結合ヒント(Transact-SQL)を参照してください。
ケニーエビット2015

CROSS APPLYすばらしいですが、クエリヒントを利用する前に、実行プランとそれらを分析する方法を読むことをお勧めします。
ケニーエビット2015
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.