/* =====================================================================
   清空数据库内所有用户表数据 —— 仅保留表结构
   ---------------------------------------------------------------------
   环境 : Microsoft SQL Server 2008 及以上(脚本只用 2005/2008 兼容语法,
          未使用 IIF / CONCAT / THROW / STRING_AGG / OFFSET-FETCH 等新语法)
   生成 : 2026-10-27
   风险 : ★★★★★ 不可逆。执行后数据无法通过任何手段恢复。
   ---------------------------------------------------------------------
   【执行前置条件 —— 逐条确认,缺一条都不要跑】
   1. 已做完整备份(FULL + 可选 LOG),并确认备份文件可还原。
   2. 该库没有事务复制 / 合并复制 / 快照复制在运行(有则先停分发代理)。
   3. 该库没有开启 CDC 变更数据捕获(否则 DELETE 会往 cdc.* 表写日志)。
   4. 没有长事务或其他会话占用该库(可在维护窗口执行)。
   5. 连接账号需要有 db_owner 或等同权限(ALTER TABLE / DBCC 需要)。
   ---------------------------------------------------------------------
   【脚本做了什么】
   - 遍历当前库全部用户表(排除 is_ms_shipped 系统表、dtproperties、sysdiagrams)
   - 记录并禁用:所有外键约束、CHECK 约束、表上的 DML 触发器
   - 逐表清空:
       默认              -> DELETE FROM(最安全,兼容索引视图/FILESTREAM)
       @UseTruncate = 1  -> 未被任何外键引用的表优先 TRUNCATE(更快且自动重置种子),
                            失败则自动回退 DELETE
   - 逐表独立事务执行,单表失败不中断整体,最后汇总失败清单
   - 重置 IDENTITY 种子为 1(仅对用 DELETE 清空的表;TRUNCATE 自带重置)
   - 恢复触发器、恢复约束(统一 WITH CHECK 重新验证;表已空,验证零成本)
   - 不删除表、索引、约束定义、视图、存储过程、函数、用户
   - 不收缩数据库文件(需要的话执行后单独 DBCC SHRINKFILE)
   ---------------------------------------------------------------------
   【万一脚本中途被中断】
   约束/触发器会停留在禁用状态,手工补恢复即可:
       -- 生成恢复语句
       SELECT N'ALTER TABLE ' + QUOTENAME(SCHEMA_NAME(t.schema_id)) + N'.'
              + QUOTENAME(t.name) + N' WITH CHECK CHECK CONSTRAINT ALL;'
       FROM sys.tables t WHERE t.is_ms_shipped = 0
       -- 触发器同理:ENABLE TRIGGER 名称 ON 表名
   直接重跑本脚本也可以(重复执行是幂等的,已空的表再删一次无害)。
   ---------------------------------------------------------------------
   【编码提醒】
   本文件是 UTF-8。用 sqlcmd 执行必须加 -f 65001,否则中文 Windows
   默认按 GBK 解码会产生字节漂移、破坏语句:
       sqlcmd -S . -d DB_SH01 -E -i "清空所有表数据_保留表结构.sql" -f 65001
   SSMS 直接打开执行则无需处理。
   ===================================================================== */

SET NOCOUNT ON;
SET ANSI_NULLS ON;
SET QUOTED_IDENTIFIER ON;
SET XACT_ABORT OFF;   -- 逐表独立事务,单表报错不影响后续表
GO

USE [DB_SH01];        -- ★★★ 改成你要清空的库名 ★★★
GO

PRINT '============================================================';
PRINT ' 目标数据库:' + DB_NAME();
PRINT ' 服务器    :' + @@SERVERNAME;
PRINT ' 执行时间  :' + CONVERT(varchar(19), GETDATE(), 120);
PRINT '============================================================';
GO

/* =====================================================================
   第 1 步(只读):预览将被清空的表清单与行数
   —— 建议先单独执行这一段,核对清单无误再继续往下
   ===================================================================== */
SELECT  s.name                              AS 架构,
        t.name                              AS 表名,
        SUM(ISNULL(p.rows, 0))              AS 约行数,
        (SELECT COUNT(*) FROM sys.foreign_keys fk
          WHERE fk.referenced_object_id = t.object_id)  AS 被几个外键引用,
        (SELECT COUNT(*) FROM sys.foreign_keys fk
          WHERE fk.parent_object_id = t.object_id)      AS 自身外键数,
        CASE t.is_replicated WHEN 1 THEN '是(注意复制)' ELSE '' END AS 是否复制表
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
LEFT JOIN sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0, 1)
WHERE t.is_ms_shipped = 0
  AND t.name NOT IN (N'dtproperties', N'sysdiagrams')
