1:每个表只能创建一个标识列。

 

如下测试所示,如果表中有一个标识列,新增一个标识列就会遇到错误Multiple identity columns specified for table 'TEST'. Only one identity column per table is allowed.

 

CREATE TABLE dbo.TEST
(
    ID        INT IDENTITY(1,1) ,
    NAME        VARCHAR(32)
);
 
ALTER TABLE dbo.TEST ADD  ID1 INT IDENTITY(10,1)

 

SQL Server解惑——标识列的限制和跳号现象-LMLPHP

 

2:标识列不能被更新。

 

 

   如果你更新标识列,就会遇到类似下面这样的错误。

 

   Cannot update identity column 'xxx'.

 

 

3:SQL Server不能通过ALTER语句修改标识列的increment值大小。

 

如果非要调整标识列的increment值大小,只能通过重建表来实现。如果想通过增加列或删除列的方法,非常麻烦。很多情况下也是不行的。例如,有些情况下需要你对新增的自增标识列更新数据才能保证数据一致性。还有一种非常规方法就是修改系统基表sys.syscolpars。这个后续整理一篇。

 

 

4:SQL Server不能通过ALTER语句修改表标识列的SEED的大小但是可以DBCC CHECKIDENT命令调整。SEED可以调大也可以调小,但是有一些限制!

 

#查看某个表中的自增列当前的值:

DBCC CHECKIDENT (TableName,NORESEED)

 

#调整标识列的当前值(SEED)为50

DBCC CHECKIDENT('dbo.TEST', RESEED, 50);

 

通过DBCC CHECKIDENT命令调整SEED值大小,也是有限制的,如下实验所示:

 

USE AdventureWorks2014;
GO
IF EXISTS (SELECT 1 FROM sys.objects WHERE type='U' AND name='TEST')
BEGIN
    DROP TABLE test;
END
GO
CREATE TABLE dbo.TEST
(
    ID        INT IDENTITY(1,1) ,
    NAME    VARCHAR(32)
);
 
INSERT INTO dbo.TEST
        (  NAME )
SELECT  'K1' UNION ALL
SELECT  'K2' UNION ALL
SELECT  'K3' UNION ALL
SELECT  'K4' UNION ALL
SELECT  'K5' UNION ALL
SELECT  'K6';
 
SET IDENTITY_INSERT dbo.TEST ON;
GO
INSERT INTO dbo.TEST
        ( ID, NAME )
SELECT 13, 'k13';
GO
SET IDENTITY_INSERT dbo.TEST OFF;
GO
 
DBCC CHECKIDENT(test)
 
DBCC CHECKIDENT('test', RESEED ,9);
 
 
 
INSERT INTO dbo.TEST
        (  NAME )
SELECT  'K9'  UNION ALL
SELECT  'K10' UNION ALL
SELECT  'K11' UNION ALL
SELECT  'K12' UNION ALL
SELECT  'K13' ;
SELECT * FROM dbo.TEST;

SQL Server解惑——标识列的限制和跳号现象-LMLPHP

 

如果你修改一下表结构,标识列为主键或有唯一约束的话,

 

CREATE TABLE dbo.TEST
(
    ID        INT IDENTITY(1,1) PRIMARY KEY,
    NAME      VARCHAR(32)
);

 

那么上面脚本运行到插入数据时就会报主键冲突。错误如下所示:

 

 

Msg 2627, Level 14, State 1, Line 38

Violation of PRIMARY KEY constraint 'PK__TEST__3214EC2731C41DF1'. Cannot insert duplicate key in object 'dbo.TEST'. The duplicate key value is (13).

 

 

那么接下来,我们将上面的脚本稍微调整一下,你会看到完全不同的结果。如下所示:

 

USE AdventureWorks2014;
GO
IF EXISTS (SELECT 1 FROM sys.objects WHERE type='U' AND name='TEST')
BEGIN
    DROP TABLE test;
END
GO
CREATE TABLE dbo.TEST
(
    ID        INT IDENTITY(1,1) ,
    NAME      VARCHAR(32)
);
 
