Filtrage des données classées par rowversion

8

J'ai une table de données SQL avec la structure suivante:

CREATE TABLE Data(
    Id uniqueidentifier NOT NULL,
    Date datetime NOT NULL,
    Value decimal(20, 10) NULL,
    RV timestamp NOT NULL,
 CONSTRAINT PK_Data PRIMARY KEY CLUSTERED (Id, Date)
)

Le nombre d'ID distincts varie de 3000 à 50000.
La taille de la table varie jusqu'à plus d'un milliard de lignes.
Un identifiant peut couvrir entre quelques lignes jusqu'à 5% du tableau.

La requête la plus exécutée sur cette table est:

SELECT Id, Date, Value, RV
FROM Data
WHERE Id = @Id
AND Date Between @StartDate AND @StopDate

Je dois maintenant implémenter la récupération incrémentielle des données sur un sous-ensemble d'ID, y compris les mises à jour.
J'ai ensuite utilisé un schéma de demande dans lequel l'appelant fournit une version de ligne spécifique, récupère un bloc de données et utilise la valeur de version de ligne maximale des données renvoyées pour l'appel suivant.

J'ai écrit cette procédure:

CREATE TYPE guid_list_tbltype AS TABLE (Id uniqueidentifier not null primary key)
CREATE PROCEDURE GetData
    @Ids guid_list_tbltype READONLY,
    @Cursor rowversion,
    @MaxRows int
AS
BEGIN
    SELECT A.* 
    FROM (
        SELECT 
            Data.Id,
            Date,
            Value,
            RV,
            ROW_NUMBER() OVER (ORDER BY RV) AS RN
        FROM Data
             inner join (SELECT Id FROM @Ids) Ids ON Ids.Id = Data.Id
        WHERE RV > @Cursor
    ) A 
    WHERE RN <= @MaxRows
END

@MaxRowsse situera entre 500 000 et 2 000 000 selon la façon dont le client voudra ses données.


J'ai essayé différentes approches:

  1. Indexation sur (Id, RV):
    CREATE NONCLUSTERED INDEX IDX_IDRV ON Data(Id, RV) INCLUDE(Date, Value);

À l'aide de l'index, la requête recherche les lignes où RV = @Cursorpour chaque Identrée @Ids, lit les lignes suivantes, puis fusionne le résultat et trie.
L'efficacité dépend alors de la position relative de la @Cursorvaleur.
Si elle est proche de la fin des données (commandée par RV), la requête est instantanée et sinon la requête peut prendre jusqu'à quelques minutes (ne jamais la laisser s'exécuter jusqu'à la fin).

le problème avec cette approche est qu'il @Cursorest proche de la fin des données et que le tri n'est pas pénible (même pas nécessaire si la requête retourne moins de lignes que @MaxRows), soit il est plus loin et la requête doit trier les @MaxRows * LEN(@Ids)lignes.

  1. Indexation sur RV:
    CREATE NONCLUSTERED INDEX IDX_RV ON Data(RV) INCLUDE(Id, Date, Value);

À l'aide de l'index, la requête recherche la ligne où RV = @Cursorlire ensuite chaque ligne en rejetant les ID non demandés jusqu'à ce qu'elle atteigne @MaxRows.
L'efficacité dépend alors du% des identifiants demandés ( LEN(@Ids) / COUNT(DISTINCT Id)) et de leur distribution.
Plus l'Id% demandé signifie moins de lignes supprimées, ce qui signifie des lectures plus efficaces, moins l'Id% demandé signifie plus de lignes supprimées, ce qui signifie plus de lectures pour la même quantité de lignes résultantes.

Le problème avec cette approche est que si les identifiants demandés ne contiennent que quelques éléments, il peut être nécessaire de lire l'index entier pour obtenir les lignes souhaitées.

  1. Utilisation d'un index filtré ou de vues indexées
    CREATE NONCLUSTERED INDEX IDX_RVClient1 ON Data(Id, RV) INCLUDE(Date, Value)
    WHERE Id IN (/* list of Ids for specific client*/);

