Alltför ofta ser vi dåligt skrivna komplexa SQL-frågor som körs mot databastabellerna. Sådana frågor kan ta mycket kort eller mycket lång tid att köra, men de förbrukar en enorm mängd CPU och andra resurser. Men i många fall ger komplexa frågor värdefull information till applikationen/personen. Därför ger den användbara tillgångar i alla varianter av applikationer.
Frågornas komplexitet
Låt oss titta närmare på problematiska frågor. Många av dem är komplexa. Det kan bero på flera skäl:
- Den datatyp som valts för data;
- Organisationen och lagringen av data i databasen;
- Transformation och sammanfogning av data i en fråga för att hämta önskad resultatuppsättning.
Du måste tänka igenom dessa tre nyckelfaktorer ordentligt och implementera dem korrekt för att få frågor att fungera optimalt.
Det kan dock bli en nästan omöjlig uppgift för både databasutvecklare och DBA:er. Det kan till exempel vara exceptionellt svårt att lägga till ny funktionalitet till befintliga Legacy-system. Ett särskilt komplicerat fall är när du behöver extrahera och transformera data från ett äldre system så att du kan jämföra det med data som produceras av det nya systemet eller funktionen. Du måste uppnå det utan att påverka den äldre applikationens funktionalitet.
Sådana frågor kan involvera komplexa kopplingar, som följande:
- En kombination av delsträng och/eller sammanlänkning av flera datakolumner;
- Inbyggda skalära funktioner;
- Anpassade UDF:er;
- Alla kombinationer av WHERE-satsjämförelser och sökvillkor.
Frågor, som beskrivits tidigare, har vanligtvis komplexa åtkomstvägar. Vad värre är, de kan ha många tabellskanningar och/eller fullständiga indexsökningar med sådana kombinationer av JOINs eller sökningar som förekommer.
Datatransformation och manipulationer i frågor
Vi måste påpeka att all data som lagras permanent i en databastabell behöver transformeras och/eller manipuleras någon gång när vi frågar efter data från tabellen. Transformationen kan sträcka sig från en enkel transformation till en mycket komplex. Beroende på hur komplex den kan vara, kan omvandlingen konsumera mycket CPU och resurser.
I de flesta fall sker transformationer som görs i JOINs efter att data har lästs och överförts till tempdb databas (SQL-server) eller arbetsfil databas / temp-tablespaces som i andra databassystem.
Eftersom data i arbetsfilen inte är indexerbar , den tid som krävs för att utföra kombinerade transformationer och JOINs ökar exponentiellt. Data som hämtas blir större. Sålunda utvecklas resulterande frågor till en prestandaflaskhals genom ytterligare datatillväxt.
Så, hur kan en databasutvecklare eller en DBA lösa dessa prestandaflaskhalsar snabbt och även ge sig själva mer tid att omkonstruera och skriva om frågorna för optimal prestanda?
Det finns två sätt att effektivt lösa sådana ihållande problem. En av dem är att använda virtuella kolumner och/eller funktionsindex.
Funktionella index och frågor
Normalt skapar du index på kolumner som antingen indikerar en unik uppsättning kolumner/värden i en rad (unika index eller primärnycklar) eller representerar en uppsättning kolumner/värden som är eller kan användas i WHERE-klausulsökningsvillkor för en fråga.
Om du inte har sådana index på plats och du har utvecklat komplexa frågor som beskrivits tidigare kommer du att märka följande:
- Reducerade prestandanivåer när du använder explain fråga och se tabellgenomsökningar eller fullständiga indexgenomsökningar
- Mycket hög CPU- och resursanvändning orsakad av frågorna;
- Långa körtider.
Samtida databaser hanterar normalt dessa problem genom att låta dig skapa en funktionell eller funktionsbaserad index, som heter i SQLServer, Oracle och MySQL (v 8.x). Eller så kan det vara Indexera på uttrycks-/uttrycksbaserad index, som i andra databaser (PostgreSQL och Db2).
Anta att du har kolumnen Köpdatum av datatypen TIMESTAMP eller DATETIME i din order tabell, och den kolumnen har indexerats. Vi börjar fråga efter ordern tabell med en WHERE-sats:
SELECT ...
FROM Order
WHERE DATE(Purchase_Date) = '03.12.2020'
Denna transaktion kommer att orsaka skanning av hela indexet. Men om kolumnen inte har indexerats får du en tabellskanning.
Efter att ha skannat hela indexet flyttas det indexet till tempdb / workfile (hela tabellen om du får en tabellskanning ) innan du matchar värdet 03.12.2020 .
Eftersom en stor ordertabell använder mycket CPU och resurser bör du skapa ett funktionsindex med uttrycket DATE (Purchase_Date ) som en av indexkolumnerna och visas nedan:
CREATE ix_DatePurchased on sales.Order(Date(Purchase_Date) desc, ... )
När du gör det gör du det matchande predikatet DATE (Purchase_Date) ='03.12.2020' indexerbar. Istället för att flytta indexet eller tabellen till tempdb / arbetsfilen före matchningen av värdet, gör vi indexet endast delvis åtkomligt och/eller skannat. Det resulterar i lägre CPU- och resursanvändning.
Ta en titt på ett annat exempel. Det finns en Kund tabell med kolumnerna förnamn, efternamn . Dessa kolumner indexeras som sådana:
CREATE INDEX ix_custname on Customer(first_name asc, last_name asc),
Dessutom har du en vy som sammanfogar dessa kolumner till kundnamn kolumn:
CREATE view v_CustomerInfo( customer_name, .... ) as
select first_name ||' '|| last_name as customer_name,.....
from Customer
where ...
Du har en fråga från ett e-handelssystem som söker efter kundens fullständiga namn:
select c.*
from v_CustomerInfo c
where c.customer_name = 'John Smith'
....
Återigen kommer denna fråga att producera en fullständig indexskanning. I värsta fall kommer det att vara en fullständig tabellskanning som flyttar all data från indexet eller tabellen till arbetsfilen innan sammanfogningen av förnamn och efternamn kolumner och matchar "John Smith"-värdet.
Ett annat fall är att skapa ett funktionsindex som visas nedan:
CREATE ix_fullcustname on sales.Customer( first_name ||' '|| last_name desc, ... )
På så sätt kan du göra sammanlänkningen i vyfrågan till ett indexerbart predikat. Istället för en fullständig indexskanning eller tabellskanning har du en partiell indexskanning. En sådan frågekörning resulterar i lägre CPU- och resursanvändning, exkluderar arbetet i arbetsfilen och säkerställer därmed snabbare exekveringstid.
Virtuella (genererade) kolumner och frågor
Genererade kolumner (virtuella kolumner eller beräknade kolumner) är kolumner som innehåller data som genereras i farten. Data kan inte uttryckligen ställas in på ett specifikt värde. Det hänvisar till data i andra kolumner som efterfrågats, infogats eller uppdaterats i en DML-fråga.
Värdegenereringen av sådana kolumner automatiseras baserat på ett uttryck. Dessa uttryck kan generera:
- En sekvens av heltalsvärden;
- Värdet baserat på värdena för andra kolumner i tabellen;
- Det kan generera värden genom att anropa inbyggda funktioner eller användardefinierade funktioner (UDF).
Det är lika viktigt att notera att i vissa databaser (SQLServer, Oracle, PostgreSQL, MySQL och MariaDB) kan dessa kolumner konfigureras för att antingen permanent lagra data med INSERT- och UPDATE-satserna, eller exekvera det underliggande kolumnuttrycket i farten om vi frågar tabellen och kolumnen sparar lagringsutrymmet.
Men när uttrycket är komplicerat, som med komplex logik i UDF-funktionen, kanske besparingarna av exekveringstid, resurser och CPU-frågekostnader inte blir så mycket som förväntat.
Således kan vi konfigurera kolumnen så att den permanent lagrar resultatet av uttrycket i en INSERT- eller UPDATE-sats. Sedan skapar vi ett vanligt index på den kolumnen. På så sätt sparar vi CPU, resursanvändning och exekveringstiden för frågan. Återigen kan det vara en viss ökning av INSERT- och UPDATE-prestandan, beroende på uttryckets komplexitet.
Låt oss titta på ett exempel. Vi deklarerar tabellen och skapar ett index enligt följande:
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å så sätt flyttar vi sammankopplingslogiken från vyn i föregående exempel ner i tabellen och lagrar data konsekvent. Vi hämtar data med hjälp av en matchande skanning på ett vanligt index. Det är det bästa möjliga resultatet här.
Genom att lägga till en genererad kolumn i en tabell och skapa ett vanligt index på den kolumnen kan vi flytta transformationslogiken ner till tabellnivå. Här lagrar vi ständigt de transformerade data i infognings- eller uppdateringssatser som annars skulle omvandlas i frågor. JOIN- och INDEX-skanningarna blir mycket enklare och snabbare.
Funktionella index, genererade kolumner och JSON
Globala webb- och mobilapplikationer använder lätta datastrukturer som JSON för att flytta data från webben/mobilenheten till databasen och vice versa. JSON-datastrukturernas lilla fotavtryck gör dataöverföringen över nätverket snabb och enkel. Det är lätt att komprimera JSON till en mycket liten storlek jämfört med andra strukturer, det vill säga XML. Det kan överträffa strukturer i runtime parsing.
På grund av den ökade användningen av JSON-datastrukturer har relationsdatabaser JSON-lagringsformatet som antingen BLOB-datatyp eller CLOB-datatyp. Båda dessa typer gör data i sådana kolumner oindexerbara som de är.
Av denna anledning introducerade databasleverantörerna JSON-funktioner för att fråga och modifiera JSON-objekt, eftersom du enkelt kan integrera dessa funktioner i SQL-frågan eller andra DML-kommandon. Dessa frågor beror dock på JSON-objekts komplexitet. De är mycket CPU- och resurskrävande, eftersom BLOB- och CLOB-objekt måste laddas ner till minnet, eller ännu värre, till arbetsfilen före fråga och/eller manipulering.
Antag att vi har en kund tabell med Kundinformation data lagras som ett JSON-objekt i en kolumn som heter CustomerDetail . Vi ställer in frågan i tabellen enligt nedan:
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 det här exemplet frågar vi efter data för kunder som bor i vissa delar av huvudstadsregionen på Island. Alla aktiva data bör hämtas till arbetsfilen innan du använder sökpredikatet. Ändå kommer hämtning att resultera i för stor CPU- och resursanvändning.
Följaktligen finns det en effektiv procedur för att få JSON-frågor att köras snabbare. Det innebär att man använder funktionaliteten genom genererade kolumner, som tidigare beskrivits.
Vi uppnår prestandaökningen genom att lägga till genererade kolumner. En genererad kolumn skulle söka igenom JSON-dokumentet efter specifik data som representeras i kolumnen med hjälp av JSON-funktionerna och lagra värdet i kolumnen.
Vi kan indexera och fråga dessa genererade kolumner med hjälp av vanliga SQL where-klausulsökvillkor. Därför går det mycket snabbt att söka efter viss data i JSON-objekt.
Vi lägger till två genererade kolumner – Land och Postkod :
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);
Dessutom skapar vi ett sammansatt index på de specifika kolumnerna. Nu kan vi ändra frågan till exemplet som visas nedan:
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')
Detta begränsar datahämtning till aktiva kunder endast i någon del av Islands huvudstadsregion. Det här sättet är snabbare och mer effektivt än den föregående frågan.
Slutsats
Sammantaget, genom att använda virtuella kolumner eller funktionsindex på tabeller som orsakar svårigheter (CPU och resurstunga frågor), kan vi eliminera problem ganska snabbt.
Virtuella kolumner och funktionella index kan hjälpa till med att söka efter komplexa JSON-objekt lagrade i vanliga relationstabeller. Vi måste dock bedöma problemen noggrant i förväg och göra nödvändiga ändringar i enlighet med detta.
I vissa fall, om fråge- och/eller JSON-datastrukturerna är mycket komplexa, kan en del av CPU- och resursanvändningen flyttas från frågorna till INSERT/UPPDATERA-processerna. Det ger oss färre totala CPU- och resursbesparingar än förväntat. Om du upplever liknande problem kan det vara oundvikligt med en mer grundlig omformning av tabeller och frågor.