I ran into this problem several times and ending up making a stored proc that can take any primary key constraint and turn into unique index. It drops and recreates anything referencing the table in question, (which is the tricky part for highly referenced tables).
It can handle multi-part primary keys.
CREATE PROCEDURE spPrimaryKeyToUniqueKey
@schema VARCHAR(255),
@tableName VARCHAR(255)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @constraintsToDrop NVARCHAR(MAX) = '';
DECLARE @constraintsToCreate NVARCHAR(MAX) = '';
DECLARE @indexesToDrop NVARCHAR(MAX) = '';
DECLARE @indexesToCreate NVARCHAR(MAX) = '';
;WITH _fks AS (
SELECT
FkName = fk.[name]
, SchemaName = sch.[name]
, TableName = tab1.[name]
, ColumnName = col1.[name]
, ReferencedSchema = sch2.[name]
, ReferencedTableName = tab2.[name]
, ReferencedColumnName = col2.[name]
, ReferencedColOrder = ic.key_ordinal
FROM sys.foreign_key_columns fkc
JOIN sys.foreign_keys fk ON fk.object_id = fkc.constraint_object_id
JOIN sys.tables tab1 ON tab1.object_id = fkc.parent_object_id
JOIN sys.schemas sch ON tab1.schema_id = sch.schema_id
JOIN sys.columns col1 ON col1.column_id = parent_column_id AND col1.object_id = tab1.object_id
JOIN sys.tables tab2 ON tab2.object_id = fkc.referenced_object_id
JOIN sys.schemas sch2 ON sch2.schema_id = tab2.schema_id
JOIN sys.columns col2 ON col2.column_id = fkc.referenced_column_id AND col2.object_id = tab2.object_id
JOIN sys.indexes i ON fk.key_index_id = i.index_id AND i.object_id = tab2.object_id
JOIN sys.index_columns ic ON col2.column_id = ic.column_id AND ic.object_id = tab2.object_id AND i.index_id = ic.index_id
)
, _fksWithColList AS (
SELECT
f.FkName
, f.SchemaName
, f.TableName
, f.ReferencedSchema
, f.ReferencedTableName
, TableColNameList = STUFF(
(SELECT ',[' + ff.ColumnName + ']'
FROM _fks ff
WHERE f.FkName = ff.FkName AND f.SchemaName = ff.SchemaName AND f.TableName = ff.TableName AND f.ReferencedSchema = ff.ReferencedSchema AND f.ReferencedTableName = ff.ReferencedTableName
ORDER BY ff.ReferencedColOrder
FOR XML PATH('')), 1, 1, '')
, ReferencedTableColNameList = STUFF(
(SELECT ',[' + ff.ReferencedColumnName + ']'
FROM _fks ff
WHERE f.FkName = ff.FkName AND f.SchemaName = ff.SchemaName AND f.TableName = ff.TableName AND f.ReferencedSchema = ff.ReferencedSchema AND f.ReferencedTableName = ff.ReferencedTableName
ORDER BY ff.ReferencedColOrder
FOR XML PATH('')), 1, 1, '')
FROM _fks f
GROUP BY f.FkName, f.SchemaName, f.TableName, f.ReferencedSchema, f.ReferencedTableName
)
, _commands AS (
SELECT *,
DropStatement = REPLACE(REPLACE(REPLACE('ALTER TABLE [{0}].[{1}] DROP CONSTRAINT IF EXISTS [{2}];',
'{0}', SchemaName),
'{1}', TableName),
'{2}', FkName),
CreateStatement = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE('ALTER TABLE [{0}].[{1}] DROP CONSTRAINT IF EXISTS [{2}]; ALTER TABLE [{0}].[{1}] ADD CONSTRAINT [{2}] FOREIGN KEY ({3}) REFERENCES [{4}].[{5}]({6});',
'{0}', SchemaName),
'{1}', TableName),
'{2}', FkName),
'{3}', TableColNameList),
'{4}', ReferencedSchema),
'{5}', ReferencedTableName),
'{6}', ReferencedTableColNameList)
FROM _fksWithColList
WHERE ReferencedSchema = @schema AND ReferencedTableName = @tableName
)
SELECT @constraintsToDrop = @constraintsToDrop + DropStatement + CHAR(13)+CHAR(10), @constraintsToCreate = @constraintsToCreate + c.CreateStatement + CHAR(13)+CHAR(10)
FROM _commands c
;WITH _indexes AS (
SELECT
TableName = t.[name]
, SchemaName = s.[name]
, IndexName = i.[name]
, ColumnName = c.[name]
, ColOrder = ic.key_ordinal
, ColDesc = ic.is_descending_key
FROM sys.tables t
JOIN sys.schemas s ON t.schema_id = s.schema_id
JOIN sys.indexes i ON t.object_id = i.object_id
JOIN sys.columns c ON t.object_id = c.object_id
JOIN sys.index_columns ic ON t.object_id = ic.object_id AND c.column_id = ic.column_id AND i.index_id = ic.index_id
WHERE s.[name] = @schema AND t.[name] = @tableName AND i.is_primary_key = 1
)
, indexesWithColList AS (
SELECT
TableName
, SchemaName
, IndexName
, IndexColList = STUFF(
(SELECT ',[' + ii.ColumnName + ']' + IIF(ii.ColDesc = 1, ' DESC', '')
FROM _indexes ii
WHERE i.TableName = ii.TableName AND i.SchemaName = ii.SchemaName AND i.IndexName = ii.IndexName AND ii.ColOrder > 0
ORDER BY ii.ColOrder
FOR XML PATH('')), 1, 1, '')
, IndexColNameList = STUFF(
(SELECT '_' + ii.ColumnName
FROM _indexes ii
WHERE i.TableName = ii.TableName AND i.SchemaName = ii.SchemaName AND i.IndexName = ii.IndexName AND ii.ColOrder > 0
ORDER BY ii.ColOrder
FOR XML PATH('')), 1, 1, '')
FROM _indexes i
GROUP BY i.TableName, i.SchemaName, i.IndexName
)
, _commands AS (
SELECT *,
DropStatement = REPLACE(REPLACE(REPLACE('ALTER TABLE [{0}].[{1}] DROP CONSTRAINT {2};',
'{0}', i.SchemaName),
'{1}', i.TableName),
'{2}', i.IndexName),
CreateStatement = REPLACE(REPLACE(REPLACE(REPLACE('CREATE UNIQUE NONCLUSTERED INDEX [{0}] ON [{1}].[{2}] ({3});',
'{0}', 'UX_' + i.SchemaName + '_' + i.TableName + '_' + i.IndexColNameList),
'{1}', i.SchemaName),
'{2}', i.TableName),
'{3}', i.IndexColList)
FROM indexesWithColList i
)
SELECT @indexesToDrop = @indexesToDrop + c.DropStatement + CHAR(13)+CHAR(10), @indexesToCreate = @indexesToCreate + c.CreateStatement + CHAR(13)+CHAR(10)
FROM _commands c
DECLARE @sql NVARCHAR(MAX);
SET @sql = REPLACE(REPLACE(REPLACE(REPLACE('
SET XACT_ABORT ON;
BEGIN TRAN t1
--Drop foreign keys
{0}
--Drop indexes
{1}
--Create indexes
{2}
--Recreate foreign keys
{3}
COMMIT TRAN t1
',
'{0}', @constraintsToDrop),
'{1}', @indexesToDrop),
'{2}', @indexesToCreate),
'{3}', @constraintsToCreate);
PRINT @sql;
EXEC sp_executesql @sql
END
GO