Une mauvaise estimation de cardinalité disqualifie INSERT de la journalisation minimale?

11

Pourquoi la deuxième INSERTinstruction est-elle 5 fois plus lente que la première?

D'après la quantité de données de journal générées, je pense que la seconde n'est pas admissible à une journalisation minimale. Cependant, la documentation du Guide de performances de chargement des données indique que les deux insertions doivent pouvoir être journalisées de manière minimale. Donc, si la journalisation minimale est la principale différence de performances, pourquoi la deuxième requête ne remplit-elle pas les conditions requises pour une journalisation minimale? Que peut-on faire pour améliorer la situation?


Requête n ° 1: insertion de lignes de 5 mm à l'aide de INSERT ... WITH (TABLOCK)

Considérez la requête suivante, qui insère des lignes de 5 mm dans un segment. Cette requête s'exécute dans 1 secondet génère 64MBdes données de journal des transactions comme indiqué par sys.dm_tran_database_transactions.

CREATE TABLE dbo.minimalLoggingTest (n INT NOT NULL)
GO
INSERT INTO dbo.minimalLoggingTest WITH (TABLOCK) (n)
SELECT n
-- Any table/view/sub-query that correctly estimates that it will generate 5MM rows
FROM dbo.fiveMillionNumbers
-- Provides greater consistency on my laptop, where other processes are running
OPTION (MAXDOP 1)
GO


Requête n ° 2: insertion des mêmes données, mais SQL sous-estime le nombre de lignes

Considérons maintenant cette requête très similaire, qui fonctionne exactement sur les mêmes données mais qui se trouve être tirée d'une table (ou d'une SELECTinstruction complexe avec de nombreuses jointures dans mon cas de production réel) où l'estimation de cardinalité est trop faible. Cette requête s'exécute dans 5.5 secondset génère 461MBdes données du journal des transactions.

CREATE TABLE dbo.minimalLoggingTest (n INT NOT NULL)
GO
INSERT INTO dbo.minimalLoggingTest WITH (TABLOCK) (n)
SELECT n
-- Any table/view/sub-query that produces 5MM rows but SQL estimates just 1000 rows
FROM dbo.fiveMillionNumbersBadEstimate
-- Provides greater consistency on my laptop, where other processes are running
OPTION (MAXDOP 1)
GO


Script complet

Voir ce Pastebin pour un ensemble complet de scripts pour générer les données de test et exécuter l'un de ces scénarios. Notez que vous devez utiliser une base de données qui se trouve dans le SIMPLE modèle de récupération .


Contexte d'affaires

Nous nous déplaçons semi-fréquemment sur des millions de lignes de données, et il est important que ces opérations soient aussi efficaces que possible, à la fois en termes de temps d'exécution et de charge d'E / S disque. Au départ, nous avions l'impression que la création d'une table de tas et l'utilisation INSERT...WITH (TABLOCK)étaient un bon moyen de le faire, mais nous sommes maintenant devenus moins confiants étant donné que nous avons observé la situation démontrée ci-dessus dans un scénario de production réel (bien qu'avec des requêtes plus complexes, pas la version simplifiée ici).

Geoff Patterson
la source

Réponses:

7

Pourquoi la deuxième requête ne remplit-elle pas les conditions requises pour une journalisation minimale?

Une journalisation minimale est disponible pour la deuxième requête, mais le moteur choisit de ne pas l'utiliser lors de l'exécution.

Il existe un seuil minimum pour INSERT...SELECTlequel il choisit de ne pas utiliser les optimisations de chargement en masse. La configuration d'une opération d'ensemble de lignes en bloc entraîne des coûts et l'insertion en bloc de seulement quelques lignes n'entraînerait pas une utilisation efficace de l'espace.

Que peut-on faire pour améliorer la situation?

Utilisez l'une des nombreuses autres méthodes (par exemple SELECT INTO) qui n'ont pas ce seuil. Vous pouvez également réécrire la requête source d'une manière ou d'une autre pour augmenter le nombre estimé de lignes / pages au-delà du seuil de INSERT...SELECT.

Voir aussi l'auto-réponse de Geoff pour plus d'informations utiles.


Anecdote potentiellement intéressante: SET STATISTICS IO signale les lectures logiques pour la table cible uniquement lorsque les optimisations de chargement en masse ne sont pas utilisées .

Paul White 9
la source
5

J'ai pu recréer le problème avec mon propre banc d'essai:

USE test;

CREATE TABLE dbo.SourceGood
(
    SourceGoodID INT NOT NULL
        CONSTRAINT PK_SourceGood
        PRIMARY KEY CLUSTERED
        IDENTITY(1,1)
    , SomeData VARCHAR(384) NOT NULL
);

CREATE TABLE dbo.SourceBad
(
    SourceBadID INT NOT NULL
        CONSTRAINT PK_SourceBad
        PRIMARY KEY CLUSTERED
        IDENTITY(-2147483647,1)
    , SomeData VARCHAR(384) NOT NULL
);

CREATE TABLE dbo.InsertTest
(
    SourceBadID INT NOT NULL
        CONSTRAINT PK_InsertTest
        PRIMARY KEY CLUSTERED
    , SomeData VARCHAR(384) NOT NULL
);
GO

INSERT INTO dbo.SourceGood WITH (TABLOCK) (SomeData) 
SELECT TOP(5000000) o.name + o1.name + o2.name
FROM syscolumns o
    , syscolumns o1
    , syscolumns o2;
GO

ALTER DATABASE test SET AUTO_UPDATE_STATISTICS OFF;
GO

