Table block with one column pulled out, representing safely dropping unused columns in SQL Server

Safely Drop Unused Columns in SQL Server: A Script That Handles Dependencies

A two-stage method to retire unused SQL Server columns, and a query that generates the drop script with default, check and foreign key constraints, indexes and statistics.

July 18, 2023

Removing a column from a table looks simple when the application no longer uses it: ALTER TABLE ... DROP COLUMN. But if the column is tied to a default constraint, a check constraint, a foreign key, an index or a manually created statistic, the command fails. More importantly, dropping a column without being sure nobody really uses it means a report or a nightly job suddenly breaking.

This article shares a two-stage method for retiring columns safely, and a query that generates the drop script automatically, together with the dependent objects.

SQL Server Consulting

Have unused structures piled up in your database?

Aryasoft identifies unused tables, columns and indexes along with their dependencies and prepares a safe cleanup plan.

Get Support

A Two-Stage Method: Mark First, Drop Later

Instead of dropping a column you think is unused right away, first mark it as “retired” by renaming it. For example, add a dep_ prefix to its name:

EXEC sys.sp_rename N'dbo.Customer.FaxNo', N'dep_FaxNo', N'COLUMN';

If a query, procedure or report still uses the column, it fails with “Invalid column name” after the rename and reveals itself. That failure is easy to undo: rename the column back, and no data is lost. If no error appears during a waiting period (for example a full business cycle, including a month-end close), the column really is unused.

Warning

sp_rename doesn’t update the stored procedures, views and functions that reference the column; because they can’t compile with the new name, they fail when they run. That’s the point of the method, but check dependencies before renaming and share the change with the application teams.

Some dependencies block the rename itself: if a CHECK constraint, a computed column, the WHERE clause of a filtered index, or a schema-bound view or function uses the column, sp_rename fails with Msg 15336 or Msg 5074. In our tests, default constraints, indexes, statistics and foreign keys didn’t block it. If you hit this error, script out the dependent object, drop it, rename the column, and recreate the object with the new name only if it’s still needed.

To find dependencies, use the column-level query in our guide to finding objects that use a table.

Generating the Drop Script

Once the waiting period is over, the query below generates the needed commands, in the right order, for every column whose name starts with dep_: first the constraints, indexes and statistics tied to the column, then the column itself. The query only generates a script; it doesn’t drop anything.

DECLARE @prefix sysname = N'dep[_]%';   -- the underscore is escaped because it's a LIKE wildcard

WITH target AS (
    SELECT c.object_id, c.column_id,
           QUOTENAME(OBJECT_SCHEMA_NAME(c.object_id)) + N'.' + QUOTENAME(OBJECT_NAME(c.object_id)) AS table_name,
           QUOTENAME(c.name) AS column_name
    FROM sys.columns AS c
    JOIN sys.tables  AS t ON t.object_id = c.object_id
    WHERE c.name LIKE @prefix
)
SELECT step, command
FROM (
    -- 1. Default constraints
    SELECT 1 AS step, N'ALTER TABLE ' + tg.table_name + N' DROP CONSTRAINT ' + QUOTENAME(dc.name) + N';' AS command
    FROM target AS tg
    JOIN sys.default_constraints AS dc ON dc.parent_object_id = tg.object_id AND dc.parent_column_id = tg.column_id
    UNION ALL
    -- 2. Column-level check constraints
    SELECT 2, N'ALTER TABLE ' + tg.table_name + N' DROP CONSTRAINT ' + QUOTENAME(cc.name) + N';'
    FROM target AS tg
    JOIN sys.check_constraints AS cc ON cc.parent_object_id = tg.object_id AND cc.parent_column_id = tg.column_id
    UNION ALL
    -- 3. Foreign keys that include the column
    SELECT DISTINCT 3, N'ALTER TABLE ' + QUOTENAME(OBJECT_SCHEMA_NAME(fk.parent_object_id)) + N'.'
                       + QUOTENAME(OBJECT_NAME(fk.parent_object_id)) + N' DROP CONSTRAINT ' + QUOTENAME(fk.name) + N';'
    FROM target AS tg
    JOIN sys.foreign_key_columns AS fkc
         ON (fkc.parent_object_id = tg.object_id AND fkc.parent_column_id = tg.column_id)
         OR (fkc.referenced_object_id = tg.object_id AND fkc.referenced_column_id = tg.column_id)
    JOIN sys.foreign_keys AS fk ON fk.object_id = fkc.constraint_object_id
    UNION ALL
    -- 4. Indexes that include the column (review: the index may include other columns)
    SELECT DISTINCT 4, N'DROP INDEX ' + QUOTENAME(i.name) + N' ON ' + tg.table_name + N';  -- review'
    FROM target AS tg
    JOIN sys.index_columns AS ic ON ic.object_id = tg.object_id AND ic.column_id = tg.column_id
    JOIN sys.indexes AS i ON i.object_id = ic.object_id AND i.index_id = ic.index_id
    WHERE i.is_primary_key = 0 AND i.is_unique_constraint = 0
    UNION ALL
    -- 5. Manually created statistics
    SELECT DISTINCT 5, N'DROP STATISTICS ' + tg.table_name + N'.' + QUOTENAME(s.name) + N';'
    FROM target AS tg
    JOIN sys.stats_columns AS sc ON sc.object_id = tg.object_id AND sc.column_id = tg.column_id
    JOIN sys.stats AS s ON s.object_id = sc.object_id AND s.stats_id = sc.stats_id
    WHERE s.user_created = 1
    UNION ALL
    -- 6. The column itself
    SELECT 6, N'ALTER TABLE ' + tg.table_name + N' DROP COLUMN ' + tg.column_name + N';'
    FROM target AS tg
) AS s
ORDER BY step, command;

Critical

Don’t run the generated script directly. The indexes in step 4 in particular may include other columns and be used by other queries; dropping an index can slow those queries down. If the column is part of a primary key or unique constraint, the script doesn’t drop it; that case needs separate consideration. Review the script, run it in a test environment, and take a backup right before running it in production.

Why Isn’t the Space Back Right Away?

DROP COLUMN is mostly a metadata operation: the column leaves the definition, but the space its data used stays in the pages. To reclaim it, rebuild the table:

-- Tables with a clustered index
ALTER INDEX ALL ON dbo.Customer REBUILD;

-- Heaps
ALTER TABLE dbo.Customer REBUILD;

On large tables this consumes resources and generates log; do it in a maintenance window. To measure table sizes before and after, see our table size guide.

Checklist

  • The column’s dependencies were checked and shared with the application teams.
  • The column was first renamed with the dep_ prefix, and a business cycle passed.
  • The drop script was generated, reviewed and run in a test environment.
  • The impact of dropping indexes on other queries was assessed.
  • A backup was taken before running in production; space was reclaimed afterwards.
ALTER TABLE DROP COLUMN fails if the column is used by a default constraint, check constraint, foreign key, index or manually created statistic. Those dependent objects have to be removed first.
Check with dependency queries, then rename the column and wait through a business cycle. Code that still uses it reveals itself by failing, and the name can easily be reverted.
DROP COLUMN is mostly a metadata operation. Rebuild the indexes, or the heap, to reclaim the space.
Primary keys and unique constraints are part of the table’s integrity and are usually referenced by other tables; removing them has to be planned separately.

SQL Server Consulting

Simplify your database safely

Aryasoft’s senior DBA team identifies unused tables, columns and indexes along with their dependencies, and simplifies your database with a two-stage, reversible cleanup plan.

4.7/5