SQL Server w logistyce

MERGE zamiast TRUNCATE: kopia kartoteki nie opróżnia się, gdy źródło zwróci zero wierszy

Gdy lokalną kopię kartoteki odświeżasz instrukcją MERGE zamiast TRUNCATE, kopia nie zostaje pusta, jeśli źródło zawiedzie. Zero wierszy ze źródła oznacza zatrzymanie odświeżenia, a nie opróżnienie kopii, chyba że zero jest dozwolonym wynikiem.

sql.server.net.pl/sql-server/
Abstrakcyjna kompozycja walców baz danych i urządzeń z wykresami na ciemnym tle
Abstrakcyjna kompozycja walców baz danych i urządzeń z wykresami na ciemnym tle
W skrócie
  • Odczyt: źródło czytamy jednym zapytaniem do tabeli pośredniej.
  • Strażnik: zero wierszy zatrzymuje odświeżenie, a kopia zostaje bez zmian.
  • MERGE: jedna instrukcja uzgadnia kopię ze źródłem, a pozycje nieobecne w źródle zostają wyłączone.
  • Test: osiem scenariuszy na poziomach zgodności 130 i 160 daje ten sam wynik.

Gdy lokalną kopię kartoteki odświeżasz instrukcją MERGE zamiast TRUNCATE, kopia nie zostaje pusta, jeśli źródło zawiedzie. Programista albo administrator bazy czyta źródło jednym zapytaniem do tabeli pośredniej, sprawdza, czy zwróciło jakiekolwiek wiersze, i dopiero wtedy zmienia kopię jedną instrukcją w transakcji. Opróżnienie kopii przed wczytaniem źródła zostawia pustą tabelę, gdy źródło zawiedzie albo zwróci zero wierszy. Odczyt do tabeli pośredniej ze strażnikiem pustego zbioru oraz jeden MERGE zostawiają poprzedni stan w obu tych przypadkach, ale nie wykrywają odczytu niepełnego.

Zero wierszy ze źródła oznacza zatrzymanie odświeżenia, a nie opróżnienie kopii, chyba że zero jest dozwolonym wynikiem. W przykładzie strażnik kończy procedurę błędem, a kopia zostaje bez zmian. Gdy źródło zwraca wiersze, jeden MERGE w transakcji uzgadnia kopię ze źródłem. Strażnik wykrywa wyłącznie pusty zbiór.

W opisywanym wdrożeniu źródłem kartoteki jest zewnętrzny program ERP, a lokalna kopia powstaje z widoku opartego na zapytaniu do serwera połączonego. Pokazany wzorzec da się przenieść do innej bazy i nie jest kodem tego wdrożenia. Skąd kopia bierze dane, opisujemy w artykułach o imporcie zamówień z widoku programu ERP i o drugiej bazie obok bazy produktu.

Opróżnienie kopii przed odczytem zostawia pustą tabelę, gdy źródło zawiedzie

Wariant „opróżnij i wstaw” najpierw usuwa wiersze kopii, a dopiero potem czyta źródło. Pokazujemy go wyłącznie jako wariant do uniknięcia, bo opróżnienie następuje, zanim wiadomo, co zwróci źródło.

  • Źródło zwraca zero wierszy. W teście wariant kończy się bez błędu i zostawia w kopii zero wierszy.
  • Źródło jest niedostępne. Wariant kończy się błędem 208 i także zostawia zero wierszy.

Niedostępność źródła odtworzono w teście przez chwilową zmianę nazwy obiektu źródłowego.

Przykład (dane wymyślone). Lokalna kartoteka ma pięć pozycji. Nocne odświeżenie czyta źródło, które z powodu przerwy w łączu zwraca zero wierszy. Wariant z opróżnieniem tabeli kończy się bez błędu, a kopia ma zero pozycji, więc kolejne ekrany nie znajdują żadnej pozycji.

Wariant ze strażnikiem kończy się błędem 50001 z czytelnym komunikatem, a kopia nadal ma pięć pozycji. W rejestrze wpisów kontrolnych nie przybywa wiersza po odrzuconym odświeżeniu. Po przywróceniu źródła następne odświeżenie przebiega bez śladów po przerwie.