INSERT INTO dbo.SourceBad WITH (TABLOCK) (SomeData)
SELECT TOP(5000000) o.name + o1.name + o2.name
FROM syscolumns o
    , syscolumns o1
    , syscolumns o2;
GO

ALTER DATABASE test SET AUTO_UPDATE_STATISTICS ON;
GO

BEGIN TRANSACTION;

INSERT INTO dbo.InsertTest WITH (TABLOCK)
SELECT *
FROM dbo.SourceGood;

SELECT * FROM sys.dm_tran_database_transactions;

/*
database_transaction_log_record_count
472 
database_transaction_log_bytes_used
692136
*/

COMMIT TRANSACTION;


BEGIN TRANSACTION;

INSERT INTO dbo.InsertTest WITH (TABLOCK)
SELECT *
FROM dbo.SourceBad;

SELECT * FROM sys.dm_tran_database_transactions;

/*
database_transaction_log_record_count   
5000003 
database_transaction_log_bytes_used
642699256
*/

COMMIT TRANSACTION;

Cela soulève la question, pourquoi ne pas "résoudre" le problème en mettant à jour les statistiques sur les tables source avant d'exécuter l'opération à journalisation minimale?

TRUNCATE TABLE dbo.InsertTest;
UPDATE STATISTICS dbo.SourceBad;

BEGIN TRANSACTION;

INSERT INTO dbo.InsertTest WITH (TABLOCK)
SELECT *
FROM dbo.SourceBad;

SELECT * FROM sys.dm_tran_database_transactions;

/*
database_transaction_log_record_count
472
database_transaction_log_bytes_used
692136
*/

COMMIT TRANSACTION;
Max Vernon
la source
2
Dans le code réel, il existe une SELECTinstruction complexe avec de nombreuses jointures qui génère le jeu de résultats pour le INSERT. Ces jointures produisent de mauvaises estimations de cardinalité pour l'opérateur d'insertion de table final (que j'ai simulé dans le script de repro via le mauvais UPDATE STATISTICSappel), et ce n'est donc pas aussi simple que d'émettre une UPDATE STATISTICScommande pour résoudre le problème. Je suis tout à fait d'accord pour dire que la simplification de la requête afin qu'il soit plus facile à comprendre pour l'estimateur de cardinalité pourrait être une bonne approche, mais ce n'est pas une solution implicite à mettre en œuvre compte tenu d'une logique métier complexe.
Geoff Patterson
Je n'ai pas d'instance SQL Server 2014 pour tester cela, cependant L' identification des problèmes de nouvel estimateur de cardinalité SQL Server 2014 et l'amélioration du Service Pack 1 parlent d'activer l'indicateur de trace 4199, entre autres, pour activer le nouvel estimateur de cardinalité. As-tu essayé ça?
Max Vernon
Bonne idée, mais cela n'a pas aidé. Je viens d'essayer TF 4199, TF 610 (assouplit les conditions de journalisation minimales), et les deux ensemble (hé, pourquoi pas?), Mais aucun changement pour la deuxième requête de test.
Geoff Patterson
4

Réécrivez la requête source d'une manière ou d'une autre pour augmenter le nombre estimé de lignes

En développant l'idée de Paul, une solution de contournement si vous êtes vraiment désespéré consiste à ajouter une table factice qui garantit que le nombre estimé de lignes pour l'insert sera suffisamment élevé pour être de qualité pour les optimisations de chargement en masse. J'ai confirmé que cela obtient une journalisation minimale et améliore les performances des requêtes.

-- Create a dummy table that SQL Server thinks has a million rows
CREATE TABLE dbo.emptyTableWithMillionRowEstimate (
    n INT PRIMARY KEY
)
GO
UPDATE STATISTICS dbo.emptyTableWithMillionRowEstimate
WITH ROWCOUNT = 1000000
GO

-- Concatenate this table into the final rowset:
INSERT INTO dbo.minimalLoggingTest WITH (TABLOCK) (n)
SELECT n
-- Any table/view/sub-query that correctly estimates that it will generate 5MM rows
FROM dbo.fiveMillionNumbersBadEstimate
-- Add in dummy rowset to ensure row estimate is high enough for bulk load optimization
UNION ALL
SELECT NULL FROM dbo.emptyTableWithMillionRowEstimate
OPTION (MAXDOP 1)

Derniers points à retenir

  1. À utiliser SELECT...INTOpour les opérations d'insertion uniques si une journalisation minimale est requise. Comme le souligne Paul, cela garantira une journalisation minimale quelle que soit l'estimation de la ligne
  2. Dans la mesure du possible, écrivez des requêtes d'une manière simple que l'optimiseur de requêtes peut raisonner efficacement. Il peut être possible de diviser une requête en plusieurs parties, par exemple, afin de permettre la construction de statistiques sur une table intermédiaire.
  3. Si vous avez accès à SQL Server 2014, essayez-le sur votre requête; dans mon cas de production réel, je viens de l'essayer et le nouvel estimateur de cardinalité a donné une estimation beaucoup plus élevée (et meilleure); la requête a ensuite été journalisée de façon minimale. Mais cela peut ne pas être utile si vous devez prendre en charge SQL 2012 et versions antérieures.
  4. Si vous êtes désespéré, des solutions hacky comme celle-ci peuvent s'appliquer!

Un article connexe

Le billet de blog de Paul White de mai 2019 Minimal Logging with INSERT… SELECT into Heap Tables couvre certaines de ces informations plus en détail.

Geoff Patterson
la source