有没有人有在 SQL Server 中实现类似序列的好方法?
有时您只是不想使用 GUID,除了它们丑陋的事实。也许你想要的序列不是数字?此外,插入一行然后询问数据库的数字是多少看起来很骇人听闻。
有没有人有在 SQL Server 中实现类似序列的好方法?
有时您只是不想使用 GUID,除了它们丑陋的事实。也许你想要的序列不是数字?此外,插入一行然后询问数据库的数字是多少看起来很骇人听闻。
Sql Server 2012 引入了SEQUENCE
objects,它允许您生成不与任何表关联的顺序数值。
创建它们很容易:
CREATE SEQUENCE Schema.SequenceName
AS int
INCREMENT BY 1 ;
在插入之前使用它们的示例:
DECLARE @NextID int ;
SET @NextID = NEXT VALUE FOR Schema.SequenceName;
-- Some work happens
INSERT Schema.Orders (OrderID, Name, Qty)
VALUES (@NextID, 'Rim', 2) ;
请参阅我的博客以深入了解如何使用序列:
http://sqljunkieshare.com/2011/12/11/sequences-in-sql-server-2012-implementingmanaging-performance/
正如sqljunkieshare正确所说,从 SQL Server 2012 开始,有一个内置SEQUENCE
功能。
最初的问题没有澄清,但我认为对序列的要求是:
我想评论原始问题中的声明:
“此外,插入一行,然后询问数据库,这个数字看起来如此骇人听闻。”
好吧,我们在这里无能为力。DB 是序列号的提供者,DB 处理所有这些您自己无法处理的并发问题。我看不到向数据库询问序列的下一个值的替代方法。必须有一个原子操作“给我序列的下一个值”,并且只有 DB 可以提供这样的原子操作。没有客户端代码可以保证他是唯一使用该序列的人。
要回答标题“你将如何实现序列”中的问题 - 我们使用的是 2008,它没有该SEQUENCE
功能,所以在阅读了这个主题之后,我得到了以下内容。
对于我需要的每个序列,我只创建一个单独的帮助表,其中只有一IDENTITY
列(以与 2012 年相同的方式,您将创建一个单独的序列对象)。
CREATE TABLE [dbo].[SequenceContractNumber]
(
[ContractNumber] [int] IDENTITY(1,1) NOT NULL,
CONSTRAINT [PK_SequenceContractNumber] PRIMARY KEY CLUSTERED ([ContractNumber] ASC)
)
您可以为其指定起始值和增量。然后我创建一个存储过程,它将返回序列的下一个值。过程将启动一个事务,在帮助表中插入一行,记住生成的标识值并回滚事务。因此,帮助表始终保持为空。
CREATE PROCEDURE [dbo].[GetNewContractNumber]
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @Result int = 0;
IF @@TRANCOUNT > 0
BEGIN
-- Procedure is called when there is an active transaction.
-- Create a named savepoint
-- to be able to roll back only the work done in the procedure.
SAVE TRANSACTION ProcedureGetNewContractNumber;
END ELSE BEGIN
-- Procedure must start its own transaction.
BEGIN TRANSACTION ProcedureGetNewContractNumber;
END;
INSERT INTO dbo.SequenceContractNumber DEFAULT VALUES;
SET @Result = SCOPE_IDENTITY();
-- Rollback to a named savepoint or named transaction
ROLLBACK TRANSACTION ProcedureGetNewContractNumber;
RETURN @Result;
END
关于程序的一些注释。
首先,如何在只有一个标识列的表中插入一行并不明显。答案是DEFAULT VALUES
。
然后,如果在另一个事务中调用它,我希望程序能够正常工作。如果存在嵌套事务,则简单ROLLBACK
回滚所有内容。在我的情况下,我只需要回滚INSERT
到辅助表中,所以我使用了SAVE TRANSACTION
.
没有 savepoint_name 或 transaction_name 的 ROLLBACK TRANSACTION 回滚到事务的开头。嵌套事务时,该语句将所有内部事务回滚到最外层的 BEGIN TRANSACTION 语句。
这就是我使用该程序的方式(在其他一些大程序中,例如,创建一个新合同):
DECLARE @VarContractNumber int;
EXEC @VarContractNumber = dbo.GetNewContractNumber;
如果您需要一次生成一个序列值,这一切都很好。在合同的情况下,每个合同都是单独创建的,因此这种方法非常有效。我可以确定所有合同总是有唯一的合同编号。
注意:只是为了防止可能的问题。这些合同号是我的合同表具有的代理身份密钥的补充。代理键是用于参照完整性的内部键。生成的合同编号是打印在合同上的人性化编号。此外,同一个 Contracts 表同时包含最终合同和提案,它们可以成为合同,也可以永久保留为提案。提案和合同都包含非常相似的数据,这就是它们保存在同一个表中的原因。只需在一行中更改标志,提案就可以成为合同。提案使用单独的数字序列编号,为此我有第二个表格SequenceProposalNumber
和第二个过程GetNewProposalNumber
。
不过,最近我遇到了一个问题。我需要批量生成序列值,而不是一个接一个。
我需要一个程序来一次性处理给定季度内收到的所有付款。这种处理的结果可能是我想在Transactions
表中记录的约 20,000 笔交易。我这里有类似的设计。Transactions
表具有最终用户永远不会看到的内部IDENTITY
列,并且它具有将打印在语句上的人性化交易编号。所以,我需要一种方法来批量生成给定数量的唯一值。
本质上,我使用了相同的方法,但很少有特殊之处。
首先,没有直接的方法可以在只有一IDENTITY
列的表中插入多行。虽然 (ab)using 有一个解决方法MERGE
,但我最终没有使用它。我决定添加一个虚拟Filler
列更容易。我的序列表将始终为空,因此额外的列并不重要。
辅助表如下所示:
CREATE TABLE [dbo].[SequenceS2TransactionNumber]
(
[S2TransactionNumber] [int] IDENTITY(1,1) NOT NULL,
[Filler] [int] NULL,
CONSTRAINT [PK_SequenceS2TransactionNumber]
PRIMARY KEY CLUSTERED ([S2TransactionNumber] ASC)
)
该过程如下所示:
-- Description: Returns a list of new unique S2 Transaction numbers of the given size
-- The caller should create a temp table #NewS2TransactionNumbers,
-- which would hold the result
CREATE PROCEDURE [dbo].[GetNewS2TransactionNumbers]
@ParamCount int -- not NULL
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF @@TRANCOUNT > 0
BEGIN
-- Procedure is called when there is an active transaction.
-- Create a named savepoint
-- to be able to roll back only the work done in the procedure.
SAVE TRANSACTION ProcedureGetNewS2TransactionNos;
END ELSE BEGIN
-- Procedure must start its own transaction.
BEGIN TRANSACTION ProcedureGetNewS2TransactionNos;
END;
DECLARE @VarNumberCount int;
SET @VarNumberCount =
(
SELECT TOP(1) dbo.Numbers.Number
FROM dbo.Numbers
ORDER BY dbo.Numbers.Number DESC
);
-- table variable is not affected by the ROLLBACK, so use it for temporary storage
DECLARE @TableTransactionNumbers table
(
ID int NOT NULL
);
IF @VarNumberCount >= @ParamCount
BEGIN
-- the Numbers table is large enough to provide the given number of rows
INSERT INTO dbo.SequenceS2TransactionNumber
(Filler)
OUTPUT inserted.S2TransactionNumber AS ID INTO @TableTransactionNumbers(ID)
-- save generated unique numbers into a table variable first
SELECT TOP(@ParamCount) dbo.Numbers.Number
FROM dbo.Numbers
OPTION (MAXDOP 1);
END ELSE BEGIN
-- the Numbers table is not large enough to provide the given number of rows
-- expand the Numbers table by cross joining it with itself
INSERT INTO dbo.SequenceS2TransactionNumber
(Filler)
OUTPUT inserted.S2TransactionNumber AS ID INTO @TableTransactionNumbers(ID)
-- save generated unique numbers into a table variable first
SELECT TOP(@ParamCount) n1.Number
FROM dbo.Numbers AS n1 CROSS JOIN dbo.Numbers AS n2
OPTION (MAXDOP 1);
END;
/*
-- this method can be used if the SequenceS2TransactionNumber
-- had only one identity column
MERGE INTO dbo.SequenceS2TransactionNumber
USING
(
SELECT *
FROM dbo.Numbers
WHERE dbo.Numbers.Number <= @ParamCount
) AS T
ON 1 = 0
WHEN NOT MATCHED THEN
INSERT DEFAULT VALUES
OUTPUT inserted.S2TransactionNumber
-- return generated unique numbers directly to the caller
;
*/
-- Rollback to a named savepoint or named transaction
ROLLBACK TRANSACTION ProcedureGetNewS2TransactionNos;
IF object_id('tempdb..#NewS2TransactionNumbers') IS NOT NULL
BEGIN
INSERT INTO #NewS2TransactionNumbers (ID)
SELECT TT.ID FROM @TableTransactionNumbers AS TT;
END
END
这就是它的使用方式(在一些计算事务的大存储过程中):
-- Generate a batch of new unique transaction numbers
-- and store them in #NewS2TransactionNumbers
DECLARE @VarTransactionCount int;
SET @VarTransactionCount = ...
CREATE TABLE #NewS2TransactionNumbers(ID int NOT NULL);
EXEC dbo.GetNewS2TransactionNumbers @ParamCount = @VarTransactionCount;
-- use the generated numbers...
SELECT ID FROM #NewS2TransactionNumbers AS TT;
这里几乎没有什么需要解释的。
我需要在表中插入给定数量的行SequenceS2TransactionNumber
。我为此使用了一个辅助Numbers
表。该表仅保存从 1 到 100,000 的整数。它也用于系统中的其他地方。我检查表中是否有足够的行,Numbers
并在需要时通过与自身交叉连接将其扩展为 100,000 * 100,000。
我必须将批量插入的结果保存在某处并以某种方式将其传递给调用者。在存储过程之外传递表的一种方法是使用临时表。我不能在这里使用表值参数,因为不幸的是它是只读的。另外,我不能直接将生成的序列值插入到临时表中#NewS2TransactionNumbers
。我不能#NewS2TransactionNumbers
在OUTPUT
子句中使用,因为ROLLBACK
会清理它。幸运的是,表变量不受ROLLBACK
.
因此,我使用表变量@TableTransactionNumbers
作为OUTPUT
子句的目标。然后我ROLLBACK
清理序列表的事务。然后将生成的序列值从表变量复制@TableTransactionNumbers
到临时表#NewS2TransactionNumbers
,因为#NewS2TransactionNumbers
存储过程的调用者只能看到临时表。@TableTransactionNumbers
存储过程的调用者看不到表变量。
此外,可以使用OUTPUT
子句将生成的序列直接发送给调用者(正如您在使用的注释变体中看到的那样MERGE
)。它本身工作正常,但我需要在某个表中生成的值以便在调用存储过程中进一步处理。当我尝试这样的事情时:
INSERT INTO @TableTransactions (ID)
EXEC dbo.GetNewS2TransactionNumbers @ParamCount = @VarTransactionCount;
我收到一个错误
不能在 INSERT-EXEC 语句中使用 ROLLBACK 语句。
但是,我需要ROLLBACK
在里面EXEC
,这就是为什么我最终有这么多临时表。
毕竟,切换到具有适当SEQUENCE
对象的最新版本的 SQL Server 是多么美好。
Identity 列大致类似于序列。
您可以只使用普通的旧表并将它们用作序列。这意味着您的插入将始终是:
BEGIN TRANSACTION
SELECT number from plain old table..
UPDATE plain old table, set the number to be the next number
INSERT your row
COMMIT
但不要这样做。锁定会很糟糕...
我从 SQL Server 开始,对我来说,Oracle“序列”方案看起来像是一个 hack。我猜你是从另一个方向过来的,而且 scope_identity() 看起来像一个黑客。
克服它。在罗马做到入乡随俗。
我用来解决这个问题的方法是一个表'Sequences',它存储了我所有的序列和一个'nextval'存储过程。
Sql 表:
CREATE TABLE Sequences (
name VARCHAR(30) NOT NULL,
value BIGINT DEFAULT 0 NOT NULL,
CONSTRAINT PK_Sequences PRIMARY KEY (name)
);
PK_Sequences只是用来确保永远不会有同名的序列。
Sql存储过程:
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'nextVal') AND type in (N'P', N'PC')) DROP PROCEDURE nextVal;
GO
CREATE PROCEDURE nextval
@name VARCHAR(30)
AS
BEGIN
DECLARE @value BIGINT
BEGIN TRANSACTION
UPDATE Sequences
SET @value=value=value + 1
WHERE name = @name;
-- SELECT @value=value FROM Sequences WHERE name=@name
COMMIT TRANSACTION
SELECT @value AS nextval
END;
插入一些序列:
INSERT INTO Sequences(name, value) VALUES ('SEQ_Workshop', 0);
INSERT INTO Sequences(name, value) VALUES ('SEQ_Participant', 0);
INSERT INTO Sequences(name, value) VALUES ('SEQ_Invoice', 0);
最后得到一个序列的下一个值,
execute nextval 'SEQ_Participant';
一些 c# 代码从序列表中获取下一个值,
public long getNextVal()
{
long nextval = -1;
SqlConnection connection = new SqlConnection("your connection string");
try
{
//Connect and execute the select sql command.
connection.Open();
SqlCommand command = new SqlCommand("nextval", connection);
command.CommandType = CommandType.StoredProcedure;
command.Parameters.Add("@name", SqlDbType.NVarChar).Value = "SEQ_Participant";
nextval = Int64.Parse(command.ExecuteScalar().ToString());
command.Dispose();
}
catch (Exception) { }
finally
{
connection.Dispose();
}
return nextval;
}
在 SQL Server 2012 中,您可以简单地使用
CREATE SEQUENCE
在 2005 年和 2008 年,您可以使用公用表表达式获得任意序列号列表。
这是一个示例(请注意,MAXRECURSION 选项很重要):
DECLARE @MinValue INT = 1;
DECLARE @MaxValue INT = 1000;
WITH IndexMaker (IndexNumber) AS
(
SELECT
@MinValue AS IndexNumber
UNION ALL SELECT
IndexNumber + 1
FROM
IndexMaker
WHERE IndexNumber < @MaxValue
)
SELECT
IndexNumber
FROM
IndexMaker
ORDER BY
IndexNumber
OPTION
(MAXRECURSION 0)
Oracle 实现的序列需要在插入之前调用数据库。SQL Server 实现的标识需要在插入后调用数据库。
一个并不比另一个更骇人听闻。最终效果是相同的 - 依赖/依赖数据存储以提供唯一的人工键值和(在大多数情况下)对存储的两次调用。
我假设您的关系模型基于人工键,在这种情况下,我将提供以下观察:
我们永远不应该试图赋予人造钥匙以意义;它们的唯一目的应该是链接相关记录。
您对订购数据有什么需求?它可以在视图(演示文稿)中处理,还是必须保留的数据的真实属性?
创建一个带有标识符的阶段表。
在加载阶段表之前,截断并重新设置标识符以从 1 开始。
加载你的表。现在每一行都有一个从 1 到 N 的唯一值。
创建一个包含序列号的表。这可能是几行,每个序列一个。
从您创建的序列表中查找序列号。通过将阶段表中的行数添加到序列号来更新序列号。
通过添加您查找的序列号来更新阶段表标识符。这是一个简单的一步过程。或加载您的目标表,在 ETL 中加载时将序列号添加到标识符中。这可以利用批量加载程序并允许其他转换。
考虑以下代码段。
CREATE TABLE [SEQUENCE](
[NAME] [varchar](100) NOT NULL,
[NEXT_AVAILABLE_ID] [int] NOT NULL,
CONSTRAINT [PK_SEQUENCES] PRIMARY KEY CLUSTERED
(
[NAME] 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
CREATE PROCEDURE CLAIM_IDS (@sequenceName varchar(100), @howMany int)
AS
BEGIN
DECLARE @result int
update SEQUENCE
set
@result = NEXT_AVAILABLE_ID,
NEXT_AVAILABLE_ID = NEXT_AVAILABLE_ID + @howMany
where Name = @sequenceName
Select @result as AVAILABLE_ID
END
GO
正如sqljunkiesshare 所述,序列已添加到 SQL Server 2012。以下是如何在 GUI 中执行此操作。这是等价的:
CREATE SEQUENCE Schema.SequenceName
AS int
INCREMENT BY 1 ;
笔记:
默认起始值、最小值和最大值由数据类型的范围确定,在这种情况下为 int。如果您想使用除 int 以外的其他内容,请参阅此处了解更多数据类型范围。
很有可能您希望您的序列从 1 开始,并且您可能也希望您的最小值为 1。
如果您使用的是 SQL Server 2005,您可以选择使用 Row_Number
我完全同意并在去年的一个项目中做到了这一点。
我刚刚创建了一个包含序列名称、当前值和增量数量的表。
然后我创建了一个 2 procs 来添加和删除它们。还有 2 个功能可以获取下一个并获取最新信息。
如果您想使用顺序键插入数据,但又不想再次查询数据库以获取刚刚插入的键,我认为您唯一的两个选择是:
如果我正在生成客户端密钥,我喜欢GUID。我觉得他们很漂亮。
row["ID"] = Guid.NewGuid();
那条线应该放在某处跑车的引擎盖上。
标识列的另一个问题是,如果您有多个表的序列号需要唯一,则标识列不起作用。就像 Corey Trager 提到的那样,自己滚动的序列实现类型可能会带来一些锁定问题。
最直接等效的解决方案似乎是创建一个 SQL Server 表,其中包含一个用于标识的列,它代替了单独类型的“序列”对象。例如,如果在 Oracle 中,您将有来自一个序列的两个表,例如 Dogs <-- 序列对象 --> Cats,那么在 SQL Server 中,您将创建三个数据库对象,所有表都像 Dogs <-- Pets with identity column -->猫。您将在 Pets 表中插入一行以获取您通常使用 NEXTVAL 的序列号,然后在您从用户那里获得宠物的实际类型后像往常一样插入到 Dogs 或 Cats 表中。任何额外的公共列都可以从 Dogs/Cats 表移动到 Pets 超类型表中,其后果是 1) 每个序列号会有一行,
交易安全!对于 2012 年之前的 SQLServer 版本...(感谢 Matt G。)此讨论中缺少的一件事是事务安全。如果您从序列中获取数字,则该数字必须是唯一的,并且没有其他应用程序或代码应该能够获取该数字。在我的例子中,我们经常从序列中提取唯一的数字,但是实际的交易可能会跨越相当长的时间,所以我们不希望其他人在我们提交交易之前得到相同的数字。 我们需要模仿 oracle 序列的行为,其中一个数字在拉取时被保留。我的解决方案是使用 xp_cmdshell 在数据库上获取单独的会话/事务,以便我们可以立即更新整个数据库的序列,甚至在事务完成之前。
--it is used like this:
-- use the sequence in either insert or select:
Insert into MyTable Values (NextVal('MySequence'), 'Foo');
SELECT NextVal('MySequence');
--you can make as many sequences as you want, by name:
SELECT NextVal('Mikes Other Sequence');
--or a blank sequence identifier
SELECT NextVal('');
该解决方案需要一个表来保存使用过的序列值,以及一个创建第二个自主事务以确保并发会话不会纠缠的过程。您可以拥有任意数量的唯一序列,它们按名称引用。下面的示例代码被修改为省略序列历史表上的请求用户和日期戳(用于审计),但我认为示例不太复杂;-)。
CREATE TABLE SequenceHolder(SeqName varchar(40), LastVal int);
GO
CREATE function NextVAL(@SEQname varchar(40))
returns int
as
begin
declare @lastval int
declare @barcode int;
set @lastval = (SELECT max(LastVal)
FROM SequenceHolder
WHERE SeqName = @SEQname);
if @lastval is null set @lastval = 0
set @barcode = @lastval + 1;
--=========== USE xp_cmdshell TO INSERT AND COMMINT NOW, IN A SEPERATE TRANSACTION =============================
DECLARE @sql varchar(4000)
DECLARE @cmd varchar(4000)
DECLARE @recorded int;
SET @sql = 'INSERT INTO SequenceHolder(SeqName, LastVal) VALUES (''' + @SEQname + ''', ' + CAST(@barcode AS nvarchar(50)) + ') '
SET @cmd = 'SQLCMD -S ' + @@servername +
' -d ' + db_name() + ' -Q "' + @sql + '"'
EXEC master..xp_cmdshell @cmd, 'no_output'
--===============================================================================================================
-- once submitted, make sure our value actually stuck in the table
set @recorded = (SELECT COUNT(*)
FROM SequenceHolder
WHERE SeqName = @SEQname
AND LastVal = @barcode);
--TRIGGER AN ERROR
IF (@recorded != 1)
return cast('Barcode was not recorded in SequenceHolder, xp_cmdshell FAILED!! [' + @cmd +']' as int);
return (@barcode)
end
GO
COMMIT;
现在要使该过程正常工作,您将需要启用 xp_cmdshell,有很多关于如何做到这一点的很好的描述,这是我在尝试使事情正常工作时所做的个人笔记。基本思想是你需要在 SQLServer Surface 中打开 xp_cmdshell 是一个配置,你需要设置一个用户帐户作为 xp_cmdshell 命令将在其下运行的帐户,该帐户将访问数据库以插入序列号并提交它。
--- LOOSEN SECURITY SO THAT xp_cmdshell will run
---- To allow advanced options to be changed.
EXEC sp_configure 'show advanced options', 1
GO
---- To update the currently configured value for advanced options.
RECONFIGURE
GO
---- To enable the feature.
EXEC sp_configure 'xp_cmdshell', 1
GO
---- To update the currently configured value for this feature.
RECONFIGURE
GO
—-Run SQLServer Management Studio as Administrator,
—- Login as domain user, not sqlserver user.
--MAKE A DATABASE USER THAT HAS LOCAL or domain LOGIN! (not SQL server login)
--insure the account HAS PERMISSION TO ACCESS THE DATABASE IN QUESTION. (UserMapping tab in User Properties in SQLServer)
—grant the following
GRANT EXECUTE on xp_cmdshell TO [domain\user]
—- run the following:
EXEC sp_xp_cmdshell_proxy_account 'domain\user', 'pwd'
--alternative to the exec cmd above:
create credential ##xp_cmdshell_proxy_account## with identity = 'domain\user', secret = 'pwd'
-—IF YOU NEED TO REMOVE THE CREDENTIAL USE THIS
EXEC sp_xp_cmdshell_proxy_account NULL;
-—ways to figure out which user is actually running the xp_cmdshell command.
exec xp_cmdshell 'whoami.exe'
EXEC xp_cmdshell 'osql -E -Q"select suser_sname()"'
EXEC xp_cmdshell 'osql -E -Q"select * from sys.login_token"'
通过 SQL 你可以使用这个策略;
CREATE SEQUENCE [dbo].[SequenceFile]
AS int
START WITH 1
INCREMENT BY 1 ;
并通过此 SQL 读取唯一的下一个值
SELECT NEXT VALUE FOR [dbo].[SequenceFile]