Ou

    CREATE VIEW vDataClient1 WITH SCHEMABINDING
    AS
    SELECT
        Id,
        Date,
        Value,
        RV
    FROM dbo.Data
    WHERE Id IN (/* list of Ids for specific client*/)
    CREATE UNIQUE CLUSTERED INDEX IDX_IDRV ON vDataClient1(Id, Rv);

Cette méthode permet une indexation et des plans d'exécution des requêtes parfaitement efficaces, mais présente des inconvénients: 1. Pratiquement, je devrai implémenter du SQL dynamique pour créer les index ou les vues et modifier la procédure de demande pour utiliser le bon index ou la bonne vue. 2. Je devrai maintenir un index ou une vue par client existant, y compris le stockage. 3. Chaque fois qu'un client devra modifier sa liste d'ID demandés, je devrai supprimer l'index ou le visualiser et le recréer.


Je n'arrive pas à trouver une méthode qui convienne à mes besoins.
Je recherche de meilleures idées pour implémenter la récupération incrémentielle des données. Ces idées pourraient impliquer de retravailler le schéma demandeur ou le schéma de base de données bien que je préfère une meilleure approche d'indexation s'il y en a une.

Paciv
la source
Crosspost avec stackoverflow.com/questions/11586004/… . J'ai supprimé la version Oracle pour le moment car j'ai découvert que ORA_ROWSCN n'est pas indexable (et à peine à travers des vues matérialisées indexées).
Paciv
Comment le champ de date s'intègre-t-il? Une ligne avec un ID et une date particuliers peut-elle être mise à jour dans le tableau? Et si oui, la date est-elle également mise à jour (comme un horodatage supplémentaire?)
8kb
On dirait que pour la tentative GetData (), la commande par doit inclure l'ID (ordre par RV, Id). Pouvez-vous commenter l'utilisation d'un index de (Rv, Id)? L'utilisation de ">" max rowversion de l'appel précédent semble également manquer des enregistrements entre les morceaux si les lignes ont la même rowversion (n'est-ce pas possible?).
crokusek
@ 8kb: les instructions de mise à jour qui s'exécutent sur la table ne modifient que la Valuecolonne. @crokusek: Ne pas commander par RV, ID au lieu de RV ne fait qu'augmenter la charge de travail de tri sans aucun avantage, je ne comprends pas le raisonnement derrière votre commentaire. D'après ce que j'ai lu, RV devrait être unique à moins d'insérer des données spécifiquement dans cette colonne, ce que l'application ne fait pas.
Paciv
Le client peut-il accepter les résultats dans l'ordre (Id, Rv) et fournir un argument LastId en plus de l'argument LastRowVersion pour éliminer le tri RV entre les ID? Mes commentaires précédents étaient tous basés sur l'hypothèse que RV avait des doublons. L'index filtré par client semblait intéressant.
crokusek

Réponses:

5

Une solution consiste à ce que l'application cliente se souvienne du maximum rowversionpar ID. Le type de table défini par l'utilisateur se changerait en:

CREATE TYPE
    dbo.guid_list_tbltype
AS TABLE 
    (
    Id      uniqueidentifier PRIMARY KEY, 
    LastRV  rowversion NOT NULL
    );

La requête dans la procédure peut ensuite être réécrite pour utiliser le APPLYmodèle (voir mes articles SQLServerCentral partie 1 et partie 2 - connexion gratuite requise). La clé d'une bonne performance ici est la ORDER BY- elle évite la prélecture non ordonnée sur la jointure des boucles imbriquées. Ceci RECOMPILEest nécessaire pour permettre à l'optimiseur de voir la cardinalité de la variable de table au moment de la compilation (résultant probablement en un plan parallèle souhaitable).

ALTER PROCEDURE dbo.GetData

    @IDs        guid_list_tbltype READONLY,
    @MaxRows    bigint

AS
BEGIN

    SELECT TOP (@MaxRows)
        d.Id,
        d.[Date],
        d.Value,
        d.RV
    FROM @Ids AS i
    CROSS APPLY
    (
        SELECT
            d.*
        FROM dbo.Data AS d
        WHERE
            d.Id = i.Id
            AND d.RV > i.LastRV
    ) AS d
    ORDER BY
        i.Id,
        d.RV
    OPTION (RECOMPILE);

