Spazio su disco pieno durante l'inserimento, cosa succede?


17

Oggi ho scoperto che il disco rigido che memorizza i miei database era pieno. Questo è successo prima, di solito la causa è abbastanza evidente. Di solito c'è una cattiva query, che causa grandi perdite di tempdb che cresce fino a quando il disco è pieno. Questa volta è stato un po 'meno evidente quello che è successo, poiché tempdb non era la causa dell'intero disco, era il database stesso.

I fatti:

  • La dimensione normale del database è di circa 55 GB, è cresciuta fino a 605 GB.
  • Il file di registro ha dimensioni normali, il file di dati è enorme.
  • Il file di dati ha l'85% di spazio disponibile (lo interpreto come "aria": spazio che è stato utilizzato, ma che è stato liberato. SQL Server riserva tutto lo spazio una volta assegnato).
  • La dimensione del tempdb è normale.

Ho trovato la causa probabile; c'è una query che seleziona troppe righe (un join errato provoca la selezione di 11 miliardi di righe dove sono attese un paio di centinaia di migliaia). Questa è una SELECT INTOquery, che mi ha fatto chiedermi se il seguente scenario avrebbe potuto accadere:

  • SELECT INTO viene eseguito
  • La tabella di destinazione è stata creata
  • I dati vengono inseriti così come sono selezionati
  • Il disco si riempie, causando un errore dell'inserimento
  • SELECT INTO viene interrotto e ripristinato
  • Il rollback libera spazio (i dati già inseriti vengono rimossi), ma SQL Server non rilascia lo spazio liberato.

In questa situazione, tuttavia, non mi sarei aspettato che la tabella creata da SELECT INTOesistesse ancora, che dovrebbe essere eliminata dal rollback. Ho provato questo:

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

ROLLBACK

SELECT  * 
FROM    TMP.test

Questo risulta in:

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

Tuttavia esiste la tabella di destinazione. Tuttavia, la query effettiva non è stata eseguita in una transazione esplicita, può spiegare l'esistenza della tabella di destinazione?

I presupposti che ho delineato qui sono corretti? È probabile che si sia verificato uno scenario?

Risposte:


17

Tuttavia, la query effettiva non è stata eseguita in una transazione esplicita, può spiegare l'esistenza della tabella di destinazione?

Sì, proprio così.

Se fai un semplice select intofuori di un explicit transaction, ce ne sono due transactionsin modalità autocommit: il primo crea il tablee il secondo lo riempie.

Puoi dimostrarlo a te stesso in questo modo:

In un dedicato databasesu un server di prova in simple recovery model, innanzitutto creare un checkpointe assicurarsi che il registro contenga solo poche righe (3 nel caso del 2016) correlate checkpoint. Quindi esegui una select intodi una riga e controlla di lognuovo, cercando un begin tranassociato con 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';

Otterrai 2 righe, a dimostrazione che ne avevi 2 transactions.

I presupposti che ho delineato qui sono corretti? È probabile che si sia verificato uno scenario?

Sì, sono corretti.

La insertparte di select intoera rolled back, ma non rilascia alcuno spazio per i dati. Puoi verificarlo eseguendo sp_spaceused; vedrai un sacco di unallocated space.

Se vuoi che il database rilasci questo spazio non allocato, dovresti avere i shrinktuoi file di dati.


15

Hai ragione, il SELECT...INTOcomando non è atomico. Questo non è stato documentato al momento del post originale, ma ora viene richiamato in modo specifico nella pagina SELECT - INTO Clause (Transact-SQL) su MS Docs (yay open source!):

L' SELECT...INTOistruzione opera in due parti: viene creata la nuova tabella e quindi vengono inserite le righe. Ciò significa che se gli inserti falliscono, verranno tutti ripristinati, ma rimarrà la nuova tabella (vuota). Se è necessario che l'intera operazione abbia esito positivo o negativo nel suo insieme, utilizzare una transazione esplicita .

Creerò un database che utilizza il modello di recupero completo. Gli darò un file di registro abbastanza piccolo, e poi gli dirò che il file di registro non può crescere automaticamente:

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
)

E poi proverò a inserire tutti i post dalla mia copia del database StackOverflow2010. Questo dovrebbe scrivere un sacco di cose nel file di registro.

USE [SelectIntoTestDB];
GO

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

Ciò ha comportato il seguente errore dopo l'esecuzione per 4 secondi:

Messaggio 9002, livello 17, stato 4, riga 1
Il registro delle transazioni per il database "SelectIntoTestDB" è pieno a causa di "ACTIVE_TRANSACTION".

Ma c'è una tabella di messaggi vuota nel mio nuovo database:

screenshot di zero risultati dalla tabella appena creata

Quindi, come sospettavi, ci CREATE TABLEriuscì, ma la INSERTparte fu ritirata. Una soluzione alternativa sarebbe quella di utilizzare una transazione esplicita (che hai già notato nella tua domanda).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.