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:
- 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)
- Datos de varias versiones (puede haber varias versiones de los mismos datos)
- Permisos a nivel de base de datos
- La coherencia eventual con la especificación ACID y la creación / actualización / eliminación segura de transacciones
- 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 Items
tabla 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 GetLatest
y InsertSnashot
permiten 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
GetLatest
resultados con datos en forma de instrucciones, considere el siguiente diagrama para explicación:
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 GetLatest
dentro 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 GetLatest
para apoyar este tipo de interfaz eficiente cada registro debe contener atributos de servicios especiales BranchId
, RecoveredOn
, CreatedOn
, UpdatedOnPrev
, UpdatedOnCurr
, UpdatedOnNext
, UpdatedOnNextId
que son utilizados por GetLatest
averiguar si el registro cae adecuadamente en el intervalo de tiempo previsto GetLatest
argumentos
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.
Los datos simplemente se insertan en la base de datos sin poder ser consultados por un
GetLatest
procedimiento almacenado.Los datos están disponibles para el
GetLatest
procedimiento almacenado, los datos están disponibles en estado normalizado (es decirdenormalized = 0
). Mientras que los datos están en estado normalizado, los campos de servicioBranchId
,RecoveredOn
,CreatedOn
,UpdatedOnPrev
,UpdatedOnCurr
,UpdatedOnNext
,UpdatedOnNextId
están siendo reconstruidos que es muy lento.Para acelerar las cosas, los datos se desnormalizan tan pronto como están disponibles para el
GetLatest
procedimiento 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
InsertSnapshot
llamada. El código para esta parte se puede encontrar entre los pasos 2 y 3 delInsertSnapshot
procedimiento 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
El problema
Una nueva función (requerida por el negocio) me obligó a refactorizar una Denormalizer
vista especial que vincula todas las funciones y se usa para ambos GetLatest
y InsertSnapshot
. Después de eso, comencé a experimentar problemas de rendimiento. Si se SELECT * FROM Denormalizer
ejecutó 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 Denormalizer
vista 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:
Mirando a la copia de seguridad original, lo que hace que el proceso sea
SELECT * FROM Denormalizer
tan lento, siento que hay un problema con la parte recursiva de la vista del normalizador, he intentado restringirdenormalized = 1
pero ninguna de mis acciones ha afectado el rendimiento.Después de ejecutar
UPDATE Items SET Denormalized = 0
haríaGetLatest
ySELECT * FROM Denormalizer
de ejecución en (pensado originalmente para ser) escenario lenta, ¿hay una manera de acelerar las cosas cuando estamos calculando campos de servicioBranchId
,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.