Проблема з виконанням рекурсивного sql запиту [закрито]


9

Це вже шостий раз я намагаюся задати це питання, і це найкоротший. Усі попередні спроби були наслідком чогось більш подібного до публікації в блозі, а не самого питання, але я запевняю вас, що моя проблема справжня, це просто те, що стосується однієї великої теми і без усіх деталей, які це питання містить, буде не ясно, в чому моя проблема. Так ось іде ...

Анотація

У мене є база даних, яка дозволяє зберігати дані на вигляд фантазії і надає кілька нестандартних функцій, необхідних моєму бізнес-процесу. Особливості наступні:

  1. Неруйнівні та неблокуючі оновлення / видалення, реалізовані за допомогою підключення лише для вставки, що дозволяє відновити дані та автоматичний журнал (кожна зміна пов'язана з користувачем, який здійснив цю зміну)
  2. Дані мультиверсії (може бути декілька версій одних і тих же даних)
  3. Дозволи на рівні бази даних
  4. Поточна узгодженість із специфікацією ACID та безпекою транзакцій створює / оновлює / видаляє
  5. Можливість перемотати назад або переадресувати поточний перегляд даних у будь-який момент часу.

Можливо, є й інші функції, які я забув згадати.

Структура бази даних

Всі користувацькі дані зберігаються в Itemsтаблиці як кодований JSON рядок ( ntext). Усі операції з базою даних проводяться за допомогою двох збережених процедур, GetLatestі InsertSnashotвони дозволяють оперувати даними, подібними до того, як GIT управляє вихідними файлами.

Отримані дані пов'язані (JOINed) на передній частині в повністю пов'язаний графік, тому немає необхідності в запитах до бази даних у більшості випадків.

Також можна зберігати дані в звичайних колонках SQL замість того, щоб зберігати їх у кодованій формі Json. Однак це збільшує загальну складність штаму.

Читання даних

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дзвінка. Код цієї частини можна знайти між кроками 2 та 3 InsertSnapshotзбереженої процедури.

Проблема

Нова функція (потрібна бізнесу) змусила мене змінити особливий Denormalizerвигляд, який поєднує всі функції разом і використовується як для, так GetLatestі для InsertSnapshot. Після цього у мене виникли проблеми з продуктивністю. Якщо спочатку SELECT * FROM Denormalizerвиконувалося лише частками секунди, то зараз обробка 10000 записів займає майже 5 хвилин.

Я не виробник БД, і мені знадобилося майже півроку, лише коли я з'явився з поточною структурою бази даних. І я витратив два тижні спочатку, щоб зробити рефактори, а потім намагаюся розібратися, що є першопричиною моєї проблеми з продуктивністю. Я просто не можу його знайти. Я надаю резервну копію бази даних (яку ви можете знайти тут), оскільки схема (з усіма індексами) досить велика, щоб вміститись у 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він буде робити GetLatestі SELECT * FROM Denormalizerбігти в (спочатку думали , що буде) повільний сценарій, є спосіб прискорити процес, коли ми обчислювальними службових полів BranchId, RecoveredOn, CreatedOn, UpdatedOnPrev, UpdatedOnCurr, UpdatedOnNext,UpdatedOnNextId

Заздалегідь спасибі

PS

Я намагаюся дотримуватися стандартного SQL, щоб зробити запит легко переносимим на інші бази даних, такі як MySQL / Oracle / SQLite для майбутніх, але якщо немає стандартного sql, це могло б допомогти мені добре з дотриманням конкретних конструкцій бази даних.


1
Що стосується стандартних SQL та перелічених БД: тут ви використовуєте CTE, і вони не підтримуються mySQL, і між основними реалізаціями є деякі синтаксичні зміни. Крім того, вони є оптимізаційною огорожею в постгресах, що може викликати серйозні проблеми. Ніщо з цього не повинно зупиняти вас на їх використанні, оскільки дерево у формі "списку суміжності", як правило, це правильний інструмент для роботи, але якщо сумісність викликає занепокоєння, це питання, про що слід пам’ятати, перш ніж вони кусають вас, щоб ви могли бути готова до будь-якої додаткової роботи, необхідної, коли міграція до інших СУБД стає реальністю.
Девід Спіллетт

Дякую, я намагаюся якомога більше дотримуватися стандартного SQL. Причиною тому є те, що я вважаю, що це повинно зменшити кількість проблем у майбутньому, коли буде потрібно перенести існуючий код до інших Баз даних. Це не завжди можливо. Існує також фактор часу, який є частиною рівняння. Я провів півроку, встановлюючи поточну структуру баз даних ... Я хотів би, щоб вона містила лише стандартні конструкції, але якщо це знадобиться ще 10 років, це не шлях ... Отже, якщо ви бачите, що є більше можливий стандартний рефакторинг, я з радістю прийму це ...
Lu4,

1
Ні, це звучить як прагматичний спосіб вирішити різні пріоритети та ускладнення у подібних випадках. Мені просто мозок кинув на розум питання, які виникли на увазі, якщо ви ще не стикалися з ними (краще знати зараз, навіть якщо зараз неможливо / практично нічого з цим робити, ніж бути покусаним сюрпризом у виробництві! ).
Девід Спіллетт

Відповіді:


9

@ Lu4 .. Я проголосував за закриття цього питання як "Порада Айсберга", але, використовуючи підказку запиту, ви зможете запустити його за 1 сек. Цей запит може бути відреставрований і використовувати його CROSS APPLY, але це буде консалтинговий концерт, а не відповідь на веб-сайті з питань запитання.

Ваш запит, як є, буде працювати протягом 13 хвилин на моєму сервері з 4 процесором та 16 Гб оперативної пам’яті.

введіть тут опис зображення

Я змінив ваш запит на використання, 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 і спробую розібратися у варіанті (MERGE JOIN). Зараз не очевидно, що здається проблемою з цим запитом, проте я майже впевнений, що я це
зрозумію,

@ Lu4 Проблема полягає в тому, що оптимізатор запитів не вибирає (або генерує) найкращий план виконання. Підказка запиту в цьому випадку 'спонукає' оптимізатора використовувати конкретну стратегію для здійснення з'єднання. Детальнішу інформацію див. У розділі Підключення до підключень (Transact-SQL) .
Кенні Евітт

CROSS APPLYчудово, але я б запропонував прочитати плани виконання та як їх проаналізувати, перш ніж намагатися використовувати підказки для запитів.
Кенні Евітт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.