有没有人有很好的方法在SQL服务器中实现类似的序列?
有时你只是不想使用GUID,除了它们是丑陋的事实。也许您想要的序列不是数字?此外,插入一行,然后向DB询问这个数字是多么的hackish。
答案 0 :(得分:50)
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/
答案 1 :(得分:14)
正如sqljunkieshare所说,从SQL Server 2012开始,内置了SEQUENCE
功能。
原始问题没有澄清,但我认为序列的要求是:
我想对原始问题中的陈述发表评论:
"此外,插入一行然后向DB询问数字是多少 看起来很苛刻。"
嗯,我们在这里做的不多。 DB是序列号的提供者,DB处理您无法自己处理的所有这些并发问题。我没有看到要求DB获取序列的下一个值的替代方法。必须有一个原子操作"给我下一个序列的值"只有DB才能提供这样的原子操作。没有客户端代码可以保证他是唯一使用该序列的人。
要回答标题中的问题"你将如何实现序列" - 我们正在使用2008,它没有SEQUENCE
功能,所以在对这个主题进行一些阅读后,我最终得到了以下内容。
对于我需要的每个序列,我创建一个单独的帮助器表,只有一个IDENTITY
列(与2012年一样,你将创建一个单独的Sequence对象)。
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)使用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
清理Sequence表的事务。然后将生成的序列值从表变量@TableTransactionNumbers
复制到临时表#NewS2TransactionNumbers
,因为只有临时表#NewS2TransactionNumbers
对于存储过程的调用者是可见的。表变量@TableTransactionNumbers
对存储过程的调用者不可见。
此外,可以使用OUTPUT
子句将生成的序列直接发送给调用者(正如您在使用MERGE
的注释变体中所看到的那样)。它本身工作正常,但我需要在某些表中生成的值,以便在调用存储过程中进一步处理。当我尝试这样的事情时:
INSERT INTO @TableTransactions (ID)
EXEC dbo.GetNewS2TransactionNumbers @ParamCount = @VarTransactionCount;
我收到了错误
无法在INSERT-EXEC语句中使用ROLLBACK语句。
但是,ROLLBACK
中我需要EXEC
,这就是为什么我最终拥有这么多临时表。
毕竟,切换到具有正确SEQUENCE
对象的最新版SQL服务器有多好。
答案 2 :(得分:5)
An Identity column大致类似于序列。
答案 3 :(得分:5)
您可以使用普通的旧表并将它们用作序列。这意味着您的插入始终是:
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“序列”方案看起来像一个黑客。我猜你是从另一个方向和你来的,而scope_identity()看起来像是一个黑客。
克服它。在罗马做到入乡随俗。
答案 4 :(得分:4)
我用来解决这个问题的方法是一个表'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#代码从Sequence表中获取下一个值,
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;
}
答案 5 :(得分:3)
Oracle实现的序列需要在插入之前调用数据库。 SQL Server实现的标识需要在插入后调用数据库。
一个人并不比另一个人更苛刻。净效果是相同的 - 依赖/依赖数据存储来提供唯一的人工键值和(在大多数情况下)两次调用商店。
我假设您的关系模型基于人工密钥,在这种情况下,我将提供以下观察:
我们永远不应该试图灌输具有意义的人工钥匙;他们唯一的目的应该是链接相关记录。
您对订购数据的需求是什么?它可以在视图(演示文稿)中处理,还是必须保留数据的真实属性?
答案 6 :(得分:3)
在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)
答案 7 :(得分:2)
创建一个带有标识符的舞台表。
在加载阶段表之前,截断并重新设置标识符以从1开始。
加载你的桌子。现在每一行都有一个从1到N的唯一值。
创建一个包含序列号的表。这可能是几行,每个序列一个。
从您创建的序列表中查找序列号。 通过将阶段表中的行数添加到序列号来更新序列号。
通过添加您查找的序列号来更新阶段表标识符。这是一个简单的一步过程。 要么 加载目标表,在ETL中加载时将序列号添加到标识符中。这可以利用批量加载器并允许其他转换。
答案 8 :(得分:2)
请考虑以下代码段。
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
答案 9 :(得分:2)
作为sqljunkiesshare states,序列被添加到SQL Server 2012中。以下是如何在GUI中执行此操作。这是等同于:
CREATE SEQUENCE Schema.SequenceName
AS int
INCREMENT BY 1 ;
注意:
默认的“开始”值,“最小值”和“最大值”由数据类型的范围确定,在这种情况下,该数据类型为int。 See here for more data type ranges if you want to use something other than an int
你很有可能希望你的序列从1开始和你一起开始 可能也希望你的最小值为1。
答案 10 :(得分:0)
我完全同意并且去年在一个项目上这样做了。
我刚刚创建了一个包含序列名称,当前值和&amp;增量。
然后我创建了一个2个触发器来添加&amp;删除它们。并且有2个功能可以获得下一个,&amp;得到最新的。
答案 11 :(得分:0)
如果您想使用顺序键插入数据,但又不想再次查询数据库以获取刚插入的密钥,我认为您只有两个选择:
如果我正在进行客户端密钥生成,我喜欢 GUID。我认为他们很漂亮。
row["ID"] = Guid.NewGuid();
这条线应该放在某个跑车的引擎盖上。
答案 12 :(得分:0)
如果您使用的是SQL Server 2005,则可以选择使用Row_Number
答案 13 :(得分:0)
标识列的另一个问题是,如果您有多个表,其中序列号必须是唯一的,则标识列不起作用。就像Corey Trager所提到的那样,滚动式自己的序列实现可能会出现一些锁定问题。
最直接等效的解决方案似乎是创建一个SQL Server表,其中包含一个用于标识的列,它取代了单独类型的“序列”对象。例如,如果在Oracle中,您将有一个序列中的两个表,例如Dogs&lt; - sequence object - &gt;然后在SQL Server中你会创建三个数据库对象,所有表格如Dogs&lt; - Pets with identity column - &gt;猫。您可以在Pets表中插入一行以获取您通常使用NEXTVAL的序列号,然后像往常一样从用户那里获得实际类型的宠物后插入Dogs或Cats表。任何其他常见列都可以从Dogs / Cats表移动到Pets supertype表中,其结果是1)每个序列号都有一行,2)获取序列号时无法填充任何列需要有默认值和3)它需要一个连接来获取所有列。
答案 14 :(得分:0)
通过SQL,您可以使用此策略;
CREATE SEQUENCE [dbo].[SequenceFile]
AS int
START WITH 1
INCREMENT BY 1 ;
并读取此SQL
的唯一下一个值SELECT NEXT VALUE FOR [dbo].[SequenceFile]
答案 15 :(得分:0)
TRANSACTION SAFE!对于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"'