Vấn đề hiệu năng truy vấn sql đệ quy [đã đóng]


9

Đây là lần thứ sáu tôi cố gắng hỏi câu hỏi này và nó cũng là câu hỏi ngắn nhất. Tất cả những nỗ lực trước đó đều dẫn đến một cái gì đó tương tự như một bài đăng trên blog hơn là chính câu hỏi, nhưng tôi đảm bảo với bạn rằng vấn đề của tôi là có thật, chỉ là nó liên quan đến một chủ đề lớn và không có tất cả những chi tiết mà câu hỏi này chứa. không rõ vấn đề của tôi là gì Vì vậy, ở đây đi ...

trừu tượng

Tôi có một cơ sở dữ liệu, nó cho phép lưu trữ dữ liệu theo cách lạ mắt và cung cấp một số tính năng không chuẩn được yêu cầu trong quy trình kinh doanh của tôi. Các tính năng như sau:

  1. Các cập nhật / xóa không phá hủy và không chặn được triển khai thông qua phương pháp chỉ chèn, cho phép khôi phục dữ liệu và ghi nhật ký tự động (mỗi thay đổi được gắn với người dùng đã thực hiện thay đổi đó)
  2. Dữ liệu đa biến (có thể có một số phiên bản của cùng một dữ liệu)
  3. Cấp quyền cơ sở dữ liệu
  4. Tính nhất quán cuối cùng với đặc tả ACID và tạo / cập nhật / xóa an toàn giao dịch
  5. Khả năng tua lại hoặc chuyển tiếp nhanh chế độ xem dữ liệu hiện tại của bạn đến bất kỳ thời điểm nào.

Có thể có các tính năng khác mà tôi đã quên đề cập.

Cấu trúc cơ sở dữ liệu

Tất cả dữ liệu người dùng được lưu trữ trong Itemsbảng dưới dạng chuỗi được mã hóa JSON ( ntext). Tất cả các hoạt động cơ sở dữ liệu được thực hiện thông qua hai thủ tục được lưu trữ GetLatestInsertSnashotchúng cho phép hoạt động trên dữ liệu tương tự như cách GIT vận hành các tệp nguồn.

Dữ liệu kết quả được liên kết (THAM GIA) trên frontend thành biểu đồ được liên kết đầy đủ, do đó không cần thực hiện các truy vấn cơ sở dữ liệu trong hầu hết các trường hợp.

Cũng có thể lưu trữ dữ liệu trong các cột SQL thông thường thay vì lưu trữ chúng ở dạng được mã hóa Json. Tuy nhiên, điều đó làm tăng sự phức tạp tổng thể.

Đọc dữ liệu

GetLatestkết quả với dữ liệu dưới dạng hướng dẫn, xem xét sơ đồ sau để giải thích:

Sơ đồ cấu trúc

Biểu đồ cho thấy một sự tiến hóa của những thay đổi đã từng được thực hiện cho một bản ghi. Các mũi tên trên sơ đồ hiển thị phiên bản dựa trên đó đã xảy ra chỉnh sửa (Hãy tưởng tượng rằng người dùng đang cập nhật một số dữ liệu ngoại tuyến, song song với các bản cập nhật được tạo bởi người dùng trực tuyến, trường hợp đó sẽ gây ra xung đột, về cơ bản là hai phiên bản dữ liệu thay vì một).

Vì vậy, việc gọi GetLatesttrong khoảng thời gian đầu vào sau sẽ dẫn đến các phiên bản ghi sau:

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.

Để GetLatesthỗ trợ giao diện hiệu quả như vậy mỗi bản ghi phải chứa các thuộc tính dịch vụ đặc biệt BranchId, RecoveredOn, CreatedOn, UpdatedOnPrev, UpdatedOnCurr, UpdatedOnNext, UpdatedOnNextIdđược sử dụng bởi GetLatestđể tìm hiểu xem kỷ lục rơi đầy đủ vào khoảng thời gian quy định GetLatestđối số

Chèn dữ liệu

