Przykład z życia oraklowego debeowaca 1.1
Wstęp 

W poprzednim materiale opisałem pierwszy przypadek z życia oraklowego debeowca. Zachęcałem do komentowania mojego podejścia i zaproponowania własnych pomysłów poprawiających wydajność SQL. Teraz zaprezentuję jak pomysły z komentarzy oraz moje wpłynęły na wydajność SQL.

Materiał na YouTube
Pomysły 

W komentarzach pod materiałem znalazłem: 

  • Przepisanie tabeli w celu defragmentacji. Poniekąd coś takiego nastąpiło. Szerzej omówię za chwilę. 
  • Hint parallel może poprawić wydajność skanowania. O tym także szerzej za chwilę. 
  • Subpartycje są strzałem w dziesiątkę. Jak sądzisz według jakiego klucza należy je stworzyć? 

Od siebie dokładam kompresję i usunięcie indeksu.

Analiza

Aby zapewnić optymalną wydajność aplikacji schemat bazy danych i pracujące na nim komendy SQL powinny ze sobą współgrać. W moim przykładzie zapytania filtrują dane według wartości kolumn data i źródło lub jedynie data.

Schemat tabeli poniekąd odpowiada części tych potrzeb. Z drugiej strony jest nieco nadmiarowy. Dane spartycjonowane są według wartości kolumny data. 

CREATE TABLE t (
            "data" DATE NOT NULL
            ,"źródło" VARCHAR2(20 CHAR) NOT NULL
[
Jakieś 20 innych kolumn
]
) PARTITION BY LIST ("data")
(
 PARTITION "P20170101" VALUES (DATE'2017-01-01')
,PARTITION "P20170102" VALUES (DATE'2017-01-02')
[
Ponad 1500 innych partycji
]
)

W każdej partycji wartości kolumny data dla wszystkich rekordów są identyczne. Zapytania zawsze wybierają dane z jednego dnia. Plany wykonania zapytań wskazują, że optymalizator bierze to pod uwagę i ogranicza skanowanie do pojedyńczej partycji. 

Optymalizator dokonuje partition pruning

Warto zwrócić uwagę na kolumnę A-Time czyli faktyczny czas w którym zapytanie zostało wykonane. Wartość dla planu wykonania używającego indeksu jest wielokrotnie większa niż dla planu wykonania skanującego całą partycję.

Dlaczego? Czyż indeksy nie są remedium na każdą bolączkę wydajnościową? 

Otóż nie. 

Odczyt wszystkich danych z partycji, a z takim mam do czynienia, jest bardziej efektywny sekwencyjnie niż losowo przy użyciu indeksu. Motor bazy danych oprócz tabeli musi także odczytać indeks i robi to blok po bloku zamiast, jak w przypadku skanowania, wiele sąsiadujących bloków na raz.

Zauważ, że sam odczyt indeksu trwa prawie 2 minuty. W tym czasie odczytywane sa informacje o 27 milionach rekordów. Czyli odczytywana jest cała partycja indeksu aby wybrać z niej wskaźniki do 2219 rekordów. Nie uważasz, że to nie jest zbyt optymalne?

Wyjątkiem są zapytania filtrujące tak po dacie jak i po źródle. Niemniej rozkład wartości źródła dla poszczególnych partycji powoduje, że użycie indeksu nie zawsze byłoby opłacalne. 

Wnioski 

Indeks zbudowany na kolumnie data należy usunąć bo szkodzi. Najbardziej efektywną metodą dostępu do danych jest skanowanie całej partycji. Odczyta wszystkie bloki partycji, ale zrobi to po wiele na raz. Na dodatek nie przejdą przez cache, więc odpadnie narzut na zarządzanie pamięcią podręczną.

Jak można zoptymalizować sekwencyjny dostęp do danych? 

Każdą z partycji można skompresować. Zajmie mniej bloków bazodanowych.

Gdyby była możliwość modyfikacji zapytań można by dopisać do nich hint parallel. Wtedy każda partycja mogłaby być skanowana kilkoma procesami. 

Podział każdej partycji na subpartycje według wartości kolumny źródło dodatkowo ograniczy ilość czytanych danych i będzie niewrażliwe na ich nierównomierny rozkład. 

O jakiej bardzo ważnej optymalizacji jeszcze nie wspomniałem? 

Masz rację – warto porozmawiać z właścicielem danych czy na pewno ich wszystkich potrzebuje. Rozmiar tabeli oraz fakt, że przechowuje dane nawet sprzed ośmiu lat wskazują, że istnieje cień szansy, że zapomniano zaimplementować cykl życia danych. Warto także wziąć pod uwagę, że cykl życia danych może być wymuszony regulacjami. 

Indeks 

Usunięcie indeksu jest najłatwiejszą do przeprowadzenia optymalizacją. Niemniej życie uczy, że może być traktowane jak herezja przez zainteresowanych. Indeksy łatwo się dodaje bo wyraźnie widać potrzebę i, gdy są używane, zazwyczaj poprawę wydajności. Trudno się je usuwa bo nikt nie wie jak sprawdzić czy na pewno nie są używane. 

Usunięcie indeksu trwa sekundy. Jest to operacja na słowniku bazy danych. W moim przypadku stabilizuje plan wykonania. Teraz nie ma innej ścieżki dostępu do danych jak pełny skan partycji.

Pełny skan partycji jest bardziej efektywny niż użycie indeksu

