[Примечание. Я нашел несколько ответов на этот вопрос, например 9911659 и 16636698, но их синтаксис недостаточно ясен.]

Я хочу вставить строку в таблицу, но только если эта строка еще не существует. Значения столбца для вставленной строки взяты из переменных (аргументов процедуры), а не из другой таблицы, поэтому я не буду использовать merge.

Я не хочу использовать отдельный if exists, за которым следует insert, но я бы хотел сделать это в одном (insert) выражении.

У меня есть @bookID, @userID, @reviewDate и @reviewYear в качестве аргументов для моего процесса, которые я хочу вставить в новую строку в таблице my_table.

Итак, у меня есть это:

insert into my_table
    (bookID, reviewYear, userID, reviewDate)
select
    @bookID, @reviewYear, @userID, @reviewDate   -- Proc arguments, values for new row
from my_table
where not exists (
    select bookID                                -- Find existing matching row
    from my_table
    where bookID = @bookID
        and reviewYear = @reviewYear
)

Другими словами, insert добавляет новую строку, только если еще не существует существующей строки с такими же bookID и reviewYear. Таким образом, данный пользователь может добавить рецензию на данную книгу за определенный год, но только если ни один пользователь еще не сделал этого.

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


Приложение (10 января 2020 г.)

Как уже указывалось, select выберет несколько строк, а весь оператор insert в конечном итоге вставит много строк, потенциально столько же строк, сколько имеется в настоящее время в my_table.

Вторая попытка, которая добавляет предложение distinct к select:

insert into my_table
    (bookID, reviewYear, userID, reviewDate)
select distinct                                  -- Only one possible match
    @bookID, @reviewYear, @userID, @reviewDate   -- Proc arguments, values for new row
from my_table
where not exists (
    select bookID                                -- Find existing matching row
    from my_table
    where bookID = @bookID
        and reviewYear = @reviewYear
)
0
David R Tribble 25 Ноя 2019 в 23:33
Это единичное заявление? И да, это правильно. Конечно, вы можете добавить уникальное ограничение, чтобы не добавлять дубликаты. Но все равно напишите свою вставку, как вы это сделали.
 – 
Dale K
25 Ноя 2019 в 23:36
1
Я не понимаю твоей цели здесь. Что делает один синтаксис «проще», чем другой, и зачем он вам нужен? Меньше набора текста? Вы уже набрали текст, так что вам нужно и почему?
 – 
Tab Alleman
25 Ноя 2019 в 23:55
- Под словом «проще» я подразумеваю «менее сложный», а также «меньшие накладные расходы на вычисления». Для этой конкретной проблемы я спрашиваю, есть ли более простой способ сделать то же самое, но с более простым подзапросом или вообще без подзапроса.
 – 
David R Tribble
26 Ноя 2019 в 00:44

2 ответа

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

create unique index unq_my_table on my_table(bookID, reviewYear)

begin try
    insert into my_table (bookID, reviewYear, userID, reviewDate)
        values ( @bookID, @reviewYear, @userID, @reviewDate )  -- Proc arguments, values for new row

end try
begin catch
-- do something here if you want
end catch;

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

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

2
Gordon Linoff 25 Ноя 2019 в 23:57

< Сильный > < EM> Edited :

Основываясь на более четком описании выше (и, возможно, на моем кофе), я должен отметить, что вы МОЖЕТЕ использовать MERGE только с переменными. Вот один из способов сделать это, используя указанные выше параметры:

WITH Src as (
    SELECT 
        @bookID AS bookID, 
        @reviewYear AS reviewYear, 
        @userID AS userID, 
        @reviewDate AS reviewDate
    )
MERGE my_table AS TARGET
USING Src AS SOURCE
ON TARGET.bookID = SOURCE.bookID
    AND TARGET.reviewYear = SOURCE.reviewYear
WHEN NOT MATCHED [BY TARGET]
    THEN INSERT (bookID, reviewYear, userID, reviewDate)
    VALUES (SOURCE.bookID, SOURCE.reviewYear, SOURCE.userID, SOURCE.reviewDate)

Оригинальный ответ.

На самом деле, я запустил этот код как опубликованный, и он не правильно ввел данные в таблицу. Ваша основная проблема здесь - это ваш SELECT ... FROM my_table. Это попытается вставить столько строк в вашу таблицу, сколько содержится в таблице. Таким образом, если таблица пуста, никакие строки не будут вставлены, но если в ней будет 20 строк, будут вставлены еще 20 строк.

Вот правильный способ сделать это. Он использует вашу основную логику, но принимает условную проверку из оператора INSERT.

CREATE TABLE #my_table (BookID int, ReviewYear int, UserId int, ReviewDate date)

DECLARE @BookID int = 1,
    @ReviewYear int = 1999,
    @UserId Int = 111,
    @ReviewDate date = '2019-09-11'

IF NOT EXISTS (
    select \*                                -- Find existing matching row
    from #my_table
    where bookID = @bookID
        and reviewYear = @reviewYear
    )
insert into #my_table 
    (bookID, reviewYear, userID, reviewDate)
VALUES 
    (@bookID, @reviewYear, @userID, @reviewDate)   -- Proc arguments, values for new row

SELECT \*
FROM #my_table
1
Laughing Vergil 13 Янв 2020 в 20:42
При этом по-прежнему выполняются отдельные операторы select и insert, чего я стараюсь избегать.
 – 
David R Tribble
11 Янв 2020 в 01:47