Problema de rendimiento de consultas sql recursivas [cerrado]


9

Esta es la sexta vez que intento hacer esta pregunta y también es la más corta. Todos los intentos anteriores resultaron con algo más similar a una publicación de blog en lugar de la pregunta en sí, pero le aseguro que mi problema es real, es solo que se trata de un tema importante y sin todos los detalles que contiene esta pregunta será No está claro cuál es mi problema. Entonces aquí va ...

Resumen

Tengo una base de datos, que permite almacenar datos de una manera elegante y proporciona varias características no estándar que requiere mi proceso comercial. Las características son las siguientes:

  1. Actualizaciones / eliminaciones no destructivas y sin bloqueo implementadas mediante un enfoque de solo inserción, que permite la recuperación de datos y el registro automático (cada cambio está vinculado al usuario que realizó ese cambio)
  2. Datos de varias versiones (puede haber varias versiones de los mismos datos)
  3. Permisos a nivel de base de datos
  4. La coherencia eventual con la especificación ACID y la creación / actualización / eliminación segura de transacciones
  5. Capacidad para rebobinar o avanzar rápidamente su vista actual de datos a cualquier punto en el tiempo.

Puede haber otras características que olvidé mencionar.

Estructura de la base de datos

Todos los datos del usuario se almacenan en la Itemstabla como una cadena codificada JSON ( ntext). Todas las operaciones de la base de datos se llevan a cabo a través de dos procedimientos almacenados GetLatesty InsertSnashotpermiten operar con datos similares a la forma en que GIT opera los archivos fuente.

Los datos resultantes están vinculados (UNIDOS) en la interfaz en un gráfico totalmente vinculado, por lo que no es necesario realizar consultas a la base de datos en la mayoría de los casos.

También es posible almacenar datos en columnas SQL regulares en lugar de almacenarlos en forma codificada Json. Sin embargo, eso aumenta la complejidad de la tensión general.

Lectura de datos

GetLatestresultados con datos en forma de instrucciones, considere el siguiente diagrama para explicación:

Diagrama de estructura

El diagrama muestra una evolución de los cambios que se realizaron en un solo registro. Las flechas en el diagrama muestran la versión en función de la cual se realizó la edición (Imagine que el usuario está actualizando algunos datos fuera de línea, en paralelo a las actualizaciones realizadas por el usuario en línea, tal caso introduciría un conflicto, que es básicamente dos versiones de datos en lugar de uno).

Entonces, llamar GetLatestdentro de los siguientes intervalos de tiempo de entrada dará como resultado las siguientes versiones de registro:

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.

A fin de que GetLatestpara apoyar este tipo de interfaz eficiente cada registro debe contener atributos de servicios especiales BranchId, RecoveredOn, CreatedOn, UpdatedOnPrev, UpdatedOnCurr, UpdatedOnNext, UpdatedOnNextIdque son utilizados por GetLatestaveriguar si el registro cae adecuadamente en el intervalo de tiempo previsto GetLatestargumentos

Insertar datos

Para respaldar la coherencia, la seguridad y el rendimiento de las transacciones, los datos se insertan en la base de datos mediante un procedimiento especial de varias etapas.

  1. Los datos simplemente se insertan en la base de datos sin poder ser consultados por un GetLatestprocedimiento almacenado.

  2. Los datos están disponibles para el GetLatestprocedimiento almacenado, los datos están disponibles en estado normalizado (es decir denormalized = 0). Mientras que los datos están en estado normalizado, los campos de servicio BranchId, RecoveredOn, CreatedOn, UpdatedOnPrev, UpdatedOnCurr, UpdatedOnNext, UpdatedOnNextIdestán siendo reconstruidos que es muy lento.

  3. Para acelerar las cosas, los datos se desnormalizan tan pronto como están disponibles para el GetLatestprocedimiento almacenado.

    • Dado que los pasos 1, 2, 3 realizados dentro de diferentes transacciones es posible que ocurra una falla de hardware en el medio de cada operación. Dejar datos en un estado intermedio. Dicha situación es normal e incluso si sucede, los datos se curarán en la siguiente InsertSnapshotllamada. El código para esta parte se puede encontrar entre los pasos 2 y 3 del InsertSnapshotprocedimiento almacenado.

El problema

Una nueva función (requerida por el negocio) me obligó a refactorizar una Denormalizervista especial que vincula todas las funciones y se usa para ambos GetLatesty InsertSnapshot. Después de eso, comencé a experimentar problemas de rendimiento. Si se SELECT * FROM Denormalizerejecutó originalmente solo en fracciones de segundo, ahora lleva casi 5 minutos procesar 10000 registros.

