您将如何在 Microsoft SQL Server 中实现序列?

发布于 2024-07-08 02:17:03 字数 127 浏览 11 评论 0 原文

有没有人有一个好的方法来实现 SQL Server 中的序列之类的东西?

有时你只是不想使用 GUID,而且事实上它们实在是太丑了。 也许您想要的序列不是数字? 此外,插入一行然后询问数据库数字是多少似乎很黑客。

Does anyone have a good way of implementing something like a sequence in SQL server?

Sometimes you just don't want to use a GUID, besides the fact that they are ugly as heck. Maybe the sequence you want isn't numeric? Besides, inserting a row and then asking the DB what the number is just seems so hackish.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(16

老街孤人 2024-07-15 02:17:04

考虑以下片段。

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

Consider the following snippet.

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
晨敛清荷 2024-07-15 02:17:04

正如 sqljunkiesshare states 所示,序列已添加到 SQL Server 2012 中。以下是在 GUI 中执行此操作的方法。 这相当于:

CREATE SEQUENCE Schema.SequenceName
AS int
INCREMENT BY 1 ;
  1. 对象资源管理器中,展开Programmability文件
  2. 夹在Programmability文件夹下,右键单击Sequences< /em>
    文件夹如下所示:

在此处输入图像描述

  1. 下划线是您要更新以获得的值
    相当于上面的 SQL 语句,但是,我会考虑
    根据您的需要更改这些(请参阅下面的注释)。

输入图片此处描述

注意:

As sqljunkiesshare states, sequences were added to SQL Server 2012. Here's how to do it in the GUI. This is the equivolent of:

CREATE SEQUENCE Schema.SequenceName
AS int
INCREMENT BY 1 ;
  1. In the Object Explorer, expand the Programmability folder
  2. Under the Programmability folder, right click on the Sequences
    folder as shown below:

enter image description here

  1. Underlined are the values that you would update to get the
    equivalent of the SQL statement above, however, I would consider
    changing these depending on your needs (see notes below).

enter image description here

Notes:

A君 2024-07-15 02:17:04

我完全同意,并在去年的一个项目中做到了这一点。

我刚刚创建了一个表,其中包含序列名称、当前值和序列名称。 增量金额。

然后我创建了 2 个进程来添加 & 删除它们。 接下来还有 2 个函数,& 获取最新信息。

I totally agree and did this last year on a project.

I just created a table with the name of the sequence, current value, & increment amount.

Then I created a 2 procs to add & delete them. And 2 functions to get next, & get current.

活雷疯 2024-07-15 02:17:04

如果您想使用顺序键插入数据,但又不想再次查询数据库来获取刚刚插入的键,我认为您唯一的两个选择是:

  1. 通过返回的存储过程执行插入新插入的密钥值 在
  2. 客户端实现序列(以便您在插入之前知道新密钥)

如果我正在执行客户端密钥生成,我喜欢 GUID。 我觉得它们真的很漂亮。

row["ID"] = Guid.NewGuid();

这条线应该放在跑车引擎盖上的某个地方。

If you want to insert data with a sequential key, but you don't want to have to query the database again to get the just-inserted key, I think your only two choices are:

  1. Perform the insert through a stored procedure which returns the newly-inserted key value
  2. Implement the sequence client-side (so that you know the new key before you insert)

If I'm doing client-side key generation, I love GUIDs. I think they're beautiful as heck.

row["ID"] = Guid.NewGuid();

That line should be laying on the hood of a sportscar somewhere.

谜泪 2024-07-15 02:17:04

如果您使用的是 SQL Server 2005,您可以选择使用 Row_Number

If you are using SQL Server 2005 you have the option of using Row_Number

妥活 2024-07-15 02:17:04

标识列的另一个问题是,如果您有多个表,其中序列号需要唯一,则标识列不起作用。 正如 Corey Trager 提到的那样,自行设计的序列实现类型可能会出现一些锁定问题。

最直接的等效解决方案似乎是创建一个带有单列标识的 SQL Server 表,它取代了单独类型的“序列”对象。 例如,如果在 Oracle 中,您将拥有来自一个序列的两个表,例如 Dogs <-- 序列对象 --> 然后,在 SQL Server 中的 Cats 中,您将创建三个数据库对象,所有表都像 Dogs <-- Pets with Identity 列 --> 猫。 您可以在 Pets 表中插入一行以获取通常使用 NEXTVAL 的序列号,然后像通常从用户那里获取宠物的实际类型后那样插入到 Dogs 或 Cats 表中。 任何其他公共列都可以从 Dogs/Cats 表移至 Pets 超类型表,从而产生一些后果:1) 每个序列号将占一行,2) 获取序列号时无法填充的任何列将被删除。需要有默认值,并且 3) 需要连接才能获取所有列。