END;

Vous devriez obtenir un plan de requête post-exécution comme celui-ci (le plan estimé sera en série):

plan de requête

Paul White 9
la source
À droite, l'une des solutions de changement de conception consiste à faire en sorte que le client se souvienne de l' MAX(RV)ID par (ou d'un système d'abonnement où l'application interne se souvient de toutes les paires ID / RV) et j'utilise ce motif pour un autre client. Une autre solution était de forcer le client à toujours récupérer tous les identifiants (ce qui rend le problème d'indexation trivial). Il ne couvre toujours pas la question du besoin particulier: récupération incrémentielle d'un sous-ensemble d'ID avec un seul compteur global fourni par le client.
Paciv
2

Si possible, je remanierais la table. Si nous pouvons avoir VersionNumber comme un entier incrémentiel sans lacunes, que la tâche de récupérer le morceau suivant est un balayage de plage totalement trivial. Tout ce dont nous avons besoin est l'index suivant:

CREATE NONCLUSTERED INDEX IDX_IDRV ON Data(Id, VersionNumber) INCLUDE(Date, Value);

Bien sûr, nous devons nous assurer que VersionNumber commence par un et n'a pas de lacunes. C'est facile à faire avec des contraintes.

AK
la source
Vous voulez dire un global ou un Id local VersionNumber? Dans les deux cas, je ne vois pas en quoi cela va aider avec la question, pourriez-vous développer davantage?
Paciv
0

Ce que j'aurais fait:

Dans ce cas, votre PK doit être un champ d'identité "Clé de substitution" qui s'incrémente automatiquement.
Puisque vous êtes déjà dans les milliards, il serait préférable d'aller avec un BigInt.
Appelons-le DataID .
Cette volonté:

  • Ajoutez 8 octets à chaque enregistrement de votre index cluster.
  • Économisez 16 octets sur chaque enregistrement de chaque index non clusterisé.
  • Ce que vous aviez était une "clé naturelle": un UniqueIdentifyer (16 octets) avec un DateTime (8 octets).
  • C'est 24 octets dans chaque enregistrement d'index pour faire référence à l'index clusterisé!
  • C'est pourquoi nous avons des clés de substitution en tant que plus petits incréments d'incrémentation.


Configurez votre nouveau BigInt PK ( DataID ) pour utiliser un index clusterisé:
Cela:

  • Assurez-vous que les enregistrements les plus récemment créés sont placés vers la fin.
  • Permet une indexation plus rapide avec d'autres index non clusterisés.
  • Permettre une future expansion en tant que FK vers d'autres tables.


Créez un index non groupé autour de (Date, Id).
Cette volonté:

  • Accélérez vos requêtes les plus couramment utilisées.
  • Vous pouvez ajouter "Value", mais cela augmentera la taille de votre index, ce qui le rend plus lent.
  • Je suggère de l'essayer à l'intérieur et à l'extérieur de l'indice pour voir s'il y a une grande différence de performances.
  • Je recommanderais de ne pas utiliser "Inclure" si vous l'ajoutez.
  • Insérez-vous comme ça (Date, Id, Value) - mais seulement si vos tests montrent qu'il améliore les performances.


Créez un index non clusterisé sur (RV, ID).
Cette volonté:

  • Gardez toujours vos index aussi petits que possible.
  • À moins que vous ne remarquiez des gains de performances incroyables avec la date et la valeur dans vos index, je vous suggère de les laisser pour économiser de l'espace disque. Essayez-les sans eux d'abord.
  • Si vous ajoutez une date ou une valeur, n'utilisez pas «Inclure», mais ajoutez-les à la place de l'ordre de l'index.
  • Grâce à l'incrémentation de DataID sur les nouvelles insertions dans votre PK en cluster, vos VR récents apparaîtront généralement vers la fin (à moins que vous ne mettiez à jour tout un tas de données du passé).
MikeTeeVee
la source