Skip to content

Instantly share code, notes, and snippets.

@tcartwright
Last active March 18, 2026 20:57
Show Gist options
  • Select an option

  • Save tcartwright/8c26dbafb37e94889478292fbf388a40 to your computer and use it in GitHub Desktop.

Select an option

Save tcartwright/8c26dbafb37e94889478292fbf388a40 to your computer and use it in GitHub Desktop.
SQL SERVER: Identity Column Capacity Report
-- ============================================================
-- Identity Column Capacity Report
-- ============================================================
-- Reports on all tables with identity columns that have 100,000
-- or more rows, showing how much ID space remains and when each
-- table is projected to exhaust its identity range.
--
-- For each qualifying table this query provides:
-- - Current identity value and data type maximum
-- - Remaining IDs available before overflow
-- - Average daily insert rate (sampled since last SQL Server
-- service restart via sys.dm_db_index_operational_stats)
-- - Estimated days remaining and projected exhaustion date
--
-- Results are sorted by risk: int columns first (lowest ceiling),
-- then by percentage of ID range already consumed descending.
-- Exhaustion dates beyond ~500 years are suppressed to avoid
-- integer overflow in DATEADD and shown as a year estimate instead.
--
-- Usage:
-- 1. Change the USE statement to your target database.
-- 2. Run as-is -- all identity tables are auto-discovered.
-- 3. Pay close attention to any int columns near exhaustion;
-- consider ALTER TABLE ... ALTER COLUMN to bigint if at risk.
-- ============================================================
USE [YOUR_DB_NAME]; -- ← set this before running
GO
-- Cap days at ~500 years before DATEADD to prevent int overflow
DECLARE @MaxMeaningfulDays INT = 182500;
-- Tables need to have this many rows to be included in the results, to avoid noise from very small tables where insert patterns may not be established
DECLARE @MinRowLimit INT = 100000
SELECT
s.name + '.' + t.name AS table_name,
ic.name AS identity_column,
tp.name AS data_type,
FORMAT(CAST(IDENT_CURRENT(
'[' + s.name + '].[' + t.name + ']')
AS BIGINT), 'N0') AS current_value,
FORMAT(
CASE tp.name
WHEN 'tinyint' THEN 255
WHEN 'smallint' THEN 32767
WHEN 'int' THEN 2147483647
WHEN 'bigint' THEN 9223372036854775807
ELSE 2147483647
END, 'N0') AS max_allowed_value,
FORMAT(
( CASE tp.name
WHEN 'tinyint' THEN 255
WHEN 'smallint' THEN 32767
WHEN 'int' THEN 2147483647
WHEN 'bigint' THEN 9223372036854775807
ELSE 2147483647
END
- CAST(IDENT_CURRENT('[' + s.name + '].[' + t.name + ']') AS BIGINT)
) / CAST(ic.increment_value AS BIGINT)
, 'N0') AS remaining_ids,
CAST(DATEDIFF(DAY, si.sqlserver_start_time, GETDATE()) AS INT)
AS stats_sample_days,
CASE
WHEN ops.leaf_insert_count = 0 THEN 'No insert activity recorded'
ELSE FORMAT(CEILING(
ops.leaf_insert_count * 1.0
/ NULLIF(DATEDIFF(DAY, si.sqlserver_start_time, GETDATE()), 0)
), 'N0')
END AS avg_inserts_per_day,
CASE
WHEN ops.leaf_insert_count = 0 THEN 'Cannot estimate'
WHEN (
( CASE tp.name
WHEN 'tinyint' THEN 255
WHEN 'smallint' THEN 32767
WHEN 'int' THEN 2147483647
WHEN 'bigint' THEN 9223372036854775807
ELSE 2147483647
END
- CAST(IDENT_CURRENT('[' + s.name + '].[' + t.name + ']') AS BIGINT)
) / CAST(ic.increment_value AS BIGINT)
) / NULLIF(ops.leaf_insert_count * 1.0
/ NULLIF(DATEDIFF(DAY, si.sqlserver_start_time, GETDATE()), 0), 0)
> @MaxMeaningfulDays
THEN FORMAT(CEILING((
( CASE tp.name
WHEN 'tinyint' THEN 255
WHEN 'smallint' THEN 32767
WHEN 'int' THEN 2147483647
WHEN 'bigint' THEN 9223372036854775807
ELSE 2147483647
END
- CAST(IDENT_CURRENT('[' + s.name + '].[' + t.name + ']') AS BIGINT)
) / CAST(ic.increment_value AS BIGINT)
) / NULLIF(ops.leaf_insert_count * 1.0
/ NULLIF(DATEDIFF(DAY, si.sqlserver_start_time, GETDATE()), 0), 0)
/ 365.25), 'N0')
ELSE FORMAT(CEILING((
( CASE tp.name
WHEN 'tinyint' THEN 255
WHEN 'smallint' THEN 32767
WHEN 'int' THEN 2147483647
WHEN 'bigint' THEN 9223372036854775807
ELSE 2147483647
END
- CAST(IDENT_CURRENT('[' + s.name + '].[' + t.name + ']') AS BIGINT)
) / CAST(ic.increment_value AS BIGINT)
) / NULLIF(ops.leaf_insert_count * 1.0
/ NULLIF(DATEDIFF(DAY, si.sqlserver_start_time, GETDATE()), 0), 0)
), 'N0')
END AS estimated_remaining,
CASE
WHEN ops.leaf_insert_count = 0 THEN 'N/A'
WHEN (
( CASE tp.name
WHEN 'tinyint' THEN 255
WHEN 'smallint' THEN 32767
WHEN 'int' THEN 2147483647
WHEN 'bigint' THEN 9223372036854775807
ELSE 2147483647
END
- CAST(IDENT_CURRENT('[' + s.name + '].[' + t.name + ']') AS BIGINT)
) / CAST(ic.increment_value AS BIGINT)
) / NULLIF(ops.leaf_insert_count * 1.0
/ NULLIF(DATEDIFF(DAY, si.sqlserver_start_time, GETDATE()), 0), 0)
> @MaxMeaningfulDays
THEN 'Years (> 500 yr range)'
ELSE 'Days'
END AS unit,
CASE
WHEN ops.leaf_insert_count = 0 THEN NULL
WHEN (
( CASE tp.name
WHEN 'tinyint' THEN 255
WHEN 'smallint' THEN 32767
WHEN 'int' THEN 2147483647
WHEN 'bigint' THEN 9223372036854775807
ELSE 2147483647
END
- CAST(IDENT_CURRENT('[' + s.name + '].[' + t.name + ']') AS BIGINT)
) / CAST(ic.increment_value AS BIGINT)
) / NULLIF(ops.leaf_insert_count * 1.0
/ NULLIF(DATEDIFF(DAY, si.sqlserver_start_time, GETDATE()), 0), 0)
> @MaxMeaningfulDays
THEN NULL
ELSE CAST(DATEADD(DAY, CAST(CEILING((
( CASE tp.name
WHEN 'tinyint' THEN 255
WHEN 'smallint' THEN 32767
WHEN 'int' THEN 2147483647
WHEN 'bigint' THEN 9223372036854775807
ELSE 2147483647
END
- CAST(IDENT_CURRENT('[' + s.name + '].[' + t.name + ']') AS BIGINT)
) / CAST(ic.increment_value AS BIGINT)
) / NULLIF(ops.leaf_insert_count * 1.0
/ NULLIF(DATEDIFF(DAY, si.sqlserver_start_time, GETDATE()), 0), 0)
) AS INT), GETDATE()) AS DATE)
END AS projected_exhaustion_date
FROM sys.identity_columns ic
JOIN sys.tables t ON ic.object_id = t.object_id
JOIN sys.schemas s ON t.schema_id = s.schema_id
JOIN sys.types tp ON ic.user_type_id = tp.user_type_id
CROSS JOIN sys.dm_os_sys_info si
JOIN (
SELECT
t2.object_id,
SUM(ops.leaf_insert_count) AS leaf_insert_count
FROM sys.tables t2
JOIN sys.identity_columns ic2 ON t2.object_id = ic2.object_id
CROSS APPLY sys.dm_db_index_operational_stats(DB_ID(), t2.object_id, NULL, NULL) ops
GROUP BY t2.object_id
) ops ON ops.object_id = t.object_id
JOIN (
SELECT object_id, SUM(row_count) AS row_count
FROM sys.dm_db_partition_stats
WHERE index_id IN (0, 1) -- heap or clustered index only
GROUP BY object_id
HAVING SUM(row_count) >= @MinRowLimit
) rc ON rc.object_id = t.object_id
ORDER BY
-- Sort tables with int columns and higher usage % to the top as they are most at risk
CASE tp.name WHEN 'int' THEN 0 ELSE 1 END,
CAST(IDENT_CURRENT('[' + s.name + '].[' + t.name + ']') AS BIGINT) * 1.0
/ CASE tp.name
WHEN 'tinyint' THEN 255
WHEN 'smallint' THEN 32767
WHEN 'int' THEN 2147483647
WHEN 'bigint' THEN 9223372036854775807
ELSE 2147483647
END DESC;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment