Database
 sql >> Base de données >  >> RDS >> Database

Journalisation minimale avec INSERT…SELECT dans les tables Heap

Introduction

Atteindre une journalisation minimale avec INSERT...SELECT peut être une entreprise compliquée. Les considérations répertoriées dans le Guide des performances de chargement des données sont encore assez complètes, bien qu'il faille également lire SQL Server 2016, Journalisation minimale et Impact de la taille de lot dans les opérations de chargement en bloc par Parikshit Savjani de l'équipe SQL Server Tiger pour obtenir l'image mise à jour pour SQL Server 2016 et versions ultérieures, lors du chargement en masse dans des tables rowstore en cluster. Cela dit, cet article vise uniquement à fournir de nouveaux détails à propos de la journalisation minimale lors du chargement en bloc de tables de tas traditionnelles (non "optimisées en mémoire") à l'aide de INSERT...SELECT . Les tables avec un index clusterisé b-tree sont traitées séparément dans la deuxième partie de cette série.

Tables de tas

Lors de l'insertion de lignes à l'aide de INSERT...SELECT dans un tas sans index non clusterisés, la documentation indique universellement que de telles insertions seront minimalement enregistrées tant qu'un TABLOCK l'indice est présent. Cela se reflète dans les tableaux récapitulatifs inclus dans le Guide des performances de chargement des données et le poste de l'équipe Tiger. Les lignes récapitulatives pour les tables de tas sans index sont les mêmes dans les deux documents (aucune modification pour SQL Server 2016) :

Un TABLOCK explicite l'indice n'est pas le seul moyen de répondre à l'exigence de verrouillage au niveau de la table . Nous pouvons également définir le 'table lock on bulk load' option pour la table cible en utilisant sp_tableoption ou en activant l'indicateur de trace documenté 715. (Remarque :ces options ne sont pas suffisantes pour activer une journalisation minimale lors de l'utilisation de INSERT...SELECT car INSERT...SELECT ne prend pas en charge les verrous de mise à jour groupée).

Le "concurrent possible" colonne dans le résumé ne s'applique qu'aux méthodes de chargement en masse autres que INSERT...SELECT . Le chargement simultané d'une table de tas n'est pas possible avec INSERT...SELECT . Comme indiqué dans le Guide des performances de chargement des données , chargement en masse avec INSERT...SELECT prend une exclusivité X verrou sur la table, pas la mise à jour en masse BU verrou requis pour les chargements en bloc simultanés.

Tout cela mis à part - et en supposant qu'il n'y a aucune autre raison de ne pas s'attendre à une journalisation minimale lors du chargement en masse d'un tas non indexé avec TABLOCK (ou équivalent) — l'insert toujours pourrait ne pas être minimalement connecté…

Une exception à la règle

Le script de démonstration suivant doit être exécuté sur une instance de développement dans une nouvelle base de données de test configuré pour utiliser le SIMPLE modèle de récupération. Il charge un certain nombre de lignes dans une table de tas en utilisant INSERT...SELECT avec TABLOCK , et des rapports sur les enregistrements du journal des transactions générés :

CREATE TABLE dbo.TestHeap( id integer NOT NULL IDENTITY, c1 integer NOT NULL, padding char(45) NOT NULL DEFAULT '');GO-- Effacer le logCHECKPOINT;GO-- Insérer des lignesINSERT dbo.TestHeap WITH (TABLOCK ) (c1)SELECT TOP (897) CHECKSUM(NEWID())FROM master.dbo.spt_values ​​AS SV;GO-- Afficher les entrées du journalSELECT FD.Operation, FD.Context, FD.[Log Record Length], FD.[Log Reserve], FD.AllocUnitName, FD.[Transaction Name], FD.[Lock Information], FD.[Description]FROM sys.fn_dblog(NULL, NULL) AS FD;GO-- Compter le nombre de lignes entièrement consignéesSELECT [ Lignes entièrement enregistrées] =COUNT_BIG(*) FROM sys.fn_dblog(NULL, NULL) AS FDWHERE FD.Operation =N'LOP_INSERT_ROWS' AND FD.Context =N'LCX_HEAP' AND FD.AllocUnitName =N'dbo.TestHeap'; 

La sortie montre que les 897 lignes ont été entièrement enregistrées bien qu'il remplisse apparemment toutes les conditions pour une journalisation minimale (seul un échantillon d'enregistrements de journal est affiché pour des raisons d'espace) :

Le même résultat est vu si l'insertion est répétée (c'est-à-dire que peu importe si la table de tas est vide ou non). Ce résultat contredit la documentation.

Le seuil de journalisation minimal pour les tas

Le nombre de lignes qu'il faut ajouter dans un seul INSERT...SELECT déclaration pour obtenir une journalisation minimale dans un tas non indexé avec le verrouillage de table activé dépend d'un calcul effectué par SQL Server lors de l'estimation de la taille totale des données à insérer. Les entrées de ce calcul sont :

  • La version de SQL Server.
  • Le nombre estimé de lignes menant à l'insertion opérateur.
  • Taille de ligne cible du tableau.

Pour SQL Server 2012 et versions antérieures , le point de transition pour cette table particulière correspond à 898 lignes . Modification du numéro dans le script de démonstration TOP clause de 897 à 898 produit la sortie suivante :

Les entrées du journal des transactions générées concernent l'allocation des pages et la maintenance de la Index Allocation Map (IAM) et espace libre de page (PFS). N'oubliez pas qu'une journalisation minimale signifie que SQL Server n'enregistre pas chaque insertion de ligne individuellement. Au lieu de cela, seules les modifications apportées aux métadonnées et aux structures d'allocation sont consignées. Le passage de 897 à 898 lignes permet une journalisation minimale pour cette table spécifique.

Pour SQL Server 2014 et versions ultérieures , le point de transition est de 950 lignes pour ce tableau. Exécution de la commande INSERT...SELECT avec TOP (949) utilisera la journalisation complète – passage à TOP (950) produira une journalisation minimale .

Les seuils ne sont pas dépend de l'estimation de la cardinalité modèle utilisé ou le niveau de compatibilité de la base de données.

Le calcul de la taille des données

Indique si SQL Server décide d'utiliser le chargement en bloc d'ensembles de lignes — et donc si la journalisation minimale est disponible ou non — dépend du résultat d'une série de calculs effectués dans une méthode appelée sqllang!CUpdUtil::FOptimizeInsert , qui renvoie soit true pour une journalisation minimale, ou false pour une journalisation complète. Un exemple de pile d'appels est illustré ci-dessous :

L'essence du test est :

  • L'insertion doit être pour plus de 250 lignes .
  • La taille totale des données d'insertion doit être calculée comme au moins 8 pages .

La vérification de plus de 250 lignes dépend uniquement du nombre estimé de lignes arrivant à l'insertion de table opérateur. Cela s'affiche dans le plan d'exécution sous la forme 'Estimation du nombre de lignes' . Soyez prudent avec cela. Il est facile de produire un plan avec un nombre de lignes estimé faible, par exemple en utilisant une variable dans le TOP clause sans OPTION (RECOMPILE) . Dans ce cas, l'optimiseur devine 100 lignes, ce qui n'atteindra pas le seuil, et empêchera ainsi le chargement en bloc et une journalisation minimale.

Le calcul de la taille totale des données est plus complexe et ne correspond pas la 'Taille de ligne estimée' s'écoulant dans l'insertion de tableau opérateur. La façon dont le calcul est effectué est légèrement différente dans SQL Server 2012 et versions antérieures par rapport à SQL Server 2014 et versions ultérieures. Pourtant, les deux produisent un résultat de taille de ligne différent de ce qui est vu dans le plan d'exécution.

Le calcul de la taille des lignes

La taille totale des données d'insertion est calculée en multipliant le nombre estimé de lignes par la taille de ligne maximale attendue . Le calcul de la taille des lignes est le point qui diffère entre les versions de SQL Server.

Dans SQL Server 2012 et versions antérieures, le calcul est effectué par sqllang!OptimizerUtil::ComputeRowLength . Pour la table de tas de test (délibérément conçue avec de simples colonnes non nulles de longueur fixe utilisant le FixedVar d'origine format de stockage de ligne) un aperçu du calcul est :

  • Initialiser une FixedVar générateur de métadonnées.
  • Obtenir des informations sur le type et l'attribut de chaque colonne dans l'insertion de tableau flux d'entrée.
  • Ajouter des colonnes typées et des attributs aux métadonnées.
  • Finalisez le générateur et demandez-lui la taille de ligne maximale.
  • Ajouter une surcharge pour le bitmap nul et le nombre de colonnes.
  • Ajouter quatre octets pour la ligne bits d'état et le décalage de ligne par rapport au nombre de données de colonnes.

Taille de ligne physique

On peut s'attendre à ce que le résultat de ce calcul corresponde à la taille de ligne physique, mais ce n'est pas le cas. Par exemple, avec la gestion des versions de ligne désactivée pour la base de données :

SELECT DDIPS.index_type_desc, DDIPS.alloc_unit_type_desc, DDIPS.page_count, DDIPS.record_count, DDIPS.min_record_size_in_bytes, DDIPS.max_record_size_in_bytes, DDIPS.avg_record_size_in_bytesFROM sys.dm_db_index_physical_stats ( N'db_index_physical_stats ( DB_ID, T'dbo.T) U'), 0, -- heap NULL, -- all partitions 'DETAILED' ) AS DDIPS;

…donne une taille d'enregistrement de 60 octets dans chaque ligne du tableau de test :

C'est comme décrit dans Estimer la taille d'un tas :

  • Taille totale en octets de tous les de longueur fixe colonnes =53 octets :
    • id integer NOT NULL =4 octets
    • c1 integer NOT NULL =4 octets
    • padding char(45) NOT NULL =45 octets.
  • Bitmap nul =3 octets :
    • =2 + int((Num_Cols + 7) / 8)
    • =2 + int((3 + 7) / 8)
    • =3 octets.
  • En-tête de ligne =4 octets .
  • Total 53 + 3 + 4 =60 octets .

Il correspond également à la taille de ligne estimée indiquée dans le plan d'exécution :

Détails du calcul interne

Le calcul interne utilisé pour déterminer si le chargement en bloc est utilisé donne un résultat différent, basé sur le suivant insert stream informations de colonne obtenues à l'aide d'un débogueur. Les numéros de type utilisés correspondent à sys.types :

  • Total de longueur fixe taille de colonne =66 octets :
    • Tapez l'identifiant 173 binary(8) =8 octets (interne).
    • Tapez l'identifiant 56 integer =4 octets (interne).
    • Tapez l'identifiant 104 bit =1 octet (interne).
    • Tapez l'identifiant 56 integer =4 octets (id colonne).
    • Tapez l'identifiant 56 integer =4 octets (c1 colonne).
    • Tapez l'identifiant 175 char(45) =45 octets (padding colonne).
  • Bitmap nul =3 octets (comme avant).
  • En-tête de ligne surcharge =4 octets (comme avant).
  • Taille de ligne calculée =66 + 3 + 4 =73 octets .

La différence est que le flux d'entrée alimentant l'insertion de table l'opérateur contient trois colonnes internes supplémentaires . Ceux-ci sont supprimés lors de la génération de showplan. Les colonnes supplémentaires constituent le localisateur d'insertion de tableau , qui inclut le signet (RID ou localisateur de ligne) comme premier composant. Il s'agit de métadonnées pour l'insertion et ne finit pas par être ajouté à la table.

Les colonnes supplémentaires expliquent l'écart entre le calcul effectué par OptimizerUtil::ComputeRowLength et la taille physique des lignes. Cela pourrait être considéré comme un bogue :SQL Server ne doit pas compter les colonnes de métadonnées dans le flux d'insertion dans la taille physique finale de la ligne. D'un autre côté, le calcul peut simplement être une estimation au mieux en utilisant la mise à jour générique opérateur.

Le calcul ne prend pas non plus en compte d'autres facteurs tels que la surcharge de 14 octets de la gestion des versions de ligne. Cela peut être testé en réexécutant le script de démonstration avec l'un des isolement d'instantané ou lire l'isolement d'instantané validé options de base de données activées. La taille physique de la ligne augmentera de 14 octets (de 60 octets à 74), mais le seuil de journalisation minimale reste inchangé à 898 lignes.

Calcul du seuil

Nous avons maintenant tous les détails dont nous avons besoin pour voir pourquoi le seuil est de 898 lignes pour cette table sur SQL Server 2012 et versions antérieures :

  • 898 lignes répondent à la première exigence pour plus de 250 lignes .
  • Taille de ligne calculée =73 octets.
  • Nombre de lignes estimé =897.
  • Taille totale des données = 73 octets x 897 lignes = 65 481 octets.
  • Nombre total de pages =65 481 / 8 192 =7,9932861328125.
    • C'est juste en dessous de la deuxième exigence pour>=8 pages.
  • Pour 898 lignes, le nombre de pages est de 8,002197265625.
    • Voici >=8 pages donc journalisation minimale est activé.

Dans SQL Server 2014 et versions ultérieures , les modifications sont :

  • La taille des lignes est calculée par le générateur de métadonnées.
  • La colonne d'entiers internes dans le localisateur de table n'est plus présent dans le flux d'insertion. Cela représente l'uniquificateur , qui ne s'applique qu'aux index. Il semble probable que cela ait été supprimé en tant que correction de bogue.
  • La taille de ligne attendue passe de 73 à 69 octets en raison de la colonne entière omise (4 octets).
  • La taille physique est toujours de 60 octets. La différence restante de 9 octets est prise en compte par les colonnes internes supplémentaires de 8 octets RID et 1 octet de bit dans le flux d'insertion.

Pour atteindre le seuil de 8 pages avec 69 octets par ligne :

  • 8 pages * 8192 octets par page =65536 octets.
  • 65 535 octets / 69 octets par ligne =949,7971014492754 lignes.
  • Nous attendons donc un minimum de 950 lignes pour activer le chargement groupé d'ensembles de lignes pour cette table sur SQL Server 2014 et versions ultérieures.

Résumé et réflexions finales

Contrairement aux méthodes de chargement en masse qui prennent en charge la taille de lot , comme indiqué dans l'article de Parikshit Savjani, INSERT...SELECT dans un tas non indexé (vide ou non) ne fait pas toujours entraîne une journalisation minimale lorsque le verrouillage de table est spécifié.

Pour activer la journalisation minimale avec INSERT...SELECT , SQL Server doit s'attendre à plus de 250 lignes avec une taille totale d'au moins une extension (8pages).

Lors du calcul de la taille d'insertion totale estimée (à comparer avec le seuil de 8 pages), SQL Server multiplie le nombre estimé de lignes par une taille de ligne maximale calculée. SQL Server compte les colonnes internes présent dans le flux d'insertion lors du calcul de la taille de la ligne. Pour SQL Server 2012 et versions antérieures, cela ajoute 13 octets par ligne. Pour SQL Server 2014 et versions ultérieures, il ajoute 9 octets par ligne. Cela n'affecte que le calcul; cela n'affecte pas la taille physique finale des lignes.

Lorsque le chargement en bloc de tas avec une journalisation minimale est actif, SQL Server ne le fait pas insérer des lignes une à la fois. Les étendues sont allouées à l'avance et les lignes à insérer sont collectées dans de nouvelles pages entières par sqlmin!RowsetBulk avant d'être ajouté à la structure existante. Un exemple de pile d'appels est illustré ci-dessous :

Les lectures logiques ne sont pas signalées pour la table cible lorsqu'un chargement en masse de tas à journalisation minimale est utilisé - l'insertion de table l'opérateur n'a pas besoin de lire une page existante pour localiser le point d'insertion de chaque nouvelle ligne.

Les plans d'exécution actuellement ne s'affichent pas combien de lignes ou de pages ont été insérées à l'aide du chargement groupé d'ensembles de lignes et journalisation minimale . Peut-être que ces informations utiles seront ajoutées au produit dans une future version.