Почему этот запрос генерирует ошибку нарушения PK? - PullRequest
0 голосов
/ 27 августа 2018

Поэтому я пытаюсь в одном запросе вставить строку, только если она еще не существует.

Мой запрос следующий:

INSERT INTO [dbo].[users_roles] ([user_id], [role_id]) 
SELECT 29851, 1 WHERE NOT EXISTS (
  SELECT 1 FROM [dbo].[users_roles] WHERE user_id = 29851 AND role_id = 1)

Иногда (очень редко, но все же) выдается следующая ошибка:

Нарушение ограничения PRIMARY KEY 'PK_USERS_ROLES'. Не могу вставьте дубликат ключа в объект 'dbo.users_roles'. Дубликат значение ключа (29851, 1).

PK_USERS_ROLES - это [user_id], [role_id]. Вот полный SQL схемы таблицы:

create table users_roles
(
    user_id int not null
        constraint FK_USERS_ROLES_USER
        references user,
    role_id int not null
        constraint FK_USERS_ROLES_USER_ROLE
        references user_role,
    constraint PK_USERS_ROLES
    primary key (user_id, role_id)
)

Контекст :

Это выполняется сценарием PHP, размещенным на сервере Apache, и "случайным образом" происходит один раз из сотен случаев (скорее всего, связанных с параллелизмом).

Подробнее:

  • SELECT @@VERSION дает:

Microsoft SQL Server 2008 R2 (SP2) - 10.50.4000.0 (X64) 28 июня 2012 г. 08:36:30 Авторское право (c) Microsoft Corporation Enterprise Edition (64-разрядная версия) в Windows NT 6.1 (сборка 7601: пакет обновления)

  • Версия SQL Server: SQL Server 2008 R2

  • Уровень изоляции транзакции: ReadCommitted

  • Это выполняется внутри явной транзакции (через операторы PHP, но я думаю, что конечный результат тот же)

Вопросы

  • Может кто-нибудь объяснить, почему / как это происходит?

  • Каким будет эффективный способ безопасной вставки за один раз (другими словами, в одном запросе)? Я видел другие ответы, такие как этот , но решения предназначены для хранимых процедур.

Спасибо.

Ответы [ 2 ]

0 голосов
/ 27 августа 2018

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

DECLARE @user_id INT; SET @user_id=29851;
DECLARE @role_id INT; SET @role_id=1;

BEGIN TRY
    BEGIN TRANSACTION;

    DECLARE @exists INT;
    SELECT @exists=1 
    FROM [dbo].[users_roles] WITH(ROWLOCK,HOLDLOCK,XLOCK)
    WHERE user_id=@user_id AND role_id=@role_id;

    IF @exists IS NULL
    BEGIN
        INSERT INTO [dbo].[users_roles] ([user_id], [role_id])
        VALUES(@user_id,@role_id);
    END

    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    ROLLBACK TRANSACTION;
END CATCH
0 голосов
/ 27 августа 2018

Эта таблица усекается или строки удаляются в какой-то момент? А как часто? Для меня имеет смысл, что строки не должны быть найдены в какой-то момент, так как вы запускаете «вставить, если не существует», и в этот момент два или более запросов могут попасть в базу данных, чтобы вставить те же данные ... только один будет ... другой ничего не должен делать, если строка была вставлена ​​до того, как ее поиск "не существует", или потерпит неудачу, если строка была вставлена ​​после поиска.

Сейчас у меня есть только база данных Oracle, чтобы выполнить некоторые тесты, и я могу воспроизвести эту проблему. Мой режим фиксации явный:

  • Создайте пустую таблицу, уникальное ограничение и выберите грант, вставьте другому пользователю:

    CREATE TABLE just_a_test (val NUMBER(3,0));
    
    ALTER TABLE just_a_test ADD CONSTRAINT foobar UNIQUE (val);
    
    GRANT SELECT, INSERT ON just_a_test TO user2;
    
  • Сеанс БД для пользователя1:

    INSERT INTO just_a_test 
    SELECT 10 
    FROM DUAL 
    WHERE NOT EXISTS 
    (
      SELECT 1 
      FROM just_a_test 
      WHERE val = 10
    )
    ;
    
    -- no commit yet...
    
  • Сеанс БД на user2:

    INSERT INTO user1.just_a_test 
    SELECT 10 
    FROM DUAL 
    WHERE NOT EXISTS 
    (
      SELECT 1 
      FROM user1.just_a_test 
      WHERE val = 10
    )
    ;
    
    
    -- no commit yet, the db just hangs til the other session commit...
    

Итак, я фиксирую первую транзакцию, вставляя строку, а затем получаю следующую ошибку в сеансе user2:

"unique constraint violated"
*Cause:    An UPDATE or INSERT statement attempted to insert a duplicate key.
           For Trusted Oracle configured in DBMS MAC mode, you may see
           this message if a duplicate entry exists at a different level.

Теперь я выполняю откат второй транзакции и снова запускаю ту же самую вставку для user2, и теперь я получаю следующий вывод:

0 rows inserted.

Возможно, ваш сценарий такой же, как этот. Надеюсь, это поможет.

РЕДАКТИРОВАТЬ

Прости. Вы задали два вопроса, а я ответил только на один Could someone explain why/how this is happening?. Поэтому я пропустил What would be an efficient way to safely insert in one go (in other words, in a single query)?.

Что именно означает "безопасно" для вас? Допустим, вы выполняете INSERT / SELECT из множества строк, и только одна из них дублируется по сравнению с сохраненными. Для вашего уровня "безопасности" вы должны игнорировать все вставляемые строки или игнорировать только дублированные, сохраняя остальные?

Опять же, сейчас у меня нет SQL Server, чтобы попробовать, но похоже, что вы можете сказать SQL Server, запрещать ли вставлять все строки в случае какого-либо дублирования, или запрещать только дубли, оставляя другие , То же самое справедливо для вставки одной строки ... если это дублирование, сгенерируйте ошибку ... или просто проигнорируйте ее в другой руке.

Синтаксис должен выглядеть следующим образом, чтобы игнорировать только строки dup и не выдавать ошибок:

    ALTER TABLE [TableName] REBUILD WITH (IGNORE_DUP_KEY = ON);

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

Таким образом, вы сохраните синтаксис INSERT / SELECT, который выглядит хорошо, imo.

Надеюсь, это поможет.

Источники:

https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-table-index-option-transact-sql?view=sql-server-2008

https://stackoverflow.com/a/11207687/1977836

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