INSERT INTO dbo.TEST
        (  NAME )
SELECT  'K1' UNION ALL
SELECT  'K2' UNION ALL
SELECT  'K3' UNION ALL
SELECT  'K4' UNION ALL
SELECT  'K5' UNION ALL
SELECT  'K6';
 
SET IDENTITY_INSERT dbo.TEST ON;
GO
INSERT INTO dbo.TEST
        ( ID, NAME )
SELECT 13, 'k13';
GO
SET IDENTITY_INSERT dbo.TEST OFF;
GO
 
 
 
DBCC CHECKIDENT('test', RESEED ,9);
GO
DBCC CHECKIDENT(test);
GO
 
INSERT INTO dbo.TEST
        (  NAME )
SELECT  'K9'  UNION ALL
SELECT  'K10' UNION ALL
SELECT  'K11' UNION ALL
SELECT  'K12' UNION ALL
SELECT  'K13' ;
SELECT * FROM dbo.TEST;

SQL Server解惑——标识列的限制和跳号现象-LMLPHP

 

这个是实验测试时意外发现的一个问题,当时,它导致我得出不同的实验结果,结论也搞错了,问题出在DBCC CHECKIDENT (table_name),如果表的当前标识值小于标识列中存储的最大标识值,则使用标识列中的最大值对其进行重置。我使用DBCC CHECKIDENT(test)本意是来查看标识列的当前值,所以正确的做法应该用DBCC CHECKIDENT(test, NORESEED)这条命令。其实这里也衍生了一个问题,由于可以人为调整SEED的值,所以标识列的值的唯一性,必须通过PRIMARY KEYUNIQUE约束或者通过UNIQUE索引来实现。将字段设置为标识列并不能保证值的唯一值。

 

 

5: 不能通过ALTER语句将已经存在的一个字段改为标识列

 

CREATE TABLE dbo.TEST
(
    ID        INT ,
    NAME      VARCHAR(32)
);
 
--这种语法是不允许的
ALTER TABLE dbo.TEST  ALTER COLUMN ID IDENTITY(10,1) 

 

6:在内存优化表中,种子和增量必须分别设置为 1、1。 将种子或增量设置为 1 以外的值会导致以下错误:内存优化表不支持使用 1 以外的种子和增量值。另外,必须同时指定种子和增量,或者二者都不指定。 如果二者都未指定,则取默认值 (1,1)

 

 

7:如果事务回滚会导致标识列跳号。如下实验所示,这种现象和Oracle、MySQL数据库的行为一致。

 

--事务回滚导致标识列自增跳号
INSERT INTO dbo.TEST
        (  NAME )
SELECT  'K1' UNION ALL
SELECT  'K2' UNION ALL
SELECT  'K3' UNION ALL
SELECT  'K4' UNION ALL
SELECT  'K5' UNION ALL
SELECT  'K6';
 
BEGIN TRAN
INSERT INTO dbo.TEST
        (  NAME )
SELECT  'K7';
ROLLBACK TRAN;
 
INSERT INTO dbo.TEST
        (  NAME )
SELECT  'KKK';
 
SELECT * FROM dbo.TEST;

 

 

8: 事务内部,可能出现标识列的跳号。

 

 

如下实验所示:

 

 

USE AdventureWorks2014;
GO
IF EXISTS (SELECT 1 FROM sys.objects WHERE type='U' AND name='TEST_TRAN')
BEGIN
    DROP TABLE TEST_TRAN;
END
GO
CREATE TABLE dbo.TEST_TRAN
(
    ID        INT IDENTITY(1,1) PRIMARY KEY,
    TRN_NAME    VARCHAR(32)
);

 

在会话1和会话2同时执行下面SQL语句,模拟并发的事务。

 

 

 

--会话1:

 

DECLARE @row_index INT;
 
SET @row_index =1;
 
BEGIN TRAN
WHILE @row_index <=10
BEGIN
    INSERT INTO TEST_TRAN
    VALUES('TRANS_1');
    SET @row_index +=1;
    WAITFOR DELAY '00:00:01';
