An insert can fail even when the database has plenty of disk space. IDENTITY columns have a finite numeric range, and an ascending int stops at 2,147,483,647. Check the remaining range while you still have time to plan the schema change.

Separate Number Space From Row Count
The identity generator allocates numbers, not a count of surviving rows. Deleted rows, rolled-back inserts, failed inserts, and cached allocations can leave gaps. A table with modest row count can therefore be near its type boundary. I inspect identity metadata before counting rows. A COUNT result cannot tell me which value the next insert will try.
The common ascending int limit is straightforward. A descending identity approaches the minimum instead. Smallint and tinyint have much smaller ranges, while bigint provides substantially more space. Decimal and numeric identity columns also exist with scale zero. Their precision determines the limit, so a complete inventory should include them.
Inventory All Identity Columns in the Database
sys.identity_columns exposes seed_value, increment_value, and last_value as sql_variant. Convert them deliberately. Join to the table catalog for names and use the underlying system type for its limits. Run the query in each user database through your existing inventory process. Metadata visibility depends on your permissions.
WITH identity_values AS
(
SELECT SCHEMA_NAME(t.schema_id) AS schema_name,
t.name AS table_name,c.name AS column_name,
TYPE_NAME(c.system_type_id) AS type_name,
c.system_type_id,c.precision,
CONVERT(decimal(38,0),c.seed_value) AS seed_value,
CONVERT(decimal(38,0),c.increment_value) AS increment_value,
CONVERT(decimal(38,0),c.last_value) AS current_value
FROM sys.identity_columns AS c
JOIN sys.tables AS t ON t.object_id=c.object_id
), limits AS
(
SELECT *,CONVERT(decimal(38,0),CASE system_type_id
WHEN 48 THEN '255' WHEN 52 THEN '32767'
WHEN 56 THEN '2147483647'
WHEN 127 THEN '9223372036854775807'
ELSE REPLICATE('9',precision) END) AS maximum_value,
CONVERT(decimal(38,0),CASE system_type_id
WHEN 48 THEN '0' WHEN 52 THEN '-32768'
WHEN 56 THEN '-2147483648'
WHEN 127 THEN '-9223372036854775808'
ELSE '-'+REPLICATE('9',precision) END) AS minimum_value
FROM identity_values
), direction AS
(
SELECT *,CASE WHEN increment_value>0
THEN maximum_value ELSE minimum_value END AS boundary
FROM limits
)
SELECT schema_name,table_name,column_name,type_name,
seed_value,increment_value,current_value,
minimum_value,maximum_value,boundary,
CASE WHEN current_value IS NULL THEN 0.0 ELSE
100.0*(CONVERT(float,current_value)-CONVERT(float,seed_value)) /
NULLIF(CONVERT(float,boundary)-CONVERT(float,seed_value),0)
END AS percent_of_seed_range_used,
CASE WHEN current_value IS NOT NULL THEN
FLOOR((CONVERT(float,boundary)-CONVERT(float,current_value)) /
CONVERT(float,increment_value))
END AS approximate_steps_remaining
FROM direction
ORDER BY percent_of_seed_range_used DESC;Read the Direction and the Percentage
The query shows the exact type boundaries and current metadata value. Its percentage uses floating-point arithmetic to avoid overflow across the widest numeric ranges. Treat the remaining-step calculation as a planning approximation for those large types. A seed equal to the boundary gives a NULL percentage once a value exists, because the span is zero. That row sorts last, yet it needs attention first.
For a positive increment, the upper bound is the destination. For a negative increment, the lower bound is the destination. An increment of ten consumes number space ten times as fast per allocation as an increment of one. Which boundary does your actual definition approach? Do not alert only on a large positive current value.
Account for Reseeded and Empty Identity Columns
last_value can be NULL for an identity that has not allocated a value. A reseed changes the generator's current position. It can make a seed-based percentage negative or otherwise surprising, and it does not prove that earlier numbers are available. Check the actual table definition and the history of reseeding before trusting the percentage as capacity.
DBCC CHECKIDENT ('dbo.YourTable', NORESEED);Replace the table name before running this read-only check. It reports the current identity value and the highest stored key. For a descending identity, the stored value it reports is the lowest one instead. I compare generator state with stored keys before recommending any reseed. A number already referenced elsewhere cannot be reclaimed just because an operator dislikes gaps.

