Last active
March 18, 2026 20:57
-
-
Save tcartwright/8c26dbafb37e94889478292fbf388a40 to your computer and use it in GitHub Desktop.
SQL SERVER: Identity Column Capacity Report
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- ============================================================ | |
| -- 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