END
COMMIT TRAN;

 

--会话2

DECLARE @row_index INT;
 
SET @row_index =1;
 
BEGIN TRAN
WHILE @row_index <=10
BEGIN
    INSERT INTO TEST_TRAN
    VALUES('TRANS_2');
    SET @row_index +=1;
 
    WAITFOR DELAY '00:00:01';
END
COMMIT TRAN;

 

执行完上面脚本后,我们可以看到在并发情况下,同一事务内可能出现跳号。这个可以称其为逻辑跳号

 

SQL Server解惑&mdash;&mdash;标识列的限制和跳号现象-LMLPHP

 

 

 

 

9数据库实例非正常重启(崩溃,故障转移或关闭而导致SQL Server服务意外重启),出现标识列的跳号

 

 

  关于这个,官方文档有简单介绍。

 

Consecutive values after server restart or other failures -SQL Server might cache identity values for performance reasons and some of the assigned values can be lost during a database failure or server restart. This can result in gaps in the identity value upon insert. If gaps are not acceptable then the application should use its own mechanism to generate key values. Using a sequence generator with the NOCACHE option can limit the gaps to transactions that are never committed.

 

  个人简单测试了一下,发现在SQL Server 2012在服务器非正常重启(崩溃,故障转移或关闭而导致SQL Server服务意外重启)后会出现跳号(identity column jump)情况。可以通过启用踪标志272解决这个问题(参考下面链接),SQL Server 2014下测试时,也是如此。注意:如果正常重启SQL Server实例,并不会出现这种情况。这个跟ORACLE SEQUENCE跳号总结中的情况有点类似。

 

https://www.dfarber.com/computer-consulting-blog/articles/how-to-solve-identity-problem-in-sql-2012/

https://blog.sqlauthority.com/2017/03/24/sql-server-jump-identity-column-restart/

https://blog.sqlauthority.com/2018/01/24/sql-server-identity-jumping-1000-identity_cache/

https://www.codeproject.com/Tips/668042/SQL-Server-2012-Auto-Identity-Column-Value-Jump-Is

 

个人测试,在任务管理器,杀掉SQL Server的进程后,发现标识列跳号的大小为1000,根据上面博客资料,标识列跳号的多少还跟标识列的数据类型有关。

 

SQL Server解惑&mdash;&mdash;标识列的限制和跳号现象-LMLPHP

 

不过在SQL Server 2017,引入了新特性IDENTITY_CACHE来解决这个问题!

 

 

按照网上搜索的资料来看,踪标志272让SQL Server使用以前的代码来实现标识列的功能。

 

That flag sets SQL 2012 back to the prior code for IDENTITY fields. However, unless you are actually running out of numbers, there is no reason to use that flag. IDENTITY fields are unique, not sequential. You probably need to rethink your method.

 

那么我们想搞清楚标识列的下一个值保存在哪里呢? SQL Server数据库有个系统视图sys.identity_columns可以查看某个表的标识列的当前值和下一个值。

 

 

SELECT  name ,
        is_identity ,
        seed_value ,
        increment_value ,
        last_value
FROM    sys.identity_columns
WHERE   object_id = OBJECT_ID('TEST');

 

但是 sys.identity_columns是一个系统视图,它的数据来自sys.syscolpars,而视图的字段last_value的值是通过内置函数IdentityProperty计算出来的

 

