Wstaw aktualizację przechowywanej procedury na SQL Server

104

Napisałem przechowywany proces, który dokona aktualizacji, jeśli istnieje rekord, w przeciwnym razie dokona wstawienia. Wygląda mniej więcej tak:

update myTable set Col1=@col1, Col2=@col2 where ID=@ID
if @@rowcount = 0
insert into myTable (Col1, Col2) values (@col1, @col2)

Moja logika stojąca za pisaniem tego w ten sposób jest taka, że ​​aktualizacja przeprowadzi niejawną selekcję przy użyciu klauzuli where i jeśli zwróci ona 0, nastąpi wstawienie.

Alternatywą dla zrobienia tego w ten sposób byłoby dokonanie wyboru, a następnie na podstawie liczby zwróconych wierszy albo zaktualizuj, albo wstaw. Uznałem to za nieefektywne, ponieważ jeśli masz wykonać aktualizację, spowoduje to 2 wybory (pierwsze jawne wywołanie wyboru i drugie niejawne w miejscu aktualizacji). Gdyby proc miał wykonać wstawkę, nie byłoby różnicy w wydajności.

Czy moja logika brzmi tutaj? Czy w ten sposób można połączyć wstawkę i aktualizację w przechowywany proces?

Chłopak
źródło

Odpowiedzi:

61

Twoje założenie jest słuszne, jest to optymalny sposób na zrobienie tego i nazywa się upsert / merge .

Znaczenie UPSERT - z sqlservercentral.com :

Dla każdej aktualizacji w powyższym przypadku usuwamy jeden dodatkowy odczyt z tabeli, jeśli użyjemy UPSERT zamiast EXISTS. Niestety w przypadku Insert, obie metody UPSERT i IF EXISTS używają tej samej liczby odczytów w tabeli. Dlatego sprawdzenie istnienia powinno być wykonywane tylko wtedy, gdy istnieje bardzo ważny powód uzasadniający dodatkowe we / wy. Zoptymalizowanym sposobem robienia rzeczy jest upewnienie się, że masz jak najmniej odczytów w bazie danych.

Najlepszą strategią jest próba aktualizacji. Jeśli aktualizacja nie ma wpływu na żadne wiersze, wstaw. W większości przypadków wiersz już istnieje i wymagane będzie tylko jedno wejście / wyjście.

Edycja : sprawdź tę odpowiedź i link do wpisu na blogu, aby dowiedzieć się o problemach z tym wzorcem i o tym, jak zapewnić jego bezpieczne działanie.

binOr
źródło
1
Cóż, myślę, że odpowiedziała przynajmniej na jedno pytanie. I nie dodałem kodu, ponieważ kod w pytaniu wydawał mi się już odpowiedni. Chociaż umieściłbym to w transakcji, nie wziąłem pod uwagę poziomu izolacji przy aktualizacji. Dziękuję za wskazanie tego w Twojej odpowiedzi!
binOr
54

Przeczytaj post na moim blogu, aby uzyskać dobry, bezpieczny wzór, którego możesz użyć. Istnieje wiele rozważań, a przyjęta odpowiedź na to pytanie jest daleka od bezpiecznej.

Aby uzyskać szybką odpowiedź, wypróbuj następujący wzór. Będzie działać dobrze na SQL 2000 i nowszych. SQL 2005 zapewnia obsługę błędów, co otwiera inne opcje, a SQL 2008 udostępnia polecenie MERGE.

begin tran
   update t with (serializable)
   set hitCount = hitCount + 1
   where pk = @id
   if @@rowcount = 0
   begin
      insert t (pk, hitCount)
      values (@id,1)
   end
commit tran
Sam Saffron
źródło
1
W swoim poście na blogu kończysz używając wskazówki WITH (updlock, serializable) w sprawdzaniu istnienia. Jednak odczyt MSDN oznacza: „UPDLOCK - określa, że ​​blokady aktualizacji mają zostać podjęte i wstrzymane do zakończenia transakcji”. Czy to oznacza, że ​​wskazówka z możliwością serializacji jest zbędna, ponieważ blokada aktualizacji i tak zostanie wstrzymana do końca transakcji, czy też coś źle zrozumiałem?
Dan Def
10

Jeśli ma być używany z SQL Server 2000/2005, oryginalny kod musi być dołączony do transakcji, aby zapewnić spójność danych w scenariuszu współbieżnym.

