MSSQL export data

Exports data rows from a SQL Server database as executable SQL statements.

This is useful for

  • Portable snapshots - move selected data between environments
  • Targeted migrations - prepare payloads for specific tables
  • Pre-execution review - inspect generated SQL before running it
  • Zero tooling install - runs with SQL scripts in restricted environments
  • Readable output - generated statements are easy to inspect and review
  • Repeatable process - same scripts can be rerun consistently across environments
  • Safer migrations - you can validate and version-control the export result
  • Works with existing data - supports delete/replace workflows when target rows already exist
  • Selective usage - supports targeted export scenarios instead of full backups

Adjust variables and execute

SET QUOTED_IDENTIFIER ON;
SET NOCOUNT ON;

DECLARE @table NVARCHAR(255) = 'dbo.YourTableName';
DECLARE @where NVARCHAR(MAX) = '1=1';
DECLARE @includeBinaryColumns BIT = 0;
DECLARE @includeDelete BIT = 1;
DECLARE @excludeColumns TABLE (ColumnName NVARCHAR(255));
-- INSERT INTO @excludeColumns (ColumnName) VALUES ('PasswordHash'), ('SecurityStamp');

DECLARE @ChunkSize INT = 50000;
DECLARE @MaxRowsPerValuesStatement INT = 500;
DECLARE @ErrorMessage NVARCHAR(4000);
DECLARE @RowCount INT = 0;

DECLARE @tableSchema NVARCHAR(255) = (CASE WHEN CHARINDEX('.', @table) > 0 THEN LEFT(@table, CHARINDEX('.', @table) - 1) ELSE 'dbo' END);
DECLARE @tableName NVARCHAR(255) = (CASE WHEN CHARINDEX('.', @table) > 0 THEN SUBSTRING(@table, CHARINDEX('.', @table) + 1, LEN(@table)) ELSE @table END);
DECLARE @qualifiedTable NVARCHAR(600) = QUOTENAME(@tableSchema) + '.' + QUOTENAME(@tableName);

