Jeśli używasz partycjonowania tabeli z co najmniej jedną partycją przechowywaną w grupie plików tylko do odczytu, instrukcje aktualizacji i usuwania SQL mogą zakończyć się niepowodzeniem z powodu błędu. Oczywiście jest to oczekiwane zachowanie, jeśli jakiekolwiek modyfikacje wymagałyby pisania do grupy plików tylko do odczytu; jednak możliwe jest również napotkanie tego błędu, w którym zmiany są ograniczone do grup plików oznaczonych jako do odczytu-zapisu.
Przykładowa baza danych
Aby zademonstrować problem, utworzymy prostą bazę danych z pojedynczą niestandardową grupą plików, którą później oznaczymy jako tylko do odczytu. Pamiętaj, że musisz dodać ścieżkę nazwy pliku, aby pasowała do twojej instancji testowej.
USE master; GO CREATE DATABASE Test; GO -- This filegroup will be marked read-only later ALTER DATABASE Test ADD FILEGROUP ReadOnlyFileGroup; GO -- Add a file to the new filegroup ALTER DATABASE Test ADD FILE ( NAME = 'Test_RO', FILENAME = '<...your path...>\MSSQL\DATA\Test_ReadOnly.ndf' ) TO FILEGROUP ReadOnlyFileGroup;
Funkcja i schemat partycji
Utworzymy teraz podstawową funkcję i schemat partycjonowania, który będzie kierować wiersze z danymi przed 1 stycznia 2000 do partycji tylko do odczytu. Późniejsze dane będą przechowywane w podstawowej grupie plików do odczytu i zapisu:
USE Test; GO CREATE PARTITION FUNCTION PF (datetime) AS RANGE RIGHT FOR VALUES ({D '2000-01-01'}); GO CREATE PARTITION SCHEME PS AS PARTITION PF TO (ReadOnlyFileGroup, [PRIMARY]);
Specyfikacja prawego zakresu oznacza, że wiersze z wartością graniczną 1 stycznia 2000 będą znajdować się w partycji do odczytu i zapisu.
Tabela i indeksy podzielone na partycje
Możemy teraz stworzyć naszą tabelę testową:
CREATE TABLE dbo.Test ( dt datetime NOT NULL, c1 integer NOT NULL, c2 integer NOT NULL, CONSTRAINT PK_dbo_Test__c1_dt PRIMARY KEY CLUSTERED (dt) ON PS (dt) ) ON PS (dt); GO CREATE NONCLUSTERED INDEX IX_dbo_Test_c1 ON dbo.Test (c1) ON PS (dt); GO CREATE NONCLUSTERED INDEX IX_dbo_Test_c2 ON dbo.Test (c2) ON PS (dt);
Tabela ma klastrowany klucz podstawowy w kolumnie datetime i jest również partycjonowana w tej kolumnie. W pozostałych dwóch kolumnach liczb całkowitych znajdują się indeksy nieklastrowane, które są podzielone na partycje w ten sam sposób (indeksy są wyrównane z tabelą podstawową).
Przykładowe dane
Na koniec dodajemy kilka wierszy przykładowych danych i sprawiamy, że partycja danych sprzed 2000 r. jest tylko do odczytu:
INSERT dbo.Test WITH (TABLOCKX) (dt, c1, c2) VALUES ({D '1999-12-31'}, 1, 1), -- Read only ({D '2000-01-01'}, 2, 2); -- Writable GO ALTER DATABASE Test MODIFY FILEGROUP ReadOnlyFileGroup READ_ONLY;
Możesz użyć następujących instrukcji aktualizacji testu, aby potwierdzić, że danych w partycji tylko do odczytu nie można modyfikować, podczas gdy dane z dt
wartość 1 stycznia 2000 lub później można zapisać w:
-- Will fail, as expected UPDATE dbo.Test SET c2 = 1 WHERE dt = {D '1999-12-31'}; -- Will succeed, as expected UPDATE dbo.Test SET c2 = 999 WHERE dt = {D '2000-01-01'}; -- Reset the value of c2 UPDATE dbo.Test SET c2 = 2 WHERE dt = {D '2000-01-01'};
Nieoczekiwana awaria
Mamy dwa wiersze:jeden tylko do odczytu (1999-12-31); i jeden odczyt-zapis (2000-01-01):
Teraz wypróbuj następujące zapytanie. Identyfikuje ten sam zapisywalny wiersz „2000-01-01”, który właśnie zaktualizowaliśmy, ale używa innego predykatu klauzuli WHERE:
UPDATE dbo.Test SET c2 = 2 WHERE c1 = 2;
Szacunkowy plan (przed realizacją) to:
Cztery (!) skalary obliczeniowe nie są ważne w tej dyskusji. Służą do określenia, czy indeks nieklastrowy musi być utrzymywany dla każdego wiersza, który dociera do operatora aktualizacji indeksu klastrowego.
Bardziej interesujące jest to, że to oświadczenie o aktualizacji nie powiodło się z błędem podobnym do:
Komunikat 652, poziom 16, stan 1Indeks "PK_dbo_Test__c1_dt" dla tabeli "dbo.Test" (RowsetId 72057594039042048) znajduje się w grupie plików tylko do odczytu ("ReadOnlyFileGroup"), której nie można modyfikować.
Nie eliminacja partycji
Jeśli pracowałeś już wcześniej z partycjonowaniem, być może myślisz, że przyczyną może być „eliminacja partycji”. Logika wyglądałaby mniej więcej tak:
W poprzednich instrukcjach w klauzuli where podano literalną wartość kolumny partycjonowania, aby SQL Server mógł od razu określić, do których partycji uzyskać dostęp. Zmieniając klauzulę where tak, aby nie odnosiła się już do kolumny partycjonowania, zmusiliśmy SQL Server do dostępu do każdej partycji za pomocą Clustered Index Scan.
Ogólnie rzecz biorąc, to wszystko prawda, ale to nie jest powód, dla którego instrukcja aktualizacji nie działa tutaj.
Oczekiwane zachowanie polega na tym, że SQL Server powinien być w stanie odczytywać z dowolnej i wszystkich partycji podczas wykonywania zapytania. Operacja modyfikacji danych powinna tylko nie powieść się jeśli silnik wykonawczy właściwie próbuje zmodyfikować wiersz przechowywany w grupie plików tylko do odczytu.
Aby to zilustrować, zróbmy małą zmianę w poprzednim zapytaniu:
UPDATE dbo.Test SET c2 = 2, dt = dt WHERE c1 = 2;
Klauzula where jest dokładnie taka sama jak poprzednio. Jedyną różnicą jest to, że teraz (celowo) ustawiamy kolumnę partycjonowania na równą sobie. Nie zmieni to wartości przechowywanej w tej kolumnie, ale wpłynie na wynik. Aktualizacja teraz udaje się (choć z bardziej złożonym planem wykonania):
Optymalizator wprowadził nowe operatory Split, Sort i Collapse oraz dodał maszynerię niezbędną do utrzymywania każdego indeksu nieklastrowanego, którego potencjalnie dotyczy to zjawisko, oddzielnie (przy użyciu strategii szerokiego lub według indeksu).
Właściwości skanowania indeksu klastrowego pokazują, że obie partycje tabeli były dostępne podczas czytania:
Natomiast aktualizacja indeksu klastrowego pokazuje, że tylko partycja do odczytu i zapisu była dostępna do zapisu:
Każdy z operatorów nieklastrowej aktualizacji indeksu pokazuje podobne informacje:tylko zapisywalna partycja (nr 2) została zmodyfikowana w czasie wykonywania, więc nie wystąpił błąd.
Ujawniony powód
Nowy plan się udaje nie ponieważ indeksy nieklastrowe są utrzymywane oddzielnie; ani czy jest to (bezpośrednio) ze względu na kombinację Split-Sort-Collapse niezbędną do uniknięcia przejściowych błędów zduplikowanych kluczy w unikalnym indeksie.
Prawdziwym powodem jest coś, o czym wspomniałem krótko w poprzednim artykule „Optymalizacja zapytań o aktualizację” – wewnętrzna optymalizacja znana jako Udostępnianie zestawu wierszy . W takim przypadku aktualizacja indeksu klastrowego udostępnia ten sam zestaw wierszy aparatu pamięci masowej, co skanowanie indeksu klastrowego, wyszukiwanie lub wyszukiwanie kluczy po stronie odczytu planu.
Z optymalizacją udostępniania wierszy, SQL Server sprawdza, czy są grupy plików offline lub tylko do odczytu podczas czytania. W planach, w których aktualizacja indeksu klastrowego używa oddzielnego zestawu wierszy, sprawdzanie trybu offline/tylko do odczytu jest wykonywane tylko dla każdego wiersza w iteratorze aktualizacji (lub usuwania).
Nieudokumentowane obejścia
Pozbądźmy się najpierw zabawnych, geekowych, ale niepraktycznych rzeczy.
Optymalizacja udostępnionego zestawu wierszy może być stosowana tylko wtedy, gdy trasa z wyszukiwania indeksu klastrowanego, skanowania lub wyszukiwania kluczy jest potoku . Operatory blokujące lub częściowo blokujące nie są dozwolone. Innymi słowy, każdy wiersz musi być w stanie dostać się ze źródła odczytu do miejsca docelowego zapisu przed odczytaniem następnego wiersza.
Przypominamy, że oto przykładowe dane, oświadczenie i plan wykonania dla nieudanego zaktualizuj ponownie:
--Change the read-write row UPDATE dbo.Test SET c2 = 2 WHERE c1 = 2;
Ochrona Halloween
Jednym ze sposobów wprowadzenia operatora blokującego do planu jest wymaganie wyraźnej ochrony Halloween (HP) dla tej aktualizacji. Oddzielenie odczytu od zapisu za pomocą operatora blokowania uniemożliwi użycie optymalizacji udostępniania zestawu wierszy (brak potoku). Nieudokumentowane i nieobsługiwane (tylko system testowy!) Flaga śledzenia 8692 dodaje bufor tabeli Chętny dla jawnego HP:
-- Works (explicit HP) UPDATE dbo.Test SET c2 = 2 WHERE c1 = 2 OPTION (QUERYTRACEON 8692);
Rzeczywisty plan wykonania (dostępny, ponieważ błąd nie jest już zgłaszany) to:
Sortowanie w kombinacji Split-Sort-Collapse widoczne we wcześniejszej pomyślnej aktualizacji zapewnia blokowanie niezbędne do wyłączenia udostępniania zestawu wierszy w tym wystąpieniu.
Flaga śledzenia zapobiegająca udostępnianiu zestawu wierszy
Istnieje inna nieudokumentowana flaga śledzenia, która wyłącza optymalizację udostępniania zestawu wierszy. Ma to tę zaletę, że nie wprowadza potencjalnie drogiego operatora blokującego. Oczywiście nie można go używać w praktyce (chyba że skontaktujesz się z Microsoft Support i otrzymasz coś na piśmie, zalecając włączenie go, jak sądzę). Niemniej jednak, w celach rozrywkowych, oto flaga śledzenia 8746 w akcji:
-- Works (no rowset sharing) UPDATE dbo.Test SET c2 = 2 WHERE c1 = 2 OPTION (QUERYTRACEON 8746);
Rzeczywisty plan wykonania tego oświadczenia to:
Możesz swobodnie poeksperymentować z różnymi wartościami (takimi, które faktycznie zmieniają zapisane wartości, jeśli chcesz), aby przekonać się o różnicy. Jak wspomniano w moim poprzednim poście, można również użyć nieudokumentowanej flagi śledzenia 8666, aby uwidocznić właściwość udostępniania zestawu wierszy w planie wykonania.
Jeśli chcesz zobaczyć błąd udostępniania zestawu wierszy z instrukcją delete, po prostu zastąp klauzule update i set klauzulą delete, używając tej samej klauzuli where.
Obsługiwane rozwiązania
Istnieje wiele potencjalnych sposobów zapewnienia, że udostępnianie zestawów wierszy nie jest stosowane w zapytaniach rzeczywistych bez użycia flag śledzenia. Teraz, gdy wiesz, że podstawowy problem wymaga współdzielonego i potokowego planu odczytu i zapisu indeksu klastrowego, prawdopodobnie możesz wymyślić własny. Mimo to istnieje kilka przykładów, którym szczególnie warto się przyjrzeć.
Indeks wymuszony / Indeks pokrycia
Jednym z naturalnych pomysłów jest wymuszenie na stronie czytającej planu używania indeksu nieklastrowego zamiast indeksu klastrowego. Nie możemy dodać wskazówki dotyczącej indeksu bezpośrednio do zapytania testowego, tak jak zostało to zapisane, ale aliasowanie tabeli pozwala na to:
UPDATE T SET c2 = 2 FROM dbo.Test AS T WITH (INDEX(IX_dbo_Test_c1)) WHERE c1 = 2;
Może się to wydawać rozwiązaniem, które optymalizator zapytań powinien był wybrać w pierwszej kolejności, ponieważ mamy indeks nieklastrowy w kolumnie predykatu klauzuli WHERE c1. Plan wykonania pokazuje, dlaczego optymalizator wybrał tak, jak zrobił:
Koszt Key Lookup wystarczy, aby przekonać optymalizatora do użycia indeksu klastrowego do odczytu. Wyszukiwanie jest potrzebne do pobrania bieżącej wartości kolumny c2, aby skalary obliczeniowe mogły zdecydować, czy należy zachować indeks nieklastrowany.
Dodanie kolumny c2 do indeksu nieklastrowanego (klucza lub dołączenia) pozwoliłoby uniknąć problemu. Optymalizator wybrałby indeks pokrywający teraz zamiast indeksu klastrowego.
To powiedziawszy, nie zawsze można przewidzieć, które kolumny będą potrzebne, lub uwzględnić je wszystkie, nawet jeśli zestaw jest znany. Pamiętaj, że kolumna jest potrzebna, ponieważ c2 znajduje się w klauzuli set oświadczenia o aktualizacji. Jeśli zapytania są doraźne (np. przesłane przez użytkowników lub wygenerowane przez narzędzie), każdy indeks nieklastrowany musiałby zawierać wszystkie kolumny, aby była to niezawodna opcja.
Jedną z interesujących rzeczy w planie z powyższym wyszukiwaniem kluczy jest to, że nie wygenerować błąd. Dzieje się tak pomimo wyszukiwania kluczy i aktualizacji indeksu klastrowego przy użyciu udostępnionego zestawu wierszy. Powodem jest to, że nieklastrowane wyszukiwanie indeksu lokalizuje wiersz z c1 =2 przed Key Lookup dotyka indeksu klastrowego. Sprawdzanie udostępnionego zestawu wierszy dla grup plików trybu offline/tylko do odczytu jest nadal wykonywane podczas wyszukiwania, ale nie dotyka partycji tylko do odczytu, więc nie zostanie zgłoszony żaden błąd. Jako ostatni (powiązany) punkt zainteresowania, zauważ, że wyszukiwanie indeksu dotyka obu partycji, ale wyszukiwanie klucza trafia tylko na jedną.
Wykluczanie partycji tylko do odczytu
Trywialnym rozwiązaniem jest poleganie na eliminacji partycji, aby strona odczytu planu nigdy nie dotykała partycji tylko do odczytu. Można to zrobić za pomocą wyraźnego predykatu, na przykład jednego z poniższych:
UPDATE dbo.Test SET c2 = 2 WHERE c1 = 2 AND dt >= {D '2000-01-01'}; UPDATE dbo.Test SET c2 = 2 WHERE c1 = 2 AND $PARTITION.PF(dt) > 1; -- Not partition #1
Tam, gdzie zmiana każdego zapytania w celu dodania predykatu eliminacji partycji jest niemożliwa lub niewygodna, odpowiednie mogą być inne rozwiązania, takie jak aktualizowanie za pomocą widoku. Na przykład:
CREATE VIEW dbo.TestWritablePartitions WITH SCHEMABINDING AS -- Only the writable portion of the table SELECT T.dt, T.c1, T.c2 FROM dbo.Test AS T WHERE $PARTITION.PF(dt) > 1; GO -- Succeeds UPDATE dbo.TestWritablePartitions SET c2 = 2 WHERE c1 = 2;
Jedną z wad korzystania z widoku jest to, że aktualizacja lub usunięcie, którego celem jest tylko do odczytu część tabeli podstawowej, zakończy się pomyślnie bez żadnych wierszy, a nie zakończy się błędem. Zamiast wyzwalacza w tabeli lub widoku może być w niektórych sytuacjach obejściem tego problemu, ale może też powodować więcej problemów… ale robię dygresję.
Jak wspomniano wcześniej, istnieje wiele potencjalnych obsługiwanych rozwiązań. Celem tego artykułu jest pokazanie, w jaki sposób udostępnianie zestawu wierszy spowodowało nieoczekiwany błąd aktualizacji.