Articles

Renaming a column in SQL Server, and what it quietly breaks

A column was misspelled when the table was created, and it has been Ammount ever since. The rename itself is one line:

EXEC sp_rename 'dbo.Invoice.Ammount', 'Amount', 'COLUMN';
Caution: Changing any part of an object name could break scripts and stored procedures.

That caution is the entire problem, stated once and never mentioned again. The rename succeeds. What breaks, breaks later, and in a different file.

Note the arguments: the old name is qualified with the table, the new name is not — write 'dbo.Invoice.Amount' as the second argument and you get a column called dbo.Invoice.Amount, brackets and all.

What follows the rename by itself

More than people expect.

Indexes keep working. The index still indexes the same column under its new name:

CREATE INDEX IX_Tag_Labell ON dbo.Tag (Labell);
EXEC sp_rename 'dbo.Tag.Labell', 'Label', 'COLUMN';

The index is intact and now covers Label. Its own name, though, is untouched — you are left with IX_Tag_Labell on a column called Label, which is how a schema starts lying about itself. Rename the index too:

EXEC sp_rename 'dbo.Tag.IX_Tag_Labell', 'IX_Tag_Label', 'INDEX';

Foreign keys keep working, and stay trusted — is_not_trusted remains 0. Nothing to do.

Unique constraints keep working, like indexes.

Default constraints keep working, and usually have nothing to rewrite — DEFAULT (0) is stored as ((0)) and names no column at all. The constraint's own name is untouched, so DF_Invoice_Ammount is still sitting on a column called Amount.

One case that refuses outright

A check constraint stops the rename dead:

CREATE TABLE dbo.ChkOnly (Id int NOT NULL PRIMARY KEY,
    Qty int NOT NULL CONSTRAINT CK_ChkOnly_Qty CHECK (Qty >= 0));

EXEC sp_rename 'dbo.ChkOnly.Qty', 'Quantity', 'COLUMN';
Msg 15336, Level 16, State 1, Procedure sp_rename, Line 563
Object 'dbo.ChkOnly.Qty' cannot be renamed because the object participates in enforced dependencies.

The constraint's definition names the column, and SQL Server will not leave it pointing at a name that no longer exists. Drop it, rename, put it back:

ALTER TABLE dbo.ChkOnly DROP CONSTRAINT CK_ChkOnly_Qty;
GO
EXEC sp_rename 'dbo.ChkOnly.Qty', 'Quantity', 'COLUMN';
GO
ALTER TABLE dbo.ChkOnly ADD CONSTRAINT CK_ChkOnly_Quantity CHECK (Quantity >= 0);
GO

Those GOs matter. Without them the whole thing is one batch, the ADD CONSTRAINT is parsed before the rename has happened, and it fails with Invalid column name 'Quantity' — leaving you with the constraint dropped and the column renamed, which is the worst of the three states.

A schema-bound view refuses for the same reason, with the same message:

CREATE VIEW dbo.PaymentBound WITH SCHEMABINDING AS SELECT PaymentId, Ammount FROM dbo.Payment;
EXEC sp_rename 'dbo.Payment.Ammount', 'Amount', 'COLUMN';
Msg 15336, Level 16, State 1, Procedure sp_rename, Line 563
Object 'dbo.Payment.Ammount' cannot be renamed because the object participates in enforced
dependencies.

Both of these are the helpful outcome: they are the dependencies that will not let you break them. Everything in the next section breaks silently instead.

What does not follow

Views, stored procedures, functions and triggers hold your text, not a reference to the column. They are not updated, and they do not complain until something runs them:

SELECT * FROM dbo.InvoiceTotal;
Msg 207, Level 16, State 1, Procedure InvoiceTotal, Line 1
Invalid column name 'Ammount'.
Msg 4413, Level 16, State 1, Line 1
Could not use view or function 'dbo.InvoiceTotal' because of binding errors.
EXEC dbo.GetInvoice @Id = 1;
Msg 207, Level 16, State 1, Procedure dbo.GetInvoice, Line 1
Invalid column name 'Ammount'.

A deployment that renames a column therefore succeeds, reports no error, and leaves a procedure that fails the first time a customer calls it. That gap — between the change and the failure — is the whole risk of the operation.

Find them before you rename

Everything that uses the table:

SELECT referencing_schema_name AS SchemaName, referencing_entity_name AS UsesTheTable
FROM   sys.dm_sql_referencing_entities('dbo.Invoice', 'OBJECT');
SchemaName  UsesTheTable
----------  ------------
dbo         GetInvoice
dbo         InvoiceTotal

That is the list to read before you decide. It will not tell you which ones name the column, so after the rename, search the definitions for the old name:

SELECT OBJECT_SCHEMA_NAME(m.object_id) AS SchemaName,
       OBJECT_NAME(m.object_id)        AS ObjectName,
       o.type_desc                     AS Kind
FROM   sys.sql_modules AS m
JOIN   sys.objects AS o ON o.object_id = m.object_id
WHERE  m.definition LIKE '%Ammount%';
SchemaName  ObjectName  Kind
----------  ----------  --------------------
dbo         GetInvoice  SQL_STORED_PROCEDURE

Fix each one with ALTER VIEW or ALTER PROCEDURE, then run that query again. When it returns no rows, nothing left in the database names the old column. It is the only check at the end of this job that means anything.

Two caveats on it. It searches text, so a column named Code matches far too much — rename misspellings and distinctive names with confidence, and read the results rather than trusting the count. And it sees only what is stored in the database: your application's SQL, reports, ETL jobs and anything built with dynamic SQL are outside it entirely.

The safer shape, when the column is in use

A rename is instant and total: the old name is gone for every reader at the same moment. Where an application is deployed separately from its database, that is a window of failure you cannot close by sequencing.

The alternative takes longer and never breaks:

  1. Add the new column, and keep both written for a while.
  2. Move the readers across, one at a time, deploying as you go.
  3. Drop the old column when nothing names it.

Worth the extra deployments only where the downtime matters — but when it matters, nothing else does the job.

Knowing what points at the table

The lists above come from the database, which is a better source than memory. What they do not show is the shape of the thing: which tables point at this one, which columns join to what, and therefore how far a rename travels.

WoodFireERD draws that from your schema — pick the table, follow its relationships out as far as you want to look, and take the change out as an ordered migration script.

An unhandled error has occurred. Reload 🗙