Skanując całą partycję instancja wykonuje mniej odczytów. Dodatkowo czyta szybciej bo ścieżką bezpośrednią. 

Skoro optymalizator nie ma wyboru to aktualność statystyk jest drugorzędna. Aby niepotrzebnie nie zużywać zasobów instancji na naliczanie statystyki można kopiować ze starszych partycji do nowszych. Rozkład danych jest podobny. 

Kompresja 

Oracle dostarcza różnych możliwości kompresowania danych. Na niektóre trzeba mieć licencję Advanced Compression OptionUżyta przeze mnie nie wymaga osobnego licencjonowania. Jej ograniczeniem jest kompresowanie jedynie danych znajdujących się w tabeli. Nowe dane nie zostaną skompresowane. Nadaje się idealnie dla mojego przypadku. Nowe dane zapisywane są w najnowszej partycji i nigdy nie są modyfikowane. 

Tworzę pętlę w PL/SQL kompresującą partycję po partycji poczynając od najnowszych. Operacja jest przezroczysta dla aplikacji. 

declare
  v_sqlstmt varchar(222);
begin
  for i in (select partition_name from user_tab_partitions where table_name='T'
            and compression!='ENABLED' order by partition_position desc)
  loop
    v_sqlstmt := 'alter table T move partition '||i.partition_name||' compress';
    execute immediate v_sqlstmt;
  end loop;
end;
/

Skompresowane partycje zajmują jedynie 40% poprzednio zajmowanej przestrzeni dyskowej. Dzięki temu zapytania wykonują się w około 20% krótszym czasie. Instancja wykonuje mniej IO aby odczytać partycję.

Zapytania na skompresowanej partycji wykonują się szybciej zużywając mniej IO

Oprócz tego tabela chudnie z 7TB do 3TB. Co ma wpływ na czas wykonania bekapu oraz czas jego odtworzenia. Nie wspominając o kosztach nośników w macierzach klasy enterprise. Należy jeszcze utworzyć job który codziennie skompresuje wypełnioną danymi partycję z dnia poprzedniego.

Subpartycje

Zasady dla których warto było partycjonować tabelę obowiązują także dla subpartycji. Zapytania filtrujące rekordy według wartości kolumn data i źródło zawsze wybierają wszystkie rekordy danej partycji mające określoną wartość w kolumnie źródło.

Indeks byłby przydatny dla wartości pojawiających się w niewielu rekordach natomiast nie byłby przydatny dla wartości pojawiającej się w znacznej liczbie rekordów. Wtedy i tak kończyłoby się skanowaniem całej partycji.

Subpartycje według listy wartości kolumny źródło rozwiązują ten problem. Optymalizator zawęzi skanowanie danych jedynie do subpartycji z pożądaną wartością w kolumnie źródło niezależnie od liczby znajdujących się w niej rekordów. 

Dodanie subpartycji to zmiana schematu tabeli wiążąca się z przepisaniem danych do nowych segmentów. Od Oracle 12.2 może być wykonana jedną komendą bez przerywania dostępu do danych. 

ALTER TABLE t MODIFY
    PARTITION BY LIST ( "data" ) AUTOMATIC
        SUBPARTITION BY LIST ( "źródło" )
        SUBPARTITION TEMPLATE ( 
            SUBPARTITION a VALUES ( 'A' ) COMPRESS
            ,SUBPARTITION b VALUES ( 'B' ) COMPRESS
	    ,…
            ,SUBPARTITION p_default VALUES ( DEFAULT ) COMPRESS
        )
    ( 
        PARTITION p20160101 VALUES ( date'2016-01-01' )
    ) ONLINE;

W wersjach wcześniejszych trzeba jawnie przepisać dane do nowej tabeli. Na przykład używając pakietu DBMS_REDEFINITION. Czas wykonania zapytania skraca się w zależności od wielkości konkretnej subpartycji.

Czas skanowania subpartycji w zależności od ilości danych
Kasowanie danych 

Właściciel danych potwierdził, że potrzebuje jedynie dane nie starsze niż trzy lata. Czyli z tabeli można usunąć pięcioletnią historię. 

Należy także wdrożyć automatyczny job kasujący najstarszą partycję w momencie gdy przestaje być potrzebna. Do odzyskania jest ok 1TB przestrzeni dyskowej.

Podsumowanie 

Dzięki zastosowaniu odpowiedniego partycjonowania i kompresji danych czas wykonania zapytania na całej partycji zmniejszył się z ponad dwóch minut do dwóch i pół sekundy. 

Ograniczenie wybieranych danych do najliczniejszego źródła skutkuje czasem wykonania poniżej dwóch sekund. 

Natomiast czas wykonania zapytania ograniczonego do najmniej licznego źródła to niespełna jedna milisekunda.

Zwróć uwagę, że tak ogromna poprawa wydajności została osiągnięta jedynie odpowiednią organizacją danych w bazie danych. Nadmiarowość w postaci indeksu mało, że nie została wykorzystana to wręcz została usunięta. Dzięki temu, niejako przy okazji, poprawiła się także wydajność ładowania danych do nowej partycji. Dane zostały w tych samych plikach bazodanowych na tych samych dyskach.

Zaowocowała znajomość danych, sposobu ich przetwarzania oraz funkcjonalności motoru.

Nic z tego nie byłoby możliwe gdyby nie współpraca administratora baz danych, dewelopera i właściciela danych.

Marcin Badtke

Przyjaźnie o SQL, bazach danych i ludziach

Może Ci się spodobać...

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *