Как получить следующий номер в последовательности

у меня есть такой стол:

+----+-----------+------+-------+--+
| id | Part      | Seq  | Model |  |
+----+-----------+------+-------+--+
| 1  | Head      | 0    | 3     |  |
| 2  | Neck      | 1    | 3     |  |
| 3  | Shoulders | 2    | 29    |  |
| 4  | Shoulders | 2    | 3     |  |
| 5  | Stomach   | 5    | 3     |  |
+----+-----------+------+-------+--+

как я могу вставить другую запись со следующим seq после Stomach для модели 3. Итак, вот как должна выглядеть новая таблица:

+----+-----------+------+-------+--+
| id | Part      | Seq  | Model |  |
+----+-----------+------+-------+--+
| 1  | Head      | 0    | 3     |  |
| 2  | Neck      | 1    | 3     |  |
| 3  | Shoulders | 2    | 29    |  |
| 4  | Shoulders | 2    | 3     |  |
| 5  | Stomach   | 5    | 3     |  |
| 6  | Groin     | 6    | 3     |  |
+----+-----------+------+-------+--+

есть ли способ создать запрос вставки, который даст следующий номер после самого высокого seq только для модели 3. Кроме того, ищет что-то, что является безопасным параллелизмом.

9 ответов


если вы не поддерживаете таблицу счетчиков, есть два варианта. В транзакции сначала выберите MAX(seq_id) С одной из следующих подсказок таблицы:

  1. WITH(TABLOCKX, HOLDLOCK)
  2. WITH(ROWLOCK, XLOCK, HOLDLOCK)

TABLOCKX + HOLDLOCK - это немного перебор. Он блокирует регулярные операторы select, которые можно считать тяжелый даже если сделка небольшая.

A ROWLOCK, XLOCK, HOLDLOCK подсказка таблицы, вероятно, лучшая идея (но: прочитайте альтернативу со встречным столом дальше). Преимущество заключается в том, что он не блокирует регулярные операторы select, т. е. когда операторы select не отображаются в SERIALIZABLE transaction, или когда операторы select не предоставляют те же табличные подсказки. Используя ROWLOCK, XLOCK, HOLDLOCK будет по-прежнему блокировать инструкции insert.

конечно, вы должны быть уверены, что никакие другие части вашей программы выберите MAX(seq_id) без этих подсказок таблицы (или вне SERIALIZABLE transaction), а затем используйте это значение для вставки строки.

обратите внимание, что в зависимости от количества строк, заблокированных таким образом, возможно, что SQL Server увеличит блокировку до блокировки таблицы. Подробнее о эскалации блокировки здесь.

процедура вставки с помощью WITH(ROWLOCK, XLOCK, HOLDLOCK) будет выглядеть следующим образом:

DECLARE @target_model INT=3;
DECLARE @part VARCHAR(128)='Spine';
BEGIN TRY
    BEGIN TRANSACTION;
    DECLARE @max_seq INT=(SELECT MAX(seq) FROM dbo.table_seq WITH(ROWLOCK,XLOCK,HOLDLOCK) WHERE model=@target_model);
    IF @max_seq IS NULL SET @max_seq=0;
    INSERT INTO dbo.table_seq(part,seq,model)VALUES(@part,@max_seq+1,@target_model);
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    ROLLBACK TRANSACTION;
END CATCH

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

CREATE TABLE dbo.counter_seq(model INT PRIMARY KEY, seq_id INT);

затем вы измените процедуру вставки следующим образом:

DECLARE @target_model INT=3;
DECLARE @part VARCHAR(128)='Spine';
BEGIN TRY
    BEGIN TRANSACTION;
    DECLARE @new_seq INT=(SELECT seq FROM dbo.counter_seq WITH(ROWLOCK,XLOCK,HOLDLOCK) WHERE model=@target_model);
    IF @new_seq IS NULL 
        BEGIN SET @new_seq=1; INSERT INTO dbo.counter_seq(model,seq)VALUES(@target_model,@new_seq); END
    ELSE
        BEGIN SET @new_seq+=1; UPDATE dbo.counter_seq SET seq=@new_seq WHERE model=@target_model; END
    INSERT INTO dbo.table_seq(part,seq,model)VALUES(@part,@new_seq,@target_model);
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    ROLLBACK TRANSACTION;
END CATCH

преимущество в том, что используется меньше блокировок строк (т. е. один на модель в dbo.counter_seq), и блокировка эскалации не может заблокировать весь dbo.table_seq таблица, таким образом, блокирует операторы select.

вы можете проверить все это и увидеть эффекты самостоятельно, разместив WAITFOR DELAY '00:01:00' после выбора последовательности от counter_seq, и возиться с таблицей(таблицами) во второй SSMS табуляция.


ПС1: с помощью ROW_NUMBER() OVER (PARTITION BY model ORDER BY ID) - это не лучший способ. Если строки будут удалены / добавлены или изменены идентификаторы, последовательность изменится (рассмотрим идентификаторы счетов, которые никогда не должны меняться). Также с точки зрения производительности определять номера строк всех предыдущих строк при получении одной строки-плохая идея.

PS2: я бы никогда не использовал внешние ресурсы для обеспечения блокировки, когда SQL Server уже обеспечивает блокировку через уровни изоляции или мелкозернистую таблицу полунамеки.


правильный способ обработки таких вставок-использовать


давайте сначала перечислим проблемы:

  1. мы не можем использовать нормальное ограничение, так как существуют нулевые значения, и нам также нужно обслуживать дубликаты, а также пробелы - если мы посмотрим на существующие данные. это прекрасно, мы разберемся; - > в шаге 3
  2. мы требуем безопасности для параллельных операций (таким образом, некоторая форма или сочетание транзакций, уровни изоляции и, возможно, "своего рода SQL-мьютекс".) Gut feel здесь хранится proc для несколько причин:

    2.1 он легче защищает от SQL-инъекций

    2.2 мы можем контролировать уровни изоляции (блокировка таблицы) более легко и восстанавливаться от некоторых проблем, которые приходят с такого рода требования

    2.3 мы можем использовать блокировки db уровня приложения для управления параллелизмом

  3. мы должны сохранить или найти следующее значение на каждой вставке. Слово параллелизм уже говорит нам, что будет раздор и, вероятно, высокий пропускная способность (иначе, пожалуйста, придерживайтесь отдельных потоков). Поэтому мы уже должны думать: не читайте Из той же таблицы, в которую хотите писать, в уже сложном мире.

Итак, с этим коротким приквелом, давайте попробуем решение:

для начала мы создаем исходную таблицу, а затем таблицу для хранения последовательности (BodyPartsCounter), которую мы устанавливаем на последнюю используемую последовательность + 1:

    CREATE TABLE BodyParts
        ([id] int identity, [Part] varchar(9), [Seq] varchar(4), [Model] int)
    ;

    INSERT INTO BodyParts
        ([Part], [Seq], [Model])
    VALUES
        ('Head', NULL, 3),
        ('Neck', '1', 3),
        ('Shoulders', '2', 29),
        ('Shoulders', '2', 3),
        ('Stomach', '5', 3)
    ;

    CREATE TABLE BodyPartsCounter
        ([id] int
        , [counter] int)
    ;

    INSERT INTO BodyPartsCounter
        ([id], [counter])
    SELECT 1, MAX(id) + 1 AS id FROM BodyParts
    ;

Затем нам нужно создать хранимую процедуру что сделает магию. Короче говоря, он действует как мьютекс, в основном гарантируя вам параллелизм (если вы не делаете вставки или обновления в те же таблицы в другом месте). Затем он получает следующий seq, обновляет его и вставляет новую строку. После того, как это все произошло, он зафиксирует транзакцию и выпустит сохраненный proc для следующего ожидающего вызывающего потока.

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:      Charlla
-- Create date: 2016-02-15
-- Description: Inserts a new row in a concurrently safe way
-- =============================================
CREATE PROCEDURE InsertNewBodyPart 
@bodypart varchar(50), 
@Model int = 3
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;

    BEGIN TRANSACTION;

    -- Get an application lock in your threaded calls
    -- Note: this is blocking for the duration of the transaction
    DECLARE @lockResult int;
    EXEC @lockResult = sp_getapplock @Resource = 'BodyPartMutex', 
                   @LockMode = 'Exclusive';
    IF @lockResult = -3 --deadlock victim
    BEGIN
        ROLLBACK TRANSACTION;
    END
    ELSE
    BEGIN
        DECLARE @newId int;
        --Get the next sequence and update - part of the transaction, so if the insert fails this will roll back
        SELECT @newId = [counter] FROM BodyPartsCounter WHERE [id] = 1;
        UPDATE BodyPartsCounter SET [counter] = @newId + 1 WHERE id = 1;

        -- INSERT THE NEW ROW
        INSERT INTO dbo.BodyParts(
            Part
            , Seq
            , Model
            )
            VALUES(
                @bodypart
                , @newId
                , @Model
            )
        -- END INSERT THE NEW ROW
        EXEC @lockResult = sp_releaseapplock @Resource = 'BodyPartMutex';
        COMMIT TRANSACTION;
    END;

END
GO

Теперь запустите тест с помощью этого:

EXEC    @return_value = [dbo].[InsertNewBodyPart]
    @bodypart = N'Stomach',
    @Model = 4

SELECT  'Return Value' = @return_value

SELECT * FROM BodyParts;
SELECT * FROM BodyPartsCounter

Это все работает - но будьте осторожны - есть много, чтобы рассмотреть с любым видом многопоточного приложения.

надеюсь, что это помогает!


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

таблицы:

CREATE TABLE dbo.counter_seq(model INT PRIMARY KEY, seq INT);
CREATE TABLE dbo.table_seq(part varchar(128), seq int, model int);

более простая версия (нет SELECT оператор для извлечения текущего seq):

DECLARE @target_model INT=3;
DECLARE @part VARCHAR(128)='Otra MAS';

BEGIN TRY
    BEGIN TRANSACTION;
    DECLARE @seq int = 1
    UPDATE dbo.counter_seq WITH(ROWLOCK,HOLDLOCK) SET @seq = seq = seq + 1 WHERE model=@target_model;
    IF @@ROWCOUNT = 0 INSERT INTO dbo.counter_seq VALUES (@target_model, 1);
    INSERT INTO dbo.table_seq(part,seq,model)VALUES(@part,@seq,@target_model);
    COMMIT
END TRY
BEGIN CATCH
    ROLLBACK TRANSACTION;
END CATCH

поскольку вы хотите, чтобы последовательность была основана на определенной модели, просто добавьте это в предложение where при выполнении select. Это гарантирует, что Max (SEQ) относится только к этой серии моделей. Также, поскольку SEQ может быть нулевым, оберните его в ISNULL, поэтому, если это null, он будет 0, поэтому 0 + 1, установит следующий 1. Основной способ сделать это :

Insert into yourtable(id, Part, Seq, Model)
    Select 6, 'Groin', ISNULL(max(Seq),0) + 1, 3 
    From yourtable
    where MODEL = 3;

Я бы не пытался хранить Seq значение в таблице в первую очередь.

как вы сказали в комментариях, ваш ID и IDENTITY, который автоматически увеличивается очень эффективным и одновременно безопасным способом сервером. Используйте его для определения порядка, в котором были вставлены строки, и порядка, в котором Seq значения должны быть созданы.

затем использовать ROW_NUMBER для генерации значений Seq перегороженную Model (последовательность перезапускается с 1 для каждого значения Model) по мере необходимости в запросе.

SELECT
    ID
    ,Part
    ,Model
    ,ROW_NUMBER() OVER(PARTITION BY Model ORDER BY ID) AS Seq
FROM YourTable

insert into tableA (id,part,seq,model)
values
(6,'Groin',(select MAX(seq)+1 from tableA where model=3),3)

create function dbo.fncalnxt(@model int)
returns int 
begin
declare @seq int
select @seq= case when @model=3 then max(id) --else
end from tblBodyParts
return @seq+1
end
--query idea To insert values, ideal if using SP to insert
insert into tblBodyParts values('groin',dbo.fncalnxt(@model),@model)

вы можете попробовать это, я думаю. Новичок, поправь меня, если я ошибаюсь. я бы предложил использовать функцию для получения значения в столбце seq на основе модели; вам придется проверить другой случай, хотя, чтобы вернуть другое значение, которое вы хотите, когда модель!=3, теперь он вернет null.


предполагая, что у вас есть следующая таблица:

CREATE TABLE tab (
    id int IDENTITY(1,1) PRIMARY KEY,
    Part VARCHAR(32) not null,
    Seq int not null,
    Model int not null
);

INSERT INTO
    tab(Part,Seq,Model)
VALUES
    ('Head', 0, 3),
    ('Neck', 1, 3),
    ('Shoulders', 2, 29),
    ('Shoulders', 2, 3),
    ('Stomach', 5, 3);

запрос ниже позволит вам импортировать несколько записей, не разрушая model_seq

INSERT INTO
    tab (model, part, model_seq)
SELECT
    n.model,
    n.part,
    -- ensure new records will get receive the proper model_seq
    IFNULL(max_seq + model_seq, model_seq) AS model_seq
FROM
    (
        SELECT
            -- row number for each model new record
            ROW_NUMBER() OVER(PARTITION BY model ORDER BY part) AS model_seq,
            n.model,
            n.part,
            MAX(t.seq) AS max_seq
        FROM
            -- Table-values constructor allows you to prepare the
            -- temporary data (with multi rows),
            -- where you could join the existing one
            -- to retrieve the max(model_seq) if any
            (VALUES
                ('Stomach',3),
                ('Legs',3),
                ('Legs',29),
                ('Arms',1)
            ) AS n(part, model)
        LEFT JOIN
            tab
        ON
            tab.model = n.model
        GROUP BY
            n.model n.part
    ) AS t

нам нужен row_number (), чтобы гарантировать, что если мы импортируем более одного значения, заказ будет сохранен. Больше информации о ROW_NUMBER () OVER () (Transact-SQL)

table-value конструктор используется для создания таблицы с новыми значениями и присоединиться к MAX model_seq для модели. Вы могли бы найти больше о табличное значение подрядчиком здесь: конструктор табличных значений (Transact-SQL)