No soy un DB pro y me tomó casi seis meses solo para salir con la estructura actual de la base de datos. Y pasé dos semanas primero para hacer las refactorizaciones y luego tratar de descubrir cuál es la causa principal de mi problema de rendimiento. Simplemente no puedo encontrarlo. Proporciono la copia de seguridad de la base de datos (que puede encontrar aquí) porque el esquema (con todos los índices) es bastante grande para caber en SqlFiddle, la base de datos también contiene datos obsoletos (más de 10000 registros) que estoy usando para fines de prueba . También proporciono el texto para la Denormalizervista que se refactorizó y se volvió dolorosamente lento:

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

Las preguntas)

Hay dos escenarios que se tienen en cuenta, los casos desnormalizados y normalizados:

  1. Mirando a la copia de seguridad original, lo que hace que el proceso sea SELECT * FROM Denormalizertan lento, siento que hay un problema con la parte recursiva de la vista del normalizador, he intentado restringir denormalized = 1pero ninguna de mis acciones ha afectado el rendimiento.

  2. Después de ejecutar UPDATE Items SET Denormalized = 0haría GetLatesty SELECT * FROM Denormalizerde ejecución en (pensado originalmente para ser) escenario lenta, ¿hay una manera de acelerar las cosas cuando estamos calculando campos de servicio BranchId, RecoveredOn, CreatedOn, UpdatedOnPrev, UpdatedOnCurr, UpdatedOnNext,UpdatedOnNextId

Gracias de antemano

PD

Estoy tratando de mantener el SQL estándar para que la consulta sea fácilmente portátil a otras bases de datos como MySQL / Oracle / SQLite para el futuro, pero si no hay un SQL estándar que pueda ayudar, estoy de acuerdo con seguir construcciones específicas de la base de datos.


1
Con respecto al SQL estándar y los DB que enumera: está utilizando un CTE aquí y mySQL no los admite y existen algunas variaciones de sintaxis entre las principales implementaciones. También son una valla de optimización en postgres actualmente que podría ser una gran preocupación de rendimiento. Nada de eso debería dejar de usarlos, con un árbol en forma de "lista de adyacencia", generalmente son la herramienta adecuada para el trabajo, pero cuando la compatibilidad es una preocupación, estos puntos son aspectos a tener en cuenta antes de morderte para que puedas ser preparado para cualquier trabajo adicional necesario cuando la migración a otros DBMS se convierta en realidad.
David Spillett

Gracias, estoy tratando de mantener el SQL estándar tanto como sea posible. La razón de esto es que creo que debería reducir la cantidad de problemas en el futuro, cuando será necesario migrar el código existente a otras bases de datos. No siempre es posible. También hay un factor de tiempo que es parte de la ecuación. He pasado medio año resolviendo la estructura actual de la base de datos ... Me gustaría que contenga construcciones solo estándar, pero si requerirá 10 años más, no es el camino a seguir ... Entonces, si ve que hay más posible refactorización estándar Estaré encantado de aceptarlo ...
Lu4

1
No, eso suena como una forma pragmática de lidiar con las diferentes prioridades y complicaciones en casos como este. Acabo de descartar los problemas que me vinieron a la mente en caso de que aún no los hayas encontrado (mejor saber ahora, incluso si no es posible / práctico hacer algo al respecto en este momento, ¡que te muerda la sorpresa en la producción! )
David Spillett

Respuestas:


9

@ Lu4 ... He votado para cerrar esta pregunta como "Sugerencia de Iceberg", pero con la sugerencia de consulta podrás ejecutarla en menos de 1 segundo. Esta consulta se puede refactorizar y usar CROSS APPLY, pero será un trabajo de consultoría y no como una respuesta en un sitio de preguntas y respuestas.

Su consulta se ejecutará durante más de 13 minutos en mi servidor con 4 CPU y 16 GB de RAM.

ingrese la descripción de la imagen aquí

Cambié su consulta para usar OPTION(MERGE JOIN)y se ejecutó en menos de 1 segundo

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

ingrese la descripción de la imagen aquí

Tenga en cuenta que no puede usar sugerencias de consulta en una vista, por lo que debe encontrar una alternativa para hacer su vista como un SP o cualquier solución alternativa


1
muchas gracias por esto, teniendo en cuenta que la pregunta está lejos de los estándares de stackoverflow, hace que su esfuerzo sea dos veces valioso para mí. Haré mi tarea en CROSS APPLY e intentaré descubrir la OPCIÓN (MERGE JOIN). Ahora no es obvio cuál parece ser el problema con esa consulta, sin embargo, estoy bastante seguro de que lo resolveré, gracias una vez más
Lu4

@ Lu4 El problema es que el optimizador de consultas no elige (o genera) el mejor plan de ejecución. La sugerencia de consulta en este caso 'alienta' al optimizador a usar una estrategia específica para implementar la unión. Consulte Sugerencias de unión (Transact-SQL) para obtener más detalles.
Kenny Evitt

CROSS APPLYes genial, pero sugeriría leer sobre los planes de ejecución y cómo analizarlos antes de intentar utilizar sugerencias de consulta.
Kenny Evitt
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.