GROUP BY s.name, t.name, t.object_id, t.is_replicated
ORDER BY s.name, t.name;

SELECT  COUNT(*) AS 待清空表总数,
        SUM(ISNULL(p.rows, 0)) AS 待清空数据总量_约
FROM sys.tables t
LEFT JOIN sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0, 1)
WHERE t.is_ms_shipped = 0
  AND t.name NOT IN (N'dtproperties', N'sysdiagrams');
GO


/* =====================================================================
   第 2 步(写操作):清空全部表数据
   ★ @Confirm 必须手动改成 1,否则脚本直接中止,防止手滑
   ===================================================================== */
SET NOCOUNT ON;

DECLARE @Confirm     BIT = 0;   -- ★★★ 改成 1 才会真正执行 ★★★
DECLARE @UseTruncate BIT = 0;   -- 1 = 无外键引用的表用 TRUNCATE 提速;0 = 全部 DELETE

IF @Confirm = 0
BEGIN
    PRINT '>>> 已中止:@Confirm 仍未置为 1。本脚本不会删除任何数据。';
    RETURN;
END

/* ---------- 2.1 生成待处理表清单 ---------- */
IF OBJECT_ID('tempdb..#tabs') IS NOT NULL DROP TABLE #tabs;
CREATE TABLE #tabs
(
    id          INT IDENTITY(1, 1) PRIMARY KEY,
    schema_name SYSNAME       NOT NULL,
    table_name  SYSNAME       NOT NULL,
    quoted      NVARCHAR(300) NOT NULL,   -- 形如 [dbo].[Table]
    fk_ref      INT           NOT NULL,   -- 被多少外键引用
    row_count   BIGINT        NULL,
    action      VARCHAR(10)   NOT NULL,
    status      NVARCHAR(1000) NULL
);

INSERT INTO #tabs (schema_name, table_name, quoted, fk_ref, row_count, action)
SELECT  s.name,
        t.name,
        QUOTENAME(s.name) + N'.' + QUOTENAME(t.name),
        (SELECT COUNT(*) FROM sys.foreign_keys fk
          WHERE fk.referenced_object_id = t.object_id),
        ISNULL((SELECT SUM(p.rows) FROM sys.partitions p
                 WHERE p.object_id = t.object_id AND p.index_id IN (0, 1)), 0),
        CASE WHEN @UseTruncate = 1
                  AND NOT EXISTS (SELECT 1 FROM sys.foreign_keys fk
                                   WHERE fk.referenced_object_id = t.object_id)
                  AND t.is_replicated = 0
             THEN 'TRUNCATE' ELSE 'DELETE'
        END
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
WHERE t.is_ms_shipped = 0
  AND t.name NOT IN (N'dtproperties', N'sysdiagrams');

DECLARE @total INT, @i INT, @q NVARCHAR(300), @act VARCHAR(10), @stmt NVARCHAR(MAX);
SELECT @total = COUNT(*) FROM #tabs;
IF @total = 0
BEGIN
    PRINT '>>> 未发现可清空的用户表,脚本结束。';
    RETURN;
END
PRINT '>>> 共发现 ' + CONVERT(varchar(10), @total) + ' 张用户表,开始处理……';

/* ---------- 2.2 记录并禁用所有 DML 触发器(防止清空动作触发业务审计逻辑) ---------- */
IF OBJECT_ID('tempdb..#trg') IS NOT NULL DROP TABLE #trg;
CREATE TABLE #trg
(
    id           INT IDENTITY(1, 1) PRIMARY KEY,
    quoted       NVARCHAR(300) NOT NULL,
    trg_name     SYSNAME       NOT NULL,
    was_disabled BIT           NOT NULL
);

INSERT INTO #trg (quoted, trg_name, was_disabled)
SELECT  QUOTENAME(SCHEMA_NAME(t.schema_id)) + N'.' + QUOTENAME(t.name),
        tr.name,
        tr.is_disabled
FROM sys.triggers tr
JOIN sys.tables  t ON t.object_id = tr.parent_id
WHERE t.is_ms_shipped = 0
  AND t.name NOT IN (N'dtproperties', N'sysdiagrams')
  AND tr.parent_class = 1;          -- 1 = 表上的 DML 触发器

SET @stmt = N'';
SELECT @stmt = @stmt + N'DISABLE TRIGGER ' + QUOTENAME(trg_name)
             + N' ON ' + quoted + N';' + CHAR(10)
FROM #trg
WHERE was_disabled = 0;
IF LEN(@stmt) > 0 EXEC sp_executesql @stmt;

