Как правильно сказал sqljunkieshare , начиная с SQL Server 2012 существует встроенная функция SEQUENCE
.
Исходный вопрос не проясняется, но я предполагаю, что требования к последовательности:
- Он должен предоставить набор уникальных растущих чисел
- Если несколько пользователей одновременно запрашивают следующее значение последовательности, все они должны получить разные значения. Другими словами, уникальность генерируемых значений гарантируется не смотря ни на что.
- Из-за вероятности того, что некоторые транзакции могут быть отменены, возможно, что конечный результат сгенерированных чисел будет иметь пропуски.
Я хотел бы прокомментировать утверждение в исходном вопросе:
"Кроме того, вставив строку, а затем спросив БД, какой номер просто
кажется таким хакерским. "
Ну, здесь мы мало что можем сделать. БД является поставщиком последовательных чисел, а БД решает все эти проблемы параллелизма, которые вы не можете решить самостоятельно. Я не вижу альтернативы запрашиванию в БД следующего значения последовательности. Должна быть операция atomic «дай мне следующее значение последовательности», и только DB может обеспечить такую операцию atomic . Ни один клиентский код не может гарантировать, что он единственный работает с последовательностью.
Чтобы ответить на вопрос в заголовке «Как бы вы реализовали последовательности» - мы используем 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
.
ROLLBACK TRANSACTION без имени точки сохранения или имени транзакции
откатывается к началу транзакции. Когда вложение
транзакции, этот же оператор откатывает все внутренние транзакции в
самый внешний оператор BEGIN TRANSACTION.
Вот как я использую процедуру (внутри какой-то другой большой процедуры, которая, например, создает новый контракт):
DECLARE @VarContractNumber int;
EXEC @VarContractNumber = dbo.GetNewContractNumber;
Все работает нормально, если вам нужно генерировать значения последовательности по одному. В случае контрактов каждый контракт создается индивидуально, поэтому этот подход работает отлично. Я могу быть уверен, что все контракты всегда имеют уникальные номера контрактов.
NB: Просто, чтобы предотвратить возможные вопросы. Эти номера контрактов являются дополнением к суррогатному идентификационному ключу, который есть в моей таблице контрактов. Суррогатный ключ - это внутренний ключ, который используется для ссылочной целостности. Сгенерированный номер контракта является удобным для человека номером, который напечатан на контракте. Кроме того, одна и та же таблица Контрактов содержит как окончательные контракты, так и Предложения, которые могут стать контрактами или могут оставаться в качестве предложений навсегда. И предложения, и контракты содержат очень похожие данные, поэтому они хранятся в одной таблице. Предложение может стать договором, просто поменяв флаг в одном ряду. Предложения нумеруются с использованием отдельной последовательности номеров, для которой у меня есть вторая таблица SequenceProposalNumber
и вторая процедура GetNewProposalNumber
.
Недавно, однако, я столкнулся с проблемой.
Мне нужно было генерировать значения последовательности в пакете, а не по одному.
Мне нужна процедура, которая бы обрабатывала все платежи, которые были получены в течение данного квартала за один раз. Результатом такой обработки может стать ~ 20 000 транзакций, которые я хочу записать в таблицу Transactions
. У меня есть похожий дизайн здесь. Таблица 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
транзакции для очистки таблицы последовательности. Затем скопируйте сгенерированные значения последовательности из табличной переменной @TableTransactionNumbers
во временную таблицу #NewS2TransactionNumbers
, поскольку только временная таблица #NewS2TransactionNumbers
может быть видимой для вызывающей стороны хранимой процедуры. Табличная переменная @TableTransactionNumbers
не видна вызывающей стороне хранимой процедуры.
Кроме того, можно использовать предложение OUTPUT
для отправки сгенерированной последовательности непосредственно вызывающей стороне (как вы можете видеть в закомментированном варианте, использующем MERGE
). Сам по себе он работает нормально, но мне понадобились сгенерированные значения в некоторой таблице для дальнейшей обработки в вызывающей хранимой процедуре. Когда я попробовал что-то вроде этого:
INSERT INTO @TableTransactions (ID)
EXEC dbo.GetNewS2TransactionNumbers @ParamCount = @VarTransactionCount;
Я получил ошибку
Невозможно использовать инструкцию ROLLBACK в инструкции INSERT-EXEC.
Но мне нужно ROLLBACK
внутри EXEC
, поэтому у меня так много временных таблиц.
После всего этого, как хорошо было бы переключиться на последнюю версию SQL-сервера, имеющего надлежащий SEQUENCE
объект.