The other problem with an identity columns is that if you have more than one table where the sequence numbers need to be unique, an identity column doesn't work. And like Corey Trager mentions, a roll-your-own type of sequence implementation might present some locking issues.

The most straightforwardly equivalent solutions seems to be to create a SQL Server table with a single column for the identity, which takes the place of a separate type of "sequence" object. For example if in Oracle you would have two tables from one sequence such as Dogs <-- sequence object --> Cats then in SQL Server you would create three database objects, all tables like Dogs <-- Pets with identity column --> Cats. You would insert a row into the Pets table to get the sequence number where you would normally use NEXTVAL and then insert into the Dogs or Cats table as you normally would once you get the actual type of pet from the user. Any additional common columns could be moved from the Dogs/Cats tables into the Pets supertype table, with some consequences that 1) there would be one row for each sequence number, 2) any columns not able to be populated when getting the sequence number would need to have default values and 3) it would require a join to get all of the columns.

坏尐絯 2024-07-15 02:17:04

通过SQL你可以使用这个策略;

CREATE SEQUENCE [dbo].[SequenceFile]
AS int
START WITH 1
INCREMENT BY 1 ;

并通过此 SQL 读取唯一的下一个值

SELECT NEXT VALUE FOR [dbo].[SequenceFile]

By SQL you can use this strategy;

CREATE SEQUENCE [dbo].[SequenceFile]
AS int
START WITH 1
INCREMENT BY 1 ;

and read the unique next value whit this SQL

SELECT NEXT VALUE FOR [dbo].[SequenceFile]
难以启齿的温柔 2024-07-15 02:17:04

交易安全!对于 2012 年之前的 SQLServer 版本...(感谢 Matt G.)
这次讨论中缺少的一件事是交易安全。 如果您从序列中获取数字,则该数字必须是唯一的,并且其他应用程序或代码不应能够获取该数字。 就我而言,我们经常从序列中提取唯一的数字,但实际的交易可能会跨越相当长的时间,因此我们不希望其他人在提交交易之前获得相同的数字。
我们需要模仿预言机序列的行为,其中一个数字在被拉取时被保留。
我的解决方案是使用 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"'

TRANSACTION SAFE ! For SQLServer versions before 2012... (thanks Matt G.)
One thing missing in this discussion is transaction safety. If you get a number from a sequence, that number must be unique, and no other app or code should be able to get that number. In my case, we often pull unique numbers from sequences, but the actual transaction may span a considerable amount of time, so we don't want anyone else getting the same number before we commit the transaction.
We needed to mimic the behavior of oracle sequences, where a number was reserved when it was pulled.
My solution is to use xp_cmdshell to get a separate session/transaction on the database, so that we can immediately update the sequence, for the whole database, even before the transaction is complete.

--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('');

The solution requires a single table to hold used sequence values, and a procedure That creates a second autonomous transaction to insure that concurrent sessions don't get tangled up. You can have as many unique sequences as you like, they are referenced by name. Example code below is modified to omit requesting user and date stamp on the sequence history table (for audit) but I thought less-complex was better for the example ;-).

  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;

