재귀 SQL 쿼리 성능 문제 [닫기]


9

이것은 내가이 질문을하려고하는 여섯 번째 시간이며 가장 짧은 질문이기도합니다. 이전의 모든 시도는 질문 자체가 아닌 블로그 게시물과 유사한 결과로 이루어졌지만 내 문제가 실제임을 확신합니다. 단지 큰 주제와 관련이 있으며이 질문에 포함 된 모든 세부 사항이없는 것입니다. 내 문제가 무엇인지 명확하지 않습니다. 그래서 여기에 ...

요약

나는 데이터베이스를 가지고 있는데, 그것은 일종의 멋진 방식으로 데이터를 저장하고 비즈니스 프로세스에 필요한 몇 가지 비표준 기능을 제공합니다. 기능은 다음과 같습니다.

  1. 데이터 복구 및 자동 로깅을 허용하는 삽입 전용 접근 방식을 통해 구현되는 비파괴 및 비 차단 업데이트 / 삭제 (각 변경은 해당 변경을 수행 한 사용자와 연결됨)
  2. 멀티 버전 데이터 (동일한 데이터의 여러 버전이있을 수 있음)
  3. 데이터베이스 수준 권한
  4. ACID 사양 및 트랜잭션 안전 생성 / 업데이트 / 삭제와의 최종 일관성
  5. 현재 데이터보기를 특정 시점으로 되감거나 빨리 감을 수 있습니다.

언급하지 않은 다른 기능이있을 수 있습니다.

데이터베이스 구조

모든 사용자 데이터는 ItemsJSON 인코딩 문자열 ( ntext) 로 테이블에 저장됩니다 . 모든 데이터베이스 작업은 두 개의 저장 프로 시저를 통해 실시하고 있습니다 GetLatest그리고 InsertSnashot그들은 GIT 소스 파일을 작동하는 방식과 유사한 데이터에 작동 할 수 있습니다.

결과 데이터는 프런트 엔드에서 완전히 연결된 그래프로 연결 (JOINed)되므로 대부분의 경우 데이터베이스 쿼리를 수행 할 필요가 없습니다.

Json 인코딩 형식으로 저장하는 대신 일반 SQL 열에 데이터를 저장할 수도 있습니다. 그러나 이는 전반적인 복잡성 부담을 증가시킵니다.

데이터 읽기

GetLatest지침 형식의 데이터가 포함 된 결과의 경우 설명을 위해 다음 다이어그램 을 고려 하십시오 .

구조도

다이어그램은 단일 레코드에 대한 변경 사항의 진화를 보여줍니다. 다이어그램의 화살표는 편집이 발생한 버전을 나타냅니다 (온라인 사용자가 수행 한 업데이트와 병행하여 사용자가 일부 데이터를 오프라인으로 업데이트한다고 가정합니다.이 경우 기본적으로 두 가지 버전의 데이터 인 충돌이 발생합니다) 하나 대신).

따라서 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인수

데이터 삽입

최종 일관성, 트랜잭션 안전성 및 성능을 지원하기 위해 데이터는 특별한 다단계 절차를 통해 데이터베이스에 삽입됩니다.

  1. GetLatest저장 프로 시저 에서 쿼리 할 수없는 상태로 데이터베이스에 데이터가 삽입되었습니다 .

  2. 데이터는 GetLatest저장 프로 시저에 대해 사용 가능하게되고 데이터는 정규화 된 (예 :) denormalized = 0상태 로 사용 가능하게됩니다 . 데이터가 표준화 상태에있는 동안, 서비스 분야는 BranchId, RecoveredOn, CreatedOn, UpdatedOnPrev, UpdatedOnCurr, UpdatedOnNext, UpdatedOnNextId정말 느리다하는 계산되고있다.

  3. 작업 속도를 높이기 위해 GetLatest저장 프로 시저에 사용할 수있게되면 데이터가 비정규 화됩니다 .

    • 1, 2, 3 단계가 다른 트랜잭션 내에서 수행되므로 각 작업 중에 하드웨어 오류가 발생할 수 있습니다. 중간 상태로 데이터를 남겨 둡니다. 이러한 상황은 정상이며 이러한 상황이 발생하더라도 다음 InsertSnapshot호출 에서 데이터가 치유됩니다 . 이 부분의 코드는 InsertSnapshot저장 프로 시저 의 2 단계와 3 단계 사이 에 있습니다.

문제

(비즈니스에서 요구하는) 새로운 기능은 특별한 리팩토링 나를 강제로 Denormalizer관계 업 모두가 함께 기능 모두에 사용되는보기 GetLatestInsertSnapshot. 그 후 성능 문제가 발생하기 시작했습니다. 원래 SELECT * FROM Denormalizer몇 초 만에 실행 되면 10000 레코드를 처리하는 데 거의 5 분이 걸립니다.

