web-dev-qa-db-fra.com

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

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). C'est un SELECT INTO requête, ce 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 à la table créée par le SELECT INTO pour exister, il devrait être supprimé 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. La requête réelle n'a cependant 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?

18
HoneyBadger

La requête réelle n'a cependant 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 into en dehors d'un explicit transaction, il y a deux transactions en mode autocommit: le premier crée le table et le second le remplit.

Vous pouvez le prouver de cette façon:

Dans un database dédié sur un serveur de test dans simple recovery model, faites d'abord un checkpoint et assurez-vous que le journal ne contient que quelques lignes (3 dans le cas de 2016) liées à checkpoint. Exécutez ensuite un select into d'une ligne et vérifiez à nouveau le log, en recherchant un begin tran associé à 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 lignes, indiquant que vous 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 partie insert de select into était rolled back, mais il ne libère aucun espace de données. Vous pouvez le vérifier en exécutant sp_spaceused; vous verrez beaucoup de unallocated space.

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

17
sepupic

Vous avez raison, la commande SELECT...INTO 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'instruction SELECT...INTO 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 un 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 trucs 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:

screenshot of zero results from the newly created table

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

15
Josh Darnell