使用自增键插入的多行的序号
Sequential numbers for many rows inserted with auto-increment key
- SQL 服务器 2008 R2
- A table 带自增键
- 许多不同的线程必须在 table 中批量插入行。
我想知道如何(如果可能的话)以一种我绝对确定一个线程插入的行的键将获得序号的方式插入行。
例如,如果同时执行 2 个线程:
- 线程 #1 插入 5 行并获取键 1,2,3,4,5
- 线程 #2 插入 5 行并获取键 6,7,8,9,10
我一定要确保不要得到:
- 线程 #1 行获取键 1,3,4,8,9
- 线程 #2 行获取键 2,5,6,7,10
显然,如果每个线程执行某种循环并执行 5 次 "INSERT INTO..." 命令,它将无法工作,因为另一个线程可以插入。
但是即使一个线程只使用一个 INSERT 命令插入许多行,是否足以保证键是顺序的?
如果是,你能帮我找到它的记录在哪里吗?因为我没有。
如果不是,如何确保?
编辑为什么我关心获得连续的数字:
这主要是一个性能问题,我们实际上有一个整数不是自动增加一行中的列,每个线程锁定该行,手动将列值增加它需要的任何数字,然后释放该行。
问题是一次只能插入一个线程,通过测试我们发现在新的 table 自动增量列中插入行,让 sql 服务器管理身份分配,要快得多。
锁定整个 table 不是一个选项,因为它会导致与锁定公共行相同的问题。
我之所以要确保插入的行 ID 对于单个线程插入是连续的,是为了减少代码重构的需要,代码重构实际上是通过只保留第一个数字和计数来工作的,因此代码可以推断出什么是其他数字。
连续的数字不是业务问题,所以如果不可能做到,我们只需要将每个行号保存在一个数组中,但有更多代码可以通过这种方式重构,所以我'我尽量避免它。
请记住,我完全知道设计可能并不理想,但我使用的是遗留 "big ball of mud" 系统,我无法对其进行太多重新设计。
使用交易。
该事务将锁定 table 直到您提交,因此在前一个事务结束之前不会启动其他事务,因此标识值是安全的
可以对整个table设置独占锁如下。
请注意 TABLOCKX
中的 X
- 这使得锁定成为独占。
BEGIN TRANSACTION;
-- select one dummy row, and set an exclusive lock
-- on the whole table (TABLOCKX)
SELECT TOP 1 *
FROM yourtab
WITH (TABLOCKX, HOLDLOCK);
-- Your INSERT statements goes here:
INSERT INTO yourtab(....) ...
COMMIT TRANSACTION;
哪个版本的 SQL 服务器?从 SQL Server 2012 开始,您可以创建所谓的 序列。 这些是为调用者创建唯一编号的对象。
保证序列是线程安全的。
在你的例子中,你需要一个递增 5 的序列:
CREATE SEQUENCE myseq
AS int
START WITH 1
INCREMENT BY 5
在您的线程中,您可以使用 NEXT VALUE
检索下一个值:
DECLARE @var int ;
SET @var = NEXT VALUE FOR myseq ;
然后您可以使用此值并将值插入您的 table。有了这个,你就可以不用锁定你的 tables.
我很好奇,想测试一下。在装有 SQL Server 2014 Express 的虚拟机上,答案是:
生成的IDENTITY
值不保证在多个线程插入值时是连续的。即使它是一次插入多行的单个 INSERT
语句。 (在默认事务隔离级别下)
您可以在 SQL Server 2008 上对其进行测试,但即使您没有看到相同的行为,依赖它也是不明智的,因为它在 2014 年肯定发生了变化。
这是重现测试的完整脚本。
Table
CREATE TABLE [dbo].[test](
[ID] [int] IDENTITY(1,1) NOT NULL,
[dt] [datetime2](7) NOT NULL,
[V] [int] NOT NULL,
CONSTRAINT [PK_test] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
插入脚本
WAITFOR TIME '22:23:24';
-- set the time to about a minute in the future
-- open two windows in SSMS and run this script (F5) in both of them
-- they will start running at the same time specified above in parallel.
-- insert 1M rows in chunks of 1000 rows
-- in the first SSMS window uncomment these lines:
--DECLARE @VarV int = 0;
--WHILE (@VarV < 1000)
-- in the second SSMS window uncomment these lines:
--DECLARE @VarV int = 10000;
--WHILE (@VarV < 11000)
BEGIN
WITH e1(n) AS
(
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
) -- 10
,e2(n) AS (SELECT 1 FROM e1 CROSS JOIN e1 AS b) -- 10*10
,e3(n) AS (SELECT 1 FROM e1 CROSS JOIN e2) -- 10*100
,CTE_rn
AS
(
SELECT ROW_NUMBER() OVER (ORDER BY n) AS rn
FROM e3
)
INSERT INTO [dbo].[test]
([dt]
,[V])
SELECT
SYSDATETIME() AS dt
,@VarV
FROM CTE_rn;
SET @VarV = @VarV + 1;
END;
验证结果
WITH
CTE
AS
(
SELECT
[V]
,MIN(ID) AS MinID
,MAX(ID) AS MaxID
,MAX(ID) - MIN(ID) + 1 AS DiffID
FROM [dbo].[test]
GROUP BY V
)
SELECT
DiffID
,COUNT(*) AS c
FROM CTE
GROUP BY DiffID
ORDER BY c DESC;
此查询为每个 V
(每块 1000 个插入行)计算 MIN
和 MAX
ID
。如果所有 IDENTITY
值都是按顺序生成的,那么 MAX
和 MIN
ID 之间的差值始终正好是 1000。正如我们在结果中看到的那样,情况并非如此:
结果
+--------+------+
| DiffID | c |
+--------+------+
| 1000 | 1940 |
| 2000 | 6 |
| 3000 | 3 |
| 1759 | 2 |
| 1477 | 2 |
| 1522 | 1 |
| 1524 | 1 |
| 1529 | 1 |
| 1538 | 1 |
| 1546 | 1 |
| 1548 | 1 |
| 1584 | 1 |
| 1585 | 1 |
| 1589 | 1 |
| 1597 | 1 |
| 1606 | 1 |
| 1611 | 1 |
| 1612 | 1 |
| 1620 | 1 |
| 1630 | 1 |
| 1631 | 1 |
| 1635 | 1 |
| 1658 | 1 |
| 1663 | 1 |
| 1675 | 1 |
| 1731 | 1 |
| 1749 | 1 |
| 1009 | 1 |
| 1038 | 1 |
| 1049 | 1 |
| 1055 | 1 |
| 1086 | 1 |
| 1102 | 1 |
| 1144 | 1 |
| 1218 | 1 |
| 1225 | 1 |
| 1263 | 1 |
| 1325 | 1 |
| 1367 | 1 |
| 1372 | 1 |
| 1415 | 1 |
| 1451 | 1 |
| 1761 | 1 |
| 1793 | 1 |
| 1832 | 1 |
| 1904 | 1 |
| 1919 | 1 |
| 1924 | 1 |
| 1954 | 1 |
| 1973 | 1 |
| 1984 | 1 |
| 2381 | 1 |
+--------+------+
在大多数情况下,的确,IDENTITY
个值是按顺序分配的,但在 2000 年中的 60 个情况下,它们不是。
如何处理?
我个人更喜欢使用 sp_getapplock
,而不是锁定 table 或增加事务隔离级别。
但是,最终结果是相同的 - 您必须确保 INSERT
语句不是 运行 并行。
在 SQL Server 2012+ 中,值得测试新 SEQUENCE
功能的行为。具体来说,sp_sequence_get_range
存储过程从序列对象生成一系列序列值。让我们把这个练习留给 reader.
- SQL 服务器 2008 R2
- A table 带自增键
- 许多不同的线程必须在 table 中批量插入行。
我想知道如何(如果可能的话)以一种我绝对确定一个线程插入的行的键将获得序号的方式插入行。
例如,如果同时执行 2 个线程:
- 线程 #1 插入 5 行并获取键 1,2,3,4,5
- 线程 #2 插入 5 行并获取键 6,7,8,9,10
我一定要确保不要得到:
- 线程 #1 行获取键 1,3,4,8,9
- 线程 #2 行获取键 2,5,6,7,10
显然,如果每个线程执行某种循环并执行 5 次 "INSERT INTO..." 命令,它将无法工作,因为另一个线程可以插入。
但是即使一个线程只使用一个 INSERT 命令插入许多行,是否足以保证键是顺序的?
如果是,你能帮我找到它的记录在哪里吗?因为我没有。
如果不是,如何确保?
编辑为什么我关心获得连续的数字:
这主要是一个性能问题,我们实际上有一个整数不是自动增加一行中的列,每个线程锁定该行,手动将列值增加它需要的任何数字,然后释放该行。
问题是一次只能插入一个线程,通过测试我们发现在新的 table 自动增量列中插入行,让 sql 服务器管理身份分配,要快得多。 锁定整个 table 不是一个选项,因为它会导致与锁定公共行相同的问题。
我之所以要确保插入的行 ID 对于单个线程插入是连续的,是为了减少代码重构的需要,代码重构实际上是通过只保留第一个数字和计数来工作的,因此代码可以推断出什么是其他数字。
连续的数字不是业务问题,所以如果不可能做到,我们只需要将每个行号保存在一个数组中,但有更多代码可以通过这种方式重构,所以我'我尽量避免它。
请记住,我完全知道设计可能并不理想,但我使用的是遗留 "big ball of mud" 系统,我无法对其进行太多重新设计。
使用交易。 该事务将锁定 table 直到您提交,因此在前一个事务结束之前不会启动其他事务,因此标识值是安全的
可以对整个table设置独占锁如下。
请注意 TABLOCKX
中的 X
- 这使得锁定成为独占。
BEGIN TRANSACTION;
-- select one dummy row, and set an exclusive lock
-- on the whole table (TABLOCKX)
SELECT TOP 1 *
FROM yourtab
WITH (TABLOCKX, HOLDLOCK);
-- Your INSERT statements goes here:
INSERT INTO yourtab(....) ...
COMMIT TRANSACTION;
哪个版本的 SQL 服务器?从 SQL Server 2012 开始,您可以创建所谓的 序列。 这些是为调用者创建唯一编号的对象。 保证序列是线程安全的。
在你的例子中,你需要一个递增 5 的序列:
CREATE SEQUENCE myseq
AS int
START WITH 1
INCREMENT BY 5
在您的线程中,您可以使用 NEXT VALUE
检索下一个值:
DECLARE @var int ;
SET @var = NEXT VALUE FOR myseq ;
然后您可以使用此值并将值插入您的 table。有了这个,你就可以不用锁定你的 tables.
我很好奇,想测试一下。在装有 SQL Server 2014 Express 的虚拟机上,答案是:
生成的IDENTITY
值不保证在多个线程插入值时是连续的。即使它是一次插入多行的单个 INSERT
语句。 (在默认事务隔离级别下)
您可以在 SQL Server 2008 上对其进行测试,但即使您没有看到相同的行为,依赖它也是不明智的,因为它在 2014 年肯定发生了变化。
这是重现测试的完整脚本。
Table
CREATE TABLE [dbo].[test](
[ID] [int] IDENTITY(1,1) NOT NULL,
[dt] [datetime2](7) NOT NULL,
[V] [int] NOT NULL,
CONSTRAINT [PK_test] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
插入脚本
WAITFOR TIME '22:23:24';
-- set the time to about a minute in the future
-- open two windows in SSMS and run this script (F5) in both of them
-- they will start running at the same time specified above in parallel.
-- insert 1M rows in chunks of 1000 rows
-- in the first SSMS window uncomment these lines:
--DECLARE @VarV int = 0;
--WHILE (@VarV < 1000)
-- in the second SSMS window uncomment these lines:
--DECLARE @VarV int = 10000;
--WHILE (@VarV < 11000)
BEGIN
WITH e1(n) AS
(
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
) -- 10
,e2(n) AS (SELECT 1 FROM e1 CROSS JOIN e1 AS b) -- 10*10
,e3(n) AS (SELECT 1 FROM e1 CROSS JOIN e2) -- 10*100
,CTE_rn
AS
(
SELECT ROW_NUMBER() OVER (ORDER BY n) AS rn
FROM e3
)
INSERT INTO [dbo].[test]
([dt]
,[V])
SELECT
SYSDATETIME() AS dt
,@VarV
FROM CTE_rn;
SET @VarV = @VarV + 1;
END;
验证结果
WITH
CTE
AS
(
SELECT
[V]
,MIN(ID) AS MinID
,MAX(ID) AS MaxID
,MAX(ID) - MIN(ID) + 1 AS DiffID
FROM [dbo].[test]
GROUP BY V
)
SELECT
DiffID
,COUNT(*) AS c
FROM CTE
GROUP BY DiffID
ORDER BY c DESC;
此查询为每个 V
(每块 1000 个插入行)计算 MIN
和 MAX
ID
。如果所有 IDENTITY
值都是按顺序生成的,那么 MAX
和 MIN
ID 之间的差值始终正好是 1000。正如我们在结果中看到的那样,情况并非如此:
结果
+--------+------+
| DiffID | c |
+--------+------+
| 1000 | 1940 |
| 2000 | 6 |
| 3000 | 3 |
| 1759 | 2 |
| 1477 | 2 |
| 1522 | 1 |
| 1524 | 1 |
| 1529 | 1 |
| 1538 | 1 |
| 1546 | 1 |
| 1548 | 1 |
| 1584 | 1 |
| 1585 | 1 |
| 1589 | 1 |
| 1597 | 1 |
| 1606 | 1 |
| 1611 | 1 |
| 1612 | 1 |
| 1620 | 1 |
| 1630 | 1 |
| 1631 | 1 |
| 1635 | 1 |
| 1658 | 1 |
| 1663 | 1 |
| 1675 | 1 |
| 1731 | 1 |
| 1749 | 1 |
| 1009 | 1 |
| 1038 | 1 |
| 1049 | 1 |
| 1055 | 1 |
| 1086 | 1 |
| 1102 | 1 |
| 1144 | 1 |
| 1218 | 1 |
| 1225 | 1 |
| 1263 | 1 |
| 1325 | 1 |
| 1367 | 1 |
| 1372 | 1 |
| 1415 | 1 |
| 1451 | 1 |
| 1761 | 1 |
| 1793 | 1 |
| 1832 | 1 |
| 1904 | 1 |
| 1919 | 1 |
| 1924 | 1 |
| 1954 | 1 |
| 1973 | 1 |
| 1984 | 1 |
| 2381 | 1 |
+--------+------+
在大多数情况下,的确,IDENTITY
个值是按顺序分配的,但在 2000 年中的 60 个情况下,它们不是。
如何处理?
我个人更喜欢使用 sp_getapplock
,而不是锁定 table 或增加事务隔离级别。
但是,最终结果是相同的 - 您必须确保 INSERT
语句不是 运行 并行。
在 SQL Server 2012+ 中,值得测试新 SEQUENCE
功能的行为。具体来说,sp_sequence_get_range
存储过程从序列对象生成一系列序列值。让我们把这个练习留给 reader.