Odczyt źródła do tabeli pośredniej oddziela odczyt od zmiany kopii

Założenia przykładu. Pierwszy z dwóch bloków kodu tworzy trzy tabele z danymi początkowymi: źródło z kluczem indeks i trzema kolumnami opisowymi, w tym nazwa i zapas_min, kopię o tych samych kolumnach z kolumną wyłączenia aktywna oraz tabelę wpisów kontrolnych z liczbą zmian. Stoi w nim też procedura wariantu „opróżnij i wstaw”, która służy wyłącznie jako kontrast w teście i nie jest rozwiązaniem. Drugi blok zawiera procedurę odświeżenia i uruchamia się go po pierwszym. Test wykonano na jednej instancji, na poziomach zgodności 130 i 160; starszych wersji silnika nie testowano.

Instrukcja CREATE OR ALTER wymaga wersji silnika z poziomem zgodności 130 i z dodatkiem Service Pack 1 albo nowszej, a MERGE i THROW są starsze. Procedura odświeżenia ma cztery kroki: odczyt do tabeli pośredniej, strażnik pustego zbioru, MERGE w transakcji oraz wpis kontrolny. Poniżej oba bloki w kolejności uruchamiania. Blok pierwszy zawiera separator GO, który jest separatorem narzędzia, a nie instrukcją T-SQL.

-- tabele pomocnicze. Zewnętrzne źródło jest symulowane tabelą lokalną (bez serwera połączonego).
SET NOCOUNT ON;
IF OBJECT_ID(N'dbo.odswiezenie_przyklad') IS NOT NULL DROP TABLE dbo.odswiezenie_przyklad;
IF OBJECT_ID(N'dbo.kartoteka_przyklad') IS NOT NULL DROP TABLE dbo.kartoteka_przyklad;
IF OBJECT_ID(N'dbo.zrodlo_przyklad') IS NOT NULL DROP TABLE dbo.zrodlo_przyklad;
GO
CREATE TABLE dbo.zrodlo_przyklad (
    indeks    varchar(20)   NOT NULL PRIMARY KEY,
    nazwa     nvarchar(100) NULL,
    jednostka varchar(5)    NULL,
    zapas_min decimal(15,3) NULL
);
CREATE TABLE dbo.kartoteka_przyklad (
    indeks    varchar(20)   NOT NULL PRIMARY KEY,
    nazwa     nvarchar(100) NULL,
    jednostka varchar(5)    NULL,
    zapas_min decimal(15,3) NULL CONSTRAINT ck_zapas_min CHECK (zapas_min >= 0),
    aktywna   bit           NOT NULL DEFAULT 1
);
CREATE TABLE dbo.odswiezenie_przyklad (
    id    int IDENTITY(1,1) PRIMARY KEY,
    kiedy datetime2(0) NOT NULL DEFAULT SYSDATETIME(),
    zmian int          NOT NULL
);
INSERT dbo.zrodlo_przyklad (indeks, nazwa, jednostka, zapas_min) VALUES
    ('A1', N'Pozycja pierwsza', 'szt', 10),
    ('A2', N'Pozycja druga',    'szt', 20),
    ('A3', N'Pozycja trzecia',  'szt', NULL),
    ('A4', N'Pozycja czwarta',  'kpl', 5),
    ('A5', N'Pozycja piąta',    'szt', 0);
GO
-- wariant NAIWNY, tylko jako kontrast w teście (nie jest rozwiązaniem)
CREATE OR ALTER PROCEDURE dbo.odswiez_kopie_naiwna_przyklad
AS
BEGIN
    TRUNCATE TABLE dbo.kartoteka_przyklad;
    INSERT dbo.kartoteka_przyklad (indeks, nazwa, jednostka, zapas_min)
    SELECT indeks, nazwa, jednostka, zapas_min FROM dbo.zrodlo_przyklad;
END
GO