SET QUOTED_IDENTIFIER ON
SET ANSI_NULLS ON
GO
CREATE VIEW sys.identity_columns AS
    SELECT object_id = id,
        name = name,
        column_id = colid,
        system_type_id = xtype,
        user_type_id = utype,
        max_length = length,
        precision = prec,
        scale = scale,
        collation_name = convert(sysname,CollationPropertyFromId(collationid,'name')),
        is_nullable = sysconv(bit, 1 - (status & 1)),     -- CPM_NOTNULL
        is_ansi_padded = sysconv(bit, status & 2),     -- CPM_NOTRIM
        is_rowguidcol = sysconv(bit, status & 8),     -- CPM_ROWGUIDCOL
        is_identity = sysconv(bit, status & 4),         -- CPM_IDENTCOL
        is_filestream = sysconv(bit, status & 32),     -- CPM_FILESTREAM
        is_replicated = sysconv(bit, status & 0x20000),     -- CPM_REPLICAT
        is_non_sql_subscribed = sysconv(bit, status & 0x40000),     -- CPM_NONSQSSUB
        is_merge_published = sysconv(bit, status & 0x80000),         -- CPM_MERGEREPL
        is_dts_replicated = sysconv(bit, status & 0x100000),         -- CPM_REPLDTS
        is_xml_document = sysconv(bit, 0),
        xml_collection_id = sysconv(int, 0),
        default_object_id = sysconv(int, 0),
        rule_object_id = sysconv(int, 0),
        seed_value = IdentityProperty(id, 'SeedValue'),
        increment_value = IdentityProperty(id, 'IncrementValue'),
        last_value = IdentityProperty(id, 'LastValue'),
        is_not_for_replication = sysconv(bit, status & 0x10000),    -- CPM_ID_REPL
        is_computed = sysconv(bit, status & 16),            -- CPM_COMPUTED                
        sysconv(bit, 0) as is_sparse,
        sysconv(bit, 0) as is_column_set
    FROM sys.syscolpars
    WHERE number = 0    -- SOC_COLUMN
        AND (status & 4) = 4     -- CPM_IDENTCOL
        AND has_access('CO', id) = 1
 
GO

 

 无法获取系统内置函数(built-in function)的定义,所以无法进一步分析标识列是如何保存last_value的,但是个人猜测可能跟系统基表sys.syscolparsidtval字段有关系。DAC模式下查询跟踪,你会发现标识列ID变化后,idtval字段的值也变化了。

 

 

SQL Server解惑&mdash;&mdash;标识列的限制和跳号现象-LMLPHP

 

新建三个表,标识列的自增值分别为1、2、3, ,然后,查询系统基表sys.syscolpars你就会有所发现,如下截图所示

CREATE TABLE test1(id INT IDENTITY(1,1), name VARCHAR(10))
CREATE TABLE test2(id INT IDENTITY(1,2), name VARCHAR(10))
CREATE TABLE test3(id INT IDENTITY(1,3), name VARCHAR(10))

SQL Server解惑&mdash;&mdash;标识列的限制和跳号现象-LMLPHP

 

 

10:TRUNCATE表后,标识列的当前值会变为1

 

 

11:与标识列相关的系统函数的区别。

 

 

SELECT IDENT_CURRENT('dbo.TEST_TRAN');

SELECT IDENT_INCR('dbo.TEST_TRAN');

SELECT IDENT_SEED('dbo.TEST_TRAN')

SELECT SCOPE_IDENTITY();

SELECT @@IDENTITY;

 

 

IDENT_CURRENT 类似于SQL Server 2000 (8.x)的标识函数 SCOPE_IDENTITY @@IDENTITY 这三个函数都返回最后生成的标识值。 但是,上述每个函数中定义的最后的作用域和会话有所不同**

·         IDENT_CURRENT 返回为某个会话和用域中的指定表生成的最新标识值。

·         @@IDENTITY 返回为跨所有作用域的当前会话中的任何表生成的最后一个标识值。

·         SCOPE_IDENTITY 返回为当前会话和当前作用域中的某个表生成的最新标识值。

如果 IDENT_CURRENT 值为 NULL(因为表从未包含行或已被截断),IDENT_CURRENT 函数将返回种子值。

 

 

参考资料:

 

https://docs.microsoft.com/zh-cn/sql/t-sql/statements/create-table-transact-sql-identity-property?view=sql-server-ver15

https://www.dfarber.com/computer-consulting-blog/articles/how-to-solve-identity-problem-in-sql-2012/

https://www.codeproject.com/Tips/668042/SQL-Server-2012-Auto-Identity-Column-Value-Jump-Is

 

09-03 14:07