Để hỗ trợ tính nhất quán cuối cùng, an toàn và hiệu suất giao dịch, dữ liệu được chèn vào cơ sở dữ liệu thông qua thủ tục đa tầng đặc biệt.

  1. Dữ liệu chỉ được chèn vào cơ sở dữ liệu mà không có khả năng được truy vấn bởi GetLatestthủ tục được lưu trữ.

  2. Dữ liệu được cung cấp cho GetLatestthủ tục được lưu trữ, dữ liệu được cung cấp ở denormalized = 0trạng thái chuẩn hóa (nghĩa là ). Trong khi dữ liệu ở trạng thái bình thường, các lĩnh vực dịch vụ BranchId, RecoveredOn, CreatedOn, UpdatedOnPrev, UpdatedOnCurr, UpdatedOnNext, UpdatedOnNextIdđang được tính là rất chậm.

  3. Để tăng tốc độ, dữ liệu đang được chuẩn hóa ngay khi có sẵn cho GetLatestthủ tục được lưu trữ.

    • Vì các bước 1,2,3 được thực hiện trong các giao dịch khác nhau, có thể xảy ra lỗi phần cứng ở giữa mỗi thao tác. Để lại dữ liệu ở trạng thái trung gian. Tình huống như vậy là bình thường và ngay cả khi nó sẽ xảy ra, dữ liệu sẽ được chữa lành trong InsertSnapshotcuộc gọi sau . Mã cho phần này có thể được tìm thấy ở giữa bước 2 và 3 của InsertSnapshotthủ tục được lưu trữ.

Vấn đề

Một tính năng mới (được yêu cầu bởi doanh nghiệp) buộc tôi phải cấu trúc lại Denormalizerchế độ xem đặc biệt liên kết tất cả các tính năng với nhau và được sử dụng cho cả hai GetLatestInsertSnapshot. Sau đó tôi đã bắt đầu gặp vấn đề về hiệu suất. Nếu ban đầu được SELECT * FROM Denormalizerthực hiện chỉ trong phân số thứ hai thì bây giờ phải mất gần 5 phút để xử lý 10000 hồ sơ.

Tôi không phải là DB pro và tôi đã mất gần sáu tháng để tìm ra cấu trúc cơ sở dữ liệu hiện tại. Và tôi đã dành hai tuần đầu tiên để thực hiện các phép tái cấu trúc và sau đó cố gắng tìm ra nguyên nhân gốc rễ cho vấn đề hiệu suất của tôi. Tôi không thể tìm thấy nó. Tôi đang cung cấp sao lưu cơ sở dữ liệu (mà bạn có thể tìm thấy ở đây) vì lược đồ (với tất cả các chỉ mục) khá lớn để phù hợp với SqlFiddle, cơ sở dữ liệu cũng chứa dữ liệu lỗi thời (hơn 10000 bản ghi) mà tôi đang sử dụng cho mục đích thử nghiệm . Ngoài ra, tôi đang cung cấp văn bản cho Denormalizerchế độ xem được tái cấu trúc và trở nên chậm chạp một cách đau đớn:

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

Câu hỏi

Có hai kịch bản được xem xét, các trường hợp không chuẩn hóa và chuẩn hóa:

  1. Nhìn vào bản sao lưu gốc, điều khiến cho SELECT * FROM Denormalizerquá chậm, tôi cảm thấy có vấn đề với phần đệ quy của chế độ xem Không bình thường, tôi đã thử hạn chế denormalized = 1nhưng không phải hành động của tôi ảnh hưởng đến hiệu suất.

  2. Sau khi chạy UPDATE Items SET Denormalized = 0nó sẽ làm GetLatestSELECT * FROM Denormalizerchạy vào (ban đầu được cho là) kịch bản chậm, là có một cách để mọi thứ tăng tốc khi chúng ta đang tính toán các lĩnh vực dịch vụ BranchId, RecoveredOn, CreatedOn, UpdatedOnPrev, UpdatedOnCurr, UpdatedOnNext,UpdatedOnNextId

Cảm ơn bạn trước

PS

Tôi đang cố gắng bám sát SQL tiêu chuẩn để giúp truy vấn dễ dàng di chuyển đến các cơ sở dữ liệu khác như MySQL / Oracle / SQLite trong tương lai, nhưng nếu không có sql tiêu chuẩn nào có thể giúp tôi ổn với việc gắn bó với các cấu trúc cụ thể của cơ sở dữ liệu.


1
Liên quan đến SQL tiêu chuẩn và DB mà bạn liệt kê: bạn đang sử dụng CTE ở đây và chúng không được hỗ trợ bởi myQuery và có một số biến thể cú pháp giữa các triển khai chính. Ngoài ra, chúng là một hàng rào tối ưu hóa trong postgres hiện có thể là một mối quan tâm hiệu suất lớn. Không ai trong số đó ngăn bạn sử dụng chúng, với một cây trong biểu mẫu "danh sách kề", chúng thường là công cụ phù hợp cho công việc, nhưng trong đó tính tương thích là mối quan tâm thì những điểm này là điều cần chú ý trước khi chúng cắn bạn để bạn có thể chuẩn bị cho mọi công việc cần thiết khi di chuyển sang các DBMS khác trở thành hiện thực.
David Spillett

Cảm ơn bạn, tôi đang cố gắng bám sát SQL tiêu chuẩn nhất có thể. Lý do cho điều đó là tôi tin rằng nó sẽ làm giảm số lượng vấn đề trong tương lai, khi nó sẽ được yêu cầu di chuyển mã hiện có sang Cơ sở dữ liệu khác. Không phải lúc nào cũng có thể. Ngoài ra còn có yếu tố thời gian là một phần của phương trình. Tôi đã dành nửa năm để giải quyết cấu trúc cơ sở dữ liệu hiện tại ... Tôi muốn nó chứa các cấu trúc chỉ tiêu chuẩn, nhưng nếu nó sẽ cần thêm 10 năm nữa, thì đó không phải là cách để đi ... Vì vậy, nếu bạn thấy rằng có nhiều hơn nữa tái cấu trúc tiêu chuẩn có thể tôi sẽ vui lòng chấp nhận nó ...
Lu4

1
Không, đó có vẻ là một cách thực dụng để đối phó với các ưu tiên và biến chứng khác nhau trong các trường hợp như thế này. Tôi chỉ vứt bỏ những vấn đề nảy sinh trong đầu trong trường hợp bạn chưa gặp chúng (tốt hơn là biết ngay bây giờ, ngay cả khi không thể thực hiện bất cứ điều gì về nó ngay bây giờ, hơn là bị bất ngờ trong sản xuất! ).
David Spillett

Câu trả lời:


9

@ Lu4 .. Tôi đã bỏ phiếu để đóng câu hỏi này là "Mẹo của Iceberg" nhưng sử dụng gợi ý truy vấn, bạn sẽ có thể chạy nó trong vòng 1 giây. Truy vấn này có thể được cấu trúc lại và có thể sử dụng CROSS APPLY, nhưng nó sẽ là một hợp đồng tư vấn và không phải là một câu trả lời trong một trang web hỏi đáp.

Truy vấn của bạn sẽ chạy trong hơn 13 phút trên máy chủ của tôi với 4 CPU và 16GB RAM.

nhập mô tả hình ảnh ở đây

Tôi đã thay đổi truy vấn của bạn để sử dụng OPTION(MERGE JOIN)nó chạy trong vòng 1 giây

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

nhập mô tả hình ảnh ở đây

Lưu ý rằng bạn không thể sử dụng các gợi ý truy vấn trong chế độ xem, do đó bạn phải tìm ra cách thay thế cho chế độ xem của mình dưới dạng SP hoặc bất kỳ cách giải quyết nào


1
cảm ơn bạn rất nhiều về điều này, có tính đến việc câu hỏi khác xa với các tiêu chuẩn stackoverflow nó làm cho nỗ lực của bạn gấp đôi giá trị đối với tôi. Tôi sẽ làm bài tập về CROSS ỨNG DỤNG và cố gắng tìm ra TÙY CHỌN (MERGE THAM GIA). Bây giờ không rõ ràng điều gì có vẻ là vấn đề với truy vấn đó tuy nhiên tôi khá chắc chắn rằng tôi sẽ tìm ra nó, cảm ơn bạn một lần nữa
Lu4

@ Lu4 Vấn đề là trình tối ưu hóa truy vấn không chọn (hoặc tạo) kế hoạch thực hiện tốt nhất. Gợi ý truy vấn trong trường hợp này 'khuyến khích' trình tối ưu hóa sử dụng một chiến lược cụ thể để thực hiện tham gia. Xem Tham gia gợi ý (Transact-SQL) để biết thêm chi tiết.
Kenny Evitt

CROSS APPLYlà tuyệt vời nhưng tôi khuyên bạn nên đọc các kế hoạch thực hiện và cách phân tích chúng trước khi thử sử dụng các gợi ý truy vấn.
Kenny Evitt
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.