Как бы вы реализовали последовательности в Microsoft SQL Server? - PullRequest
33 голосов
/ 12 ноября 2008

У кого-нибудь есть хороший способ реализовать что-то вроде последовательности в SQL-сервере?

Иногда вы просто не хотите использовать GUID, кроме того факта, что они безобразны. Может быть, последовательность, которую вы хотите, не числовая? Кроме того, вставить строку, а затем спросить у БД, какое это число, кажется таким хакерским.

Ответы [ 16 ]

50 голосов
/ 11 декабря 2011

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

См. Мой блог для более глубокого взгляда на то, как использовать последовательности:

http://sqljunkieshare.com/2011/12/11/sequences-in-sql-server-2012-implementingmanaging-performance/

14 голосов
/ 10 декабря 2014

Как правильно сказал sqljunkieshare , начиная с SQL Server 2012 существует встроенная функция SEQUENCE.

Исходный вопрос не проясняется, но я предполагаю, что требования к последовательности:

  1. Он должен предоставить набор уникальных растущих чисел
  2. Если несколько пользователей одновременно запрашивают следующее значение последовательности, все они должны получить разные значения. Другими словами, уникальность генерируемых значений гарантируется не смотря ни на что.
  3. Из-за вероятности того, что некоторые транзакции могут быть отменены, возможно, что конечный результат сгенерированных чисел будет иметь пропуски.

Я хотел бы прокомментировать утверждение в исходном вопросе:

"Кроме того, вставив строку, а затем спросив БД, какой номер просто кажется таким хакерским. "

Ну, здесь мы мало что можем сделать. БД является поставщиком последовательных чисел, а БД решает все эти проблемы параллелизма, которые вы не можете решить самостоятельно. Я не вижу альтернативы запрашиванию в БД следующего значения последовательности. Должна быть операция 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 объект.

5 голосов
/ 12 ноября 2008

Вы можете просто использовать старые таблицы и использовать их как последовательности. Это означает, что ваши вставки всегда будут:

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 () выглядит как хак

Преодолей это. Когда в Риме, делай, как римляне.

5 голосов
/ 12 ноября 2008

Столбец идентификации примерно аналогичен последовательности.

4 голосов
/ 28 августа 2013

Для решения этой проблемы я использовал таблицу «Последовательности», в которой хранятся все мои последовательности, и хранимую процедуру «nextval».

Таблица Sql:

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

PK_Sequence используется только для того, чтобы быть уверенным, что никогда не будет последовательностей с одинаковым именем.

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;
}
3 голосов
/ 23 января 2013

В 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)
3 голосов
/ 12 ноября 2008

Последовательности, реализованные Oracle, требуют вызова базы данных перед вставкой. идентификаторы, реализованные в SQL Server, требуют вызова базы данных после вставки.

Один не более хакерский, чем другой. Чистый эффект тот же - зависимость / зависимость от хранилища данных для предоставления уникальных значений искусственного ключа и (в большинстве случаев) два вызова хранилища.

Я предполагаю, что ваша реляционная модель основана на искусственных ключах, и в этом контексте я предложу следующее наблюдение:

Мы никогда не должны стремиться наполнить искусственные ключи смыслом; их единственная цель должна состоять в том, чтобы связать связанные записи.

Какая ваша потребность связана с данными заказа? может ли он обрабатываться в представлении (презентации) или это истинный атрибут ваших данных, который необходимо сохранить?

2 голосов
/ 29 сентября 2016

Поскольку sqljunkiesshare сообщает , последовательности были добавлены в SQL Server 2012. Вот как это сделать в графическом интерфейсе. Это эквивалентно:

CREATE SEQUENCE Schema.SequenceName
AS int
INCREMENT BY 1 ;
  1. В Обозревателе объектов разверните папку Программируемость
  2. В папке Программируемость щелкните правой кнопкой мыши Последовательности папка, как показано ниже:

enter image description here

  1. Подчеркнуты значения, которые вы бы обновили, чтобы получить эквивалент оператора SQL выше, однако, я бы рассмотрел меняя их в зависимости от ваших потребностей (см. примечания ниже).

enter image description here

Примечания:

2 голосов
/ 08 октября 2012

Рассмотрим следующий фрагмент.

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
2 голосов
/ 13 января 2011

Создайте таблицу этапов с идентификатором.

Перед загрузкой таблицы этапов обрежьте и заново заполните идентификатор, чтобы начать с 1.

Загрузите вашу таблицу. Каждая строка теперь имеет уникальное значение от 1 до N.

Создать таблицу с порядковыми номерами. Это может быть несколько строк, по одной для каждой последовательности.

Поиск порядкового номера из созданной вами таблицы последовательности. Обновите номер последовательности, добавив число строк в таблице этапов к порядковому номеру.

Обновите идентификатор таблицы этапов, добавив к нему номер последовательности, который вы посмотрели. Это простой одношаговый процесс. или же Загрузите вашу целевую таблицу, добавьте порядковый номер к идентификатору при загрузке в ETL. Это может использовать преимущества загрузчика и позволяет выполнять другие преобразования.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...