Now to get that procedure to work, you are going to need to enable xp_cmdshell, there are lots of good descriptions of how to do that, here are my personal notes that I made when I was trying to get things to work. Basic idea is that you need xp_cmdshell turned on in SQLServer Surface Are a configuration and you need to set a user account as the account that the xp_cmdshell command will run under, that will access the database to insert the sequence number and commit it.

--- 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"'
静水深流 2024-07-15 02:17:03

Sql Server 2012 引入了 SEQUENCE 对象 ,它允许您生成不与任何表关联的连续数值。

创建它们很容易:

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) ;

Sql Server 2012 has introduced SEQUENCE objects, which allow you to generate sequential numeric values not associated with any table.

Creating them are easy:

CREATE SEQUENCE Schema.SequenceName
AS int
INCREMENT BY 1 ;

An example of using them before insertion:

DECLARE @NextID int ;
SET @NextID = NEXT VALUE FOR Schema.SequenceName;
-- Some work happens
INSERT Schema.Orders (OrderID, Name, Qty)
  VALUES (@NextID, 'Rim', 2) ;
梦归所梦 2024-07-15 02:17:03

正如 sqljunkieshare 正确所说,从 SQL Server 2012 开始,有一个内置的 SEQUENCE 功能。

最初的问题没有澄清,但我假设序列的要求是:

  1. 它必须提供一组唯一的增长数字
  2. 如果多个用户同时请求序列的下一个值,他们都应该得到不同的值。 换句话说,无论如何都能保证生成值的唯一性。
  3. 由于某些交易可能会回滚,因此生成的数字的最终结果可能会存在差距。

我想评论一下原来问题中的陈述:

“此外,插入一行然后询问数据库这个数字是什么
看起来很黑客。”

好吧,我们对此无能为力。数据库是序列号的提供者,数据库处理所有这些你自己无法处理的并发问题。除了询问之外,我没有其他选择序列的下一个值的数据库必须有一个“给我序列的下一个值”的原子操作,并且只有数据库可以提供这样的原子操作。没有客户端代码可以保证他是唯一使用该序列的人。

要回答标题“您将如何实现序列”中的问题 - 我们使用的是 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

关于该过程的一些注释。

首先,如何将一行插入只有一个标识列的表中并不明显。 答案是默认值

然后,如果在另一个事务中调用该程序,我希望该程序能够正常工作。 简单的 ROLLBACK 会回滚所有内容(如果存在)是嵌套事务。 就我而言,我只需要回滚 INSERT 到辅助表中,因此我使用了 保存交易

没有 savepoint_name 或 transaction_name 的回滚事务
回滚到事务的开头。 筑巢时
事务,同一语句将所有内部事务回滚到
最外面的 BEGIN TRANSACTION 语句。

这就是我使用该过程的方式(在其他一些大过程中,例如创建一个新合约):

DECLARE @VarContractNumber int;
EXEC @VarContractNumber = dbo.GetNewContractNumber;

如果您需要一次生成一个序列值,那么一切都可以正常工作。 对于合同,每个合同都是单独创建的,因此这种方法非常有效。 我可以确定所有合同始终都有唯一的合同编号。

注意:只是为了防止可能出现的问题。 这些合同号是我的合同表所具有的代理身份密钥的补充。 代理键是用于引用完整性的内部键。 生成的合同号是打印在合同上的人性化号码。 此外,同一个合同表包含最终合同和提案,它们可以成为合同,也可以永远保留为提案。 提案和合同都保存非常相似的数据,这就是为什么它们保存在同一个表中。 只需更改一行中的标志即可将提案变成合同。 提案使用单独的数字序列进行编号,为此我有第二个表 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。 我无法在 OUTPUT 子句中使用 #NewS2TransactionNumbers,因为 ROLLBACK 会清理它。 幸运的是,表变量不受ROLLBACK 的影响。