BEGIN TRANSACTION Upsert
update myTable set Col1=@col1, Col2=@col2 where ID=@ID
if @@rowcount = 0
insert into myTable (Col1, Col2) values (@col1, @col2)
COMMIT TRANSACTION Upsert

Spowoduje to dodatkowe koszty wydajności, ale zapewni integralność danych.

Dodaj, jak już zasugerowano, należy użyć MERGE, jeśli jest dostępny.

Dima Malenko
źródło
8

Nawiasem mówiąc, MERGE to jedna z nowych funkcji SQL Server 2008.

Jon Galloway
źródło
i zdecydowanie powinieneś używać tego raczej tego trudnego do odczytania nonsensu homebrew. Dobry przykład jest tutaj - mssqltips.com/sqlservertip/1704/ ...
Rich Bryant
6

Nie tylko musisz uruchamiać go w transakcji, ale także wymaga wysokiego poziomu izolacji. W rzeczywistości domyślny poziom izolacji to Read Commited i ten kod wymaga serializacji.

SET transaction isolation level SERIALIZABLE
BEGIN TRANSACTION Upsert
UPDATE myTable set Col1=@col1, Col2=@col2 where ID=@ID
if @@rowcount = 0
  begin
    INSERT into myTable (ID, Col1, Col2) values (@ID @col1, @col2)
  end
COMMIT TRANSACTION Upsert

Może dodanie również sprawdzania błędów @@ i cofania zmian mogłoby być dobrym pomysłem.

Tomas Tintera
źródło
@Munish Goyal Ponieważ w bazie danych wiele poleceń i precedensów działa równolegle. Wtedy inny wątek może wstawić wiersz zaraz po uruchomieniu aktualizacji i przed uruchomieniem wstawiania.
Tomas Tintera
5

Jeśli nie wykonujesz scalania w SQL 2008, musisz zmienić to na:

if @@ rowcount = 0 i @@ error = 0

w przeciwnym razie, jeśli aktualizacja nie powiedzie się z jakiegoś powodu, spróbuje później wstawić, ponieważ liczba wierszy w nieudanej instrukcji wynosi 0

Simon Munro
źródło
3

Wielki fan UPSERT, naprawdę ogranicza kod do zarządzania. Oto inny sposób, w jaki to robię: jeden z parametrów wejściowych to ID, jeśli ID ma wartość NULL lub 0, wiesz, że to INSERT, w przeciwnym razie jest to aktualizacja. Zakłada, że ​​aplikacja wie, czy istnieje identyfikator, więc nie będzie działać we wszystkich sytuacjach, ale jeśli to zrobisz, skróci wykonywanie o połowę.

Natron
źródło
2

Zmodyfikowany post Dimy Malenko:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE 

BEGIN TRANSACTION UPSERT 

UPDATE MYTABLE 
SET    COL1 = @col1, 
       COL2 = @col2 
WHERE  ID = @ID 

IF @@rowcount = 0 
  BEGIN 
      INSERT INTO MYTABLE 
                  (ID, 
                   COL1, 
                   COL2) 
      VALUES      (@ID, 
                   @col1, 
                   @col2) 
  END 

IF @@Error > 0 
  BEGIN 
      INSERT INTO MYERRORTABLE 
                  (ID, 
                   COL1, 
                   COL2) 
      VALUES      (@ID, 
                   @col1, 
                   @col2) 
  END 

COMMIT TRANSACTION UPSERT 

Możesz przechwycić błąd i wysłać rekord do uszkodzonej tabeli wstawiania.
Musiałem to zrobić, ponieważ pobieramy wszelkie dane wysyłane przez WSDL i, jeśli to możliwe, naprawiamy je wewnętrznie.

zbiry78013
źródło
1

Twoja logika wydaje się rozsądna, ale możesz rozważyć dodanie kodu, aby zapobiec wstawianiu, jeśli przekazałeś określony klucz podstawowy.

W przeciwnym razie, jeśli zawsze robisz wstawianie, jeśli aktualizacja nie wpłynęła na żadne rekordy, co się stanie, gdy ktoś usunie rekord przed uruchomieniem „UPSERT”? Teraz rekord, który próbujesz zaktualizować, nie istnieje, więc zamiast tego utworzy rekord. Prawdopodobnie nie jest to zachowanie, którego szukałeś.

Kevin Fairchild
źródło