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:
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:
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?
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.
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:
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).