BEGIN TRY
    -- Merge statement format should be:
    -- MERGE <target_table> AS target
    -- USING (VALUES (<row_expression>), (<row_expression>)) AS source
    -- ON (<pk_where_clause>)
    -- WHEN MATCHED THEN UPDATE SET <update_clause>
    -- WHEN NOT MATCHED THEN INSERT (<column_list>) VALUES (<insert_values_clause>);

    -- Get primary key columns
    DECLARE @PKColumns TABLE (
        ColumnName NVARCHAR(255),
        OrdinalPosition INT,
        DataType NVARCHAR(255)
    );

    INSERT INTO @PKColumns (ColumnName, OrdinalPosition, DataType)
    SELECT
        kcu.COLUMN_NAME,
        kcu.ORDINAL_POSITION,
        c.DATA_TYPE
    FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
    INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
        ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
        AND tc.TABLE_SCHEMA = kcu.TABLE_SCHEMA
        AND tc.TABLE_NAME = kcu.TABLE_NAME
    INNER JOIN INFORMATION_SCHEMA.COLUMNS c
        ON kcu.TABLE_SCHEMA = c.TABLE_SCHEMA
        AND kcu.TABLE_NAME = c.TABLE_NAME
        AND kcu.COLUMN_NAME = c.COLUMN_NAME
    WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
        AND tc.TABLE_SCHEMA = @tableSchema
        AND tc.TABLE_NAME = @tableName
    ORDER BY kcu.ORDINAL_POSITION;

    IF NOT EXISTS (SELECT 1 FROM @PKColumns)
    BEGIN
        SET @ErrorMessage = 'Table ' + @table + ' does not have a primary key.';
        RAISERROR(@ErrorMessage, 16, 1);
        RETURN;
    END

    -- Get all columns
    DECLARE @Columns TABLE (
        ColumnName NVARCHAR(255),
        DataType NVARCHAR(255),
        IsNullable BIT,
        OrdinalPosition INT,
        IsBinary BIT,
        ColumnDefault NVARCHAR(MAX),
        CharacterMaxLength INT,
        NumericPrecision INT,
        NumericScale INT
    );

    INSERT INTO @Columns (ColumnName, DataType, IsNullable, OrdinalPosition, IsBinary, ColumnDefault, CharacterMaxLength, NumericPrecision, NumericScale)
    SELECT
        c.COLUMN_NAME,
        c.DATA_TYPE,
        CASE WHEN c.IS_NULLABLE = 'YES' THEN 1 ELSE 0 END,
        c.ORDINAL_POSITION,
        CASE
            WHEN c.DATA_TYPE IN ('image', 'varbinary', 'binary') OR
                 (c.DATA_TYPE = 'varbinary' AND c.CHARACTER_MAXIMUM_LENGTH = -1) OR
                 (c.DATA_TYPE = 'binary' AND c.CHARACTER_MAXIMUM_LENGTH = -1)
            THEN 1
            ELSE 0
        END,
        c.COLUMN_DEFAULT,
        c.CHARACTER_MAXIMUM_LENGTH,
        c.NUMERIC_PRECISION,
        c.NUMERIC_SCALE
    FROM INFORMATION_SCHEMA.COLUMNS c
    WHERE c.TABLE_SCHEMA = @tableSchema
        AND c.TABLE_NAME = @tableName
    ORDER BY c.ORDINAL_POSITION;

    -- Filter columns based on binary handling
    DECLARE @SelectedColumns TABLE (
        ColumnName NVARCHAR(255),
        DataType NVARCHAR(255),
        IsNullable BIT,
        OrdinalPosition INT,
        ColumnDefault NVARCHAR(MAX),
        CharacterMaxLength INT,
        NumericPrecision INT,
        NumericScale INT
    );

    INSERT INTO @SelectedColumns (ColumnName, DataType, IsNullable, OrdinalPosition, ColumnDefault, CharacterMaxLength, NumericPrecision, NumericScale)
    SELECT c.ColumnName, c.DataType, c.IsNullable, c.OrdinalPosition, c.ColumnDefault, c.CharacterMaxLength, c.NumericPrecision, c.NumericScale
    FROM @Columns c
    WHERE (@includeBinaryColumns = 1 OR c.IsBinary = 0)
        AND NOT EXISTS (SELECT 1 FROM @excludeColumns e WHERE e.ColumnName = c.ColumnName);

    DECLARE @HasIdentityColumn BIT = 0;
    DECLARE @TableObjectId INT = OBJECT_ID(@qualifiedTable);
    IF @TableObjectId IS NOT NULL
        AND EXISTS (
            SELECT 1
            FROM @SelectedColumns s
            WHERE COLUMNPROPERTY(@TableObjectId, s.ColumnName, 'IsIdentity') = 1
        )
    BEGIN
        SET @HasIdentityColumn = 1;
    END

    -- Build column lists
    DECLARE @ColumnList NVARCHAR(MAX);
    DECLARE @PKInsertedSelectClause NVARCHAR(MAX);
    DECLARE @PKWhereClause NVARCHAR(MAX);
    DECLARE @PKSelectClause NVARCHAR(MAX);
    DECLARE @PKTargetSelectClause NVARCHAR(MAX);
    DECLARE @UpdateClause NVARCHAR(MAX);
    DECLARE @InsertValuesClause NVARCHAR(MAX);

    SELECT @ColumnList = STUFF((
        SELECT ', ' + QUOTENAME(ColumnName)
        FROM @SelectedColumns
        ORDER BY OrdinalPosition
        FOR XML PATH(''), TYPE
    ).value('.', 'NVARCHAR(MAX)'), 1, 2, '');

    SELECT @PKWhereClause = STUFF((
        SELECT ' AND ' + 'target.' + QUOTENAME(pk.ColumnName) + ' = source.' + QUOTENAME(pk.ColumnName)
        FROM @PKColumns pk
        WHERE EXISTS (SELECT 1 FROM @SelectedColumns s WHERE s.ColumnName = pk.ColumnName)
        ORDER BY pk.OrdinalPosition
        FOR XML PATH(''), TYPE
    ).value('.', 'NVARCHAR(MAX)'), 1, 5, '');

    SELECT @PKSelectClause = STUFF((
        SELECT ', ' + QUOTENAME(pk.ColumnName)
        FROM @PKColumns pk
        WHERE EXISTS (SELECT 1 FROM @SelectedColumns s WHERE s.ColumnName = pk.ColumnName)
        ORDER BY pk.OrdinalPosition
        FOR XML PATH(''), TYPE
    ).value('.', 'NVARCHAR(MAX)'), 1, 2, '');

    SELECT @PKInsertedSelectClause = STUFF((
        SELECT ', inserted.' + QUOTENAME(pk.ColumnName)
        FROM @PKColumns pk
        WHERE EXISTS (SELECT 1 FROM @SelectedColumns s WHERE s.ColumnName = pk.ColumnName)
        ORDER BY pk.OrdinalPosition
        FOR XML PATH(''), TYPE
    ).value('.', 'NVARCHAR(MAX)'), 1, 2, '');

    SELECT @PKTargetSelectClause = STUFF((
        SELECT ', target.' + QUOTENAME(pk.ColumnName)
        FROM @PKColumns pk
        WHERE EXISTS (SELECT 1 FROM @SelectedColumns s WHERE s.ColumnName = pk.ColumnName)
        ORDER BY pk.OrdinalPosition
        FOR XML PATH(''), TYPE
    ).value('.', 'NVARCHAR(MAX)'), 1, 2, '');

    IF @PKWhereClause IS NULL OR LTRIM(RTRIM(@PKWhereClause)) = ''
    BEGIN
        SET @ErrorMessage = 'At least one primary key column must be included (cannot exclude all PK columns).';
        RAISERROR(@ErrorMessage, 16, 1);
        RETURN;
    END

    SELECT @UpdateClause = STUFF((
        SELECT ', ' + QUOTENAME(ColumnName) + ' = source.' + QUOTENAME(ColumnName)
        FROM @SelectedColumns
        WHERE ColumnName NOT IN (SELECT ColumnName FROM @PKColumns)
        ORDER BY OrdinalPosition
        FOR XML PATH(''), TYPE
    ).value('.', 'NVARCHAR(MAX)'), 1, 2, '');

    IF @UpdateClause IS NULL OR LTRIM(RTRIM(@UpdateClause)) = ''
        SELECT @UpdateClause = (SELECT TOP 1 QUOTENAME(ColumnName) + ' = source.' + QUOTENAME(ColumnName) FROM @SelectedColumns ORDER BY OrdinalPosition);

    SELECT @InsertValuesClause = STUFF((
        SELECT ', ' + 'source.' + QUOTENAME(ColumnName)
        FROM @SelectedColumns
        ORDER BY OrdinalPosition
        FOR XML PATH(''), TYPE
    ).value('.', 'NVARCHAR(MAX)'), 1, 2, '');

    -- Build value formatter expression per column (for literal VALUES)
    DECLARE @VarCharType NVARCHAR(20) = 'var' + 'char';
    DECLARE @CharType NVARCHAR(20) = 'c' + 'har';
    DECLARE @ValueFormats TABLE (
        OrdinalPosition INT,
        ValueExpression NVARCHAR(MAX)
    );

    INSERT INTO @ValueFormats (OrdinalPosition, ValueExpression)
    SELECT
        OrdinalPosition,
        CASE
            WHEN DataType = 'bit' THEN 'CASE WHEN t.' + QUOTENAME(ColumnName) + ' IS NULL THEN ''NULL'' WHEN t.' + QUOTENAME(ColumnName) + ' = 1 THEN ''1'' ELSE ''0'' END'
            WHEN DataType = 'int' THEN 'CASE WHEN t.' + QUOTENAME(ColumnName) + ' IS NULL THEN ''NULL'' ELSE CAST(t.' + QUOTENAME(ColumnName) + ' AS NVARCHAR(MAX)) END'
            WHEN DataType = 'bigint' THEN 'CASE WHEN t.' + QUOTENAME(ColumnName) + ' IS NULL THEN ''NULL'' ELSE CAST(t.' + QUOTENAME(ColumnName) + ' AS NVARCHAR(MAX)) END'
            WHEN DataType = 'smallint' THEN 'CASE WHEN t.' + QUOTENAME(ColumnName) + ' IS NULL THEN ''NULL'' ELSE CAST(t.' + QUOTENAME(ColumnName) + ' AS NVARCHAR(MAX)) END'
            WHEN DataType = 'tinyint' THEN 'CASE WHEN t.' + QUOTENAME(ColumnName) + ' IS NULL THEN ''NULL'' ELSE CAST(t.' + QUOTENAME(ColumnName) + ' AS NVARCHAR(MAX)) END'
            WHEN DataType = 'decimal' THEN 'CASE WHEN t.' + QUOTENAME(ColumnName) + ' IS NULL THEN ''NULL'' ELSE CAST(t.' + QUOTENAME(ColumnName) + ' AS NVARCHAR(MAX)) END'
            WHEN DataType = 'numeric' THEN 'CASE WHEN t.' + QUOTENAME(ColumnName) + ' IS NULL THEN ''NULL'' ELSE CAST(t.' + QUOTENAME(ColumnName) + ' AS NVARCHAR(MAX)) END'
            WHEN DataType = 'float' THEN 'CASE WHEN t.' + QUOTENAME(ColumnName) + ' IS NULL THEN ''NULL'' ELSE CAST(t.' + QUOTENAME(ColumnName) + ' AS NVARCHAR(MAX)) END'
            WHEN DataType = 'real' THEN 'CASE WHEN t.' + QUOTENAME(ColumnName) + ' IS NULL THEN ''NULL'' ELSE CAST(t.' + QUOTENAME(ColumnName) + ' AS NVARCHAR(MAX)) END'
            WHEN DataType = 'money' THEN 'CASE WHEN t.' + QUOTENAME(ColumnName) + ' IS NULL THEN ''NULL'' ELSE CAST(t.' + QUOTENAME(ColumnName) + ' AS NVARCHAR(MAX)) END'
            WHEN DataType = 'smallmoney' THEN 'CASE WHEN t.' + QUOTENAME(ColumnName) + ' IS NULL THEN ''NULL'' ELSE CAST(t.' + QUOTENAME(ColumnName) + ' AS NVARCHAR(MAX)) END'
            WHEN DataType = 'date' THEN 'CASE WHEN t.' + QUOTENAME(ColumnName) + ' IS NULL THEN ''NULL'' ELSE CHAR(39) + CONVERT(NVARCHAR(50), t.' + QUOTENAME(ColumnName) + ', 121) + CHAR(39) END'
            WHEN DataType = 'datetime' THEN 'CASE WHEN t.' + QUOTENAME(ColumnName) + ' IS NULL THEN ''NULL'' ELSE CHAR(39) + CONVERT(NVARCHAR(50), t.' + QUOTENAME(ColumnName) + ', 121) + CHAR(39) END'
            WHEN DataType = 'datetime2' THEN 'CASE WHEN t.' + QUOTENAME(ColumnName) + ' IS NULL THEN ''NULL'' ELSE CHAR(39) + CONVERT(NVARCHAR(50), t.' + QUOTENAME(ColumnName) + ', 121) + CHAR(39) END'
            WHEN DataType = 'smalldatetime' THEN 'CASE WHEN t.' + QUOTENAME(ColumnName) + ' IS NULL THEN ''NULL'' ELSE CHAR(39) + CONVERT(NVARCHAR(50), t.' + QUOTENAME(ColumnName) + ', 121) + CHAR(39) END'
            WHEN DataType = 'time' THEN 'CASE WHEN t.' + QUOTENAME(ColumnName) + ' IS NULL THEN ''NULL'' ELSE CHAR(39) + CONVERT(NVARCHAR(50), t.' + QUOTENAME(ColumnName) + ', 121) + CHAR(39) END'
            WHEN DataType = 'uniqueidentifier' THEN 'CASE WHEN t.' + QUOTENAME(ColumnName) + ' IS NULL THEN ''NULL'' ELSE CHAR(39) + CAST(t.' + QUOTENAME(ColumnName) + ' AS NVARCHAR(36)) + CHAR(39) END'
            WHEN DataType IN ('nvarchar', 'nchar') THEN 'CASE WHEN t.' + QUOTENAME(ColumnName) + ' IS NULL THEN ''NULL'' ELSE NCHAR(78) + CHAR(39) + REPLACE(REPLACE(REPLACE(REPLACE(CAST(t.' + QUOTENAME(ColumnName) + ' AS NVARCHAR(MAX)), CHAR(13), CHAR(32)), CHAR(10), CHAR(32)), CHAR(9), CHAR(32)), CHAR(39), CHAR(39)+CHAR(39)) + CHAR(39) END'
            WHEN DataType IN (@VarCharType, @CharType) THEN 'CASE WHEN t.' + QUOTENAME(ColumnName) + ' IS NULL THEN ''NULL'' ELSE CHAR(39) + REPLACE(REPLACE(REPLACE(REPLACE(CAST(t.' + QUOTENAME(ColumnName) + ' AS NVARCHAR(MAX)), CHAR(13), CHAR(32)), CHAR(10), CHAR(32)), CHAR(9), CHAR(32)), CHAR(39), CHAR(39)+CHAR(39)) + CHAR(39) END'
            ELSE 'CASE WHEN t.' + QUOTENAME(ColumnName) + ' IS NULL THEN ''NULL'' ELSE NCHAR(78) + CHAR(39) + REPLACE(REPLACE(REPLACE(REPLACE(CAST(t.' + QUOTENAME(ColumnName) + ' AS NVARCHAR(MAX)), CHAR(13), CHAR(32)), CHAR(10), CHAR(32)), CHAR(9), CHAR(32)), CHAR(39), CHAR(39)+CHAR(39)) + CHAR(39) END'
        END
    FROM @SelectedColumns;

    DECLARE @RowExpression NVARCHAR(MAX) = '';
    DECLARE @ValExpr NVARCHAR(MAX);
    DECLARE @First BIT = 1;
    DECLARE row_cursor CURSOR LOCAL FAST_FORWARD FOR
        SELECT ValueExpression FROM @ValueFormats ORDER BY OrdinalPosition;
    OPEN row_cursor;
    FETCH NEXT FROM row_cursor INTO @ValExpr;
    WHILE @@FETCH_STATUS = 0
    BEGIN
        SET @RowExpression = @RowExpression + CASE WHEN @First = 1 THEN '' ELSE ' + '', '' + ' END + @ValExpr;
        SET @First = 0;
        FETCH NEXT FROM row_cursor INTO @ValExpr;
    END;
    CLOSE row_cursor;
    DEALLOCATE row_cursor;

    -- Get row count and build MERGE with literal VALUES via dynamic SQL
    DECLARE @CountSql NVARCHAR(MAX) = 'SELECT @cnt = COUNT(*) FROM ' + @table + ' WHERE ' + @where;
    EXEC sp_executesql @CountSql, N'@cnt INT OUTPUT', @cnt = @RowCount OUTPUT;

    DECLARE @CRLF NCHAR(2) = NCHAR(13) + NCHAR(10);
    DECLARE @DeleteKeyCreateSql NVARCHAR(MAX) = NULL;
    DECLARE @DeleteKeyDropSql NVARCHAR(MAX) = NULL;
    DECLARE @DeleteKeyIndexSql NVARCHAR(MAX) = NULL;
    DECLARE @DeleteKeyTable NVARCHAR(258) = NULL;
    DECLARE @DeleteSql NVARCHAR(MAX) = NULL;
    DECLARE @IdentityInsertOnSql NVARCHAR(MAX) = NULL;
    DECLARE @IdentityInsertOffSql NVARCHAR(MAX) = NULL;
    DECLARE @MergeOutputClause NVARCHAR(MAX) = '';

    IF @includeDelete = 1 AND @RowCount > 0
    BEGIN
        DECLARE @DeleteKeySuffix NVARCHAR(32) = REPLACE(CONVERT(NVARCHAR(36), NEWID()), '-', '');
        DECLARE @DeleteKeyIndexName NVARCHAR(258) = QUOTENAME('IX_ExportKeys_' + @DeleteKeySuffix);
        SET @DeleteKeyTable = QUOTENAME('#ExportKeys_' + @DeleteKeySuffix);
        SET @DeleteKeyCreateSql = 'SELECT TOP (0) ' + @PKTargetSelectClause + ' INTO ' + @DeleteKeyTable + ' FROM ' + @table + ' AS target UNION ALL SELECT TOP (0) ' + @PKTargetSelectClause + ' FROM ' + @table + ' AS target;';
        SET @DeleteKeyIndexSql = 'CREATE UNIQUE CLUSTERED INDEX ' + @DeleteKeyIndexName + ' ON ' + @DeleteKeyTable + ' (' + @PKSelectClause + ');';
        SET @DeleteSql = 'DELETE target FROM ' + @table + ' AS target WHERE ' + @where + ' AND NOT EXISTS (SELECT 1 FROM ' + @DeleteKeyTable + ' AS source WHERE ' + @PKWhereClause + ');';
        SET @DeleteKeyDropSql = 'DROP TABLE ' + @DeleteKeyTable + ';';
        SET @MergeOutputClause = @CRLF + 'OUTPUT ' + @PKInsertedSelectClause + ' INTO ' + @DeleteKeyTable + ' (' + @PKSelectClause + ')';
    END

    IF @HasIdentityColumn = 1 AND @RowCount > 0
    BEGIN
        SET @IdentityInsertOnSql = 'SET IDENTITY_INSERT ' + @table + ' ON;';
        SET @IdentityInsertOffSql = 'SET IDENTITY_INSERT ' + @table + ' OFF;';
    END

    DECLARE @ChunkOutput TABLE (
        ChunkNumber INT,
        TableName NVARCHAR(255),
        SqlStatement NVARCHAR(MAX),
        ChunkRowCount INT,
        StatementSize INT,
        StatementType NVARCHAR(50)
    );

    IF @RowCount = 0
    BEGIN
        INSERT INTO @ChunkOutput (ChunkNumber, TableName, SqlStatement, ChunkRowCount, StatementSize, StatementType)
        SELECT
            1,
            @table,
            '-- No rows to export from ' + @table + ' WHERE ' + @where,
            0,
            0,
            'MERGE';
    END
    ELSE
    BEGIN
        IF @DeleteKeyCreateSql IS NOT NULL
            INSERT INTO @ChunkOutput (ChunkNumber, TableName, SqlStatement, ChunkRowCount, StatementSize, StatementType)
            SELECT
                0,
                @table,
                @DeleteKeyCreateSql,
                0,
                LEN(@DeleteKeyCreateSql),
                'DELETE';

        IF @IdentityInsertOnSql IS NOT NULL
            INSERT INTO @ChunkOutput (ChunkNumber, TableName, SqlStatement, ChunkRowCount, StatementSize, StatementType)
            SELECT
                (SELECT ISNULL(MAX(ChunkNumber), -1) + 1 FROM @ChunkOutput),
                @table,
                @IdentityInsertOnSql,
                0,
                LEN(@IdentityInsertOnSql),
                'IDENTITY_INSERT_ON';

        DECLARE @ValueRowDelimiter NVARCHAR(64) = N'__ROW_' + REPLACE(CONVERT(NVARCHAR(36), NEWID()), '-', '') + N'__';
        DECLARE @BuildSql NVARCHAR(MAX) = 'SELECT @out = STUFF((SELECT @delimiter + ''('' + ' + @RowExpression + ' + '')'' FROM ' + @table + ' t WHERE ' + @where + ' FOR XML PATH(''''), TYPE).value(''.'', ''NVARCHAR(MAX)''), 1, LEN(@delimiter), '''')';
        DECLARE @ValuesResult NVARCHAR(MAX) = NULL;
        EXEC sp_executesql @BuildSql, N'@delimiter NVARCHAR(64), @out NVARCHAR(MAX) OUTPUT', @delimiter = @ValueRowDelimiter, @out = @ValuesResult OUTPUT;

        DECLARE @MergePrefix NVARCHAR(MAX) = 'MERGE ' + @table + ' AS target' + @CRLF + 'USING (VALUES ';
        DECLARE @MergeSuffix NVARCHAR(MAX) = ') AS source(' + @ColumnList + ')' + @CRLF + 'ON (' + @PKWhereClause + ')' + @CRLF + 'WHEN MATCHED THEN UPDATE SET ' + ISNULL(@UpdateClause, '') + @CRLF + 'WHEN NOT MATCHED THEN INSERT (' + @ColumnList + ') VALUES (' + @InsertValuesClause + ')' + @MergeOutputClause + ';';
        DECLARE @MaxValuesLen INT = @ChunkSize - LEN(@MergePrefix) - LEN(@MergeSuffix);

        DECLARE @Remaining NVARCHAR(MAX) = @ValuesResult;
        DECLARE @ChunkNum INT = (SELECT ISNULL(MAX(ChunkNumber), 0) + 1 FROM @ChunkOutput);
        DECLARE @ChunkValues NVARCHAR(MAX) = '';
        DECLARE @ChunkRowCount INT = 0;
        DECLARE @DelimiterPosition INT;
        DECLARE @NextRow NVARCHAR(MAX);

        WHILE LEN(@Remaining) > 0
        BEGIN
            SET @DelimiterPosition = CHARINDEX(@ValueRowDelimiter, @Remaining);
            IF @DelimiterPosition > 0
            BEGIN
                SET @NextRow = LEFT(@Remaining, @DelimiterPosition - 1);
                SET @Remaining = SUBSTRING(@Remaining, @DelimiterPosition + LEN(@ValueRowDelimiter), LEN(@Remaining));
            END
            ELSE
            BEGIN
                SET @NextRow = @Remaining;
                SET @Remaining = '';
            END;

            IF @ChunkRowCount > 0
                AND (@ChunkRowCount >= @MaxRowsPerValuesStatement OR LEN(@ChunkValues) + 2 + LEN(@NextRow) > @MaxValuesLen)
            BEGIN
                INSERT INTO @ChunkOutput (ChunkNumber, TableName, SqlStatement, ChunkRowCount, StatementSize, StatementType)
                SELECT
                    @ChunkNum,
                    @table,
                    @MergePrefix + @ChunkValues + @MergeSuffix,
                    @ChunkRowCount,
                    LEN(@MergePrefix) + LEN(@ChunkValues) + LEN(@MergeSuffix),
                    'MERGE';

                SET @ChunkNum = @ChunkNum + 1;
                SET @ChunkValues = '';
                SET @ChunkRowCount = 0;
            END;

            SET @ChunkValues = @ChunkValues + CASE WHEN @ChunkRowCount = 0 THEN '' ELSE ', ' END + @NextRow;
            SET @ChunkRowCount = @ChunkRowCount + 1;
        END;

        IF @ChunkRowCount > 0
            INSERT INTO @ChunkOutput (ChunkNumber, TableName, SqlStatement, ChunkRowCount, StatementSize, StatementType)
            SELECT
                @ChunkNum,
                @table,
                @MergePrefix + @ChunkValues + @MergeSuffix,
                @ChunkRowCount,
                LEN(@MergePrefix) + LEN(@ChunkValues) + LEN(@MergeSuffix),
                'MERGE';
    END;

    IF @DeleteSql IS NOT NULL
    BEGIN
        INSERT INTO @ChunkOutput (ChunkNumber, TableName, SqlStatement, ChunkRowCount, StatementSize, StatementType)
        SELECT
            (SELECT ISNULL(MAX(ChunkNumber), 0) + 1 FROM @ChunkOutput),
            @table,
            @DeleteKeyIndexSql,
            0,
            LEN(@DeleteKeyIndexSql),
            'DELETE';

        INSERT INTO @ChunkOutput (ChunkNumber, TableName, SqlStatement, ChunkRowCount, StatementSize, StatementType)
        SELECT
            (SELECT ISNULL(MAX(ChunkNumber), 0) + 1 FROM @ChunkOutput),
            @table,
            @DeleteSql,
            0,
            LEN(@DeleteSql),
            'DELETE';

        INSERT INTO @ChunkOutput (ChunkNumber, TableName, SqlStatement, ChunkRowCount, StatementSize, StatementType)
        SELECT
            (SELECT ISNULL(MAX(ChunkNumber), 0) + 1 FROM @ChunkOutput),
            @table,
            @DeleteKeyDropSql,
            0,
            LEN(@DeleteKeyDropSql),
            'DELETE';
    END

    IF @IdentityInsertOffSql IS NOT NULL
        INSERT INTO @ChunkOutput (ChunkNumber, TableName, SqlStatement, ChunkRowCount, StatementSize, StatementType)
        SELECT
            (SELECT ISNULL(MAX(ChunkNumber), 0) + 1 FROM @ChunkOutput),
            @table,
            @IdentityInsertOffSql,
            0,
            LEN(@IdentityInsertOffSql),
            'IDENTITY_INSERT_OFF';

    -- Keep each result cell below SSMS's 65,535-character grid limit.
    DECLARE @OutputFragmentSize INT = 20000;
    DECLARE @MaxOutputStatementSize INT = 60000;
    DECLARE @OutputVariablePrefix NVARCHAR(50) = '@ExportStatement' + REPLACE(CONVERT(NVARCHAR(36), NEWID()), '-', '') + '_';

    ;WITH LongStatementParts AS (
        SELECT
            c.ChunkNumber,
            c.TableName,
            c.SqlStatement AS OriginalSqlStatement,
            c.ChunkRowCount,
            c.StatementType,
            1 AS PartNumber,
            SUBSTRING(c.SqlStatement, 1, @OutputFragmentSize) AS StatementPart,
            @OutputFragmentSize + 1 AS NextPosition
        FROM @ChunkOutput c
        WHERE LEN(c.SqlStatement) > @MaxOutputStatementSize

        UNION ALL

        SELECT
            p.ChunkNumber,
            p.TableName,
            p.OriginalSqlStatement,
            p.ChunkRowCount,
            p.StatementType,
            p.PartNumber + 1,
            SUBSTRING(p.OriginalSqlStatement, p.NextPosition, @OutputFragmentSize),
            p.NextPosition + @OutputFragmentSize
        FROM LongStatementParts p
        WHERE p.NextPosition <= LEN(p.OriginalSqlStatement)
    ),
    OutputRows AS (
        SELECT
            c.ChunkNumber,
            0 AS OutputPartNumber,
            c.TableName,
            c.SqlStatement,
            c.ChunkRowCount,
            c.StatementType
        FROM @ChunkOutput c
        WHERE LEN(c.SqlStatement) <= @MaxOutputStatementSize

        UNION ALL

        SELECT
            c.ChunkNumber,
            0,
            c.TableName,
            CAST('DECLARE ' + @OutputVariablePrefix + CONVERT(NVARCHAR(11), c.ChunkNumber) + ' NVARCHAR(MAX) = N' + NCHAR(39) + NCHAR(39) + ';' AS NVARCHAR(MAX)),
            0,
            c.StatementType
        FROM @ChunkOutput c
        WHERE LEN(c.SqlStatement) > @MaxOutputStatementSize

        UNION ALL

        SELECT
            p.ChunkNumber,
            p.PartNumber,
            p.TableName,
            CAST('SET ' + @OutputVariablePrefix + CONVERT(NVARCHAR(11), p.ChunkNumber) + ' += N' + NCHAR(39) + REPLACE(p.StatementPart, NCHAR(39), NCHAR(39) + NCHAR(39)) + NCHAR(39) + ';' AS NVARCHAR(MAX)),
            0,
            p.StatementType
        FROM LongStatementParts p

        UNION ALL

        SELECT
            c.ChunkNumber,
            CAST(CEILING(1.0 * LEN(c.SqlStatement) / @OutputFragmentSize) AS INT) + 1,
            c.TableName,
            CAST('EXEC sys.sp_executesql ' + @OutputVariablePrefix + CONVERT(NVARCHAR(11), c.ChunkNumber) + ';' AS NVARCHAR(MAX)),
            c.ChunkRowCount,
            c.StatementType
        FROM @ChunkOutput c
        WHERE LEN(c.SqlStatement) > @MaxOutputStatementSize
    )
    SELECT
        ChunkNumber,
        TableName,
        SqlStatement,
        ChunkRowCount AS [RowCount],
        LEN(SqlStatement) AS StatementSize,
        StatementType
    FROM OutputRows
    ORDER BY ChunkNumber, OutputPartNumber
    OPTION (MAXRECURSION 0);

END TRY
BEGIN CATCH
    SET @ErrorMessage = ERROR_MESSAGE();
    RAISERROR(@ErrorMessage, 16, 1);
    RETURN;
END CATCH