因此,我使用表变量@TableTransactionNumbers 作为OUTPUT 子句的目标。 然后我ROLLBACK事务来清理序列表。 然后将生成的序列值从表变量@TableTransactionNumbers复制到临时表#NewS2TransactionNumbers,因为只有临时表#NewS2TransactionNumbers对存储过程的调用者。 表变量@TableTransactionNumbers 对于存储过程的调用者不可见。

此外,还可以使用 OUTPUT 子句将生成的序列直接发送给调用者(正如您在使用 MERGE 的注释变体中看到的那样)。 它本身工作得很好,但我需要在某个表中生成的值,以便在调用存储过程中进行进一步处理。 当我尝试这样的事情时:

INSERT INTO @TableTransactions (ID)
EXEC dbo.GetNewS2TransactionNumbers @ParamCount = @VarTransactionCount;

我收到错误

不能在 INSERT-EXEC 语句中使用 ROLLBACK 语句。

但是,我需要在 EXEC 中使用 ROLLBACK,这就是为什么我最终有这么多临时表。

毕竟,如果切换到具有正确 SEQUENCE 对象的最新版本的 SQL Server,该多好啊。

As sqljunkieshare correctly said, starting from SQL Server 2012 there is a built-in SEQUENCE feature.

The original question doesn't clarify, but I assume that requirements for the Sequence are:

  1. It has to provide a set of unique growing numbers
  2. If several users request next value of the sequence simultaneously they all should get different values. In other words, uniqueness of generated values is guaranteed no matter what.
  3. Because of possibility that some transactions can be rolled back it is possible that end result of generated numbers will have gaps.

I'd like to comment the statement in the original question:

"Besides, inserting a row and then asking the DB what the number just
seems so hackish."

Well, there is not much we can do about it here. The DB is a provider of the sequential numbers and DB handles all these concurrency issues that you can't handle yourself. I don't see alternative to asking the DB for the next value of the sequence. There has to be an atomic operation "give me the next value of the sequence" and only DB can provide such atomic operation. No client code can guarantee that he is the only one working with the sequence.

To answer the question in the title "how would you implement sequences" - We are using 2008, which doesn't have the SEQUENCE feature, so after some reading on this topic I ended up with the following.

For each sequence that I need I create a separate helper table with just one IDENTITY column (in the same fashion as in 2012 you would create a separate Sequence object).

CREATE TABLE [dbo].[SequenceContractNumber]
(
    [ContractNumber] [int] IDENTITY(1,1) NOT NULL,

    CONSTRAINT [PK_SequenceContractNumber] PRIMARY KEY CLUSTERED ([ContractNumber] ASC)
)

You can specify starting value and increment for it.
Then I create a stored procedure that would return the next value of the sequence.
Procedure would start a transaction, insert a row into the helper table, remember the generated identity value and roll back the transaction. Thus the helper table always remains empty.

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

Few notes about the procedure.

First, it was not obvious how to insert a row into a table that has only one identity column. The answer is DEFAULT VALUES.

Then, I wanted procedure to work correctly if it was called inside another transaction. The simple ROLLBACK rolls back everything if there are nested transactions. In my case I need to roll back only INSERT into the helper table, so I used SAVE TRANSACTION.

ROLLBACK TRANSACTION without a savepoint_name or transaction_name
rolls back to the beginning of the transaction. When nesting
transactions, this same statement rolls back all inner transactions to
the outermost BEGIN TRANSACTION statement.

This is how I use the procedure (inside some other big procedure that, for example, creates a new contract):

DECLARE @VarContractNumber int;
EXEC @VarContractNumber = dbo.GetNewContractNumber;

It all works fine if you need to generate sequence values one at a time. In case of contracts, each contract is created individually, so this approach works perfectly. I can be sure that all contracts always have unique contract numbers.

