Named Constraints: Small Habit, Big Impact

Imagine you’re a developer working on an OLTP system.

You add a quick check constraint to enforce valid values for a column — nothing fancy, just a simple rule to keep bad data out. It passes tests, goes through code review, and ships to production.

A few months later, the requirements change. The business logic expands overnight. Your company’s policy is strict: any change script must be tested in development first, then run exactly as-is in production.

So you write your ALTER TABLE ... DROP CONSTRAINT statement — but what’s the name of the constraint in production? You never named it. SQL Server did. And it probably generated something like CK__Orders__Status__5AEE82B9. Good luck guessing that from dev, where it’s a different random hash altogether.

Now you have two options:

  1. Query the system catalog manually, adjust the script by hand, hope you don’t mistype it, then explain why the dev and prod scripts aren’t identical.
  2. Or, if you’d named it in the first place — CK_Orders_Status — you’d have one clean, portable, predictable script that works every time.

This is why I always name my constraints — every primary key, unique key, check, default, and foreign key. It’s a tiny habit that prevents hours of confusion and unnecessary risk later on.

How Unnamed Constraints Happen

By default, if you don’t provide a name, SQL Server generates one for you, but it’s random and unpredictable.

CREATE TABLE [dbo].[Order] 
(
    [Id] INT NOT NULL PRIMARY KEY,
    [OrderNumber] VARCHAR(50) NOT NULL UNIQUE,
    [Status] VARCHAR(20) NOT NULL 
		CHECK ([Status] IN ('New', 'Shipped', 'Cancelled')),
    [OrderedAt] DATETIME2(2) DEFAULT (SYSUTCDATETIME()),
    [CustomerId] INT,
	FOREIGN KEY ([CustomerId]) 
		REFERENCES [dbo].[Customer] ([Id])
);

Practical Syntax — All Constraints, Named

Use the syntax below to define named constraints directly when creating the table.

CREATE TABLE [dbo].[Order] 
(
    [Id] INT NOT NULL CONSTRAINT [PK_Order] PRIMARY KEY,
    [OrderNumber] VARCHAR(50) NOT NULL,
    [Status] VARCHAR(20) NOT NULL,		
    [OrderedAt] DATETIME2(2) CONSTRAINT [DF_Order_OrderedAt] 
		DEFAULT (SYSUTCDATETIME()),
    [CustomerId] INT,
	CONSTRAINT [UQ_Order_OrderNumber] 
		UNIQUE ([OrderNumber]),
	CONSTRAINT [CK_Order_Status]
		CHECK ([Status] IN ('New', 'Shipped', 'Cancelled')),
	CONSTRAINT [FK_Order_Customer] FOREIGN KEY ([CustomerId]) 
		REFERENCES [dbo].[Customer] ([Id])
);

You can use the following syntax to define named constraints after creating a table.

ALTER TABLE [dbo].[Order] ADD CONSTRAINT [PK_Order] 
PRIMARY KEY ([Id]);

ALTER TABLE [dbo].[Order] ADD CONSTRAINT [UQ_Order_OrderNumber]
UNIQUE ([OrderNumber]);

ALTER TABLE [dbo].[Order] ADD CONSTRAINT [CK_Order_Status]
CHECK ([Status] IN ('New', 'Shipped', 'Cancelled'));

ALTER TABLE [dbo].[Order] ADD CONSTRAINT [DF_Order_OrderedAt]
DEFAULT (SYSUTCDATETIME()) FOR [OrderedAt];

ALTER TABLE [dbo].[Order] ADD CONSTRAINT [FK_Order_Customer] 
FOREIGN KEY ([CustomerId]) REFERENCES [dbo].[Customer] ([Id]);

How to identify unnamed constraints

You can use the following script to list all unnamed constraints in all user databases on your SQL Server instance.