Tabele przykładowe, dane początkowe i wariant „opróżnij i wstaw” użyty w teście jako kontrast.

CREATE OR ALTER PROCEDURE dbo.odswiez_kartoteke_przyklad
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;
    -- jeden odczyt źródła do tabeli pośredniej
    SELECT indeks, nazwa, jednostka, zapas_min INTO #zrodlo FROM dbo.zrodlo_przyklad;
    -- strażnik: pusty zbiór nie może wyczyścić kopii
    IF NOT EXISTS (SELECT 1 FROM #zrodlo)
        THROW 50001, N'Źródło nie zwróciło wierszy, kopia bez zmian.', 1;
    BEGIN TRY
        BEGIN TRAN;
        MERGE dbo.kartoteka_przyklad WITH (HOLDLOCK) AS c
        USING #zrodlo AS z ON z.indeks = c.indeks
        WHEN MATCHED AND (c.aktywna = 0 OR EXISTS
             (SELECT c.nazwa, c.jednostka, c.zapas_min EXCEPT SELECT z.nazwa, z.jednostka, z.zapas_min))
            THEN UPDATE SET nazwa = z.nazwa, jednostka = z.jednostka, zapas_min = z.zapas_min, aktywna = 1
        WHEN NOT MATCHED BY TARGET
            THEN INSERT (indeks, nazwa, jednostka, zapas_min)
                 VALUES (z.indeks, z.nazwa, z.jednostka, z.zapas_min)
        WHEN NOT MATCHED BY SOURCE AND c.aktywna = 1
            THEN UPDATE SET aktywna = 0;
        INSERT dbo.odswiezenie_przyklad (zmian) VALUES (@@ROWCOUNT);
        COMMIT;
    END TRY
    BEGIN CATCH
        IF @@TRANCOUNT > 0 ROLLBACK;
        THROW;
    END CATCH
END

Procedura odświeżenia z czterema krokami opisanymi w tekście.

Procedura czyta źródło jednym poleceniem SELECT z INTO do tabeli pośredniej i dalej pracuje już na tej tabeli, a nie na źródle. Jeśli odczyt źródła się nie uda, w teście był to błąd 208, procedura kończy się, zanim dotknie kopii, a kopia zostaje bez zmian. Odczyt jest więc oddzielony od zmiany: pierwszy może się nie udać, a drugi zaczyna się dopiero wtedy, gdy w tabeli pośredniej są wiersze.

Odświeżanie kopii ze strażnikiem pustego zbioruSchemat odświeżania kopii. Strażnik stoi między tabelą pośrednią a kopią i zatrzymuje pusty zbiór; niedostępne źródło kończy odświeżenie kodem 208. Pod spodem wariant do uniknięcia, w którym kopia jest opróżniana przed odczytem. Odświeżanie kopii ze strażnikiem pustego zbioru Źródło trafia do tabeli pośredniej, a MERGE rusza kopię dopiero wtedy, gdy strażnik przepuści odczyt. jedna transakcja Źródło Zewnętrzne źródło kartoteki. Może zwrócić zero wierszy albo być niedostępne. jeden odczyt Tabela pośrednia Jeden odczyt źródła. Na niej działa strażnik i MERGE. Czy źródło zwróciło wiersze? tak Kopia lokalna MERGE zmienia wiersze albo nie zmienia ich wcale. Wpis kontrolny Zapisany w tej samej transakcji. pusty: kod 50001 Kopia bez zmian, odświeżenie zatrzymane Czytelny komunikat zamiast pustej tabeli. niedostępne: kod 208 Wariant do uniknięcia: opróżnij i wstaw do uniknięcia Opróżnienie kopii Pierwszy krok, przed odczytem źródła. Źródło zawodzi albo jest puste Zero wierszy albo kod 208. Pusta kopia Zero wierszy przy kodzie 0 albo 208.
Schemat: strażnik stoi między tabelą pośrednią a kopią i zatrzymuje pusty zbiór.

Strażnik zatrzymuje odświeżenie, gdy źródło zwróciło zero wierszy

Tuż po odczycie stoi jedno sprawdzenie, strażnik: IF NOT EXISTS na tabeli pośredniej. Gdy tabela jest pusta, procedura wywołuje THROW z numerem 50001 i komunikatem, że źródło nie zwróciło wierszy, a kopia pozostaje bez zmian. Strażnik stoi przed otwarciem transakcji, więc przy pustym źródle procedura w ogóle nie zaczyna zmian w kopii. W scenariuszu czwartym, w którym źródło zwraca zero wierszy, w kopii zostaje pięć aktywnych wierszy i sześć wierszy razem, a nowy wpis kontrolny nie powstaje.

Zero wierszy ze źródła jest powodem do zatrzymania odświeżenia, a nie do opróżnienia kopii, o ile zero nie jest dozwolonym wynikiem.

Zasada obowiązuje tylko wtedy, gdy zero nie jest dozwolonym wynikiem źródła. Kontrolę wsadu w tabeli roboczej opisuje osobny artykuł o walidacji wsadu w tabeli roboczej. Oba warianty przy pustym źródle różnią się, jak pokazuje poniższa lista.

  • Strażnik. Procedura kończy się błędem 50001, kopia ma pięć aktywnych wierszy i sześć razem, a wpis kontrolny nie powstaje.
  • Wariant „opróżnij i wstaw”. Procedura kończy się bez błędu, a kopia ma zero wierszy.

Jeden MERGE w transakcji zmienia kopię albo nie zmienia jej wcale

Instrukcja MERGE zestawia tabelę pośrednią z kopią po kolumnie indeks i ma trzy gałęzie, a całość stoi w transakcji otwartej po strażniku.

  • Zmiana pozycji. Pozycja obecna w obu miejscach jest zmieniana, gdy jest wyłączona albo gdy porównanie wykryje różnicę; zmiana ustawia ją jako aktywną.
  • Nowa pozycja. Pozycja, której nie ma w kopii, jest wstawiana.
  • Pozycja nieobecna w źródle. Aktywna pozycja kopii, której źródło nie zawiera, zostaje wyłączona, a nie usunięta.
  • Blokada zakresu. Wskazówka HOLDLOCK na kopii utrzymuje blokadę zakresu kluczy na czas instrukcji.

Porównanie ma postać EXISTS z zapytaniem SELECT ... EXCEPT SELECT ..., które zestawia kolumny kopii z kolumnami źródła. W scenariuszu drugim wykryło ono zmianę z NULL na liczbę. Cztery zmiany w źródle (zmieniona nazwa, NULL zmieniony na liczbę, zniknięta pozycja, nowa pozycja) dają liczbę zmian 4, a zniknięta pozycja zostaje w kopii jako wyłączona, więc kopia ma razem sześć wierszy, w tym pięć aktywnych. Wyłączenie zachowuje ślad i pozwala wrócić do pozycji; usunięcie byłoby możliwe w tym samym MERGE jako decyzja projektowa.

HOLDLOCK ma zapobiec temu, żeby równoległa sesja wstawiła ten sam klucz między porównaniem a zapisem. W teście dwóch procesów, które wywołały procedurę po 150 razy, wersja z HOLDLOCK dała 150 z 150 udanych wywołań w obu procesach i żadnego błędu. Wersja bez HOLDLOCK kończyła się błędem klucza głównego 2627 w od 4 do 8 wywołań na proces. Test powtórzono na poziomach zgodności 130 i 160, po dwa procesy na poziom; zakleszczeń nie zaobserwowano.

Wpis kontrolny w tej samej transakcji wycofuje się razem ze zmianami

Po MERGE procedura dopisuje wpis kontrolny z liczbą zmian, którą zwraca @@ROWCOUNT, i dopiero potem zamyka transakcję poleceniem COMMIT. Wpis stoi w tej samej transakcji co MERGE, więc razem ze zmianami zostaje zapisany albo wycofany. Przy pierwszym odświeżeniu pustej kopii ze źródła o pięciu wierszach powstaje pięć aktywnych wierszy i wpis kontrolny z liczbą zmian 5. Powtórzenie bez zmian w źródle daje liczbę zmian 0, a wpis kontrolny i tak powstaje jako trzeci.

Scenariusz szósty sprawdza wycofanie. Wpis kontrolny odrzuca ograniczenie po udanym MERGE, procedura zgłasza błąd 547, a trzy nowe wiersze wstawione przez MERGE są wycofane razem z nim, więc kopia nadal ma sześć wierszy. Kolejność ROLLBACK i THROW w bloku CATCH omawia artykuł o THROW po ROLLBACK, a pytanie, gdzie zapisać informację o odrzuconym odświeżeniu, skoro wpis w tej samej transakcji by się wycofał, dotyczy materiału o zmiennej tabelowej przy ROLLBACK. Wyniki ośmiu scenariuszy zbiera tabela, a na poziomach zgodności 130 i 160 są one identyczne. Wiersze siódmy i ósmy pokazują wariant „opróżnij i wstaw”.

Wynik ośmiu scenariuszy odświeżania kopii
ScenariuszKod błęduAktywnychRazemWpisów kontrolnych
1 pierwsze odświeżenie0551
2 cztery zmiany w źródle0562
3 powtórzenie bez zmian0563
4 źródło puste (strażnik)50001563
5 źródło niedostępne208563
6 odrzucony wpis kontrolny547563
7 wariant naiwny, źródło puste0003
8 wariant naiwny, źródło niedostępne208004

Wynik ośmiu scenariuszy, taki sam na poziomach zgodności 130 i 160.

W wierszu 8 wpisów jest 4, bo przed próbą kopię przywrócono poprawnym odświeżeniem, co dało jeden dodatkowy wpis. Wynik z tabeli pokazuje też makieta, która zestawia siatkę wyników z blokiem kodu.

Makieta wyniku testu odświeżania kopiiMakieta poglądowa, dane przykładowe: siatka z ośmioma scenariuszami (kod błędu, wiersze aktywne i razem, wpisy kontrolne) oraz fragment procedury ze strażnikiem i MERGE. Wynik testu odświeżania kopii poziomy zgodności 130 i 160, ten sam wynik Scenariusz Kod błędu Aktywnych Razem Wpisów kontrolnych 1 pierwsze odświeżenie 0 5 5 1 2 cztery zmiany w źródle 0 5 6 2 3 powtórzenie bez zmian 0 5 6 3 4 źródło puste (strażnik) 50001 5 6 3 5 źródło niedostępne 208 5 6 3 6 odrzucony wpis kontrolny 547 5 6 3 7 wariant naiwny, źródło puste 0 0 0 3 8 wariant naiwny, źródło niedostępne 208 0 0 4 W wierszu 8 wpisów jest 4, bo przed próbą przywrócono kopię poprawnym odświeżeniem. Strażnik i MERGE (fragment, nazwy przykładowe) SELECT indeks, nazwa, jednostka, zapas_min INTO #zrodlo FROM dbo.zrodlo_przyklad; IF NOT EXISTS (SELECT 1 FROM #zrodlo) THROW 50001, N'Źródło nie zwróciło wierszy, kopia bez zmian.', 1; BEGIN TRY BEGIN TRAN; MERGE dbo.kartoteka_przyklad WITH (HOLDLOCK) AS c USING #zrodlo AS z ON z.indeks = c.indeks -- (klauzule WHEN pominięte w tym fragmencie) INSERT dbo.odswiezenie_przyklad (zmian) VALUES (@@ROWCOUNT); COMMIT; Makieta poglądowa, dane przykładowe
Makieta poglądowa, dane przykładowe. Siatka z ośmioma scenariuszami i blok kodu procedury.

Strażnik nie wykrywa odczytu niepełnego, więc próg jest decyzją biznesową

Strażnik wykrywa pusty zbiór, ale nie odczyt niepełny. Gdy źródło zwróciło jeden wiersz z pięciu, strażnik milczy, cztery pozycje zostają wyłączone, a liczba zmian wynosi 4. Próg względem poprzedniego odświeżenia jest więc decyzją biznesową i w teście nie występuje. Pozostałe granice wzorca dotyczą jakości samego źródła i kontekstu wywołania.

  • Powtórzony klucz w źródle. Dla źródła bez klucza wynik zależy od przypadku: gdy oba wiersze różnią się od kopii, procedura zgłasza błąd 8672 i wycofuje zmiany, a gdy jeden jest identyczny z kopią, a drugi inny, błędu nie ma i kopia dostaje wartości drugiego wiersza. Dwa razy ta sama nowa pozycja daje błąd 2627.
  • NULL w kluczu. NULL w kluczu źródła daje błąd 515.
  • Spacja i wielkość liter. Spacja na końcu nazwy i zmiana wielkości liter w nazwie nie są wykrywane jako zmiana: liczba zmian wynosi 0, bo kolacja w teście nie rozróżnia wielkości liter.
  • Transakcja zewnętrzna. Gdy wywołujący ma własną otwartą transakcję, ROLLBACK w bloku CATCH wycofuje także ją.

Pozostałe teksty o wzorcach T-SQL zbiera przegląd artykułów o SQL Server.

Pytania do analizy

Przed zastosowaniem wzorca odpowiedz na cztery pytania.

  1. Jaka liczba wierszy w źródle jest podejrzana i kto o tym decyduje?
  2. Czy pozycje, które zniknęły ze źródła, mają być w kopii wyłączane, czy usuwane?
  3. Gdzie zapisać informację o odrzuconym odświeżeniu, skoro wpis w tej samej transakcji by się wycofał?
  4. Które inne tabele w bazie są zasilane przez opróżnienie i wstawienie?
FAQ

Najczęstsze pytania o odświeżanie kopii

01

Czy strażnik wykrywa odczyt niepełny źródła?

Nie. Wykrywa tylko pusty zbiór. Próg względem poprzedniego odświeżenia jest decyzją biznesową i w teście nie występuje.

02

Po co HOLDLOCK przy MERGE?

Utrzymuje blokadę zakresu kluczy na czas instrukcji, żeby równoległa sesja nie wstawiła tego samego klucza między porównaniem a zapisem. W teście współbieżnym dwóch procesów po 150 wywołań wersja z HOLDLOCK nie dała błędów, a bez niego pojawił się błąd klucza głównego 2627; zakleszczeń nie zaobserwowano.

03

Czy MERGE jest szybszy od opróżnienia i wstawienia?

W teście nie mierzono czasu, więc ten artykuł nie porównuje szybkości obu wariantów.

04

Dlaczego pozycje nieobecne w źródle są wyłączane, a nie usuwane?

W przykładzie wyłączenie zachowuje ślad i pozwala wrócić do pozycji; usunięcie jest możliwe w tym samym MERGE jako decyzja projektowa.

05

Jakiej wersji SQL Server wymaga przykład?

Instrukcja CREATE OR ALTER wymaga wersji silnika z poziomem zgodności 130 i z dodatkiem Service Pack 1 albo nowszej. Przykład działa na poziomach zgodności 130 i 160 na jednej instancji; starszych wersji nie testowano.

Słownik pojęć

Słownik pojęć

Pojęcia z procedury i testu opisanych w artykule.

KKopia lokalna
tabela w bazie magazynowej, której zawartość pochodzi z zewnętrznego źródła.
TTabela pośrednia
tymczasowa tabela z jednym odczytem źródła, na której działa strażnik i MERGE.
SStrażnik
warunek, który zatrzymuje odświeżenie, gdy źródło zwróciło zero wierszy.
MMERGE
instrukcja, która w jednym poleceniu wstawia nowe wiersze i zmienia istniejące na podstawie źródła.
WWpis kontrolny
wiersz z liczbą zmian zapisany w tej samej transakcji co zmiany.
PPoziom zgodności
ustawienie bazy, które określa dostępne funkcje języka (tu 130 i 160).

Zobacz to w praktyce

Sprawdź, jak systemy SoftwareStudio oparte na MS SQL Server wspierają ten obszar Twojej firmy.