NB: Just to prevent possible questions. These contract numbers are in addition to surrogate identity key that my Contracts table has. The surrogate key is internal key that is used for referential integrity. The generated contract number is a human-friendly number that is printed on the contract. Besides, the same Contracts table contains both final contracts and Proposals, which can become contracts or can remain as proposals forever. Both Proposals and Contracts hold very similar data, that's why they are kept in the same table. Proposal can become a contract by simply changing the flag in one row. Proposals are numbered using a separate sequence of numbers, for which I have a second table SequenceProposalNumber and second procedure GetNewProposalNumber.


Recently, though, I came across a problem.
I needed to generate sequence values in a batch, rather than one-by-one.

I need a procedure that would process all payments that were received during a given quarter in one go. The result of such processing could be ~20,000 transactions that I want to record in the Transactions table. I have similar design here. Transactions table has internal IDENTITY column that end user never sees and it has a human-friendly Transaction Number that would be printed on the statement. So, I need a way to generate a given number of unique values in a batch.

Essentially, I used the same approach, but there are few peculiarities.

First, there is no direct way to insert multiple rows in a table with only one IDENTITY column. Though there is a workaround by (ab)using MERGE, I didn't use it in the end. I decided that it was easier to add a dummy Filler column. My Sequence table is going to be always empty, so extra column doesn't really matter.

The helper table looks like this:

CREATE TABLE [dbo].[SequenceS2TransactionNumber]
(
    [S2TransactionNumber] [int] IDENTITY(1,1) NOT NULL,
    [Filler] [int] NULL,
    CONSTRAINT [PK_SequenceS2TransactionNumber] 
    PRIMARY KEY CLUSTERED ([S2TransactionNumber] ASC)
)

The procedure looks like this:

-- 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

And this is how it is used (inside some big stored procedure that calculates transactions):

-- 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;

There are few things here that require explanation.

I need to insert a given number of rows into the SequenceS2TransactionNumber table. I use a helper Numbers table for this. This table simply holds integer numbers from 1 to 100,000. It is used in other places in the system as well. I check if there is enough rows in the Numbers table and expand it to 100,000 * 100,000 by cross joining with itself if needed.

I have to save the result of the bulk insert somewhere and pass it to the caller somehow. One way to pass a table outside of the stored procedure is to use a temporary table. I can't use table-valued parameter here, because it is read-only unfortunately. Also, I can't directly insert the generated sequence values into the temporary table #NewS2TransactionNumbers. I can't use #NewS2TransactionNumbers in the OUTPUT clause, because ROLLBACK will clean it up. Fortunately, the table variables are not affected by the ROLLBACK.

So, I use table variable @TableTransactionNumbers as a destination of the OUTPUT clause. Then I ROLLBACK the transaction to clean up the Sequence table. Then copy the generated sequence values from table variable @TableTransactionNumbers to the temporary table #NewS2TransactionNumbers, because only temporary table #NewS2TransactionNumbers can be visible to the caller of the stored procedure. The table variable @TableTransactionNumbers is not visible to the caller of the stored procedure.

Also, it is possible to use OUTPUT clause to send the generated sequence directly to the caller (as you can see in the commented variant that uses MERGE). It works fine by itself, but I needed the generated values in some table for further processing in the calling stored procedure. When I tried something like this:

INSERT INTO @TableTransactions (ID)
EXEC dbo.GetNewS2TransactionNumbers @ParamCount = @VarTransactionCount;

I was getting an error

Cannot use the ROLLBACK statement within an INSERT-EXEC statement.

But, I need ROLLBACK inside the EXEC, that's why I ended up having so many temporary tables.

After all this, how nice would it be to switch to the latest version of SQL server which has a proper SEQUENCE object.

苹果你个爱泡泡 2024-07-15 02:17:03

身份列大致类似于序列。

An Identity column is roughly analogous to a sequence.

雾里花 2024-07-15 02:17:03

您可以只使用普通的旧表并将它们用作序列。 这意味着您的插入内容将始终是:

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() 看起来像是一个黑客。

克服它。 在罗马做到入乡随俗。

