Was passiert, wenn der Speicherplatz während des Einfügens voll ist?

17

Heute habe ich festgestellt, dass die Festplatte, auf der meine Datenbanken gespeichert sind, voll ist. Das ist schon mal passiert, normalerweise ist die Ursache ziemlich offensichtlich. Normalerweise gibt es eine fehlerhafte Abfrage, die zu großen Verschüttungen von Tempdb führt, die zunehmen, bis die Festplatte voll ist. Diesmal war es weniger offensichtlich, was passierte, da tempdb nicht die Ursache für das volle Laufwerk war, sondern die Datenbank selbst.

Die Fakten:

  • Die übliche Datenbankgröße beträgt ca. 55 GB, sie ist auf 605 GB angewachsen.
  • Protokolldatei hat normale Größe, Datendatei ist riesig.
  • Datendatei verfügt über 85% verfügbaren Speicherplatz (ich interpretiere dies als "Luft": Speicherplatz, der verwendet wurde, aber freigegeben wurde. SQL Server reserviert den gesamten Speicherplatz, sobald er zugewiesen wurde).
  • Die Tempdb-Größe ist normal.

Ich habe die wahrscheinliche Ursache gefunden. Es gibt eine Abfrage, die viel zu viele Zeilen auswählt (eine fehlerhafte Verknüpfung führt zur Auswahl von 11 Milliarden Zeilen, von denen einige Hunderttausend erwartet werden). Dies ist eine SELECT INTOAbfrage, die mich gefragt hat, ob das folgende Szenario hätte passieren können:

  • SELECT INTO wird ausgeführt
  • Zieltabelle wird erstellt
  • Daten werden beim Auswählen eingefügt
  • Die Festplatte füllt sich und der Einsatz schlägt fehl
  • SELECT INTO wird abgebrochen und zurückgesetzt
  • Beim Rollback wird Speicherplatz freigegeben (bereits eingefügte Daten werden entfernt), SQL Server gibt den freigegebenen Speicherplatz jedoch nicht frei.

In dieser Situation hätte ich jedoch nicht erwartet, dass die von der erstellte Tabelle SELECT INTOnoch vorhanden ist. Sie sollte durch das Rollback gelöscht werden. Ich habe das getestet:

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

ROLLBACK

SELECT  * 
FROM    TMP.test

Das führt zu:

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

Die Zieltabelle existiert jedoch. Die eigentliche Abfrage wurde jedoch nicht in einer expliziten Transaktion ausgeführt. Kann das die Existenz der Zieltabelle erklären?

Sind die hier skizzierten Annahmen korrekt? Ist das ein wahrscheinliches Szenario?

Honig Dachs
quelle

Antworten:

17

Die eigentliche Abfrage wurde jedoch nicht in einer expliziten Transaktion ausgeführt. Kann das die Existenz der Zieltabelle erklären?

Ja genau so

Wenn Sie ein einfaches select intoOutside von einem ausführen explicit transaction, gibt es zwei transactionsim Autocommit-Modus: Der erste erstellt das tableund der zweite füllt es auf.

Sie können es sich auf diese Weise beweisen:

In einem dedizierten databaseauf einem Testserver in simple recovery modelmachen Sie zuerst ein checkpointund stellen Sie sicher, dass das Protokoll nur wenige Zeilen enthält (3 im Fall von 2016), die sich auf beziehen checkpoint. Führen Sie dann eine select intoZeile aus, und überprüfen Sie die logerneut, und suchen Sie nach einer begin tranVerknüpfung mit 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';

Sie erhalten 2 Zeilen, was zeigt, dass Sie 2 hatten transactions.

Sind die hier skizzierten Annahmen korrekt? Ist das ein wahrscheinliches Szenario?

Ja, sie sind richtig.

Der insertTeil von select intowar rolled back, gibt aber keinen Datenraum frei. Sie können dies überprüfen, indem Sie Folgendes ausführen sp_spaceused: Sie werden viel von sehen unallocated space.

Wenn Sie möchten, dass die Datenbank diesen nicht zugewiesenen Speicherplatz freigibt, sollten Sie shrinkIhre Datendatei (en) verwenden.

Sepupic
quelle
15

Sie haben Recht, der SELECT...INTOBefehl ist nicht atomar. Dies war zum Zeitpunkt des ursprünglichen Beitrags noch nicht dokumentiert, wird aber jetzt speziell auf der Seite SELECT - INTO (Transact-SQL) unter MS Docs (yay open source!) Beschrieben:

Die SELECT...INTOAnweisung besteht aus zwei Teilen: Die neue Tabelle wird erstellt, und anschließend werden Zeilen eingefügt. Wenn die Einfügungen fehlschlagen, werden alle zurückgesetzt, die neue (leere) Tabelle bleibt jedoch erhalten. Verwenden Sie eine explizite Transaktion, wenn der gesamte Vorgang erfolgreich sein oder fehlschlagen soll .

Ich erstelle eine Datenbank, die das vollständige Wiederherstellungsmodell verwendet. Ich gebe ihm eine ziemlich kleine Protokolldatei und sage ihm dann, dass die Protokolldatei nicht automatisch wachsen kann:

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
)

Und dann werde ich versuchen, alle Posts aus meiner Kopie der StackOverflow2010-Datenbank einzufügen. Dies sollte eine Menge Dinge in die Protokolldatei schreiben .

USE [SelectIntoTestDB];
GO

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

Dies führte nach 4 Sekunden Laufzeit zu folgendem Fehler:

Meldung 9002, Ebene 17,
Status 4, Zeile 1 Das Transaktionsprotokoll für die Datenbank 'SelectIntoTestDB' ist aufgrund von 'ACTIVE_TRANSACTION' voll.

In meiner neuen Datenbank befindet sich jedoch eine leere Posts-Tabelle:

Screenshot der Nullergebnisse aus der neu erstellten Tabelle

Also, wie Sie vermutet haben, ist das CREATE TABLEgelungen, aber die INSERTPortion wurde allesamt zurückgerollt. Eine Problemumgehung wäre die Verwendung einer expliziten Transaktion (die Sie bereits in Ihrer Frage notiert haben).

Josh Darnell
quelle