Espace disque plein pendant l'insertion, que se passe-t-il?

17

Aujourd'hui, j'ai découvert que le disque dur qui stocke mes bases de données était plein. Cela s'est déjà produit auparavant, généralement la cause est assez évidente. Habituellement, il y a une mauvaise requête, ce qui provoque d'énormes déversements sur tempdb qui augmente jusqu'à ce que le disque soit plein. Cette fois, ce qui s'est passé était un peu moins évident, car tempdb n'était pas la cause du lecteur complet, c'était la base de données elle-même.

Les faits:

  • La taille habituelle de la base de données est d'environ 55 Go, elle est passée à 605 Go.
  • Le fichier journal a une taille normale, le fichier de données est énorme.
  • Le fichier de données dispose de 85% d'espace disponible (j'interprète cela comme «aérien»: espace qui a été utilisé, mais qui a été libéré. ​​SQL Server réserve tout l'espace une fois alloué).
  • La taille de Tempdb est normale.

J'ai trouvé la cause probable; il y a une requête qui sélectionne beaucoup trop de lignes (une mauvaise jointure entraîne la sélection de 11 milliards de lignes où quelques centaines de milliers sont attendues). Il s'agit d'une SELECT INTOrequête qui m'a fait me demander si le scénario suivant aurait pu se produire:

  • SELECT INTO est exécuté
  • La table cible est créée
  • Les données sont insérées lorsqu'elles sont sélectionnées
  • Le disque se remplit, provoquant l'échec de l'insertion
  • SELECT INTO est abandonné et annulé
  • La restauration libère de l'espace (les données déjà insérées sont supprimées), mais SQL Server ne libère pas l'espace libéré.

Dans cette situation, cependant, je ne m'attendais pas à ce que la table créée par le SELECT INTOexiste toujours, elle devrait être supprimée par la restauration. J'ai testé ceci:

BEGIN TRANSACTION 
SELECT  T.x
INTO    TMP.test
FROM    (VALUES(1))T(x)

ROLLBACK

SELECT  * 
FROM    TMP.test

Il en résulte:

(1 row affected)
Msg 208, Level 16, State 1, Line 8
Invalid object name 'TMP.test'.

Pourtant, la table cible existe. Cependant, la requête réelle n'a pas été exécutée dans une transaction explicite, cela peut-il expliquer l'existence de la table cible?

Les hypothèses que j'ai esquissées ici sont-elles correctes? Est-ce un scénario probable qui s'est produit?

HoneyBadger
la source

Réponses:

17

Cependant, la requête réelle n'a pas été exécutée dans une transaction explicite, cela peut-il expliquer l'existence de la table cible?

Oui exactement.

Si vous faites un simple select intoextérieur à un explicit transaction, il y en a deux transactionsen mode de validation automatique: le premier crée le tableet le second le remplit.

Vous pouvez le prouver de cette façon:

Dans un databaseserveur dédié sur un serveur de test dans simple recovery model, faites d'abord un checkpointet assurez-vous que le journal ne contient que quelques lignes (3 en cas de 2016) liées à checkpoint. Exécutez ensuite select intoune ligne et vérifiez à lognouveau, en recherchant un begin tranassocié à select into:

checkpoint;

select *
from sys.fn_dblog(null, null);

select 'a' as col
into dbo.t3;  

select *
from sys.fn_dblog(null, null)
where Operation = 'LOP_BEGIN_XACT'
      and [Transaction Name] = 'SELECT INTO';

Vous obtiendrez 2 rangées, indiquant que vous en aviez 2 transactions.

Les hypothèses que j'ai esquissées ici sont-elles correctes? Est-ce un scénario probable qui s'est produit?

Oui, ils sont corrects.

La insertpartie de select intowas rolled back, mais elle ne libère aucun espace de données. Vous pouvez le vérifier en exécutant sp_spaceused; vous en verrez plein unallocated space.

Si vous souhaitez que la base de données libère cet espace non alloué, vous devez shrinkvos fichiers de données.

sepupic
la source
15

Vous avez raison, la SELECT...INTOcommande n'est pas atomique. Cela n'était pas documenté au moment de la publication d'origine, mais est maintenant spécifiquement mentionné sur la page SELECT - INTO Clause (Transact-SQL) sur MS Docs (yay open source!):

L' SELECT...INTOinstruction fonctionne en deux parties - la nouvelle table est créée, puis des lignes sont insérées. Cela signifie que si les insertions échouent, elles seront toutes annulées, mais la nouvelle table (vide) restera. Si vous avez besoin de l'opération entière pour réussir ou échouer dans son ensemble, utilisez une transaction explicite .

Je vais créer une base de données qui utilise le modèle de récupération complète. Je vais lui donner un fichier journal assez petit, puis lui dire que le fichier journal ne peut pas se développer automatiquement:

CREATE DATABASE [SelectIntoTestDB]
ON PRIMARY 
( 
    NAME = N'SelectIntoTestDB', 
    FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL14.SQL2017\MSSQL\DATA\SelectIntoTestDB.mdf', 
    SIZE = 8192KB, 
    FILEGROWTH = 65536KB
)
LOG ON 
( 
    NAME = N'SelectIntoTestDB_log', 
    FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL14.SQL2017\MSSQL\DATA\SelectIntoTestDB_log.ldf', 
    SIZE = 8192KB, 
    FILEGROWTH = 0
)

Et puis je vais essayer d'insérer tous les messages de ma copie de la base de données StackOverflow2010. Cela devrait écrire un tas de choses dans le fichier journal.

USE [SelectIntoTestDB];
GO

SELECT *
INTO dbo.Posts
FROM StackOverflow2010.dbo.Posts;

Cela a entraîné l'erreur suivante après avoir exécuté pendant 4 secondes:

Msg 9002, niveau 17, état 4, ligne 1
Le journal des transactions de la base de données «SelectIntoTestDB» est plein en raison de «ACTIVE_TRANSACTION».

Mais il y a une table Posts vide dans ma nouvelle base de données:

capture d'écran de zéro résultat de la table nouvellement créée

Donc, comme vous le soupçonniez, cela a CREATE TABLEréussi, mais la INSERTportion a été annulée. Une solution de contournement consisterait à utiliser une transaction explicite (que vous avez déjà notée dans votre question).

Josh Darnell
la source