You could just use plain old tables and use them as sequences. That means your inserts would always be:

BEGIN TRANSACTION  
SELECT number from plain old table..  
UPDATE plain old table, set the number to be the next number  
INSERT your row  
COMMIT  

But don't do this. The locking would be bad...

I started on SQL Server and to me, the Oracle "sequence" scheme looked like a hack. I guess you are coming from the other direction and to you, and scope_identity() looks like a hack.

Get over it. When in Rome, do as the Romans do.

终止放荡 2024-07-15 02:17:03

我用来解决这个问题的方法是使用一个表“序列”来存储我的所有序列和一个“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;
}

The way that i used to solve this problem was a table 'Sequences' that stores all my sequences and a 'nextval' stored procedure.

Sql Table:

CREATE TABLE Sequences (  
    name VARCHAR(30) NOT NULL,  
    value BIGINT DEFAULT 0 NOT NULL,  
    CONSTRAINT PK_Sequences PRIMARY KEY (name)  
);

The PK_Sequences is used just to be sure that there will never be sequences with the same name.

Sql Stored Procedure:

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 some sequences:

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);  

Finally get next value of a sequence,

execute nextval 'SEQ_Participant';

Some c# code to get the next value from Sequence table,

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;
}
‖放下 2024-07-15 02:17:03

在 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)

In SQL Server 2012, you can simply use

CREATE SEQUENCE

In 2005 and 2008, you can get an arbitrary list of sequential numbers using a common table expression.

Here's an example (note that the MAXRECURSION option is important):

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)
寂寞清仓 2024-07-15 02:17:03

Oracle 实现的序列需要在插入之前调用数据库。
SQL Server 实现的身份需要在插入后调用数据库。

其中一个并不比另一个更黑客。 最终效果是相同的 - 依赖/依赖数据存储来提供唯一的人工键值以及(在大多数情况下)对存储的两次调用。

我假设您的关系模型基于人工键,在这种情况下,我将提供以下观察结果:

我们永远不应该寻求赋予人工键以意义; 它们的唯一目的应该是链接相关记录。

您对订购数据有何需求? 它可以在视图(演示文稿)中处理吗?还是它是必须保留的数据的真实属性?

Sequences as implemented by Oracle require a call to the database before the insert.
identities as implemented by SQL Server require a call to the database after the insert.

One is no more hackish than the other. The net effect is the same - a reliance/dependency on the data store to provide unique artificial key values and (in most cases) two calls to the store.

I'm assuming that your relational model is based on artificial keys, and in this context, I'll offer the following observation:

We should never seek to imbue artificial keys with meaning; their only purpose should be to link related records.

What is your need related to ordering data? can it be handled in the view (presentation) or is it a true attribute of your data which must be persisted?

骄兵必败 2024-07-15 02:17:03

创建一个带有标识符的阶段表。

在加载阶段表之前,截断并重新设定标识符以从 1.

加载表开始。 现在,每一行都有一个从 1 到 N 的唯一值。

创建一个包含序列号的表。 这可能是多行,每个序列一行。

从您创建的序列表中查找序列号。
通过将阶段表中的行数添加到序列号来更新序列号。

通过添加您查找的序列号来更新阶段表标识符。 这是一个简单的一步过程。
或者
加载目标表,在 ETL 中加载时将序列号添加到标识符中。 这可以利用批量加载器并允许其他转换。

Create a stage table with an identifier on it.

Before loading the stage table, truncate and reseed the identifier to start at 1.

Load your table. Each row now has a unique value from 1 to N.

Create a table that holds sequence numbers. This could be several rows, one for each sequence.

Lookup the sequence number from the sequence table you created.
Update the seqence number by adding the number of rows in the stage table to the sequence number.

Update the stage table identifier by adding the seqence number you looked up to it. This is an easy one step process.
or
Load your target table, add the sequence number to the identifier as you load in ETL. This can take advantage of the bulk loader and allow for other transformations.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文