Estimate Time to Exhaustion
Store identity snapshots with capture time. Compare allocated-value change over a representative interval, adjusting for increment direction. Use busy periods and batch loads, not just a quiet weekend. Project a planning horizon from allocation rate and remaining steps. Set an alert far enough ahead to cover design, deployment, rollback preparation, and client testing.
A percentage alone hides scale. Ninety percent of a tiny range and ninety percent of bigint are very different situations. I report the current value, remaining range, allocation rate, and estimated maintenance lead time together. The next insert has no interest in your project's approval calendar.
Plan the Bigint Move for Identity Columns
Changing an int identity to bigint can affect indexes, primary keys, foreign keys, replication features, application models, and downstream data stores. Inventory every referencing key and test the migration on a restored copy. Expect storage, logging, and locking costs. A large table alteration needs a measured window or a staged migration design.
SELECT fk.name AS foreign_key_name,
OBJECT_SCHEMA_NAME(fkc.parent_object_id) AS child_schema,
OBJECT_NAME(fkc.parent_object_id) AS child_table,
COL_NAME(fkc.parent_object_id,fkc.parent_column_id) AS child_column
FROM sys.foreign_key_columns AS fkc
JOIN sys.foreign_keys AS fk ON fk.object_id=fkc.constraint_object_id
WHERE fkc.referenced_object_id=OBJECT_ID(N'dbo.YourTable');The query finds declared database relationships, not application assumptions. Check client integer types, serialized messages, report calculations, and integration schemas too. Preserve existing key values so relationships remain valid. A larger database column does not automatically enlarge every consumer's integer field.
Treat Negative Reseeding as a Temporary Measure
An ascending identity that began at one has an unused negative range only if no negative keys already exist. Reseeding there can buy time, but only after checking collisions and business rules. It also changes ordering assumptions and can break software that treats positive IDs as mandatory. The eventual type migration remains necessary.
Do not run a generic negative reseed across a server. Use an approved table-specific plan, verify all dependent paths, and test the next allocations in isolation. I regard it as an emergency bridge with an end date. It is not a substitute for fixing the key's capacity.
Keep the Capacity Check in Operations
An identity primary key can be referenced outside declared foreign keys. Search integration contracts and application models for a 32-bit assumption. A bigint database value can overflow a client's integer even when the insert succeeds. Test generated-key retrieval and round trips through serialization, caches, and messages. Include the highest values the new design must support.
If you stage a replacement table, preserve the existing keys exactly. Review IDENTITY_INSERT, indexes, permissions, triggers, and dependent foreign keys in the migration plan. Reconcile row counts and key ranges before switching writers. Then test the generator's next allocation separately. Copying all rows does not prove that the next generated key will be correct. I keep that check visible in the handoff.
After migration, verify identity properties, key values, foreign keys, and application inserts. Keep the inventory scheduled because another smallint column can become the next bottleneck. Store snapshots outside the application table and alert on direction-aware remaining capacity.
I prefer this check while inserts still succeed. It turns an arithmetic-overflow incident into a planned change with evidence. Read the type, seed, increment, current state, and dependencies together before deciding how much room remains.
Related reading on this blog: ALTER Column from INT to BIGINT: Error and Solutions and Identity Jumping 1000: IDENTITY_CACHE.

An identity check is not a row count, it is a capacity check for the next allocation.
Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.
Discover more from SQL Authority with Pinal Dave
Subscribe to get the latest posts sent to your email.




