Introduction
Agar tum SQL Server me kaam karte ho, to tumne ye problem zarur dekhi hogi:
Different tables me different default values
Constraint naming random hoti hai
Audit trail nahi hota
Change karte time risk hota hai
๐ Isi problem ko solve karne ke liye hum ek smart stored procedure + logging system banayenge jo:
Default constraints ko standardize karega
Safe tareeke se fix karega
Har change ka log rakhega
Rollback ka option dega
โ ๏ธ Problem Statement
Real production database me ye issues common hote hain:
โ Default values inconsistent (0, '', NULL mix)
โ Naming convention follow nahi hota
โ Manual fixes risky hote hain
โ Audit / rollback missing hota hai
๐ Result: Data inconsistency + debugging nightmare
๐ก Solution Overview
Hum 2 cheeze banayenge:
Logging Table โ
ADT_ConstraintLogsSmart SP โ
dbusp_ConstraintMaintenance
๐งพ Logging Table โ ADT_ConstraintLogs
Ye table har ek change ko track karegi:
Old vs New values
Apply script
Rollback script
Status tracking
๐ฆ Table Script
IF OBJECT_ID('dbo.ADT_ConstraintLogs', 'U') IS NULL
BEGIN
CREATE TABLE [dbo].[ADT_ConstraintLogs](
[LogID] [int] IDENTITY(1,1) PRIMARY KEY,
[BatchID] [uniqueidentifier] NOT NULL,
[ExecutedOn] [datetime] NOT NULL,
[SchemaName] [sysname] NOT NULL,
[TableName] [sysname] NOT NULL,
[ColumnName] [sysname] NOT NULL,
[DataType] [sysname] NOT NULL,
[IssueType] [varchar](30) NOT NULL,
[OldConstraint] [sysname] NULL,
[OldDefault] [nvarchar](4000) NULL,
[NewConstraint] [nvarchar](128) NOT NULL,
[NewDefault] [nvarchar](4000) NOT NULL,
[ApplyScript] [nvarchar](max) NOT NULL,
[RollbackScript] [nvarchar](max) NOT NULL,
[Status] [varchar](20) NOT NULL,
[ErrorMessage] [nvarchar](max) NULL,
[ExecutedBy] [sysname] NOT NULL,
[LinkedBatchID] [uniqueidentifier] NULL
)
END
Stored Procedure Overview
IF OBJECT_ID('dbusp_ConstraintMaintenance','P') IS NOT NULL
BEGIN
DROP PROC dbusp_ConstraintMaintenance
END
GO
-- ============================================================
-- SP: dbusp_ConstraintMaintenance
-- ============================================================
-- Parameters:
-- @TableName โ Specific table ya NULL for all
-- @SchemaName โ Default 'dbo'
-- @DryRun โ 1 = sirf report + log, kuch change nahi
-- 0 = actual fix apply karo
--
-- Usage Examples:
-- EXEC dbusp_ConstraintMaintenance @TableName='UserSessions', @DryRun=1
-- EXEC dbusp_ConstraintMaintenance @TableName='UserSessions', @DryRun=0
-- EXEC dbusp_ConstraintMaintenance @DryRun=1 -- all tables
-- ============================================================
CREATE PROCEDURE [dbo].[dbusp_ConstraintMaintenance]
@TableName SYSNAME = '',
@SchemaName SYSNAME = 'dbo',
@DryRun BIT = 1
AS
BEGIN
SET NOCOUNT ON;
IF @TableName = '' SET @TableName = NULL;
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-- Generate a unique BatchID to group all records from this execution
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
DECLARE @BatchID UNIQUEIDENTIFIER = NEWID();
PRINT REPLICATE('=', 65);
PRINT ' SP : dbusp_ConstraintMaintenance';
PRINT ' Batch ID : ' + CAST(@BatchID AS VARCHAR(36));
PRINT ' Schema : ' + @SchemaName;
PRINT ' Table : ' + ISNULL(@TableName, '[ALL TABLES]');
PRINT ' Mode : ' + CASE @DryRun
WHEN 1 THEN 'DRY RUN โ no changes will be applied'
ELSE 'LIVE RUN โ changes WILL be applied'
END;
PRINT REPLICATE('=', 65);
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-- Working table to hold analysis results before any execution
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
DECLARE @Work TABLE
(
RowID INT IDENTITY(1,1),
SchemaName SYSNAME NOT NULL,
TableName SYSNAME NOT NULL,
ColumnName SYSNAME NOT NULL,
DataType SYSNAME NOT NULL,
OldConstraint SYSNAME NULL,
OldDefinition NVARCHAR(4000) NULL, -- Raw definition from sys.default_constraints
NewConstraint NVARCHAR(128) NOT NULL,
FinalDefault NVARCHAR(4000) NOT NULL,
IsNameMismatch BIT NOT NULL,
IsValueMismatch BIT NOT NULL,
IssueType VARCHAR(30) NULL,
ApplyScript NVARCHAR(MAX) NULL,
RollbackScript NVARCHAR(MAX) NULL
);
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-- STEP A: Analyse all eligible columns
-- Eligible = NOT NULL, non-identity, non-primary-key columns
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
INSERT INTO @Work
(
SchemaName, TableName, ColumnName, DataType,
OldConstraint, OldDefinition,
NewConstraint, FinalDefault,
IsNameMismatch, IsValueMismatch
)
SELECT
s.name AS SchemaName,
t.name AS TableName,
c.name AS ColumnName,
ty.name AS DataType,
dc.name AS OldConstraint,
dc.definition AS OldDefinition,
'DF_' + t.name + '_' + c.name AS NewConstraint,
-- โโ FinalDefault: determine what the default value should be โโโโโ
--
-- Rule 1: If an existing default is already a custom (non-standard) value
-- i.e. not one of (0 / '' / NULL) โ preserve it as-is.
-- SUBSTRING(2, LEN-2) strips only the outer parens that
-- SQL Server wraps around dc.definition e.g. (getdate()) โ getdate()
--
-- Rule 2: For standard/missing defaults, assign based on data type:
-- Numeric โ 0
-- String โ ''
-- DateTime โ NULL
-- Others โ NULL
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
CASE
-- Existing custom default: not 0, not '', not NULL โ preserve as-is
WHEN dc.definition IS NOT NULL
AND LOWER(REPLACE(REPLACE(dc.definition, '(', ''), ')', ''))
NOT IN ('0', '''''', 'null', '')
THEN
-- Strip only outer parens: (getdate()) โ getdate() | ('Active') โ 'Active'
SUBSTRING(dc.definition, 2, LEN(dc.definition) - 2)
-- Numeric types: default = 0
WHEN ty.name IN (
'int', 'bigint', 'smallint', 'tinyint',
'bit', 'decimal', 'numeric',
'float', 'real', 'money', 'smallmoney'
)
THEN '0'
-- String types: default = '' (empty string)
WHEN ty.name IN (
'char', 'varchar', 'nchar', 'nvarchar', 'text', 'ntext'
)
THEN ''''''
-- Date/time types: default = NULL
WHEN ty.name IN (
'date', 'datetime', 'datetime2',
'smalldatetime', 'time', 'datetimeoffset'
)
THEN 'NULL'
-- All other unrecognised types: default = NULL
ELSE 'NULL'
END AS FinalDefault,
-- โโ IsNameMismatch: NULL-safe check โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-- If no constraint exists, ISNULL converts NULL to '' so comparison works correctly
CASE
WHEN ISNULL(dc.name, '') <> 'DF_' + t.name + '_' + c.name
THEN 1
ELSE 0
END AS IsNameMismatch,
-- โโ IsValueMismatch: does the existing default match the expected? โ
CASE
-- No constraint at all โ definitely a mismatch
WHEN dc.definition IS NULL
THEN 1
-- Custom (non-standard) value โ preserved as-is โ no mismatch
WHEN LOWER(REPLACE(REPLACE(dc.definition, '(', ''), ')', ''))
NOT IN ('0', '''''', 'null', '')
THEN 0
-- Compare cleaned existing value against expected value
WHEN LOWER(REPLACE(REPLACE(dc.definition, '(', ''), ')', '')) <>
LOWER(
CASE
WHEN ty.name IN (
'int', 'bigint', 'smallint', 'tinyint',
'bit', 'decimal', 'numeric',
'float', 'real', 'money', 'smallmoney'
) THEN '0'
WHEN ty.name IN (
'char', 'varchar', 'nchar', 'nvarchar', 'text', 'ntext'
) THEN ''''''
WHEN ty.name IN (
'date', 'datetime', 'datetime2',
'smalldatetime', 'time', 'datetimeoffset'
) THEN 'null'
ELSE 'null'
END
)
THEN 1
ELSE 0
END AS IsValueMismatch
FROM sys.tables t
JOIN sys.schemas s ON t.schema_id = s.schema_id
JOIN sys.columns c ON t.object_id = c.object_id
JOIN sys.types ty ON c.user_type_id = ty.user_type_id
LEFT JOIN sys.default_constraints dc
ON dc.parent_object_id = c.object_id
AND dc.parent_column_id = c.column_id
WHERE
c.is_nullable = 0 -- Only NOT NULL columns
AND c.is_identity = 0 -- Exclude IDENTITY columns
AND s.name = @SchemaName
AND (t.name = @TableName OR @TableName IS NULL)
-- Exclude primary key columns
AND c.column_id NOT IN
(
SELECT ic.column_id
FROM sys.index_columns ic
JOIN sys.indexes i
ON i.object_id = ic.object_id
AND i.index_id = ic.index_id
WHERE i.is_primary_key = 1
AND ic.object_id = t.object_id
);
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-- STEP B: Set IssueType label for each row
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
UPDATE @Work
SET IssueType =
CASE
WHEN OldConstraint IS NULL THEN 'Missing'
WHEN IsNameMismatch = 1 AND IsValueMismatch = 1 THEN 'Both'
WHEN IsNameMismatch = 1 THEN 'NameMismatch'
WHEN IsValueMismatch = 1 THEN 'ValueMismatch'
ELSE 'OK'
END;
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-- STEP C: Build ApplyScript and RollbackScript for each problem row
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
UPDATE @Work
SET
-- ApplyScript: drop old constraint (if any), then add new one
ApplyScript =
CASE
WHEN OldConstraint IS NOT NULL
THEN 'ALTER TABLE [' + SchemaName + '].[' + TableName + ']'
+ ' DROP CONSTRAINT [' + OldConstraint + '];' + CHAR(10)
ELSE ''
END
+ 'ALTER TABLE [' + SchemaName + '].[' + TableName + ']'
+ ' ADD CONSTRAINT [' + LEFT(NewConstraint, 128) + ']'
+ ' DEFAULT ' + FinalDefault
+ ' FOR [' + ColumnName + '];',
-- RollbackScript: drop the new constraint, restore the old one if it existed
RollbackScript =
'/* ROLLBACK: Run this script to undo the fix for ['
+ TableName + '].[' + ColumnName + '] */' + CHAR(10)
-- Drop the newly added constraint
+ 'ALTER TABLE [' + SchemaName + '].[' + TableName + ']'
+ ' DROP CONSTRAINT IF EXISTS [' + LEFT(NewConstraint, 128) + '];'
+ CASE
WHEN OldConstraint IS NOT NULL
-- Restore previous constraint using the original raw definition
THEN CHAR(10)
+ 'ALTER TABLE [' + SchemaName + '].[' + TableName + ']'
+ ' ADD CONSTRAINT [' + OldConstraint + ']'
+ ' DEFAULT ' + OldDefinition
+ ' FOR [' + ColumnName + '];'
ELSE
CHAR(10) + '/* No previous constraint existed โ nothing to restore. */'
END
WHERE IssueType <> 'OK';
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-- STEP D: Print and return the analysis result set
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
DECLARE @IssueCount INT;
SELECT @IssueCount = COUNT(*) FROM @Work WHERE IssueType <> 'OK';
PRINT 'Total issues found: ' + CAST(@IssueCount AS VARCHAR(10));
-- Return result set regardless of DryRun or Live mode
SELECT
SchemaName,
TableName,
ColumnName,
DataType,
IssueType,
OldConstraint,
OldDefinition AS OldDefault,
NewConstraint,
FinalDefault AS NewDefault,
ApplyScript,
RollbackScript
FROM @Work
WHERE IssueType <> 'OK'
ORDER BY SchemaName, TableName, ColumnName;
IF @IssueCount = 0
BEGIN
PRINT 'No issues found. All default constraints are correct.';
RETURN;
END;
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-- STEP E: DRY RUN โ Log with Status = 'DryRun', no DDL executed
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
IF @DryRun = 1
BEGIN
INSERT INTO [dbo].[ADT_ConstraintLogs]
(
BatchID, SchemaName, TableName, ColumnName, DataType, IssueType,
OldConstraint, OldDefault, NewConstraint, NewDefault,
ApplyScript, RollbackScript, Status
)
SELECT
@BatchID,
SchemaName, TableName, ColumnName, DataType, IssueType,
OldConstraint, OldDefinition, NewConstraint, FinalDefault,
ApplyScript, RollbackScript,
'DryRun'
FROM @Work
WHERE IssueType <> 'OK';
PRINT '';
PRINT 'DRY RUN complete. No changes were applied.';
PRINT CAST(@IssueCount AS VARCHAR(10)) + ' issue(s) logged with Status = DryRun.';
PRINT 'Batch ID : ' + CAST(@BatchID AS VARCHAR(36));
PRINT '';
PRINT 'Review : SELECT * FROM ADT_ConstraintLogs WHERE BatchID = '''
+ CAST(@BatchID AS VARCHAR(36)) + '''';
PRINT 'Go Live : EXEC dbusp_ConstraintMaintenance'
+ ' @TableName=''' + ISNULL(@TableName, '') + ''','
+ ' @SchemaName=''' + @SchemaName + ''','
+ ' @DryRun=0';
RETURN;
END;
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-- STEP E-2: Mark all previous DryRun records for this table as Superseded
-- This prevents confusion when multiple dry runs exist
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
UPDATE [dbo].[ADT_ConstraintLogs]
SET Status = 'Superseded',
LinkedBatchID = @BatchID -- Point to current Live BatchID
WHERE Status = 'DryRun'
AND SchemaName = @SchemaName
AND (TableName = @TableName OR @TableName IS NULL);
PRINT CAST(@@ROWCOUNT AS VARCHAR(10))
+ ' previous DryRun record(s) marked as Superseded.';
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-- STEP F: LIVE RUN โ Execute each fix with TRY/CATCH + per-row transaction
-- Each row is an independent transaction so one failure
-- does not block other rows from being processed.
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
DECLARE
@C_Schema SYSNAME,
@C_Table SYSNAME,
@C_Column SYSNAME,
@C_DataType SYSNAME,
@C_OldConst SYSNAME,
@C_OldDef NVARCHAR(4000),
@C_NewConst NVARCHAR(128),
@C_FinalDef NVARCHAR(4000),
@C_ApplyScript NVARCHAR(MAX),
@C_RollScript NVARCHAR(MAX),
@C_IssueType VARCHAR(30),
@ExecSQL NVARCHAR(MAX),
@LogID INT,
@SuccessCount INT = 0,
@FailCount INT = 0;
DECLARE cur CURSOR LOCAL FAST_FORWARD FOR
SELECT
SchemaName, TableName, ColumnName, DataType,
OldConstraint, OldDefinition,
NewConstraint, FinalDefault,
ApplyScript, RollbackScript, IssueType
FROM @Work
WHERE IssueType <> 'OK'
ORDER BY SchemaName, TableName, ColumnName;
OPEN cur;
FETCH NEXT FROM cur INTO
@C_Schema, @C_Table, @C_Column, @C_DataType,
@C_OldConst, @C_OldDef,
@C_NewConst, @C_FinalDef,
@C_ApplyScript, @C_RollScript, @C_IssueType;
WHILE @@FETCH_STATUS = 0
BEGIN
-- Insert a Pending log record before executing,
-- so we have a record even if something goes critically wrong
INSERT INTO [dbo].[ADT_ConstraintLogs]
(
BatchID, SchemaName, TableName, ColumnName, DataType, IssueType,
OldConstraint, OldDefault, NewConstraint, NewDefault,
ApplyScript, RollbackScript, Status
)
VALUES
(
@BatchID, @C_Schema, @C_Table, @C_Column, @C_DataType, @C_IssueType,
@C_OldConst, @C_OldDef, @C_NewConst, @C_FinalDef,
@C_ApplyScript, @C_RollScript, 'Pending'
);
SET @LogID = SCOPE_IDENTITY();
BEGIN TRY
BEGIN TRANSACTION;
-- Drop old constraint only if one existed
IF @C_OldConst IS NOT NULL
BEGIN
SET @ExecSQL =
'ALTER TABLE [' + @C_Schema + '].[' + @C_Table + ']'
+ ' DROP CONSTRAINT [' + @C_OldConst + '];';
PRINT 'DROP โ ' + @ExecSQL;
EXEC sp_executesql @ExecSQL;
END;
-- Add the correctly named constraint with the correct default value
SET @ExecSQL =
'ALTER TABLE [' + @C_Schema + '].[' + @C_Table + ']'
+ ' ADD CONSTRAINT [' + LEFT(@C_NewConst, 128) + ']'
+ ' DEFAULT ' + @C_FinalDef
+ ' FOR [' + @C_Column + '];';
PRINT 'ADD โ ' + @ExecSQL;
EXEC sp_executesql @ExecSQL;
COMMIT TRANSACTION;
-- Update log record to Success
UPDATE [dbo].[ADT_ConstraintLogs]
SET Status = 'Success'
WHERE LogID = @LogID;
SET @SuccessCount += 1;
PRINT 'OK โ [' + @C_Table + '].[' + @C_Column + ']';
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
-- Update log record to Failed with error details
UPDATE [dbo].[ADT_ConstraintLogs]
SET Status = 'Failed',
ErrorMessage = ERROR_MESSAGE()
WHERE LogID = @LogID;
SET @FailCount += 1;
PRINT 'FAIL โ [' + @C_Table + '].[' + @C_Column + ']'
+ ' Error: ' + ERROR_MESSAGE();
END CATCH;
FETCH NEXT FROM cur INTO
@C_Schema, @C_Table, @C_Column, @C_DataType,
@C_OldConst, @C_OldDef,
@C_NewConst, @C_FinalDef,
@C_ApplyScript, @C_RollScript, @C_IssueType;
END;
CLOSE cur;
DEALLOCATE cur;
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-- STEP G: Print final summary
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
PRINT '';
PRINT REPLICATE('=', 65);
PRINT ' EXECUTION SUMMARY';
PRINT ' Success : ' + CAST(@SuccessCount AS VARCHAR(10));
PRINT ' Failed : ' + CAST(@FailCount AS VARCHAR(10));
PRINT ' Batch ID : ' + CAST(@BatchID AS VARCHAR(36));
PRINT REPLICATE('=', 65);
PRINT ' To rollback any successful fix, run:';
PRINT ' SELECT RollbackScript FROM ADT_ConstraintLogs';
PRINT ' WHERE BatchID = ''' + CAST(@BatchID AS VARCHAR(36))
+ ''' AND Status = ''Success''';
PRINT REPLICATE('=', 65);
-- Return status summary as a result set
SELECT
Status,
COUNT(*) AS [Count]
FROM [dbo].[ADT_ConstraintLogs]
WHERE BatchID = @BatchID
GROUP BY Status
ORDER BY Status;
END;
GO
๐ Ye SP kaise kaam karti hai?
Step 1: Analysis
All tables scan karta hai
Only NOT NULL + non PK + non identity columns
Step 2: Default Rule Engine
Data Type | Default |
|---|---|
Numeric | 0 |
String | '' |
DateTime | NULL |
๐ Custom values (like GETDATE()) preserve hoti hain
Step 3: Issue Detection
Missing
NameMismatch
ValueMismatch
Both
Step 4: Script Generation
Har column ke liye:
Apply Script
Rollback Script
Step 5: Dry Run Mode
EXEC dbusp_ConstraintMaintenance @DryRun = 1
๐ Sirf check karega, kuch change nahi karega
Step 6: Live Execution
EXEC dbusp_ConstraintMaintenance @DryRun = 0
๐ Actual fix apply karega safely
Step 7: Logging System
Har step log hota hai:
Pending
Success
Failed
Step 8: Rollback Ready
Agar kuch galat ho:
SELECT RollbackScript
FROM ADT_ConstraintLogs
WHERE Status = 'Success'
Best Practices
โ DryRun always before Live
โ Naming standard fix karo
โ Logging maintain karo
โ Rollback script ready rakho
Final Verdict
๐ Ye system ek production-grade solution hai jo:
Safe hai
Scalable hai
Automated hai
Audit-ready hai
Agar tum SQL Developer ho, to ye tumhare toolkit me hona hi chahiye.