Zbyt często widzimy źle napisane złożone zapytania SQL działające na tabelach bazy danych. Wykonanie takich zapytań może trwać bardzo krótko lub bardzo długo, ale pochłaniają ogromną ilość procesora i innych zasobów. Niemniej jednak w wielu przypadkach złożone zapytania dostarczają aplikacji/osobie cennych informacji. W związku z tym zapewnia przydatne zasoby we wszystkich odmianach zastosowań.
Złożoność zapytań
Przyjrzyjmy się bliżej problematycznym zapytaniom. Wiele z nich jest skomplikowanych. Może to wynikać z kilku powodów:
- Typ danych wybrany dla danych;
- Organizacja i przechowywanie danych w bazie danych;
- Przekształcanie i łączenie danych w zapytaniu w celu pobrania żądanego zestawu wyników.
Musisz odpowiednio przemyśleć te trzy kluczowe czynniki i poprawnie je zaimplementować, aby zapytania działały optymalnie.
Jednak może się to stać prawie niewykonalnym zadaniem zarówno dla programistów baz danych, jak i administratorów baz danych. Na przykład dodanie nowej funkcjonalności do istniejących starszych systemów może być wyjątkowo trudne. Szczególnie skomplikowanym przypadkiem jest sytuacja, gdy trzeba wyodrębnić i przekształcić dane ze starszego systemu, aby móc je porównać z danymi wygenerowanymi przez nowy system lub funkcjonalność. Musisz to osiągnąć bez wpływu na funkcjonalność starszej aplikacji.
Takie zapytania mogą obejmować złożone sprzężenia, takie jak:
- Kombinacja podciągu i/lub konkatenacji kilku kolumn danych;
- Wbudowane funkcje skalarne;
- Dostosowane UDF;
- Dowolna kombinacja porównań klauzul WHERE i warunków wyszukiwania.
Zapytania, jak opisano wcześniej, zwykle mają złożone ścieżki dostępu. Co gorsza, mogą mieć wiele skanów tabel i/lub pełnych skanów indeksów z takimi kombinacjami JOIN lub występujących wyszukiwań.
Transformacja danych i manipulacje w zapytaniach
Musimy zaznaczyć, że wszystkie dane przechowywane na stałe w tabeli bazy danych wymagają transformacji i/lub manipulacji w pewnym momencie, gdy wysyłamy zapytanie do tabeli. Transformacja może wahać się od prostej transformacji do bardzo złożonej. W zależności od tego, jak złożona może być, transformacja może zużywać dużo procesora i zasobów.
W większości przypadków przekształcenia wykonywane w połączeniach JOIN następują po odczytaniu danych i przeniesieniu ich do tempdb baza danych (SQL Server) lub plik roboczy baza danych / temp-tablespaces jak w innych systemach bazodanowych.
Ponieważ dane w pliku roboczym nie są indeksowane , czas wymagany do wykonania połączonych transformacji i sprzężeń zwiększa się wykładniczo. Pobrane dane stają się większe. W ten sposób powstałe zapytania stają się wąskim gardłem wydajności poprzez dodatkowy wzrost ilości danych.
Jak więc programista baz danych lub administrator baz danych może szybko rozwiązać te wąskie gardła wydajności, a także zapewnić sobie więcej czasu na przeprojektowanie i przepisanie zapytań w celu uzyskania optymalnej wydajności?
Istnieją dwa sposoby skutecznego rozwiązywania takich uporczywych problemów. Jednym z nich jest użycie wirtualnych kolumn i/lub indeksów funkcjonalnych.
Funkcjonalne indeksy i zapytania
Zwykle tworzy się indeksy na kolumnach, które albo wskazują unikalny zestaw kolumn/wartości w wierszu (indeksy niepowtarzalne lub klucze podstawowe), albo reprezentują zestaw kolumn/wartości, które są lub mogą być używane w warunkach wyszukiwania klauzuli WHERE zapytania.
Jeśli nie masz takich indeksów, a opracowałeś złożone zapytania, jak opisano wcześniej, zauważysz, co następuje:
- Obniżenie poziomów wydajności podczas korzystania z wyjaśnienia zapytanie i wyświetlanie skanów tabel lub pełnych skanów indeksów
- Bardzo wysokie zużycie procesora i zasobów spowodowane zapytaniami;
- Długie czasy wykonania.
Współczesne bazy danych zwykle rozwiązują te problemy, umożliwiając utworzenie funkcjonalnego lub oparte na funkcjach indeks, jak nazwano w SQLServer, Oracle i MySQL (v 8.x). Lub może to być Indeks na na podstawie wyrażeń/wyrażeń indeksy, podobnie jak w innych bazach danych (PostgreSQL i Db2).
Załóżmy, że masz kolumnę Data_zakupu typu danych TIMESTAMP lub DATETIME w Twoim zamówieniu tabeli i ta kolumna została zindeksowana. Zaczynamy kwestionować zamówienie tabela z klauzulą WHERE:
SELECT ...
FROM Order
WHERE DATE(Purchase_Date) = '03.12.2020'
Ta transakcja spowoduje skanowanie całego indeksu. Jeśli jednak kolumna nie została zindeksowana, otrzymasz skan tabeli.
Po zeskanowaniu całego indeksu, indeks ten przenosi się do tempdb/workfile (cały stół jeśli otrzymasz skan tabeli ) przed dopasowaniem wartości 03.12.2020 .
Ponieważ duża tabela zamówień zużywa dużo procesora i zasobów, należy utworzyć indeks funkcjonalny z wyrażeniem DATE (Purchase_Date ) jako jedną z kolumn indeksu i pokazano poniżej:
CREATE ix_DatePurchased on sales.Order(Date(Purchase_Date) desc, ... )
W ten sposób tworzysz odpowiedni predykat DATE (Purchase_Date) =„03.12.2020” indeksowalny. Tak więc, zamiast przenosić indeks lub tabelę do tempdb/pliku roboczego przed dopasowaniem wartości, sprawiamy, że indeks jest tylko częściowo dostępny i/lub skanowany. Powoduje to mniejsze zużycie procesora i zasobów.
Spójrz na inny przykład. Jest Klient tabela z kolumnami imię, nazwisko . Te kolumny są indeksowane w następujący sposób:
CREATE INDEX ix_custname on Customer(first_name asc, last_name asc),
Poza tym masz widok, który łączy te kolumny w nazwa_klienta kolumna:
CREATE view v_CustomerInfo( customer_name, .... ) as
select first_name ||' '|| last_name as customer_name,.....
from Customer
where ...
Masz zapytanie z systemu handlu elektronicznego, które wyszukuje pełną nazwę klienta:
select c.*
from v_CustomerInfo c
where c.customer_name = 'John Smith'
....
Ponownie, to zapytanie wygeneruje pełny skan indeksu. W najgorszym przypadku będzie to pełne skanowanie tabeli przenoszące wszystkie dane z indeksu lub tabeli do pliku roboczego przed połączeniem imienia i nazwisko kolumny i pasujące do wartości „Jan Kowalski”.
Innym przypadkiem jest utworzenie indeksu funkcjonalnego, jak pokazano poniżej:
CREATE ix_fullcustname on sales.Customer( first_name ||' '|| last_name desc, ... )
W ten sposób można przekształcić konkatenację w kwerendzie widoku w predykat do indeksowania. Zamiast pełnego skanowania indeksu lub skanowania tabeli masz częściowe skanowanie indeksu. Takie wykonanie zapytania skutkuje niższym zużyciem procesora i zasobów, wyłączając pracę w pliku roboczym, a tym samym zapewniając szybszy czas wykonania.
Wirtualne (generowane) kolumny i zapytania
Wygenerowane kolumny (wirtualne lub wyliczane) to kolumny, które przechowują dane generowane w locie. Danych nie można jawnie ustawić na określoną wartość. Odnosi się do danych w innych kolumnach, których dotyczyło zapytanie, wstawienie lub zaktualizowanie w zapytaniu DML.
Generowanie wartości takich kolumn jest zautomatyzowane na podstawie wyrażenia. Te wyrażenia mogą generować:
- Sekwencja wartości całkowitych;
- Wartość oparta na wartościach innych kolumn w tabeli;
- Może generować wartości przez wywołanie funkcji wbudowanych lub funkcji zdefiniowanych przez użytkownika (UDF).
Równie ważne jest, aby pamiętać, że w niektórych bazach danych (SQLServer, Oracle, PostgreSQL, MySQL i MariaDB) kolumny te można skonfigurować tak, aby trwale przechowywać dane za pomocą instrukcji INSERT i UPDATE lub wykonywać podstawowe wyrażenie kolumny w locie jeśli zapytamy tabelę i kolumnę, oszczędzając miejsce do przechowywania.
Jednak gdy wyrażenie jest skomplikowane, jak w przypadku złożonej logiki w funkcji UDF, oszczędność czasu wykonania, zasobów i kosztów zapytań procesora może nie być tak duża, jak oczekiwano.
W ten sposób możemy skonfigurować kolumnę tak, aby trwale przechowywała wynik wyrażenia w instrukcji INSERT lub UPDATE. Następnie tworzymy zwykły indeks w tej kolumnie. W ten sposób zaoszczędzimy procesor, zużycie zasobów i czas wykonania zapytania. Ponownie, może to być niewielki wzrost wydajności INSERT i UPDATE, w zależności od złożoności wyrażenia.
Spójrzmy na przykład. Deklarujemy tabelę i tworzymy indeks w następujący sposób:
CREATE TABLE Customer as (
customerID Int GENERATED ALWAYS AS IDENTITY,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
customer_name as (first_name ||' '|| last_name) PERSISTED
...
);
CREATE ix_fullcustname on sales.Customer( customer_name desc, ... )
W ten sposób przenosimy logikę konkatenacji z widoku z poprzedniego przykładu w dół do tabeli i trwale przechowujemy dane. Pobieramy dane za pomocą pasującego skanu na zwykłym indeksie. To najlepszy możliwy wynik.
Dodając wygenerowaną kolumnę do tabeli i tworząc zwykły indeks w tej kolumnie, możemy przenieść logikę transformacji na poziom tabeli. Tutaj trwale przechowujemy przekształcone dane w instrukcjach wstawiania lub aktualizowania, które w przeciwnym razie zostałyby przekształcone w zapytaniach. Skany JOIN i INDEX będą znacznie prostsze i szybsze.
Funkcjonalne indeksy, wygenerowane kolumny i JSON
Globalne aplikacje internetowe i mobilne używają lekkich struktur danych, takich jak JSON, do przenoszenia danych z urządzenia internetowego/mobilnego do bazy danych i odwrotnie. Niewielkie rozmiary struktur danych JSON sprawiają, że przesyłanie danych przez sieć jest szybkie i łatwe. Łatwo jest skompresować JSON do bardzo małego rozmiaru w porównaniu do innych struktur, np. XML. Może przewyższać struktury podczas analizowania w czasie wykonywania.
Ze względu na zwiększone wykorzystanie struktur danych JSON, relacyjne bazy danych mają format przechowywania JSON jako typ danych BLOB lub typ danych CLOB. Oba te typy sprawiają, że dane w takich kolumnach są nieindeksowalne.
Z tego powodu dostawcy baz danych wprowadzili funkcje JSON do odpytywania i modyfikowania obiektów JSON, ponieważ można łatwo zintegrować te funkcje z zapytaniem SQL lub innymi poleceniami DML. Jednak te zapytania zależą od złożoności obiektów JSON. Są bardzo obciążające procesor i zasoby, ponieważ obiekty BLOB i CLOB muszą zostać przeniesione do pamięci lub, co gorsza, do pliku roboczego przed zapytaniem i/lub manipulacją.
Załóżmy, że mamy Klienta tabela z Dane klienta dane przechowywane jako obiekt JSON w kolumnie o nazwie CustomerDetail . Skonfigurowaliśmy zapytanie do tabeli jak poniżej:
SELECT CustomerID,
JSON_VALUE(CustomerDetail, '$.customer.Name') AS Name,
JSON_VALUE(CustomerDetail, '$.customer.Surname') AS Surname,
JSON_VALUE(CustomerDetail, '$.customer.address.PostCode') AS PostCode,
JSON_VALUE(CustomerDetail, '$.customer.address."Address Line 1"') + ' '
+ JSON_VALUE(CustomerDetail, '$.customer.address."Address Line 2"') AS Address,
JSON_QUERY(CustomerDetail, '$.customer.address.Country') AS Country
FROM Customer
WHERE ISJSON(CustomerDetail) > 0
AND JSON_VALUE(CustomerDetail, '$.customer.address.Country') = 'Iceland'
AND JSON_VALUE(CustomerDetail, '$.customer.address.PostCode') IN (101,102,110,210,220)
AND Status = 'Active'
ORDER BY JSON_VALUE(CustomerDetail, '$.customer.address.PostCode')
W tym przykładzie pytamy o dane klientów mieszkających w niektórych częściach regionu stołecznego w Islandii. Wszystkie Aktywne dane należy pobrać do pliku roboczego przed zastosowaniem predykatu wyszukiwania. Mimo to pobieranie spowoduje zbyt duże zużycie procesora i zasobów.
W związku z tym istnieje skuteczna procedura przyspieszająca działanie zapytań JSON. Polega na wykorzystaniu funkcjonalności poprzez wygenerowane kolumny, jak opisano wcześniej.
Wzrost wydajności osiągamy dodając wygenerowane kolumny. Wygenerowana kolumna przeszukałaby dokument JSON pod kątem określonych danych reprezentowanych w kolumnie za pomocą funkcji JSON i zapisałaby wartość w kolumnie.
Możemy indeksować i wyszukiwać te wygenerowane kolumny przy użyciu zwykłych warunków wyszukiwania klauzuli SQL gdzie. Dlatego wyszukiwanie określonych danych w obiektach JSON staje się bardzo szybkie.
Dodajemy dwie wygenerowane kolumny – Kraj i Kod Pocztowy :
ALTER TABLE Customer
ADD Country as JSON_VALUE(CustomerDetail,'$.customer.address.Country');
ALTER TABLE Customer
ADD PostCode as JSON_VALUE(CustomerDetail,'$.customer.address.PostCode');
CREATE INDEX ix_CountryPostCode on Country(Country asc,PostCode asc);
Ponadto tworzymy indeks złożony na określonych kolumnach. Teraz możemy zmienić zapytanie na przykład pokazany poniżej:
SELECT CustomerID,
JSON_VALUE(CustomerDetail, '$.customer.customer.Name') AS Name,
JSON_VALUE(CustomerDetail, '$.customer.customer.Surname') AS Surname,
JSON_VALUE(CustomerDetail, '$.customer.address.PostCode') AS PostCode,
JSON_VALUE(CustomerDetail, '$.customer.address."Address Line 1"') + ' '
+ JSON_VALUE(CustomerDetail, '$.customer.address."Address Line 2"') AS Address,
JSON_QUERY(CustomerDetail, '$.customer.address.Country') AS Country
FROM Customer
WHERE ISJSON(CustomerDetail) > 0
AND Country = 'Iceland'
AND PostCode IN (101,102,110,210,220)
AND Status = 'Active'
ORDER BY JSON_VALUE(CustomerDetail, '$.customer.address.PostCode')
Ogranicza to pobieranie danych do Aktywnych Klientów tylko w niektórych częściach Regionu Stołecznego Islandii. W ten sposób jest szybszy i bardziej wydajny niż poprzednie zapytanie.
Wniosek
Podsumowując, stosując wirtualne kolumny lub indeksy funkcjonalne do tabel, które powodują trudności (zapytania dotyczące procesora i zasobów), możemy dość szybko wyeliminować problemy.
Kolumny wirtualne i indeksy funkcjonalne mogą pomóc w zapytaniach złożonych obiektów JSON przechowywanych w zwykłych tabelach relacyjnych. Jednak musimy wcześniej dokładnie ocenić problemy i odpowiednio wprowadzić niezbędne zmiany.
W niektórych przypadkach, jeśli struktury zapytań i/lub danych JSON są bardzo złożone, część wykorzystania procesora i zasobów może przesunąć się z zapytań na procesy INSERT/UPDATE. Daje nam to mniej ogólnych oszczędności procesora i zasobów niż oczekiwano. Jeśli wystąpią podobne problemy, bardziej szczegółowe przeprojektowanie tabel i zapytań może być nieuniknione.