/*
================================================================================
  Script:     FindUnnamedConstraints_AllDatabases.sql
  Purpose:    List all system-named (unnamed) constraints across ALL user 
              databases on this SQL Server instance. Helps ensure consistent 
              naming conventions for Primary Keys, Unique, Check, Default, 
              and Foreign Key constraints.

  Author:     Ryszard Kozlowski
  Website:    https://www.ittiger.net/

  Usage:
    1. Run this script in the context of the master database (or any DB).
    2. It will scan all user databases (excluding system DBs).
    3. It outputs a list showing:
         - Database name
         - Schema
         - Table name
         - Constraint type (PK, UQ, C, D, F)
         - Actual constraint name
    4. Use this list to decide which constraints need renaming.

  Notes:
    - No data or constraints are changed. 
    - Safe to run multiple times.
================================================================================
*/

SET NOCOUNT ON;

-- Clean up temp tables if they exist
DROP TABLE IF EXISTS #Databases;
DROP TABLE IF EXISTS #Constraints;

-- Table to store user database names
CREATE TABLE #Databases
(
    Id INT NOT NULL PRIMARY KEY,
    Name NVARCHAR(128) NOT NULL UNIQUE
);

-- Table to store found unnamed constraints
CREATE TABLE #Constraints
(
    Id INT IDENTITY(1,1) PRIMARY KEY,
    DatabaseName NVARCHAR(128),
    TableSchema NVARCHAR(128),
    TableName NVARCHAR(128),
    ConstraintType CHAR(2),
    ConstraintName NVARCHAR(128),
    UNIQUE (DatabaseName, TableSchema, TableName, ConstraintType, ConstraintName)
);

-- Populate user database list (excluding system DBs)
INSERT INTO #Databases (Id, Name)
SELECT database_id, name
FROM sys.databases
WHERE database_id > 4 -- skip master, tempdb, model, msdb
  AND state_desc = 'ONLINE';

-- Loop variables
DECLARE @DbId INT = (SELECT MIN(Id) FROM #Databases);
DECLARE @DbName NVARCHAR(128);
DECLARE @Sql NVARCHAR(MAX);

-- Core query to find unnamed constraints in the current DB
DECLARE @QueryTemplate NVARCHAR(MAX) = N'
SELECT
    DB_NAME() AS [DatabaseName],
    SCHEMA_NAME(t.schema_id) AS [TableSchema],
    t.name AS [TableName],
    c.type AS [ConstraintType],
    c.name AS [ConstraintName]
FROM
    sys.objects AS c
    JOIN sys.tables AS t ON t.object_id = c.parent_object_id
    LEFT JOIN sys.default_constraints dc ON dc.object_id = c.object_id AND dc.is_system_named = 1
    LEFT JOIN sys.check_constraints cc ON cc.object_id = c.object_id AND cc.is_system_named = 1
    LEFT JOIN sys.foreign_keys fk ON fk.object_id = c.object_id AND fk.is_system_named = 1
    LEFT JOIN sys.key_constraints k ON k.object_id = c.object_id AND k.is_system_named = 1
WHERE
    c.type IN (''PK'', ''UQ'', ''C'', ''D'', ''F'')
    AND (
        dc.is_system_named = 1
        OR cc.is_system_named = 1
        OR fk.is_system_named = 1
        OR k.is_system_named = 1
    )
ORDER BY
    SCHEMA_NAME(t.schema_id), t.name, c.type;';

-- Loop through each user DB
WHILE @DbId IS NOT NULL
BEGIN
    SELECT @DbName = Name FROM #Databases WHERE Id = @DbId;

    SET @Sql = N'USE ' + QUOTENAME(@DbName) + '; ' + @QueryTemplate;

    INSERT INTO #Constraints (DatabaseName, TableSchema, TableName, ConstraintType, ConstraintName)
    EXEC (@Sql);

    -- Next DB
    SELECT @DbId = MIN(Id) FROM #Databases WHERE Id > @DbId;
END;

-- Final output
SELECT 
    DatabaseName,
    TableSchema,
    TableName,
    ConstraintType,
    ConstraintName
FROM #Constraints
ORDER BY 
    DatabaseName, 
    TableSchema, 
    TableName, 
    ConstraintType, 
    ConstraintName;

-- Cleanup
DROP TABLE IF EXISTS #Databases;
DROP TABLE IF EXISTS #Constraints;

How to rename unnamed constraints

You can use the following script to generate a rename statement for an unnamed constraint in your current database.

Note: This script does not rename constraints automatically. It only generates a list of sp_rename statements that you can — and shouldreview carefully before execution.

/*
================================================================================
  Script:     GenerateRenameStatements_UnnamedConstraints.sql
  Purpose:    In the current database, find all constraints that were 
              system-named (unnamed by the developer) and generate 
              `sp_rename` statements to rename them according to a 
              consistent convention.

  Constraint types: 
    - PK (Primary Key)
    - UQ (Unique Constraint)
    - CK (Check Constraint)
    - DF (Default Constraint)
    - FK (Foreign Key)

  Behavior:
    - This script DOES NOT rename constraints automatically.
    - It only prints recommended `sp_rename` commands for review.
    - Safe to run multiple times.

  Author:     Ryszard Kozlowski
  Website:    https://www.ittiger.net/

  Compatibility:
    - Works on older SQL Server versions (no STRING_AGG)
================================================================================
*/

SET NOCOUNT ON;

-- Clean up temp table if it exists
DROP TABLE IF EXISTS #RenameStatements;

CREATE TABLE #RenameStatements
(
    Id INT IDENTITY(1,1) PRIMARY KEY,
    RenameStatement NVARCHAR(MAX)
);

-- Insert sp_rename commands for all system-named constraints in the current DB
INSERT INTO #RenameStatements (RenameStatement)
SELECT 
    'EXEC sp_rename N''' 
        + QUOTENAME(OBJECT_SCHEMA_NAME(c.parent_object_id)) + '.' + QUOTENAME(c.name) + ''', N'''
        + 
        CASE c.type
            WHEN 'PK' THEN 'PK_' + t.name
            WHEN 'UQ' THEN 'UQ_' + t.name + '_' + STUFF((
                SELECT '_' + c2.name
                FROM sys.index_columns ic
                JOIN sys.columns c2 
                    ON c2.object_id = ic.object_id AND c2.column_id = ic.column_id
                WHERE ic.object_id = i.object_id AND ic.index_id = i.index_id
                ORDER BY ic.key_ordinal
                FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
            WHEN 'C'  THEN 'CK_' + t.name + '_' + col.name
            WHEN 'D'  THEN 'DF_' + t.name + '_' + col.name
            WHEN 'F'  THEN 'FK_' + t.name + '_' + rt.name
            ELSE 'UNKNOWN_' + c.name
        END + ''', ''OBJECT'';'
FROM 
    sys.objects AS c
    JOIN sys.tables AS t ON t.object_id = c.parent_object_id
    LEFT JOIN sys.default_constraints dc ON dc.object_id = c.object_id AND dc.is_system_named = 1
    LEFT JOIN sys.check_constraints cc ON cc.object_id = c.object_id AND cc.is_system_named = 1
    LEFT JOIN sys.foreign_keys fk ON fk.object_id = c.object_id AND fk.is_system_named = 1
    LEFT JOIN sys.key_constraints k ON k.object_id = c.object_id AND k.is_system_named = 1
    LEFT JOIN sys.indexes i ON i.object_id = t.object_id AND i.name = c.name
    LEFT JOIN sys.columns col 
        ON col.object_id = t.object_id 
        AND (
            (dc.parent_column_id = col.column_id) OR 
            (cc.parent_column_id = col.column_id)
        )
    LEFT JOIN sys.foreign_keys f ON f.object_id = c.object_id
    LEFT JOIN sys.tables rt ON rt.object_id = f.referenced_object_id
WHERE 
    c.type IN ('PK','UQ','C','D','F')
    AND (
        dc.is_system_named = 1 
        OR cc.is_system_named = 1
        OR fk.is_system_named = 1
        OR k.is_system_named = 1
    )
ORDER BY t.name, c.type;

-- Output sp_rename statements for review
SELECT RenameStatement
FROM #RenameStatements
ORDER BY Id;

-- Clean up
DROP TABLE IF EXISTS #RenameStatements;

Bottom line

Naming your constraints is a small habit with a big impact. It keeps your deployments predictable and your scripts portable — exactly how robust systems should be.