DECLARE @trgCnt INT;
SELECT @trgCnt = COUNT(*) FROM #trg WHERE was_disabled = 0;
PRINT '>>> 已临时禁用 DML 触发器 ' + CONVERT(varchar(10), @trgCnt) + ' 个(原本已禁用的保持原状)。';

/* ---------- 2.3 禁用所有外键与 CHECK 约束(允许任意顺序清空) ---------- */
SET @stmt = N'';
SELECT @stmt = @stmt + N'ALTER TABLE ' + quoted + N' NOCHECK CONSTRAINT ALL;' + CHAR(10)
FROM #tabs;
EXEC sp_executesql @stmt;
PRINT '>>> 已禁用全部外键与 CHECK 约束。';

/* ---------- 2.4 逐表清空(逐表独立事务,失败不中断) ---------- */
SET @i = 1;
WHILE @i <= @total
BEGIN
    SELECT @q = quoted, @act = action FROM #tabs WHERE id = @i;

    IF @act = 'TRUNCATE'
    BEGIN
        BEGIN TRY
            SET @stmt = N'TRUNCATE TABLE ' + @q + N';';
            EXEC sp_executesql @stmt;
            UPDATE #tabs SET status = N'OK(TRUNCATE)' WHERE id = @i;
        END TRY
        BEGIN CATCH
            -- TRUNCATE 受限(如被索引视图引用)时回退为 DELETE
            BEGIN TRY
                SET @stmt = N'DELETE FROM ' + @q + N';';
                EXEC sp_executesql @stmt;
                UPDATE #tabs SET status = N'OK(DELETE 回退)' WHERE id = @i;
            END TRY
            BEGIN CATCH
                UPDATE #tabs SET status = N'FAIL: ' + ERROR_MESSAGE() WHERE id = @i;
            END CATCH
        END CATCH
    END
    ELSE
    BEGIN
        BEGIN TRY
            SET @stmt = N'DELETE FROM ' + @q + N';';
            EXEC sp_executesql @stmt;
            UPDATE #tabs SET status = N'OK(DELETE)' WHERE id = @i;
        END TRY
        BEGIN CATCH
            UPDATE #tabs SET status = N'FAIL: ' + ERROR_MESSAGE() WHERE id = @i;
        END CATCH
    END

    SET @i = @i + 1;
END
PRINT '>>> 数据清空阶段完成。';

/* ---------- 2.5 重置 IDENTITY 种子(仅 DELETE 过的表;TRUNCATE 已自动重置) ---------- */
SET @stmt = N'';
SELECT @stmt = @stmt
             + N'DBCC CHECKIDENT(''' + REPLACE(t.quoted, '''', '''''') + N''', RESEED, 0) WITH NO_INFOMSGS;'
             + CHAR(10)
FROM #tabs t
WHERE t.status LIKE N'OK(DELETE%'
  AND EXISTS (SELECT 1 FROM sys.identity_columns ic
               WHERE ic.object_id = OBJECT_ID(t.quoted));
IF LEN(@stmt) > 0 EXEC sp_executesql @stmt;
PRINT '>>> IDENTITY 种子已重置。';

/* ---------- 2.6 恢复约束:WITH CHECK 重新验证(表已空,验证无成本) ---------- */
SET @stmt = N'';
SELECT @stmt = @stmt + N'ALTER TABLE ' + quoted + N' WITH CHECK CHECK CONSTRAINT ALL;' + CHAR(10)
FROM #tabs;
EXEC sp_executesql @stmt;
PRINT '>>> 外键与 CHECK 约束已恢复并置为已信任状态。';

/* ---------- 2.7 恢复触发器(仅恢复原本处于启用状态的) ---------- */
SET @stmt = N'';
SELECT @stmt = @stmt + N'ENABLE TRIGGER ' + QUOTENAME(trg_name)
             + N' ON ' + quoted + N';' + CHAR(10)
FROM #trg
WHERE was_disabled = 0;
IF LEN(@stmt) > 0 EXEC sp_executesql @stmt;
PRINT '>>> DML 触发器已恢复。';

/* ---------- 2.8 汇总报告 ---------- */
DECLARE @okT INT, @okD INT, @fail INT;
SELECT  @okT  = SUM(CASE WHEN status LIKE N'OK(TRUNCATE%' THEN 1 ELSE 0 END),
        @okD  = SUM(CASE WHEN status LIKE N'OK(DELETE%'   THEN 1 ELSE 0 END),
        @fail = SUM(CASE WHEN status LIKE N'FAIL%'        THEN 1 ELSE 0 END)
FROM #tabs;

