C'est la sixième fois que j'essaie de poser cette question et c'est aussi la plus courte. Toutes les tentatives précédentes ont abouti à quelque chose de plus similaire à un article de blog plutôt qu'à la question elle-même, mais je vous assure que mon problème est réel, c'est juste qu'il concerne un grand sujet et sans tous ces détails que cette question contient, il sera pas clair quel est mon problème. Alors voilà ...
Abstrait
J'ai une base de données, elle permet de stocker des données de manière un peu fantaisiste et fournit plusieurs fonctionnalités non standard requises par mon processus métier. Les fonctionnalités sont les suivantes:
- Mises à jour / suppressions non destructives et non bloquantes implémentées via une approche d'insertion uniquement, qui permet la récupération de données et la journalisation automatique (chaque modification est liée à l'utilisateur qui a effectué cette modification)
- Données multiversionnelles (il peut y avoir plusieurs versions des mêmes données)
- Autorisations au niveau de la base de données
- Éventuelle cohérence avec la spécification ACID et création / mise à jour / suppression de transactions sécurisées
- Possibilité de rembobiner ou d'avancer rapidement votre vue actuelle des données à tout moment.
Il y a peut-être d'autres fonctionnalités que j'ai oublié de mentionner.
Structure de la base de données
Toutes les données utilisateur sont stockées dans la Items
table sous forme de chaîne codée JSON ( ntext
). Toutes les opérations de base de données sont effectuées via deux procédures stockées GetLatest
et InsertSnashot
permettent de fonctionner sur des données similaires à la façon dont GIT exploite les fichiers source.
Les données résultantes sont liées (JOINed) sur le frontend dans un graphique entièrement lié, il n'est donc pas nécessaire de faire des requêtes de base de données dans la plupart des cas.
Il est également possible de stocker des données dans des colonnes SQL régulières au lieu de les stocker sous forme codée Json. Cependant, cela augmente la contrainte de complexité globale.
Lecture des données
GetLatest
résultats avec des données sous forme d'instructions, considérez le diagramme suivant pour l'explication:
Le diagramme montre une évolution des modifications qui ont été apportées à un seul enregistrement. Les flèches sur le diagramme montrent la version en fonction de laquelle la modification a eu lieu (imaginez que l'utilisateur met à jour certaines données hors ligne, en parallèle aux mises à jour effectuées par l'utilisateur en ligne, ce cas introduirait un conflit, qui est essentiellement deux versions de données au lieu d'un).
Ainsi, l'appel GetLatest
dans les délais d'entrée suivants se traduira par les versions d'enregistrement suivantes:
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.
Pour GetLatest
de soutenir cette interface efficace chaque enregistrement doit contenir des attributs de service spécial BranchId
, RecoveredOn
, CreatedOn
, UpdatedOnPrev
, UpdatedOnCurr
, UpdatedOnNext
, UpdatedOnNextId
qui sont utilisés par GetLatest
pour savoir si le document est suffisamment dans le laps de temps prévu des GetLatest
arguments
Insérer des données
Afin de soutenir la cohérence, la sécurité et les performances des transactions, les données sont insérées dans la base de données via une procédure spéciale à plusieurs étapes.
Les données sont simplement insérées dans la base de données sans pouvoir être interrogées par la
GetLatest
procédure stockée.Les données sont rendues disponibles pour la
GetLatest
procédure stockée, les données sont rendues disponibles dans un état normalisé (c'est-à-diredenormalized = 0
). Bien que les données sont dans un état normalisé, les champs de serviceBranchId
,RecoveredOn
,CreatedOn
,UpdatedOnPrev
,UpdatedOnCurr
,UpdatedOnNext
,UpdatedOnNextId
sont calculées qui est vraiment lent.Afin d'accélérer les choses, les données sont dénormalisées dès qu'elles sont disponibles pour la
GetLatest
procédure stockée.- Étant donné que les étapes 1, 2, 3 effectuées dans différentes transactions, il est possible qu'une défaillance matérielle se produise au milieu de chaque opération. Laisser les données dans un état intermédiaire. Une telle situation est normale et même si cela se produit, les données seront guéries dans l'
InsertSnapshot
appel suivant . Le code de cette partie se trouve entre les étapes 2 et 3 de laInsertSnapshot
procédure stockée.
- Étant donné que les étapes 1, 2, 3 effectuées dans différentes transactions, il est possible qu'une défaillance matérielle se produise au milieu de chaque opération. Laisser les données dans un état intermédiaire. Une telle situation est normale et même si cela se produit, les données seront guéries dans l'
Le problème
Une nouvelle fonctionnalité (requise par les entreprises) m'a obligé à refactoriser la Denormalizer
vue spéciale qui relie toutes les fonctionnalités et est utilisée à la fois pour GetLatest
et InsertSnapshot
. Après cela, j'ai commencé à rencontrer des problèmes de performances. S'il était initialement SELECT * FROM Denormalizer
exécuté en quelques fractions de seconde, il faut maintenant près de 5 minutes pour traiter 10000 enregistrements.
Je ne suis pas un pro de la base de données et il m'a fallu près de six mois pour sortir de la structure de base de données actuelle. Et j'ai d'abord passé deux semaines à faire les refactorings puis à essayer de comprendre quelle est la cause première de mon problème de performance. Je ne le trouve pas. Je fournis la sauvegarde de la base de données (que vous pouvez trouver ici) car le schéma (avec tous les index) est assez volumineux pour tenir dans SqlFiddle, la base de données contient également des données obsolètes (10000+ enregistrements) que j'utilise à des fins de test . Je fournis également le texte à Denormalizer
afficher qui a été refactorisé et est devenu douloureusement lent:
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
Questions)
Deux scénarios sont pris en considération, les cas dénormalisés et normalisés:
En regardant la sauvegarde d'origine, ce qui rend la
SELECT * FROM Denormalizer
tâche si lente, j'ai l'impression qu'il y a un problème avec la partie récursive de la vue Denormalizer, j'ai essayé de restreindredenormalized = 1
mais aucune de mes actions n'a affecté les performances.Après avoir exécuté
UPDATE Items SET Denormalized = 0
il seraitGetLatest
etSELECT * FROM Denormalizer
INTRODUISE ( d' abord pensé être) scénario lent, est - il un moyen de accélérer les choses quand nous informatique champs de serviceBranchId
,RecoveredOn
,CreatedOn
,UpdatedOnPrev
,UpdatedOnCurr
,UpdatedOnNext
,UpdatedOnNextId
Merci d'avance
PS
J'essaie de m'en tenir au SQL standard pour rendre la requête facilement portable vers d'autres bases de données telles que MySQL / Oracle / SQLite pour l'avenir, mais s'il n'y a pas de SQL standard, cela pourrait aider, je suis d'accord avec le respect des constructions spécifiques à la base de données.