Troppo spesso, vediamo query SQL complesse scritte male in esecuzione sulle tabelle del database. Tali query possono richiedere un tempo molto breve o molto lungo per l'esecuzione, ma consumano un'enorme quantità di CPU e altre risorse. Tuttavia, in molti casi, query complesse forniscono informazioni preziose all'applicazione/persona. Pertanto, offre risorse utili in tutte le varietà di applicazioni.
Complessità delle query
Diamo un'occhiata più da vicino alle domande problematiche. Molti di loro sono complessi. Ciò può essere dovuto a diversi motivi:
- Il tipo di dati scelto per i dati;
- L'organizzazione e l'archiviazione dei dati nel database;
- Trasformazione e unione dei dati in una query per recuperare il set di risultati desiderato.
Devi considerare correttamente questi tre fattori chiave e implementarli correttamente per fare in modo che le query funzionino in modo ottimale.
Tuttavia, potrebbe diventare un compito quasi impossibile sia per gli sviluppatori di database che per i DBA. Ad esempio, può essere eccezionalmente difficile aggiungere nuove funzionalità ai sistemi legacy esistenti. Un caso particolarmente complicato è quando è necessario estrarre e trasformare i dati da un sistema legacy in modo da poterli confrontare con i dati prodotti dal nuovo sistema o funzionalità. Devi raggiungerlo senza influire sulla funzionalità dell'applicazione legacy.
Tali query possono comportare join complessi, come i seguenti:
- Una combinazione di sottostringa e/o concatenazione di più colonne di dati;
- Funzioni scalari integrate;
- UDF personalizzate;
- Qualsiasi combinazione di confronti tra clausole WHERE e condizioni di ricerca.
Le query, come descritto in precedenza, di solito hanno percorsi di accesso complessi. Quel che è peggio, potrebbero avere molte scansioni di tabelle e/o scansioni complete dell'indice con tali combinazioni di JOIN o ricerche in corso.
Trasformazione e manipolazione dei dati nelle query
Dobbiamo sottolineare che tutti i dati archiviati in modo persistente in una tabella di database necessitano di trasformazione e/o manipolazione a un certo punto quando interroghiamo quei dati dalla tabella. La trasformazione può variare da una semplice trasformazione a una molto complessa. A seconda di quanto possa essere complessa, la trasformazione può consumare molta CPU e risorse.
Nella maggior parte dei casi, le trasformazioni eseguite nelle JOIN avvengono dopo che i dati sono stati letti e scaricati nel tempdb database (SQL Server) o file di lavoro database / spazi temporanei delle tabelle come in altri sistemi di database.
Poiché i dati nel file di lavoro non sono indicizzabili , il tempo necessario per eseguire trasformazioni combinate e JOIN aumenta in modo esponenziale. I dati recuperati diventano più grandi. Pertanto, le query risultanti si trasformano in un collo di bottiglia delle prestazioni a causa della crescita aggiuntiva dei dati.
Quindi, in che modo uno sviluppatore di database o un DBA può risolvere rapidamente questi colli di bottiglia delle prestazioni e concedersi più tempo per riprogettare e riscrivere le query per prestazioni ottimali?
Ci sono due modi per risolvere efficacemente tali problemi persistenti. Uno di questi è l'utilizzo di colonne virtuali e/o indici funzionali.
Indici e query funzionali
Normalmente, si creano indici su colonne che indicano un insieme univoco di colonne/valori in una riga (indici univoci o chiavi primarie) o rappresentano un insieme di colonne/valori che sono o possono essere utilizzati nelle condizioni di ricerca della clausola WHERE di una query.
Se non disponi di tali indici e hai sviluppato query complesse come descritto in precedenza, noterai quanto segue:
- Riduzione dei livelli di prestazioni durante l'utilizzo di spiega interrogare e visualizzare le scansioni delle tabelle o le scansioni complete dell'indice
- Utilizzo molto elevato della CPU e delle risorse causato dalle query;
- Lunghi tempi di esecuzione.
I database contemporanei normalmente affrontano questi problemi consentendo di creare un funzionale o basato su funzioni index, come denominato in SQLServer, Oracle e MySQL (v 8.x). Oppure può essere Indice su basato su espressioni/espressioni indici, come in altri database (PostgreSQL e Db2).
Supponi di avere una colonna Data_acquisto del tipo di dati TIMESTAMP o DATETIME nel tuo Ordine tabella e quella colonna è stata indicizzata. Iniziamo a interrogare l'Ordine tabella con una clausola WHERE:
SELECT ...
FROM Order
WHERE DATE(Purchase_Date) = '03.12.2020'
Questa transazione provocherà la scansione dell'intero indice. Tuttavia, se la colonna non è stata indicizzata, ottieni una scansione della tabella.
Dopo aver scansionato l'intero indice, quell'indice si sposta in tempdb/file di lavoro (intero tavolo se ottieni una scansione tabella ) prima di abbinare il valore 03.12.2020 .
Poiché una tabella Order di grandi dimensioni utilizza molta CPU e risorse, dovresti creare un indice funzionale con l'espressione DATE (Purchase_Date ) come una delle colonne dell'indice e mostrata di seguito:
CREATE ix_DatePurchased on sales.Order(Date(Purchase_Date) desc, ... )
In tal modo, crei il predicato corrispondente DATE (Purchase_Date) ='03.12.2020' indicizzabile. Pertanto, invece di spostare l'indice o la tabella nel tempdb/file di lavoro prima della corrispondenza del valore, rendiamo l'indice accessibile e/o scansionato solo parzialmente. Ne risulta un minor utilizzo di CPU e risorse.
Dai un'occhiata a un altro esempio. C'è un Cliente tabella con le colonne nome, cognome . Tali colonne sono indicizzate come tali:
CREATE INDEX ix_custname on Customer(first_name asc, last_name asc),
Inoltre, hai una vista che concatena queste colonne nel nome_cliente colonna:
CREATE view v_CustomerInfo( customer_name, .... ) as
select first_name ||' '|| last_name as customer_name,.....
from Customer
where ...
Hai una query da un sistema di eCommerce che cerca il nome completo del cliente:
select c.*
from v_CustomerInfo c
where c.customer_name = 'John Smith'
....
Anche in questo caso, questa query produrrà una scansione completa dell'indice. Nel peggiore dei casi, sarà una scansione completa della tabella che sposta tutti i dati dall'indice o dalla tabella al file di lavoro prima della concatenazione del first_name e cognome colonne e corrispondenti al valore "John Smith".
Un altro caso è la creazione di un indice funzionale come mostrato di seguito:
CREATE ix_fullcustname on sales.Customer( first_name ||' '|| last_name desc, ... )
In questo modo, puoi trasformare la concatenazione nella query di visualizzazione in un predicato indicizzabile. Invece di una scansione completa dell'indice o della tabella, hai una scansione parziale dell'indice. Tale esecuzione di query comporta un minore utilizzo della CPU e delle risorse, escludendo il lavoro nel file di lavoro e garantendo così tempi di esecuzione più rapidi.
Colonne e query virtuali (generate)
Le colonne generate (colonne virtuali o colonne calcolate) sono colonne che contengono i dati generati al volo. I dati non possono essere impostati in modo esplicito su un valore specifico. Si riferisce ai dati in altre colonne sottoposte a query, inserite o aggiornate in una query DML.
La generazione dei valori di tali colonne è automatizzata in base a un'espressione. Queste espressioni potrebbero generare:
- Una sequenza di valori interi;
- Il valore basato sui valori di altre colonne nella tabella;
- Potrebbe generare valori chiamando funzioni integrate o funzioni definite dall'utente (UDF).
È altrettanto importante notare che in alcuni database (SQLServer, Oracle, PostgreSQL, MySQL e MariaDB) queste colonne possono essere configurate per archiviare in modo persistente i dati con l'esecuzione delle istruzioni INSERT e UPDATE o per eseguire al volo l'espressione della colonna sottostante se interroghiamo la tabella e la colonna risparmiando spazio di archiviazione.
Tuttavia, quando l'espressione è complicata, come con la logica complessa nella funzione UDF, il risparmio di tempo di esecuzione, risorse e costi di query della CPU potrebbe non essere tanto quanto previsto.
Pertanto, possiamo configurare la colonna in modo che memorizzi in modo persistente il risultato dell'espressione in un'istruzione INSERT o UPDATE. Quindi, creiamo un indice regolare su quella colonna. In questo modo, risparmieremo la CPU, l'utilizzo delle risorse e il tempo di esecuzione della query. Anche in questo caso, potrebbe esserci un leggero aumento delle prestazioni di INSERT e UPDATE, a seconda della complessità dell'espressione.
Diamo un'occhiata a un esempio. Dichiariamo la tabella e creiamo un indice come segue:
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, ... )
In questo modo, spostiamo la logica di concatenazione dalla vista nell'esempio precedente verso il basso nella tabella e memorizziamo i dati in modo persistente. Recuperiamo i dati utilizzando una scansione di corrispondenza su un indice normale. È il miglior risultato possibile qui.
Aggiungendo una colonna generata a una tabella e creando un indice regolare su quella colonna, possiamo spostare la logica di trasformazione al livello della tabella. Qui, memorizziamo in modo persistente i dati trasformati in istruzioni di inserimento o aggiornamento che altrimenti verrebbero trasformate in query. Le scansioni JOIN e INDEX saranno molto più semplici e veloci.
Indici funzionali, colonne generate e JSON
Le applicazioni Web e mobili globali utilizzano strutture dati leggere come JSON per spostare i dati dal dispositivo Web/mobile al database e viceversa. L'ingombro ridotto delle strutture dati JSON rende il trasferimento dei dati sulla rete semplice e veloce. È facile comprimere JSON a dimensioni molto ridotte rispetto ad altre strutture, ad esempio XML. Può superare le strutture nell'analisi di runtime.
A causa del maggiore utilizzo delle strutture di dati JSON, i database relazionali hanno il formato di archiviazione JSON come tipo di dati BLOB o tipo di dati CLOB. Entrambi questi tipi rendono i dati in tali colonne non indicizzabili così come sono.
Per questo motivo, i fornitori di database hanno introdotto funzioni JSON per eseguire query e modificare oggetti JSON, poiché è possibile integrare facilmente queste funzioni nella query SQL o altri comandi DML. Tuttavia, queste query dipendono dalla complessità degli oggetti JSON. Consumano molto CPU e risorse, poiché gli oggetti BLOB e CLOB devono essere scaricati in memoria o, peggio, nel file di lavoro prima di interrogare e/o manipolare.
Supponiamo di avere un Cliente tabella con i Dettagli cliente dati archiviati come oggetto JSON in una colonna denominata CustomerDetail . Abbiamo impostato l'interrogazione della tabella come di seguito:
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')
In questo esempio, stiamo interrogando i dati per i clienti che vivono in alcune parti della regione della capitale in Islanda. Tutti Attivi i dati devono essere recuperati nel file di lavoro prima di applicare il predicato di ricerca. Tuttavia, il recupero comporterà un utilizzo eccessivo della CPU e delle risorse.
Di conseguenza, esiste una procedura efficace per velocizzare l'esecuzione delle query JSON. Implica l'utilizzo della funzionalità tramite colonne generate, come descritto in precedenza.
Otteniamo l'aumento delle prestazioni aggiungendo colonne generate. Una colonna generata cercherà nel documento JSON dati specifici rappresentati nella colonna utilizzando le funzioni JSON e memorizzerebbe il valore nella colonna.
Possiamo indicizzare e interrogare queste colonne generate usando le normali condizioni di ricerca della clausola SQL. Quindi, la ricerca di dati particolari negli oggetti JSON diventa molto veloce.
Aggiungiamo due colonne generate:Paese e Codice Postale :
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);
Inoltre, creiamo un indice composito sulle colonne specifiche. Ora possiamo modificare la query nell'esempio visualizzato di seguito:
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')
Ciò limita il recupero dei dati ai clienti attivi solo in alcune parti della regione della capitale islandese. In questo modo è più veloce ed efficiente rispetto alla query precedente.
Conclusione
Tutto sommato, applicando colonne virtuali o indici funzionali a tabelle che causano difficoltà (CPU e query ad alto consumo di risorse), possiamo eliminare i problemi abbastanza rapidamente.
Le colonne virtuali e gli indici funzionali possono aiutare a eseguire query su oggetti JSON complessi archiviati in normali tabelle relazionali. Tuttavia, dobbiamo valutare attentamente i problemi in anticipo e apportare le modifiche necessarie di conseguenza.
In alcuni casi, se la query e/o le strutture dati JSON sono molto complesse, una parte dell'utilizzo della CPU e delle risorse potrebbe spostarsi dalle query ai processi INSERT/UPDATE. Ci offre un minor risparmio complessivo di CPU e risorse del previsto. Se riscontri problemi simili, potrebbe essere inevitabile riprogettare tabelle e query più approfondite.