PRINT '';
PRINT '============================================================';
PRINT ' 清空结果汇总';
PRINT '============================================================';
PRINT ' 处理表总数      :' + CONVERT(varchar(10), @total);
PRINT ' 成功(TRUNCATE)  :' + CONVERT(varchar(10), ISNULL(@okT, 0));
PRINT ' 成功(DELETE)    :' + CONVERT(varchar(10), ISNULL(@okD, 0));
PRINT ' 失败            :' + CONVERT(varchar(10), ISNULL(@fail, 0));
PRINT '============================================================';

-- 失败清单(正常应为 0 行)
SELECT  id                AS 序号,
        quoted            AS 表,
        action            AS 采用方式,
        status            AS 结果
FROM #tabs
WHERE status LIKE N'FAIL%'
ORDER BY id;

-- 完整明细(按行数倒序,便于核对)
SELECT  quoted     AS 表,
        row_count  AS 清空前行数,
        action     AS 采用方式,
        status     AS 结果
FROM #tabs
ORDER BY row_count DESC, quoted;
GO


/* =====================================================================
   第 3 步(只读):收尾自检
   —— 正常应返回 0 行(即已无任何表的剩余行数 > 0)
   ===================================================================== */
-- 3.1 残留数据的表
SELECT  s.name                      AS 架构,
        t.name                      AS 表名,
        SUM(ISNULL(p.rows, 0))      AS 剩余行数
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
LEFT JOIN sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0, 1)
WHERE t.is_ms_shipped = 0
  AND t.name NOT IN (N'dtproperties', N'sysdiagrams')
GROUP BY s.name, t.name
HAVING SUM(ISNULL(p.rows, 0)) > 0
ORDER BY 剩余行数 DESC;

-- 3.2 禁用状态残留检查(正常应返回 0 行)
SELECT  '外键' AS 类型, OBJECT_NAME(fk.parent_object_id) AS 表名, fk.name AS 名称
FROM sys.foreign_keys fk
WHERE fk.is_disabled = 1
UNION ALL
SELECT  'CHECK 约束', OBJECT_NAME(cc.parent_object_id), cc.name
FROM sys.check_constraints cc
WHERE cc.is_disabled = 1
UNION ALL
SELECT  '触发器', OBJECT_NAME(tr.parent_id), tr.name
FROM sys.triggers tr
WHERE tr.is_disabled = 1
  AND tr.parent_class = 1;

-- 3.3 表结构与索引完好性确认
SELECT  COUNT(*) AS 用户表数量
FROM sys.tables
WHERE is_ms_shipped = 0 AND name NOT IN (N'dtproperties', N'sysdiagrams');

SELECT  COUNT(*) AS 索引总数
FROM sys.indexes i
JOIN sys.tables  t ON t.object_id = i.object_id
WHERE t.is_ms_shipped = 0 AND i.index_id > 0;

SELECT  COUNT(*) AS 列总数
FROM sys.columns c
JOIN sys.tables  t ON t.object_id = c.object_id
WHERE t.is_ms_shipped = 0;
GO

PRINT '>>> 全部步骤执行完毕。若 3.1 返回 0 行、3.2 返回 0 行,说明数据已清空且约束/触发器状态正常。';
GO


/* =====================================================================
   附:常见后续操作(按需手工执行,本脚本未包含)
   ---------------------------------------------------------------------
   -- 收缩数据库文件(清空大量数据后释放磁盘;会锁表,务必在维护窗口做)
   -- DBCC SHRINKDATABASE (N'DB_SH01', 10);

   -- 单个文件收缩
   -- DBCC SHRINKFILE (N'DB_SH01_Data', 1024);
   ===================================================================== */

/* =====================================================================
   附:关于性能 —— 什么时候 @UseTruncate 也不够快
   ---------------------------------------------------------------------
   DELETE 的代价与行数、以及表上有多少非聚集索引成正比,而且会完整写事务日志。
   经验值:百万行级以上的表,DELETE 可能要几分钟到几十分钟,日志文件会明显膨胀。

   如果你的库存在这种量级的表,最快的路径不是 TRUNCATE,而是:
       DROP 所有外键 -> TRUNCATE 全部表 -> 按原定义重建外键
   TRUNCATE 是页级释放,几乎瞬时且日志量极小。代价是外键必须先物理删除,
   重建外键的语句需要从 sys.foreign_keys / sys.foreign_key_columns 生成。

   判断是否需要走这条路,先跑这一句看最大的表:
       SELECT TOP 20 OBJECT_NAME(object_id) AS 表, SUM(rows) AS 行数
       FROM sys.partitions WHERE index_id IN (0,1)
       GROUP BY object_id ORDER BY SUM(rows) DESC;
   ===================================================================== */

最后修改:2026 年 10 月 28 日
如果觉得我的文章对你有用,请随意赞赏