저는 DB 전문가가 아니며 현재 데이터베이스 구조를 만드는 데 거의 6 개월이 걸렸습니다. 그리고 리팩토링을하기 위해 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

질문

비정규 화 된 사례와 정규화 된 사례는 두 가지 시나리오로 고려됩니다.

  1. 원래 백업을 살펴보면 SELECT * FROM Denormalizer고통스럽게 느리게 만드는 이유는 Denormalizer보기의 재귀 부분에 문제가있는 것 같습니다. 제한을 시도 denormalized = 1했지만 내 행동 중 일부가 성능에 영향을 미치지 않았습니다.

  2. 실행 한 후 UPDATE Items SET Denormalized = 0그것을 만들 것 GetLatestSELECT * FROM Denormalizer실행에 느린 시나리오 (원래로 생각), 속도 것들까지 우리가 서비스 필드를 계산하는에 방법이 BranchId, RecoveredOn, CreatedOn, UpdatedOnPrev, UpdatedOnCurr, UpdatedOnNext,UpdatedOnNextId

미리 감사합니다

추신

미래에 MySQL / Oracle / SQLite와 같은 다른 데이터베이스로 쿼리를 쉽게 이식 할 수 있도록 표준 SQL을 고수하려고 노력하고 있지만 표준 SQL이 없으면 데이터베이스 관련 구문을 고수하는 데 도움이 될 수 있습니다.


1
표준 SQL 및 나열하는 DB와 관련하여 여기에서 CTE를 사용하고 있으며 mySQL에서 지원하지 않으며 주요 구현간에 구문 변형이 있습니다. 또한 현재 postgres의 최적화 펜스로 큰 성능 문제가 될 수 있습니다. "인접 목록"양식의 트리를 사용하여 사용을 중단해서는 안됩니다. 일반적으로 작업에 적합한 도구이지만 호환성이 문제가되는 경우 이러한 점은 귀하를 물기 전에 알아야 할 사항이므로 다른 DBMS로 마이그레이션 할 때 필요한 추가 작업에 대비합니다.
David Spillett

감사합니다. 가능한 한 표준 SQL을 고수하려고합니다. 그 이유는 기존 코드를 다른 데이터베이스로 마이그레이션해야 할 때 앞으로 문제의 양을 줄여야한다고 생각하기 때문입니다. 항상 가능하지는 않습니다. 방정식의 일부인 시간 요소도 있습니다. 나는 현재 데이터베이스 구조를 정립하기 위해 반년을 보냈습니다 ... 표준 전용 구조를 포함하고 싶지만 10 년이 더 걸리면 갈 길이 없습니다 ... 그래서 더 많은 것이 있다면 표준 리팩토링 가능 나는 그것을 기꺼이 받아 들일 것이다 ...
Lu4

1
아닙니다. 이런 경우에 다른 우선 순위와 합병증을 처리하는 실용적인 방법 인 것 같습니다. 나는 단지 당신이 아직 그들에게 오지 않았을 때를 대비하여 떠오르는 문제들을 버렸다. ).
David Spillett

답변:


9

@ Lu4 ..이 질문을 "빙산의 팁 (Tip of Iceberg)"으로 닫기로 투표했지만 쿼리 힌트를 사용하면 1 초 이내에 실행할 수 있습니다. 이 쿼리는 리팩토링되고 사용할 수 CROSS APPLY있지만 Q & A 사이트에서는 답변이 아닌 컨설팅 공연이 될 것입니다.

귀하의 쿼리는 4 CPU 및 16GB RAM이있는 서버에서 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
질문이 stackoverflow 표준과는 거리가 멀다는 점을 고려할 때 매우 감사드립니다. CROSS APPLY에 관한 숙제를하고 OPTION (MERGE JOIN)을 알아 내려고합니다. 그 질문에 문제가있는 것 같지는 않지만 지금은 그것을 알아낼 것입니다. 다시 한 번 감사드립니다
Lu4

@ Lu4 문제는 쿼리 최적화 프로그램이 최상의 실행 계획을 선택하지 않거나 생성하지 않는다는 것입니다. 이 경우 쿼리 힌트는 옵티마이 저가 특정 전략을 사용하여 조인을 구현하도록 권장합니다. 자세한 내용은 조인 힌트 (Transact-SQL) 를 참조하십시오.
Kenny Evitt

CROSS APPLY훌륭하지만 쿼리 힌트를 사용하기 전에 실행 계획과 분석 방법을 읽는 것이 좋습니다.
Kenny Evitt
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.