Alt for ofte ser vi dårligt skrevne komplekse SQL-forespørgsler køre mod databasetabellerne. Sådanne forespørgsler kan tage meget kort eller meget lang tid at udføre, men de bruger en enorm mængde CPU og andre ressourcer. Ikke desto mindre giver komplekse forespørgsler i mange tilfælde værdifuld information til applikationen/personen. Derfor bringer det nyttige aktiver i alle varianter af applikationer.
Kompleksiteten af forespørgsler
Lad os se nærmere på problematiske forespørgsler. Mange af dem er komplekse. Det kan skyldes flere årsager:
- Den valgte datatype for dataene;
- Organiseringen og lagringen af dataene i databasen;
- Transformation og sammenføjning af data i en forespørgsel for at hente det ønskede resultatsæt.
Du skal gennemtænke disse tre nøglefaktorer korrekt og implementere dem korrekt for at få forespørgsler til at fungere optimalt.
Det kan dog blive en næsten umulig opgave for både databaseudviklere og DBA'er. For eksempel kan det være usædvanligt svært at tilføje ny funktionalitet til eksisterende Legacy-systemer. En særlig kompliceret sag er, når du skal udtrække og transformere data fra et ældre system, så du kan sammenligne dem med de data, der produceres af det nye system eller funktionalitet. Du skal opnå det uden at påvirke den ældre applikationsfunktionalitet.
Sådanne forespørgsler kan involvere komplekse joinforbindelser, såsom følgende:
- En kombination af understreng og/eller sammenkædning af flere datakolonner;
- Indbyggede skalarfunktioner;
- Tilpassede UDF'er;
- Enhver kombination af WHERE-sætningssammenligninger og søgebetingelser.
Forespørgsler, som beskrevet tidligere, har normalt komplekse adgangsstier. Hvad værre er, kan de have mange tabelscanninger og/eller fulde indeksscanninger med sådanne kombinationer af JOINs eller søgninger, der forekommer.
Datatransformation og manipulationer i forespørgsler
Vi skal påpege, at alle data, der er lagret vedvarende i en databasetabel, har brug for transformation og/eller manipulation på et tidspunkt, når vi forespørger om disse data fra tabellen. Transformationen kan variere fra en simpel transformation til en meget kompleks. Afhængigt af hvor kompleks den kan være, kan transformationen forbruge en masse CPU og ressourcer.
I de fleste tilfælde sker transformationer udført i JOINs, efter at dataene er læst og overført til tempdb database (SQL-server) eller arbejdsfil database / temp-tablespaces som i andre databasesystemer.
Da dataene i arbejdsfilen ikke kan indekseres , den tid, der kræves til at udføre kombinerede transformationer og JOINs, øges eksponentielt. De hentede data bliver større. De resulterende forespørgsler udvikler sig således til en præstationsflaskehals gennem yderligere datavækst.
Så hvordan kan en databaseudvikler eller en DBA løse disse præstationsflaskehalse hurtigt og også give sig selv mere tid til at omstrukturere og omskrive forespørgslerne for optimal ydeevne?
Der er to måder at løse sådanne vedvarende problemer effektivt. En af dem er ved at bruge virtuelle kolonner og/eller funktionelle indekser.
Funktionelle indekser og forespørgsler
Normalt opretter du indekser på kolonner, der enten angiver et unikt sæt af kolonner/værdier i en række (unikke indekser eller primærnøgler) eller repræsenterer et sæt kolonner/værdier, der er eller kan bruges i WHERE-sætningssøgebetingelser for en forespørgsel.
Hvis du ikke har sådanne indekser på plads, og du har udviklet komplekse forespørgsler som beskrevet tidligere, vil du bemærke følgende:
- Reduktion i ydeevneniveauer ved brug af forklar forespørge og se tabelscanninger eller fulde indeksscanninger
- Meget højt CPU- og ressourceforbrug forårsaget af forespørgslerne;
- Lange udførelsestider.
Moderne databaser løser normalt disse problemer ved at give dig mulighed for at oprette en funktionel eller funktionsbaseret indeks, som navngivet i SQLServer, Oracle og MySQL (v 8.x). Eller det kan være Indeks på udtryks-/udtryksbaseret indekser, som i andre databaser (PostgreSQL og Db2).
Antag, at du har en Købsdato-kolonne af datatypen TIMESTAMP eller DATETIME i din ordre tabel, og den kolonne er blevet indekseret. Vi begynder at forespørge på ordren tabel med en WHERE-sætning:
SELECT ...
FROM Order
WHERE DATE(Purchase_Date) = '03.12.2020'
Denne transaktion vil forårsage scanning af hele indekset. Men hvis kolonnen ikke er blevet indekseret, får du en tabelscanning.
Efter at have scannet hele indekset, flytter det indeks til tempdb / workfile (hele tabellen hvis du får en tabelscanning ) før den matcher værdien 03.12.2020 .
Da en stor ordretabel bruger masser af CPU og ressourcer, bør du oprette et funktionelt indeks med udtrykket DATE (Purchase_Date ) som en af indekskolonnerne og vist nedenfor:
CREATE ix_DatePurchased on sales.Order(Date(Purchase_Date) desc, ... )
Når du gør det, laver du det matchende prædikat DATE (Purchase_Date) ='03.12.2020' indekserbar. I stedet for at flytte indekset eller tabellen til tempdb / workfilen før matchningen af værdien, gør vi indekset kun delvist tilgået og/eller scannet. Det resulterer i lavere CPU- og ressourceforbrug.
Se et andet eksempel. Der er en kunde tabel med kolonnerne fornavn, efternavn . Disse kolonner er indekseret som sådan:
CREATE INDEX ix_custname on Customer(first_name asc, last_name asc),
Desuden har du en visning, der sammenkæder disse kolonner i kundenavn kolonne:
CREATE view v_CustomerInfo( customer_name, .... ) as
select first_name ||' '|| last_name as customer_name,.....
from Customer
where ...
Du har en forespørgsel fra et e-handelssystem, der søger efter det fulde kundenavn:
select c.*
from v_CustomerInfo c
where c.customer_name = 'John Smith'
....
Igen vil denne forespørgsel producere en fuld indeksscanning. I det værste tilfælde vil det være en fuld tabelscanning, der flytter alle data fra indekset eller tabellen til arbejdsfilen før sammenkædningen af fornavn og efternavn kolonner og matcher "John Smith"-værdien.
Et andet tilfælde er at oprette et funktionelt indeks som vist nedenfor:
CREATE ix_fullcustname on sales.Customer( first_name ||' '|| last_name desc, ... )
På denne måde kan du lave sammenkædningen i visningsforespørgslen til et indekserbart prædikat. I stedet for en fuld indeksscanning eller tabelscanning har du en delvis indeksscanning. En sådan udførelse af forespørgsler resulterer i lavere CPU- og ressourceforbrug, udelukker arbejdet i arbejdsfilen og sikrer dermed hurtigere eksekveringstid.
Virtuelle (genererede) kolonner og forespørgsler
Genererede kolonner (virtuelle kolonner eller beregnede kolonner) er kolonner, der rummer de data, der genereres i farten. Dataene kan ikke udtrykkeligt indstilles til en bestemt værdi. Det refererer til data i andre kolonner, der forespørges, indsættes eller opdateres i en DML-forespørgsel.
Værdigenereringen af sådanne kolonner er automatiseret baseret på et udtryk. Disse udtryk kan generere:
- En sekvens af heltalsværdier;
- Værdien baseret på værdierne i andre kolonner i tabellen;
- Det kan generere værdier ved at kalde indbyggede funktioner eller brugerdefinerede funktioner (UDF'er).
Det er lige så vigtigt at bemærke, at i nogle databaser (SQLServer, Oracle, PostgreSQL, MySQL og MariaDB) kan disse kolonner konfigureres til enten vedvarende at gemme dataene med INSERT- og UPDATE-sætningsudførelsen eller udføre det underliggende kolonneudtryk i farten hvis vi forespørger i tabellen og kolonnen, sparer lagerpladsen.
Men når udtrykket er kompliceret, som med kompleks logik i UDF-funktionen, er besparelsen af eksekveringstid, ressourcer og CPU-forespørgselsomkostninger muligvis ikke så meget som forventet.
Således kan vi konfigurere kolonnen, så den vedvarende gemmer resultatet af udtrykket i en INSERT- eller UPDATE-sætning. Derefter opretter vi et almindeligt indeks på den kolonne. På denne måde gemmer vi CPU'en, ressourceforbruget og forespørgselsudførelsestiden. Igen kan det være en lille stigning i INSERT og UPDATE ydeevnen, afhængigt af udtrykkets kompleksitet.
Lad os se på et eksempel. Vi erklærer tabellen og opretter et indeks som følger:
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, ... )
På denne måde flytter vi sammenkædningslogikken fra visningen i det foregående eksempel ned i tabellen og gemmer dataene vedvarende. Vi henter dataene ved hjælp af en matchende scanning på et almindeligt indeks. Det er det bedst mulige resultat her.
Ved at tilføje en genereret kolonne til en tabel og oprette et regulært indeks på den kolonne, kan vi flytte transformationslogikken ned til tabelniveauet. Her gemmer vi vedvarende de transformerede data i insert- eller opdateringssætninger, som ellers ville blive transformeret i forespørgsler. JOIN- og INDEX-scanningerne vil være meget enklere og hurtigere.
Funktionelle indekser, genererede kolonner og JSON
Globale web- og mobilapplikationer bruger lette datastrukturer såsom JSON til at flytte data fra nettet/mobilenheden til databasen og omvendt. Det lille fodaftryk af JSON-datastrukturer gør dataoverførslen over netværket hurtig og nem. Det er nemt at komprimere JSON til en meget lille størrelse sammenlignet med andre strukturer, det vil sige XML. Det kan overgå strukturer i runtime-parsing.
På grund af den øgede brug af JSON-datastrukturer har relationsdatabaser JSON-lagerformatet som enten BLOB-datatype eller CLOB-datatype. Begge disse typer gør dataene i sådanne kolonner uindekserbare, som de er.
Af denne grund introducerede databaseleverandørerne JSON-funktioner til at forespørge og ændre JSON-objekter, da du nemt kan integrere disse funktioner i SQL-forespørgslen eller andre DML-kommandoer. Disse forespørgsler afhænger dog af JSON-objekters kompleksitet. De er meget CPU- og ressourcekrævende, da BLOB- og CLOB-objekter skal overføres til hukommelsen, eller endnu værre, til arbejdsfilen før forespørgsel og/eller manipulation.
Antag, at vi har en kunde tabel med Kundedetaljer data gemt som et JSON-objekt i en kolonne kaldet CustomerDetail . Vi konfigurerede forespørgsler i tabellen som nedenfor:
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')
I dette eksempel forespørger vi efter data for kunder, der bor i nogle dele af hovedstadsregionen i Island. Alle Aktive data skal hentes ind i arbejdsfilen før du anvender søgeprædikatet. Alligevel vil hentning resultere i for stort CPU- og ressourceforbrug.
Derfor er der en effektiv procedure til at få JSON-forespørgsler til at køre hurtigere. Det involverer at bruge funktionaliteten gennem genererede kolonner, som tidligere beskrevet.
Vi opnår præstationsboostet ved at tilføje genererede kolonner. En genereret kolonne vil søge gennem JSON-dokumentet efter specifikke data repræsenteret i kolonnen ved hjælp af JSON-funktionerne og gemme værdien i kolonnen.
Vi kan indeksere og forespørge på disse genererede kolonner ved hjælp af almindelige SQL where-klausulsøgningsbetingelser. Derfor bliver det meget hurtigt at søge efter bestemte data i JSON-objekter.
Vi tilføjer to genererede kolonner – Land og Postnummer :
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);
Vi opretter også et sammensat indeks på de specifikke kolonner. Nu kan vi ændre forespørgslen til eksemplet vist nedenfor:
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')
Dette begrænser datahentningen til kun aktive kunder i nogle dele af Islands hovedstadsregion. Denne måde er hurtigere og mere effektiv end den forrige forespørgsel.
Konklusion
Alt i alt kan vi ved at anvende virtuelle kolonner eller funktionelle indekser på tabeller, der forårsager vanskeligheder (CPU og ressourcetunge forespørgsler), eliminere problemer ret hurtigt.
Virtuelle kolonner og funktionelle indekser kan hjælpe med at forespørge på komplekse JSON-objekter, der er gemt i almindelige relationelle tabeller. Men vi er nødt til at vurdere problemerne omhyggeligt på forhånd og foretage de nødvendige ændringer i overensstemmelse hermed.
I nogle tilfælde, hvis forespørgslen og/eller JSON-datastrukturerne er meget komplekse, kan en del af CPU- og ressourceforbruget skifte fra forespørgslerne til INSERT/UPDATE-processerne. Det giver os færre samlede CPU- og ressourcebesparelser end forventet. Hvis du oplever lignende problemer, kan mere grundige tabeller og forespørgsler omdesign være uundgåelige.