Я бы порекомендовал альтернативный дизайн данных.Этот тип шаблона ключей и последовательностей очень сложно реализовать должным образом в реляционной базе данных, и недостатки часто перевешивают преимущества.
У вас есть довольно много вариантов, но самые простые из них начинаются с разделения таблицыв двух:
CREATE TABLE DRAWING
(
ID INT IDENTITY(1, 1),
PRIMARY KEY (ID)
);
CREATE TABLE DRAWING_REVISION
(
ID INT IDENTITY(1, 1),
DRAWING_ID INT,
INFO VARCHAR(50),
PRIMARY KEY (ID),
CONSTRAINT FK_DRAWING_REVISION_DRAWING FOREIGN KEY (DRAWING_ID) REFERENCES DRAWING(ID)
);
Преимущество состоит в точном и правильном представлении данных без дополнительных усилий с вашей стороны.Просто добавьте строку в таблицу DRAWING_REVISION
, если вы хотите добавить новую редакцию в чертеж.Поскольку первичные ключи используют спецификацию IDENTITY
, вам не нужно искать следующую ID
.
Очевидное решение и его недостатки
Если вам нуженВпрочем, читаемый человеком номер ревизии, а не только для глаз вашего сервера ID
, это можно сделать двумя способами.Они оба начинают с добавления REV INT
к определению данных для DRAWING_REVISION
вместе с CONSTRAINT UK_DRAWING_REVISION_DRAWING_ID_REV UNIQUE (DRAWING_ID, REV)
.Хитрость тогда, конечно, состоит в том, чтобы найти следующий номер ревизии для данного чертежа.
Если вы ожидаете, что только у каждого будет крошечное число одновременных пользователей, вы можете просто SELECT MAX(REV) + 1 FROM DRAWING_REVISION WHERE DRAWING_ID = @DRAWING_ID
, либо в вашемкод приложения или в триггере INSTEAD OF INSERT
.Однако при высоком параллелизме или неудаче пользователи могут блокировать друг друга, потому что они могут попытаться вставить одну и ту же комбинацию DRAWING_ID
и REV
в DRAWING_REVISION
.
Some Background
На самом деле есть только одно решение этой проблемы, хотя для объяснения, почему есть только одно решение, требуется немного справочной информации.Рассмотрим следующий код:
BEGIN TRAN
INSERT DRAWING DEFAULT VALUES;
INSERT DRAWING DEFAULT VALUES;
SELECT ID FROM DRAWING; -- Output: 1, 2
ROLLBACK TRAN
BEGIN TRAN
INSERT DRAWING DEFAULT VALUES;
SELECT ID FROM DRAWING; -- Output: 3
ROLLBACK TRAN
Конечно, выходные данные будут отличаться при последующих выполнениях.За кулисами SQL-сервер выдает значения IDENTITY
и увеличивает счетчик.Если вы никогда не зафиксируете значение, сервер не будет пытаться «заполнить» дыры в последовательности - значения предоставляются только для пересылки.
Это функция, а не ошибка.IDENTITY
колонны предназначены для заказа и уникальны, но не обязательно плотно упакованы.Единственный способ гарантировать плотную упаковку - это сериализовать все входящие запросы, убедившись, что каждый из них завершает или завершает работу до начала следующего;в противном случае сервер может попытаться выполнить обратное заполнение значения IDENTITY
, которое было выдано полчаса назад, только для того, чтобы длительная транзакция (т. е. первоначальный получатель этого значения IDENTITY
) зафиксировала строку с дубликатом.Первичный ключ.
(Стоит отметить, что когда я говорю «транзакция», это не обязательно означает TSQL TRANSACTION
, хотя я бы порекомендовал их использовать. Это может быть абсолютно любая процедура насторона приложения или SQL-сервера, которая может занять любое количество времени, даже если это время - только время, необходимое для SELECT
следующего номера редакции и сразу после этого INSERT
нового DRAWING_REVISION
.)
Эта попытка обратного заполнения значений является просто замаскированной сериализацией, поскольку в ситуации с двумя одновременными запросами INSERT
она наказывает второй запрос на принятие.Это вынуждает последнего пришедшего повторить попытку (возможно, несколько раз, пока не произойдет, что конфликта не будет).Одновременно выполняется одно успешное представление: сериализация, но без использования очереди.
Подход SELECT MAX(REV) + 1
имеет тот же недостаток.Естественно, подход MAX
не предпринимает никаких попыток обратного заполнения значений, но он заставляет каждый параллельный запрос бороться за один и тот же номер ревизии с одинаковыми результатами.
Почему это плохо?Системы баз данных рассчитаны на параллелизм и валюту: эта возможность является одним из основных преимуществ управляемой базы данных по сравнению с форматом плоских файлов.
Поддельное правильное
Итак, после всего этого долгогоВыдвинутая экспозиция, что вы можете сделать, чтобы решить проблему?Вы можете скрестить пальцы и надеяться, что вы никогда не увидите много одновременных пользователей, но почему вы хотите отказаться от широкого использования вашего собственного приложения?В конце концов, вы не хотите, чтобы успех был вашим падением.
Решение состоит в том, чтобы сделать то, что SQL Server делает со столбцами IDENTITY
: разложить их, а затем выбросить.Вы можете использовать что-то вроде следующего кода SQL или использовать эквивалентный код приложения:
ALTER TABLE DRAWING ADD REV INT NOT NULL DEFAULT(0);
GO
CREATE PROCEDURE GET_REVISION_NUMBER (@DRAWING_ID INT) AS
BEGIN
DECLARE @ATTEMPTS INT;
SET @ATTEMPTS = 0;
DECLARE @ATTEMPT_LIMIT INT;
SET @ATTEMPT_LIMIT = 5;
DECLARE @CURRENT_REV INT;
LOOP:
SET @CURRENT_REV = (SELECT REV FROM DRAWING WHERE DRAWING.ID = @DRAWING_ID);
UPDATE DRAWING SET REV = @CURRENT_REV + 1 WHERE DRAWING.ID = @DRAWING_ID AND REV = @CURRENT_REV;
SET @ATTEMPTS = @ATTEMPTS + 1;
IF (@@ROWCOUNT = 0)
BEGIN
IF (@ATTEMPTS >= @ATTEMPT_LIMIT) RETURN NULL;
GOTO LOOP;
END
RETURN @CURRENT_REV + 1;
END
Проверка @@ ROWCOUNT очень важна - эта процедура должна быть нетранзакционной, потому что вы не хотитескрыть конфликты от одновременных запросов;Вы хотите разрешить их.Единственный способ убедиться, что ваше обновление определенно прошло, - это проверить, обновлялись ли какие-либо строки.
Конечно, вы могли догадаться, что этот подход не защищен от ошибок.Единственный способ «разрешить» конфликты - это попробовать несколько раз, прежде чем сдаться.Ни одно решение для домашнего пивоварения не будет столь же хорошим, как жестко запрограммированное в программное обеспечение сервера базы данных.Но это может быть довольно близко!
Хранимая процедура не устраняет конфликты, но она значительно сокращает промежуток времени, в течение которого может возникнуть конфликт.Вместо того, чтобы «резервировать» номер редакции для ожидающей транзакции INSERT
, вы получаете последний номер редакции и обновляете статический счетчик как можно быстрее, убирая путь для следующего вызова на GET_REVISION_NUMBER
.(Это, конечно, сериализуется, но только для очень маленькой части процедуры, которая должна выполняться последовательно; в отличие от многих других методов, остальная часть алгоритма может выполняться параллельно.)
Моя команда использовала решение, аналогичное описанному выше, и мы обнаружили, что количество блокирующих конфликтов уменьшилось на несколько порядков.Мы смогли отправить тысячи параллельных запросов с полдюжины машин в локальной сети, прежде чем один из них застрял.
Застрявшая машина попала в цикл, запрашивая новый номер отSQL-сервер, всегда получая нулевой результат.Так сказать, он не мог сказать ни слова.Это похоже на конфликтное поведение в случае SELECT MAX
, но гораздо реже.Вы обмениваете гарантированную последовательную нумерацию подхода SELECT MAX
(и любого связанного подхода) на увеличение масштабируемости в тысячу раз.Этот компромисс является более или менее фундаментальным: насколько мне известно, не существует последовательного, не сериализованного решения.
Еда на вынос
Конечно, весь этот обман основан напри необходимости локализованного, полупоследовательного номера.Если вы можете жить с менее удобными номерами ревизий, вы можете просто выставить DRAWING_REVISION.ID
.(Разоблачение суррогатных ключей по-своему неприятно, если вы спросите меня.)
Реальный вывод здесь заключается в том, что настраиваемые столбцы идентификаторов сложнее реализовать, чем может показаться на первый взгляд, и любое приложение, которое можетКогда требуется масштабируемость, нужно очень внимательно следить за тем, как он выбирает новые пользовательские значения идентификаторов.