插入时磁盘空间已满,会发生什么?

Hon*_*ger 18 sql-server insert rollback sql-server-2016 select-into

今天我发现存储我的数据库的硬盘已满。这种情况以前发生过,通常原因很明显。通常有一个错误的查询,这会导致对 tempdb 的大量溢出,它会一直增长到磁盘已满。这次发生的事情不太明显,因为 tempdb 不是驱动器满的原因,而是数据库本身。

事实:

  • 通常的数据库大小约为 55 GB,它增长到 605 GB。
  • 日志文件大小正常,数据文件很大。
  • 数据文件有 85% 的可用空间(我将其解释为“空气”:已使用但已释放的空间。一旦分配,SQL Server 将保留所有空间)。
  • Tempdb 大小正常。

我找到了可能的原因;有一个查询选择了太多的行(错误连接会导致选择 110 亿行,而预计会有几十万行)。这是一个SELECT INTO查询,这让我怀疑是否可能发生以下情况:

  • SELECT INTO 被执行
  • 目标表已创建
  • 数据在选择时插入
  • 磁盘已满,导致插入失败
  • SELECT INTO 被中止并回滚
  • 回滚释放空间(删除已插入的数据),但 SQL Server 不会释放释放的空间。

但是,在这种情况下,我不希望 由 创建的表SELECT INTO仍然存在,它应该被回滚删除。我测试了这个:

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

ROLLBACK

SELECT  * 
FROM    TMP.test
Run Code Online (Sandbox Code Playgroud)

这导致:

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

然而目标表确实存在。不过,实际查询并未在显式事务中执行,这能解释目标表的存在吗?

我在这里勾画的假设是否正确?这是可能发生的情况吗?

sep*_*pic 17

不过,实际查询并未在显式事务中执行,这能解释目标表的存在吗?

是的,正是如此。

如果你select into在 an 之外做一个简单的explicit transactiontransactions在自动提交模式下有两个:第一个创建table,第二个填充它。

您可以通过以下方式向自己证明:

在一个专用database的测试服务器中simple recovery model,首先制作一个checkpoint并确保日志只包含与checkpoint. 然后运行select into一行中的 a 并log再次检查,寻找与以下begin tran相关的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';
Run Code Online (Sandbox Code Playgroud)

你会得到 2 行,显示你有 2 transactions

我在这里勾画的假设是否正确?这是可能发生的情况吗?

是的,他们是正确的。

was的insert部分,但它不释放任何数据空间。您可以通过执行来验证这一点;你会看到很多。select intorolled backsp_spaceusedunallocated space

如果您希望数据库释放这个未分配的空间,您应该使用shrink您的数据文件。


Jos*_*ell 16

你是对的,SELECT...INTO命令不是原子的。这在原始帖子发布时没有记录,但现在在 MS Docs上的SELECT - INTO Clause (Transact-SQL)页面上特别指出(是开源的!):

SELECT...INTO语句分为两部分 - 创建新表,然后插入行。这意味着如果插入失败,它们将全部回滚,但新的(空)表将保留。如果您需要整个操作作为一个整体成功或失败,请使用显式事务

我将创建一个使用完整恢复模型的数据库。我会给它一个相当小的日志文件,然后告诉它日志文件不能自动增长:

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
)
Run Code Online (Sandbox Code Playgroud)

然后我将尝试插入我的 StackOverflow2010 数据库副本中的所有帖子。这应该将一堆东西写入日志文件。

USE [SelectIntoTestDB];
GO

SELECT *
INTO dbo.Posts
FROM StackOverflow2010.dbo.Posts;
Run Code Online (Sandbox Code Playgroud)

这导致运行4秒后出现以下错误:

消息 9002,级别 17,状态 4,第 1 行
由于“ACTIVE_TRANSACTION”,数据库“SelectIntoTestDB”的事务日志已满。

但是我的新数据库中有一个空的 Posts 表:

新创建的表中零结果的屏幕截图

因此,正如您所怀疑的那样,CREATE TABLE成功了,但该INSERT部分已全部回滚。一种解决方法是使用显式事务(您已在问题中指出)。