In questo articolo, discuteremo degli errori tipici che gli sviluppatori principianti possono incontrare durante la progettazione del codice T-SQL. Inoltre, daremo uno sguardo alle best practice e ad alcuni suggerimenti utili che possono aiutarti quando lavori con SQL Server, oltre a soluzioni alternative per migliorare le prestazioni.
Contenuto:
1. Tipi di dati
2. *
3. Alias
4. Ordine delle colonne
5. NOT IN vs NULL
6. Formato data
7. Filtro data
8. Calcolo
9. Converti implicito
10. MI PIACE &Indice soppresso
11. Unicode vs ANSI
12. COLLEGA
13. COLLEZIONI BINARIE
14. Stile del codice
15. [var]char
16. Lunghezza dei dati
17. ISNULL vs COALESCE
18. Matematica
19. UNION vs UNION ALL
20. Rileggi
21. Sottointerrogazione
22. CASO QUANDO
23. Funzione scalare
24. VISUALIZZAZIONI
25. CURSORI
26. STRING_CONCAT
27. Iniezione SQL
Tipi di dati
Il problema principale che dobbiamo affrontare quando lavoriamo con SQL Server è una scelta errata dei tipi di dati.
Supponiamo di avere due tabelle identiche:
DECLARE @Employees1 TABLE ( EmployeeID BIGINT PRIMARY KEY , IsMale VARCHAR(3) , BirthDate VARCHAR(20) ) INSERT INTO @Employees1 VALUES (123, 'YES', '2012-09-01') DECLARE @Employees2 TABLE ( EmployeeID INT PRIMARY KEY , IsMale BIT , BirthDate DATE ) INSERT INTO @Employees2 VALUES (123, 1, '2012-09-01')
Eseguiamo una query per verificare qual è la differenza:
DECLARE @BirthDate DATE = '2012-09-01' SELECT * FROM @Employees1 WHERE BirthDate = @BirthDate SELECT * FROM @Employees2 WHERE BirthDate = @BirthDate
Nel primo caso, i tipi di dati sono più ridondanti di quanto potrebbero essere. Perché dovremmo memorizzare un valore bit come SI/NO riga? Perché dovremmo memorizzare una data come riga? Perché dovremmo usare BIGINT per i dipendenti nella tabella, anziché INT ?
Porta ai seguenti inconvenienti:
- Le tabelle possono occupare molto spazio sul disco;
- Dobbiamo leggere più pagine e inserire più dati in BufferPool per gestire i dati.
- Prestazioni scarse.
*
Ho affrontato la situazione in cui gli sviluppatori recuperano tutti i dati da una tabella e quindi sul lato client utilizzano DataReader per selezionare solo i campi obbligatori. Non consiglio di utilizzare questo approccio:
USE AdventureWorks2014 GO SET STATISTICS TIME, IO ON SELECT * FROM Person.Person SELECT BusinessEntityID , FirstName , MiddleName , LastName FROM Person.Person SET STATISTICS TIME, IO OFF
Ci sarà una differenza significativa nel tempo di esecuzione della query. Inoltre, l'indice di copertura può ridurre un numero di letture logiche.
Table 'Person'. Scan count 1, logical reads 3819, physical reads 3, ... SQL Server Execution Times: CPU time = 31 ms, elapsed time = 1235 ms. Table 'Person'. Scan count 1, logical reads 109, physical reads 1, ... SQL Server Execution Times: CPU time = 0 ms, elapsed time = 227 ms.
Alias
Creiamo una tabella:
USE AdventureWorks2014 GO IF OBJECT_ID('Sales.UserCurrency') IS NOT NULL DROP TABLE Sales.UserCurrency GO CREATE TABLE Sales.UserCurrency ( CurrencyCode NCHAR(3) PRIMARY KEY ) INSERT INTO Sales.UserCurrency VALUES ('USD')
Supponiamo di avere una query che restituisce la quantità di righe identiche in entrambe le tabelle:
SELECT COUNT_BIG(*) FROM Sales.Currency WHERE CurrencyCode IN ( SELECT CurrencyCode FROM Sales.UserCurrency )
Tutto funzionerà come previsto, finché qualcuno non rinomina una colonna in Sales.UserCurrency tabella:
EXEC sys.sp_rename 'Sales.UserCurrency.CurrencyCode', 'Code', 'COLUMN'
Successivamente, eseguiremo una query e vedremo di ottenere tutte le righe in Sales.Currency tabella, invece di 1 riga. Durante la creazione di un piano di esecuzione, nella fase di associazione, SQL Server controlla le colonne di Sales.UserCurrency, non troverà CurrencyCode lì e decide che questa colonna appartiene a Sales.Currency tavolo. Dopodiché, un ottimizzatore rilascerà CurrencyCode =CurrencyCode condizione.
Pertanto, consiglio di utilizzare gli alias:
SELECT COUNT_BIG(*) FROM Sales.Currency c WHERE c.CurrencyCode IN ( SELECT u.CurrencyCode FROM Sales.UserCurrency u )
Ordine delle colonne
Supponiamo di avere una tabella:
IF OBJECT_ID('dbo.DatePeriod') IS NOT NULL DROP TABLE dbo.DatePeriod GO CREATE TABLE dbo.DatePeriod ( StartDate DATE , EndDate DATE )
Inseriamo sempre i dati lì in base alle informazioni sull'ordine delle colonne.
INSERT INTO dbo.DatePeriod SELECT '2015-01-01', '2015-01-31'
Supponiamo che qualcuno modifichi l'ordine delle colonne:
CREATE TABLE dbo.DatePeriod ( EndDate DATE , StartDate DATE )
I dati verranno inseriti in un ordine diverso. In questo caso, è una buona idea specificare esplicitamente le colonne nell'istruzione INSERT:
INSERT INTO dbo.DatePeriod (StartDate, EndDate) SELECT '2015-01-01', '2015-01-31'
Ecco un altro esempio:
SELECT TOP(1) * FROM dbo.DatePeriod ORDER BY 2 DESC
Su quale colonna ordineremo i dati? Dipenderà dall'ordine delle colonne in una tabella. Nel caso in cui si modifichi l'ordine otteniamo risultati errati.
NON IN vs NULL
Parliamo del NON IN dichiarazione.
Ad esempio, devi scrivere un paio di query:restituire i record della prima tabella, che non esistono nella seconda tabella e visa verse. Di solito, gli sviluppatori junior usano IN e NON IN :
DECLARE @t1 TABLE (t1 INT, UNIQUE CLUSTERED(t1)) INSERT INTO @t1 VALUES (1), (2) DECLARE @t2 TABLE (t2 INT, UNIQUE CLUSTERED(t2)) INSERT INTO @t2 VALUES (1) SELECT * FROM @t1 WHERE t1 NOT IN (SELECT t2 FROM @t2) SELECT * FROM @t1 WHERE t1 IN (SELECT t2 FROM @t2)
La prima query ha restituito 2, la seconda – 1. Inoltre, aggiungeremo un altro valore nella seconda tabella – NULL :
INSERT INTO @t2 VALUES (1), (NULL)
Quando si esegue la query con NON IN , non otterremo alcun risultato. Perché IN funziona e NON in no? Il motivo è che SQL Server utilizza TRUE , FALSO e SCONOSCIUTO logica quando si confrontano i dati.
Quando si esegue una query, SQL Server interpreta la condizione IN nel modo seguente:
a IN (1, NULL) == a=1 OR a=NULL
NON IN :
a NOT IN (1, NULL) == a<>1 AND a<>NULL
Quando si confronta un valore con NULL, SQL Server restituisce SCONOSCIUTO. O 1=NULL o NULL=NULL – entrambi risultano in SCONOSCIUTO. Per quanto abbiamo AND nell'espressione, entrambe le parti restituiscono SCONOSCIUTO.
Vorrei sottolineare che questo caso non è raro. Ad esempio, contrassegni una colonna come NON NULL. Dopo un po', un altro sviluppatore decide di consentire NULL per quella colonna. Ciò può portare alla situazione in cui un rapporto client smette di funzionare una volta inserito qualsiasi valore NULL nella tabella.
In questo caso, consiglierei di escludere i valori NULL:
SELECT * FROM @t1 WHERE t1 NOT IN ( SELECT t2 FROM @t2 WHERE t2 IS NOT NULL )
Inoltre, è possibile utilizzare EXCEPT :
SELECT * FROM @t1 EXCEPT SELECT * FROM @t2
In alternativa, puoi utilizzare NON ESISTE :
SELECT * FROM @t1 WHERE NOT EXISTS( SELECT 1 FROM @t2 WHERE t1 = t2 )
Quale opzione è più preferibile? Quest'ultima opzione con NON ESISTE sembra essere il più produttivo in quanto genera il predicato pushdown più ottimale operatore per accedere ai dati della seconda tabella.
In realtà, i valori NULL possono restituire un risultato imprevisto.
Consideralo su questo esempio particolare:
USE AdventureWorks2014 GO SELECT COUNT_BIG(*) FROM Production.Product SELECT COUNT_BIG(*) FROM Production.Product WHERE Color = 'Grey' SELECT COUNT_BIG(*) FROM Production.Product WHERE Color <> 'Grey'
Come puoi vedere, non hai ottenuto il risultato atteso perché i valori NULL hanno operatori di confronto separati:
SELECT COUNT_BIG(*) FROM Production.Product WHERE Color IS NULL SELECT COUNT_BIG(*) FROM Production.Product WHERE Color IS NOT NULL
Ecco un altro esempio con CHECK vincoli:
IF OBJECT_ID('tempdb.dbo.#temp') IS NOT NULL DROP TABLE #temp GO CREATE TABLE #temp ( Color VARCHAR(15) --NULL , CONSTRAINT CK CHECK (Color IN ('Black', 'White')) )
Creiamo una tabella con il permesso di inserire solo i colori bianco e nero:
INSERT INTO #temp VALUES ('Black') (1 row(s) affected)
Tutto funziona come previsto.
INSERT INTO #temp VALUES ('Red') The INSERT statement conflicted with the CHECK constraint... The statement has been terminated.
Ora aggiungiamo NULL:
INSERT INTO #temp VALUES (NULL) (1 row(s) affected)
Perché il vincolo CHECK ha superato il valore NULL? Bene, il motivo è che c'è abbastanza il NON FALSO condizione per fare un record. La soluzione alternativa consiste nel definire esplicitamente una colonna come NON NULL oppure usa NULL nel vincolo.
Formato data
Molto spesso potresti avere difficoltà con i tipi di dati.
Ad esempio, è necessario ottenere la data corrente. Per fare ciò, puoi utilizzare la funzione GETDATE:
SELECT GETDATE()
Quindi copia semplicemente il risultato restituito in una query richiesta ed elimina l'ora:
SELECT * FROM sys.objects WHERE create_date < '2016-11-14'
È corretto?
La data è specificata da una costante stringa:
SET LANGUAGE English SET DATEFORMAT DMY DECLARE @d1 DATETIME = '05/12/2016' , @d2 DATETIME = '2016/12/05' , @d3 DATETIME = '2016-12-05' , @d4 DATETIME = '05-dec-2016' SELECT @d1, @d2, @d3, @d4
Tutti i valori hanno un'interpretazione a un valore:
----------- ----------- ----------- ----------- 2016-12-05 2016-05-12 2016-05-12 2016-12-05
Non causerà alcun problema fino a quando la query con questa logica aziendale non verrà eseguita su un altro server in cui le impostazioni potrebbero differire:
SET DATEFORMAT MDY DECLARE @d1 DATETIME = '05/12/2016' , @d2 DATETIME = '2016/12/05' , @d3 DATETIME = '2016-12-05' , @d4 DATETIME = '05-dec-2016' SELECT @d1, @d2, @d3, @d4
Tuttavia, queste opzioni possono portare a un'interpretazione errata della data:
----------- ----------- ----------- ----------- 2016-05-12 2016-12-05 2016-12-05 2016-12-05
Inoltre, questo codice può portare sia a un bug visibile che latente.
Considera il seguente esempio. Abbiamo bisogno di inserire i dati in una tabella di test. Su un server di prova tutto funziona perfettamente:
DECLARE @t TABLE (a DATETIME) INSERT INTO @t VALUES ('05/13/2016')
Tuttavia, sul lato client questa query avrà problemi poiché le impostazioni del nostro server differiscono:
DECLARE @t TABLE (a DATETIME) SET DATEFORMAT DMY INSERT INTO @t VALUES ('05/13/2016')
Msg 242, Level 16, State 3, Line 28 The conversion of a varchar data type to a datetime data type resulted in an out-of-range value.
Quindi, quale formato dovremmo usare per dichiarare le costanti di data? Per rispondere a questa domanda, esegui questa query:
SET DATEFORMAT YMD SET LANGUAGE English DECLARE @d1 DATETIME = '2016/01/12' , @d2 DATETIME = '2016-01-12' , @d3 DATETIME = '12-jan-2016' , @d4 DATETIME = '20160112' SELECT @d1, @d2, @d3, @d4 GO SET LANGUAGE Deutsch DECLARE @d1 DATETIME = '2016/01/12' , @d2 DATETIME = '2016-01-12' , @d3 DATETIME = '12-jan-2016' , @d4 DATETIME = '20160112' SELECT @d1, @d2, @d3, @d4
L'interpretazione delle costanti può variare a seconda della lingua installata:
----------- ----------- ----------- ----------- 2016-01-12 2016-01-12 2016-01-12 2016-01-12 ----------- ----------- ----------- ----------- 2016-12-01 2016-12-01 2016-01-12 2016-01-12
Pertanto, è meglio utilizzare le ultime due opzioni. Inoltre, vorrei aggiungere che specificare esplicitamente la data non è una buona idea:
SET LANGUAGE French DECLARE @d DATETIME = '12-jan-2016' Msg 241, Level 16, State 1, Line 29 Échec de la conversion de la date et/ou de l'heure à partir d'une chaîne de caractères.
Pertanto, se vuoi che le costanti con le date vengano interpretate correttamente, devi specificarle nel seguente formato AAAAMMGG.
Inoltre, vorrei attirare la vostra attenzione sul comportamento di alcuni tipi di dati:
SET LANGUAGE English SET DATEFORMAT YMD DECLARE @d1 DATE = '2016-01-12' , @d2 DATETIME = '2016-01-12' SELECT @d1, @d2 GO SET LANGUAGE Deutsch SET DATEFORMAT DMY DECLARE @d1 DATE = '2016-01-12' , @d2 DATETIME = '2016-01-12' SELECT @d1, @d2
A differenza di DATETIME, la DATE type viene interpretato correttamente con varie impostazioni su un server:
---------- ---------- 2016-01-12 2016-01-12 ---------- ---------- 2016-01-12 2016-12-01
Filtro data
Per andare avanti, considereremo come filtrare i dati in modo efficace. Cominciamo da loro DATETIME/DATE:
USE AdventureWorks2014 GO UPDATE TOP(1) dbo.DatabaseLog SET PostTime = '20140716 12:12:12'
Ora proveremo a scoprire quante righe restituisce la query per un giorno specificato:
SELECT COUNT_BIG(*) FROM dbo.DatabaseLog WHERE PostTime = '20140716'
La query restituirà 0. Durante la creazione di un piano di esecuzione, SQL Server tenta di eseguire il cast di una stringa costante sul tipo di dati della colonna che dobbiamo filtrare:
Crea un indice:
CREATE NONCLUSTERED INDEX IX_PostTime ON dbo.DatabaseLog (PostTime)
Esistono opzioni corrette e non corrette per l'output dei dati. Ad esempio, devi eliminare la colonna dell'ora:
SELECT COUNT_BIG(*) FROM dbo.DatabaseLog WHERE CONVERT(CHAR(8), PostTime, 112) = '20140716' SELECT COUNT_BIG(*) FROM dbo.DatabaseLog WHERE CAST(PostTime AS DATE) = '20140716'
Oppure dobbiamo specificare un intervallo:
SELECT COUNT_BIG(*) FROM dbo.DatabaseLog WHERE PostTime BETWEEN '20140716' AND '20140716 23:59:59.997' SELECT COUNT_BIG(*) FROM dbo.DatabaseLog WHERE PostTime >= '20140716' AND PostTime < '20140717'
Tenendo conto dell'ottimizzazione, posso dire che queste due query sono le più corrette. Il punto è che tutte le conversioni e i calcoli delle colonne dell'indice che vengono filtrate possono ridurre drasticamente le prestazioni e aumentare il tempo delle letture logiche:
Table 'DatabaseLog'. Scan count 1, logical reads 7, ... Table 'DatabaseLog'. Scan count 1, logical reads 2, ...
Il PostTime campo non era stato incluso nell'indice prima e non abbiamo potuto vedere alcuna efficienza nell'utilizzo di questo approccio corretto nel filtraggio. Un'altra cosa è quando dobbiamo produrre dati per un mese:
SELECT COUNT_BIG(*) FROM dbo.DatabaseLog WHERE CONVERT(CHAR(8), PostTime, 112) LIKE '201407%' SELECT COUNT_BIG(*) FROM dbo.DatabaseLog WHERE DATEPART(YEAR, PostTime) = 2014 AND DATEPART(MONTH, PostTime) = 7 SELECT COUNT_BIG(*) FROM dbo.DatabaseLog WHERE YEAR(PostTime) = 2014 AND MONTH(PostTime) = 7 SELECT COUNT_BIG(*) FROM dbo.DatabaseLog WHERE EOMONTH(PostTime) = '20140731' SELECT COUNT_BIG(*) FROM dbo.DatabaseLog WHERE PostTime >= '20140701' AND PostTime < '20140801'
Ancora una volta, quest'ultima opzione è più preferibile:
Inoltre, puoi sempre creare un indice basato su un campo calcolato:
IF COL_LENGTH('dbo.DatabaseLog', 'MonthLastDay') IS NOT NULL ALTER TABLE dbo.DatabaseLog DROP COLUMN MonthLastDay GO ALTER TABLE dbo.DatabaseLog ADD MonthLastDay AS EOMONTH(PostTime) --PERSISTED GO CREATE INDEX IX_MonthLastDay ON dbo.DatabaseLog (MonthLastDay)
Rispetto alla query precedente, la differenza nelle letture logiche può essere significativa (se sono in questione tabelle di grandi dimensioni):
SET STATISTICS IO ON SELECT COUNT_BIG(*) FROM dbo.DatabaseLog WHERE PostTime >= '20140701' AND PostTime < '20140801' SELECT COUNT_BIG(*) FROM dbo.DatabaseLog WHERE MonthLastDay = '20140731' SET STATISTICS IO OFF Table 'DatabaseLog'. Scan count 1, logical reads 7, ... Table 'DatabaseLog'. Scan count 1, logical reads 3, ...
Calcolo
Come è già stato discusso, qualsiasi calcolo sulle colonne dell'indice diminuisce le prestazioni e aumenta il tempo di lettura logica:
USE AdventureWorks2014 GO SET STATISTICS IO ON SELECT BusinessEntityID FROM Person.Person WHERE BusinessEntityID * 2 = 10000 SELECT BusinessEntityID FROM Person.Person WHERE BusinessEntityID = 2500 * 2 SELECT BusinessEntityID FROM Person.Person WHERE BusinessEntityID = 5000 Table 'Person'. Scan count 1, logical reads 67, ... Table 'Person'. Scan count 0, logical reads 3, ...
Se esaminiamo i piani di esecuzione, nel primo SQL Server esegue IndexScan :
Quindi, quando non ci sono calcoli sulle colonne dell'indice, vedremo IndexSeek :
Convertire implicito
Diamo un'occhiata a queste due query che filtrano per lo stesso valore:
USE AdventureWorks2014 GO SELECT BusinessEntityID, NationalIDNumber FROM HumanResources.Employee WHERE NationalIDNumber = 30845 SELECT BusinessEntityID, NationalIDNumber FROM HumanResources.Employee WHERE NationalIDNumber = '30845'
I piani di esecuzione forniscono le seguenti informazioni:
- Avviso e IndexScan al primo piano
- Ricerca indice – sul secondo.
Table 'Employee'. Scan count 1, logical reads 4, ... Table 'Employee'. Scan count 0, logical reads 2, ...
Il Numero ID Nazionale la colonna contiene NVARCHAR(15) tipo di dati. La costante che utilizziamo per filtrare i dati è impostata come INT che ci porta a una conversione implicita del tipo di dati. A sua volta, potrebbe ridurre le prestazioni. Puoi monitorarlo quando qualcuno modifica il tipo di dati nella colonna, tuttavia, le query non vengono modificate.
È importante comprendere che una conversione implicita del tipo di dati può causare errori in fase di esecuzione. Ad esempio, prima che il campo PostalCode fosse numerico, si è scoperto che un codice postale poteva contenere lettere. Pertanto, il tipo di dati è stato aggiornato. Tuttavia, se inseriamo un codice postale alfabetico, la vecchia query non funzionerà più:
SELECT AddressID FROM Person.[Address] WHERE PostalCode = 92700 SELECT AddressID FROM Person.[Address] WHERE PostalCode = '92700' Msg 245, Level 16, State 1, Line 16 Conversion failed when converting the nvarchar value 'K4B 1S2' to data type int.
Un altro esempio è quando devi usare EntityFramework sul progetto, che per impostazione predefinita interpreta tutti i campi riga come Unicode:
SELECT CustomerID, AccountNumber FROM Sales.Customer WHERE AccountNumber = N'AW00000009' SELECT CustomerID, AccountNumber FROM Sales.Customer WHERE AccountNumber = 'AW00000009'
Pertanto, vengono generate query errate:
Per risolvere questo problema, assicurati che i tipi di dati corrispondano.
MI PIACE e indice soppresso
Infatti, avere un indice di copertura non significa che lo utilizzerai in modo efficace.
Controlliamolo su questo particolare esempio. Supponiamo di dover restituire tutte le righe che iniziano con...
USE AdventureWorks2014 GO SET STATISTICS IO ON SELECT AddressLine1 FROM Person.[Address] WHERE SUBSTRING(AddressLine1, 1, 3) = '100' SELECT AddressLine1 FROM Person.[Address] WHERE LEFT(AddressLine1, 3) = '100' SELECT AddressLine1 FROM Person.[Address] WHERE CAST(AddressLine1 AS CHAR(3)) = '100' SELECT AddressLine1 FROM Person.[Address] WHERE AddressLine1 LIKE '100%'
Otterremo le seguenti letture logiche e piani di esecuzione:
Table 'Address'. Scan count 1, logical reads 216, ... Table 'Address'. Scan count 1, logical reads 216, ... Table 'Address'. Scan count 1, logical reads 216, ... Table 'Address'. Scan count 1, logical reads 4, ...
Pertanto, se esiste un indice, non dovrebbe contenere calcoli o conversioni di tipi, funzioni, ecc.
Ma cosa fai se devi trovare l'occorrenza di una sottostringa in una stringa?
SELECT AddressLine1 FROM Person.[Address] WHERE AddressLine1 LIKE '%100%'v
Torneremo su questa domanda più avanti.
Unicode vs ANSI
È importante ricordare che ci sono gli UNICODE e ANSI stringhe. Il tipo UNICODE include NVARCHAR/NCHAR (2 byte per un simbolo). Per memorizzare ANSI stringhe, è possibile utilizzare VARCHAR/CHAR (da 1 byte a 1 simbolo). C'è anche TESTO/NEXT , ma non consiglio di usarli in quanto potrebbero ridurre le prestazioni.
Se si specifica una costante Unicode in una query, è necessario precederla con il simbolo N. Per verificarlo, esegui la seguente query:
SELECT '文本 ANSI' , N'文本 UNICODE' ------- ------------ ?? ANSI 文本 UNICODE
Se N non precede la costante, SQL Server tenterà di trovare un simbolo adatto nella codifica ANSI. Se non riesce a trovare, mostrerà un punto interrogativo.
COLLEGA
Molto spesso, quando viene intervistato per la posizione di sviluppatore DB Medio/Senior, un intervistatore fa spesso la seguente domanda:questa query restituirà i dati?
DECLARE @a NCHAR(1) = 'Ё' , @b NCHAR(1) = 'Ф' SELECT @a, @b WHERE @a = @b
Dipende. In primo luogo, il simbolo N non precede una costante stringa, quindi verrà interpretato come ANSI. In secondo luogo, molto dipende dal valore COLLATE corrente, che è un insieme di regole, quando si seleziona e si confrontano i dati della stringa.
USE [master] GO IF DB_ID('test') IS NOT NULL BEGIN ALTER DATABASE test SET SINGLE_USER WITH ROLLBACK IMMEDIATE DROP DATABASE test END GO CREATE DATABASE test COLLATE Latin1_General_100_CI_AS GO USE test GO DECLARE @a NCHAR(1) = 'Ё' , @b NCHAR(1) = 'Ф' SELECT @a, @b WHERE @a = @b
Questa dichiarazione COLLATE restituirà punti interrogativi poiché i loro simboli sono uguali:
---- ---- ? ?
Se cambiamo l'istruzione COLLATE con un'altra istruzione:
ALTER DATABASE test COLLATE Cyrillic_General_100_CI_AS
In questo caso, la query non restituirà nulla, poiché i caratteri cirillici verranno interpretati correttamente.
Pertanto, se una costante stringa occupa UNICODE, è necessario impostare N prima di una costante stringa. Tuttavia, non consiglierei di impostarlo ovunque per i motivi di cui abbiamo discusso sopra.
Un'altra domanda da porsi nell'intervista riguarda il confronto delle righe.
Considera il seguente esempio:
DECLARE @a VARCHAR(10) = 'TEXT' , @b VARCHAR(10) = 'text' SELECT IIF(@a = @b, 'TRUE', 'FALSE')
Queste righe sono uguali? Per verificarlo, dobbiamo specificare esplicitamente COLLATE:
DECLARE @a VARCHAR(10) = 'TEXT' , @b VARCHAR(10) = 'text' SELECT IIF(@a COLLATE Latin1_General_CS_AS = @b COLLATE Latin1_General_CS_AS, 'TRUE', 'FALSE')
Poiché ci sono le COLLATE con distinzione tra maiuscole e minuscole (CS) e senza distinzione tra maiuscole e minuscole (CI) quando si confrontano e si selezionano le righe, non possiamo dire con certezza se sono uguali. Inoltre, ci sono varie COLLATE sia su un server di prova che su un lato client.
C'è un caso in cui COLLATEs di una base di destinazione e tempdb non corrispondono.
Crea un database con COLLATE:
USE [master] GO IF DB_ID('test') IS NOT NULL BEGIN ALTER DATABASE test SET SINGLE_USER WITH ROLLBACK IMMEDIATE DROP DATABASE test END GO CREATE DATABASE test COLLATE Albanian_100_CS_AS GO USE test GO CREATE TABLE t (c CHAR(1)) INSERT INTO t VALUES ('a') GO IF OBJECT_ID('tempdb.dbo.#t1') IS NOT NULL DROP TABLE #t1 IF OBJECT_ID('tempdb.dbo.#t2') IS NOT NULL DROP TABLE #t2 IF OBJECT_ID('tempdb.dbo.#t3') IS NOT NULL DROP TABLE #t3 GO CREATE TABLE #t1 (c CHAR(1)) INSERT INTO #t1 VALUES ('a') CREATE TABLE #t2 (c CHAR(1) COLLATE database_default) INSERT INTO #t2 VALUES ('a') SELECT c = CAST('a' AS CHAR(1)) INTO #t3 DECLARE @t TABLE (c VARCHAR(100)) INSERT INTO @t VALUES ('a') SELECT 'tempdb', DATABASEPROPERTYEX('tempdb', 'collation') UNION ALL SELECT 'test', DATABASEPROPERTYEX(DB_NAME(), 'collation') UNION ALL SELECT 't', SQL_VARIANT_PROPERTY(c, 'collation') FROM t UNION ALL SELECT '#t1', SQL_VARIANT_PROPERTY(c, 'collation') FROM #t1 UNION ALL SELECT '#t2', SQL_VARIANT_PROPERTY(c, 'collation') FROM #t2 UNION ALL SELECT '#t3', SQL_VARIANT_PROPERTY(c, 'collation') FROM #t3 UNION ALL SELECT '@t', SQL_VARIANT_PROPERTY(c, 'collation') FROM @t
Quando si crea una tabella, eredita COLLATE da un database. L'unica differenza per la prima tabella temporanea, per la quale determiniamo esplicitamente una struttura senza COLLATE, è che eredita COLLATE dal tempdb banca dati.
------ -------------------------- tempdb Cyrillic_General_CI_AS test Albanian_100_CS_AS t Albanian_100_CS_AS #t1 Cyrillic_General_CI_AS #t2 Albanian_100_CS_AS #t3 Albanian_100_CS_AS @t Albanian_100_CS_AS
Descriverò il caso in cui COLLATEs non corrisponde nell'esempio particolare con #t1.
Ad esempio, i dati non vengono filtrati correttamente, poiché COLLATE potrebbe non tenere conto di un caso:
SELECT * FROM #t1 WHERE c = 'A'
In alternativa, potremmo avere un conflitto per connettere tabelle con COLLATE differenti:
SELECT * FROM #t1 JOIN t ON [#t1].c = t.c
Tutto sembra funzionare perfettamente su un server di prova, mentre su un server client viene visualizzato un errore:
Msg 468, Level 16, State 9, Line 93 Cannot resolve the collation conflict between "Albanian_100_CS_AS" and "Cyrillic_General_CI_AS" in the equal to operation.
Per aggirare il problema, dobbiamo impostare hack ovunque:
SELECT * FROM #t1 JOIN t ON [#t1].c = t.c COLLATE database_default
RACCOLTA BINARIA
Ora scopriremo come utilizzare COLLATE a tuo vantaggio.
Considera l'esempio con l'occorrenza di una sottostringa in una stringa:
SELECT AddressLine1 FROM Person.[Address] WHERE AddressLine1 LIKE '%100%'
È possibile ottimizzare questa query e ridurne il tempo di esecuzione.
All'inizio, dobbiamo generare una tabella di grandi dimensioni:
USE [master] GO IF DB_ID('test') IS NOT NULL BEGIN ALTER DATABASE test SET SINGLE_USER WITH ROLLBACK IMMEDIATE DROP DATABASE test END GO CREATE DATABASE test COLLATE Latin1_General_100_CS_AS GO ALTER DATABASE test MODIFY FILE (NAME = N'test', SIZE = 64MB) GO ALTER DATABASE test MODIFY FILE (NAME = N'test_log', SIZE = 64MB) GO USE test GO CREATE TABLE t ( ansi VARCHAR(100) NOT NULL , unicod NVARCHAR(100) NOT NULL ) GO ;WITH E1(N) AS ( SELECT * FROM ( VALUES (1),(1),(1),(1),(1), (1),(1),(1),(1),(1) ) t(N) ), E2(N) AS (SELECT 1 FROM E1 a, E1 b), E4(N) AS (SELECT 1 FROM E2 a, E2 b), E8(N) AS (SELECT 1 FROM E4 a, E4 b) INSERT INTO t SELECT v, v FROM ( SELECT TOP(50000) v = REPLACE(CAST(NEWID() AS VARCHAR(36)) + CAST(NEWID() AS VARCHAR(36)), '-', '') FROM E8 ) t
Crea colonne calcolate con COLLATE binari e indici:
ALTER TABLE t ADD ansi_bin AS UPPER(ansi) COLLATE Latin1_General_100_Bin2 ALTER TABLE t ADD unicod_bin AS UPPER(unicod) COLLATE Latin1_General_100_BIN2 CREATE NONCLUSTERED INDEX ansi ON t (ansi) CREATE NONCLUSTERED INDEX unicod ON t (unicod) CREATE NONCLUSTERED INDEX ansi_bin ON t (ansi_bin) CREATE NONCLUSTERED INDEX unicod_bin ON t (unicod_bin)
Eseguire il processo di filtraggio:
SET STATISTICS TIME, IO ON SELECT COUNT_BIG(*) FROM t WHERE ansi LIKE '%AB%' SELECT COUNT_BIG(*) FROM t WHERE unicod LIKE '%AB%' SELECT COUNT_BIG(*) FROM t WHERE ansi_bin LIKE '%AB%' --COLLATE Latin1_General_100_BIN2 SELECT COUNT_BIG(*) FROM t WHERE unicod_bin LIKE '%AB%' --COLLATE Latin1_General_100_BIN2 SET STATISTICS TIME, IO OFF
Come puoi vedere, questa query restituisce il seguente risultato:
SQL Server Execution Times: CPU time = 350 ms, elapsed time = 354 ms. SQL Server Execution Times: CPU time = 335 ms, elapsed time = 355 ms. SQL Server Execution Times: CPU time = 16 ms, elapsed time = 18 ms. SQL Server Execution Times: CPU time = 17 ms, elapsed time = 18 ms.
Il punto è che il filtro basato sul confronto binario richiede meno tempo. Pertanto, se è necessario filtrare l'occorrenza di stringhe frequentemente e rapidamente, è possibile memorizzare i dati con COLLATE che termina con BIN. Tuttavia, va notato che tutti i COLLATE binari fanno distinzione tra maiuscole e minuscole.
Stile del codice
Uno stile di codifica è strettamente individuale. Tuttavia, questo codice dovrebbe essere semplicemente mantenuto da altri sviluppatori e soddisfare determinate regole.
Crea un database separato e una tabella all'interno:
USE [master] GO IF DB_ID('test') IS NOT NULL BEGIN ALTER DATABASE test SET SINGLE_USER WITH ROLLBACK IMMEDIATE DROP DATABASE test END GO CREATE DATABASE test COLLATE Latin1_General_CI_AS GO USE test GO CREATE TABLE dbo.Employee (EmployeeID INT PRIMARY KEY)
Quindi, scrivi la query:
select employeeid from employee
Ora, cambia COLLATE in uno qualsiasi con distinzione tra maiuscole e minuscole:
ALTER DATABASE test COLLATE Latin1_General_CS_AI
Quindi, prova a eseguire nuovamente la query:
Msg 208, Level 16, State 1, Line 19 Invalid object name 'employee'.
Un ottimizzatore utilizza le regole per il COLLATE corrente nella fase di associazione quando controlla tabelle, colonne e altri oggetti e confronta ogni oggetto dell'albero della sintassi con un oggetto reale di un catalogo di sistema.
Se desideri generare query manualmente, devi utilizzare sempre il caso corretto nei nomi degli oggetti.
Per quanto riguarda le variabili, le COLLATE vengono ereditate dal database master. Pertanto, è necessario utilizzare anche il caso corretto per lavorare con loro:
SELECT DATABASEPROPERTYEX('master', 'collation') DECLARE @EmpID INT = 1 SELECT @empid
In questo caso, non riceverai un errore:
----------------------- Cyrillic_General_CI_AS ----------- 1
Tuttavia, è possibile che venga visualizzato un caso di errore su un altro server:
-------------------------- Latin1_General_CS_AS Msg 137, Level 15, State 2, Line 4 Must declare the scalar variable "@empid".
[var]char
Come sai, ci sono fissi (CHAR , NCHAR ) e variabile (VARCHAR , NVARCHAR ) tipi di dati:
DECLARE @a CHAR(20) = 'text' , @b VARCHAR(20) = 'text' SELECT LEN(@a) , LEN(@b) , DATALENGTH(@a) , DATALENGTH(@b) , '"' + @a + '"' , '"' + @b + '"' SELECT [a = b] = IIF(@a = @b, 'TRUE', 'FALSE') , [b = a] = IIF(@b = @a, 'TRUE', 'FALSE') , [a LIKE b] = IIF(@a LIKE @b, 'TRUE', 'FALSE') , [b LIKE a] = IIF(@b LIKE @a, 'TRUE', 'FALSE')
Se una riga ha una lunghezza fissa, diciamo 20 simboli, ma hai scritto solo 4 simboli, SQL Server aggiungerà 16 spazi vuoti a destra per impostazione predefinita:
--- --- ---- ---- ---------------------- ---------------------- 4 4 20 4 "text " "text"
In addition, it is important to understand that when comparing rows with =, blanks on the right are not taken into account:
a = b b = a a LIKE b b LIKE a ----- ----- -------- -------- TRUE TRUE TRUE FALSE
As for the LIKE operator, blanks will be always inserted.
SELECT 1 WHERE 'a ' LIKE 'a' SELECT 1 WHERE 'a' LIKE 'a ' -- !!! SELECT 1 WHERE 'a' LIKE 'a' SELECT 1 WHERE 'a' LIKE 'a%'
Data length
It is always necessary to specify type length.
Consider the following example:
DECLARE @a DECIMAL , @b VARCHAR(10) = '0.1' , @c SQL_VARIANT SELECT @a = @b , @c = @a SELECT @a , @c , SQL_VARIANT_PROPERTY(@c,'BaseType') , SQL_VARIANT_PROPERTY(@c,'Precision') , SQL_VARIANT_PROPERTY(@c,'Scale')
As you can see, the type length was not specified explicitly. Thus, the query returned an integer instead of a decimal value:
---- ---- ---------- ----- ----- 0 0 decimal 18 0
As for rows, if you do not specify a row length explicitly, then its length will contain only 1 symbol:
----- ------------------------------------------ ---- ---- ---- ---- 40 123456789_123456789_123456789_123456789_ 1 1 30 30
In addition, if you do not need to specify a length for CAST/CONVERT, then only 30 symbols will be used.
ISNULL vs COALESCE
There are two functions:ISNULL and COALESCE. On the one hand, everything seems to be simple. If the first operator is NULL, then it will return the second or the next operator, if we talk about COALESCE. On the other hand, there is a difference – what will these functions return?
DECLARE @a CHAR(1) = NULL SELECT ISNULL(@a, 'NULL'), COALESCE(@a, 'NULL') DECLARE @i INT = NULL SELECT ISNULL(@i, 7.1), COALESCE(@i, 7.1)
The answer is not obvious, as the ISNULL function converts to the smallest type of two operands, whereas COALESCE converts to the largest type.
---- ---- N NULL ---- ---- 7 7.1
As for performance, ISNULL will process a query faster, COALESCE is split into the CASE WHEN operator.
Math
Math seems to be a trivial thing in SQL Server.
SELECT 1 / 3 SELECT 1.0 / 3
However, it is not. Everything depends on the fact what data is used in a query. If it is an integer, then it returns the integer result.
----------- 0 ----------- 0.333333
Also, let’s consider this particular example:
SELECT COUNT(*) , COUNT(1) , COUNT(val) , COUNT(DISTINCT val) , SUM(val) , SUM(DISTINCT val) FROM ( VALUES (1), (2), (2), (NULL), (NULL) ) t (val) SELECT AVG(val) , SUM(val) / COUNT(val) , AVG(val * 1.) , AVG(CAST(val AS FLOAT)) FROM ( VALUES (1), (2), (2), (NULL), (NULL) ) t (val)
This query COUNT(*)/COUNT(1) will return the total amount of rows. COUNT on the column will return the amount of non-NULL rows. If we add DISTINCT, then it will return the amount of non-NULL unique values.
The AVG operation is divided into SUM and COUNT. Thus, when calculating an average value, NULL is not applicable.
UNION vs UNION ALL
When the data is not overridden, then it is better to use UNION ALL to improve performance. In order to avoid replication, you may use UNION.
Still, if there is no replication, it is preferable to use UNION ALL:
SELECT [object_id] FROM sys.system_objects UNION SELECT [object_id] FROM sys.objects SELECT [object_id] FROM sys.system_objects UNION ALL SELECT [object_id] FROM sys.objects
Also, I would like to point out the difference of these operators:the UNION operator is executed in a parallel way, the UNION ALL operator – in a sequential way.
Assume, we need to retrieve 1 row on the following conditions:
DECLARE @AddressLine NVARCHAR(60) SET @AddressLine = '4775 Kentucky Dr.' SELECT TOP(1) AddressID FROM Person.[Address] WHERE AddressLine1 = @AddressLine OR AddressLine2 = @AddressLine
As we have OR in the statement, we will receive IndexScan:
Table 'Address'. Scan count 1, logical reads 90, ...
Now, we will re-write the query using UNION ALL:
SELECT TOP(1) AddressID FROM ( SELECT TOP(1) AddressID FROM Person.[Address] WHERE AddressLine1 = @AddressLine UNION ALL SELECT TOP(1) AddressID FROM Person.[Address] WHERE AddressLine2 = @AddressLine ) t
When the first subquery had been executed, it returned 1 row. Thus, we have received the required result, and SQL Server stopped looking for, using the second subquery:
Table 'Worktable'. Scan count 0, logical reads 0, ... Table 'Address'. Scan count 1, logical reads 3, ...
Re-read
Very often, I faced the situation when the data can be retrieved with one JOIN. In addition, a lot of subqueries are created in this query:
USE AdventureWorks2014 GO SET STATISTICS IO ON SELECT e.BusinessEntityID , ( SELECT p.LastName FROM Person.Person p WHERE e.BusinessEntityID = p.BusinessEntityID ) , ( SELECT p.FirstName FROM Person.Person p WHERE e.BusinessEntityID = p.BusinessEntityID ) FROM HumanResources.Employee e SELECT e.BusinessEntityID , p.LastName , p.FirstName FROM HumanResources.Employee e JOIN Person.Person p ON e.BusinessEntityID = p.BusinessEntityID
The fewer there are unnecessary table lookups, the fewer logical readings we have:
Table 'Person'. Scan count 0, logical reads 1776, ... Table 'Employee'. Scan count 1, logical reads 2, ... Table 'Person'. Scan count 0, logical reads 888, ... Table 'Employee'. Scan count 1, logical reads 2, ...
SubQuery
The previous example works only if there is a one-to-one connection between tables.
Assume tables Person.Person and Sales.SalesPersonQuotaHistory were directly connected. Thus, one employee had only one record for a share size.
USE AdventureWorks2014 GO SET STATISTICS IO ON SELECT p.BusinessEntityID , ( SELECT s.SalesQuota FROM Sales.SalesPersonQuotaHistory s WHERE s.BusinessEntityID = p.BusinessEntityID ) FROM Person.Person p
However, as settings on the client server may differ, this query may lead to the following error:
Msg 512, Level 16, State 1, Line 6 Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
It is possible to solve such issues by adding TOP(1) and ORDER BY. Using the TOP operation makes an optimizer force using IndexSeek. The same refers to using OUTER/CROSS APPLY with TOP:
SELECT p.BusinessEntityID , ( SELECT TOP(1) s.SalesQuota FROM Sales.SalesPersonQuotaHistory s WHERE s.BusinessEntityID = p.BusinessEntityID ORDER BY s.QuotaDate DESC ) FROM Person.Person p SELECT p.BusinessEntityID , t.SalesQuota FROM Person.Person p OUTER APPLY ( SELECT TOP(1) s.SalesQuota FROM Sales.SalesPersonQuotaHistory s WHERE s.BusinessEntityID = p.BusinessEntityID ORDER BY s.QuotaDate DESC ) t
When executing these queries, we will get the same issue – multiple IndexSeek operators:
Table 'SalesPersonQuotaHistory'. Scan count 19972, logical reads 39944, ... Table 'Person'. Scan count 1, logical reads 67, ...
Re-write this query with a window function:
SELECT p.BusinessEntityID , t.SalesQuota FROM Person.Person p LEFT JOIN ( SELECT s.BusinessEntityID , s.SalesQuota , RowNum = ROW_NUMBER() OVER (PARTITION BY s.BusinessEntityID ORDER BY s.QuotaDate DESC) FROM Sales.SalesPersonQuotaHistory s ) t ON p.BusinessEntityID = t.BusinessEntityID AND t.RowNum = 1
We get the following result:
Table 'Person'. Scan count 1, logical reads 67, ... Table 'SalesPersonQuotaHistory'. Scan count 1, logical reads 4, ...
CASE WHEN
Since this operator is used very often, I would like to specify its features. Regardless, how we wrote the CASE WHEN operator:
USE AdventureWorks2014 GO SELECT BusinessEntityID , Gender , Gender = CASE Gender WHEN 'M' THEN 'Male' WHEN 'F' THEN 'Female' ELSE 'Unknown' END FROM HumanResources.Employee
SQL Server will decompose the statement to the following:
SELECT BusinessEntityID , Gender , Gender = CASE WHEN Gender = 'M' THEN 'Male' WHEN Gender = 'F' THEN 'Female' ELSE 'Unknown' END FROM HumanResources.Employee
Thus, this will lead to the main issue:each condition will be executed in a sequential order until one of them returns TRUE or ELSE.
Consider this issue on a particular example. To do this, we will create a scalar-valued function which will return the right part of a postal code:
IF OBJECT_ID('dbo.GetMailUrl') IS NOT NULL DROP FUNCTION dbo.GetMailUrl GO CREATE FUNCTION dbo.GetMailUrl ( @Email NVARCHAR(50) ) RETURNS NVARCHAR(50) AS BEGIN RETURN SUBSTRING(@Email, CHARINDEX('@', @Email) + 1, LEN(@Email)) END
Then, configure SQL Profiler to build SQL events:StmtStarting / SP:StmtCompleted (if you want to do this with XEvents :sp_statement_starting / sp_statement_completed ).
Execute the query:
SELECT TOP(10) EmailAddressID , EmailAddress , CASE dbo.GetMailUrl(EmailAddress) --WHEN 'microsoft.com' THEN 'Microsoft' WHEN 'adventure-works.com' THEN 'AdventureWorks' END FROM Person.EmailAddress
The function will be executed for 10 times. Now, delete a comment from the condition:
SELECT TOP(10) EmailAddressID , EmailAddress , CASE dbo.GetMailUrl(EmailAddress) WHEN 'microsoft.com' THEN 'Microsoft' WHEN 'adventure-works.com' THEN 'AdventureWorks' END FROM Person.EmailAddress
In this case, the function will be executed for 20 times. The thing is that it is not necessary for a statement to be a must function in CASE. It may be a complicated calculation. As it is possible to decompose CASE, it may lead to multiple calculations of the same operators.
You may avoid it by using subqueries:
SELECT EmailAddressID , EmailAddress , CASE MailUrl WHEN 'microsoft.com' THEN 'Microsoft' WHEN 'adventure-works.com' THEN 'AdventureWorks' END FROM ( SELECT TOP(10) EmailAddressID , EmailAddress , MailUrl = dbo.GetMailUrl(EmailAddress) FROM Person.EmailAddress ) t
In this case, the function will be executed 10 times.
In addition, we need to avoid replication in the CASE operator:
SELECT DISTINCT CASE WHEN Gender = 'M' THEN 'Male' WHEN Gender = 'M' THEN '...' WHEN Gender = 'M' THEN '......' WHEN Gender = 'F' THEN 'Female' WHEN Gender = 'F' THEN '...' ELSE 'Unknown' END FROM HumanResources.Employee
Though statements in CASE are executed in a sequential order, in some cases, SQL Server may execute this operator with aggregate functions:
DECLARE @i INT = 1 SELECT CASE WHEN @i = 1 THEN 1 ELSE 1/0 END GO DECLARE @i INT = 1 SELECT CASE WHEN @i = 1 THEN 1 ELSE MIN(1/0) END
Scalar func
It is not recommended to use scalar functions in T-SQL queries.
Consider the following example:
USE AdventureWorks2014 GO UPDATE TOP(1) Person.[Address] SET AddressLine2 = AddressLine1 GO IF OBJECT_ID('dbo.isEqual') IS NOT NULL DROP FUNCTION dbo.isEqual GO CREATE FUNCTION dbo.isEqual ( @val1 NVARCHAR(100), @val2 NVARCHAR(100) ) RETURNS BIT AS BEGIN RETURN CASE WHEN (@val1 IS NULL AND @val2 IS NULL) OR @val1 = @val2 THEN 1 ELSE 0 END END
The queries return the identical data:
SET STATISTICS TIME ON SELECT AddressID, AddressLine1, AddressLine2 FROM Person.[Address] WHERE dbo.IsEqual(AddressLine1, AddressLine2) = 1 SELECT AddressID, AddressLine1, AddressLine2 FROM Person.[Address] WHERE (AddressLine1 IS NULL AND AddressLine2 IS NULL) OR AddressLine1 = AddressLine2 SELECT AddressID, AddressLine1, AddressLine2 FROM Person.[Address] WHERE AddressLine1 = ISNULL(AddressLine2, '') SET STATISTICS TIME OFF
However, as each call of the scalar function is a resource-intensive process, we can monitor this difference:
SQL Server Execution Times: CPU time = 63 ms, elapsed time = 57 ms. SQL Server Execution Times: CPU time = 0 ms, elapsed time = 1 ms. SQL Server Execution Times: CPU time = 0 ms, elapsed time = 1 ms.
In addition, when using a scalar function, it is not possible for SQL Server to build parallel execution plans, which may lead to poor performance in a huge volume of data.
Sometimes scalar functions may have a positive effect. For example, when we have SCHEMABINDING in the statement:
IF OBJECT_ID('dbo.GetPI') IS NOT NULL DROP FUNCTION dbo.GetPI GO CREATE FUNCTION dbo.GetPI () RETURNS FLOAT WITH SCHEMABINDING AS BEGIN RETURN PI() END GO SELECT dbo.GetPI() FROM Sales.Currency
In this case, the function will be considered as deterministic and executed 1 time.
VIEWs
Here I would like to talk about features of views.
Create a test table and view on its base:
IF OBJECT_ID('dbo.tbl', 'U') IS NOT NULL DROP TABLE dbo.tbl GO CREATE TABLE dbo.tbl (a INT, b INT) GO INSERT INTO dbo.tbl VALUES (0, 1) GO IF OBJECT_ID('dbo.vw_tbl', 'V') IS NOT NULL DROP VIEW dbo.vw_tbl GO CREATE VIEW dbo.vw_tbl AS SELECT * FROM dbo.tbl GO SELECT * FROM dbo.vw_tbl
As you can see, we get the correct result:
a b ----------- ----------- 0 1
Now, add a new column in the table and retrieve data from the view:
ALTER TABLE dbo.tbl ADD c INT NOT NULL DEFAULT 2 GO SELECT * FROM dbo.vw_tbl
We receive the same result:
a b ----------- ----------- 0 1
Thus, we need either to explicitly set columns or recompile a script object to get the correct result:
EXEC sys.sp_refreshview @viewname = N'dbo.vw_tbl' GO SELECT * FROM dbo.vw_tbl
Result:
a b c ----------- ----------- ----------- 0 1 2
When you directly refer to the table, this issue will not take place.
Now, I would like to discuss a situation when all the data is combined in one query as well as wrapped in one view. I will do it on this particular example:
ALTER VIEW HumanResources.vEmployee AS SELECT e.BusinessEntityID , p.Title , p.FirstName , p.MiddleName , p.LastName , p.Suffix , e.JobTitle , pp.PhoneNumber , pnt.[Name] AS PhoneNumberType , ea.EmailAddress , p.EmailPromotion , a.AddressLine1 , a.AddressLine2 , a.City , sp.[Name] AS StateProvinceName , a.PostalCode , cr.[Name] AS CountryRegionName , p.AdditionalContactInfo FROM HumanResources.Employee e JOIN Person.Person p ON p.BusinessEntityID = e.BusinessEntityID JOIN Person.BusinessEntityAddress bea ON bea.BusinessEntityID = e.BusinessEntityID JOIN Person.[Address] a ON a.AddressID = bea.AddressID JOIN Person.StateProvince sp ON sp.StateProvinceID = a.StateProvinceID JOIN Person.CountryRegion cr ON cr.CountryRegionCode = sp.CountryRegionCode LEFT JOIN Person.PersonPhone pp ON pp.BusinessEntityID = p.BusinessEntityID LEFT JOIN Person.PhoneNumberType pnt ON pp.PhoneNumberTypeID = pnt.PhoneNumberTypeID LEFT JOIN Person.EmailAddress ea ON p.BusinessEntityID = ea.BusinessEntityID
What should you do if you need to get only a part of information? For example, you need to get Fist Name and Last Name of employees:
SELECT BusinessEntityID , FirstName , LastName FROM HumanResources.vEmployee SELECT p.BusinessEntityID , p.FirstName , p.LastName FROM Person.Person p WHERE p.BusinessEntityID IN ( SELECT e.BusinessEntityID FROM HumanResources.Employee e )
Look at the execution plan in the case of using a view:
Table 'EmailAddress'. Scan count 290, logical reads 640, ... Table 'PersonPhone'. Scan count 290, logical reads 636, ... Table 'BusinessEntityAddress'. Scan count 290, logical reads 636, ... Table 'Person'. Scan count 0, logical reads 897, ... Table 'Employee'. Scan count 1, logical reads 2, ...
Now, we will compare it with the query we have written manually:
Table 'Person'. Scan count 0, logical reads 897, ... Table 'Employee'. Scan count 1, logical reads 2, ...
When creating an execution plan, an optimizer in SQL Server drops unused connections.
However, sometimes when there is no valid foreign key between tables, it is not possible to check whether a connection will impact the sample result. It may also be applied to the situation when tables are connecteCURSORs
I recommend that you do not use cursors for iteration data modification.
You can see the following code with a cursor:
DECLARE @BusinessEntityID INT DECLARE cur CURSOR FOR SELECT BusinessEntityID FROM HumanResources.Employee OPEN cur FETCH NEXT FROM cur INTO @BusinessEntityID WHILE @@FETCH_STATUS = 0 BEGIN UPDATE HumanResources.Employee SET VacationHours = 0 WHERE BusinessEntityID = @BusinessEntityID FETCH NEXT FROM cur INTO @BusinessEntityID END CLOSE cur DEALLOCATE cur
Though, it is possible to re-write the code by dropping the cursor:
UPDATE HumanResources.Employee SET VacationHours = 0 WHERE VacationHours <> 0
In this case, it will improve performance and decrease the time to execute a query.
STRING_CONCAT
To concatenate rows, the STRING_CONCAT could be used. However, as there is no such a function in the SQL Server, we will do this by assigning a value to the variable.
To do this, create a test table:
IF OBJECT_ID('tempdb.dbo.#t') IS NOT NULL DROP TABLE #t GO CREATE TABLE #t (i CHAR(1)) INSERT INTO #t VALUES ('1'), ('2'), ('3')
Then, assign values to the variable:
DECLARE @txt VARCHAR(50) = '' SELECT @txt += i FROM #t SELECT @txt -------- 123
Everything seems to be working fine. However, MS hints that this way is not documented and you may get this result:
DECLARE @txt VARCHAR(50) = '' SELECT @txt += i FROM #t ORDER BY LEN(i) SELECT @txt -------- 3
Alternatively, it is a good idea to use XML as a workaround:
SELECT [text()] = i FROM #t FOR XML PATH('') -------- 123
It should be noted that it is necessary to concatenate rows per each data, rather than into a single set of data:
SELECT [name], STUFF(( SELECT ', ' + c.[name] FROM sys.columns c WHERE c.[object_id] = t.[object_id] FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '') FROM sys.objects t WHERE t.[type] = 'U' ------------------------ ------------------------------------ ScrapReason ScrapReasonID, Name, ModifiedDate Shift ShiftID, Name, StartTime, EndTime
In addition, it is recommended that you should avoid using the XML method for parsing as it is a high-runner process:
Alternatively, it is possible to do this less time-consuming:
SELECT [name], STUFF(( SELECT ', ' + c.[name] FROM sys.columns c WHERE c.[object_id] = t.[object_id] FOR XML PATH(''), TYPE).value('(./text())[1]', 'NVARCHAR(MAX)'), 1, 2, '') FROM sys.objects t WHERE t.[type] = 'U'
But, it does not change the main point.
Now, execute the query without using the value method:
SELECT t.name , STUFF(( SELECT ', ' + c.name FROM sys.columns c WHERE c.[object_id] = t.[object_id] FOR XML PATH('')), 1, 2, '') FROM sys.objects t WHERE t.[type] = 'U'
This option would work perfect. However, it may fail. If you want to check it, execute the following query:
SELECT t.name , STUFF(( SELECT ', ' + CHAR(13) + c.name FROM sys.columns c WHERE c.[object_id] = t.[object_id] FOR XML PATH('')), 1, 2, '') FROM sys.objects t WHERE t.[type] = 'U'
If there are special symbols in rows, such as tabulation, line break, etc., then we will get incorrect results.
Thus, if there are no special symbols, you can create a query without the value method, otherwise, use value(‘(./text())[1]’… .
SQL Injection
Assume we have a code:
DECLARE @param VARCHAR(MAX) SET @param = 1 DECLARE @SQL NVARCHAR(MAX) SET @SQL = 'SELECT TOP(5) name FROM sys.objects WHERE schema_id = ' + @param PRINT @SQL EXEC (@SQL)
Create the query:
SELECT TOP(5) name FROM sys.objects WHERE schema_id = 1
If we add any additional value to the property,
SET @param = '1; select ''hack'''
Then our query will be changed to the following construction:
SELECT TOP(5) name FROM sys.objects WHERE schema_id = 1; select 'hack'
This is called SQL injection when it is possible to execute a query with any additional information.
If the query is formed with String.Format (or manually) in the code, then you may get SQL injection:
using (SqlConnection conn = new SqlConnection()) { conn.ConnectionString = @"Server=.;Database=AdventureWorks2014;Trusted_Connection=true"; conn.Open(); SqlCommand command = new SqlCommand( string.Format("SELECT TOP(5) name FROM sys.objects WHERE schema_id = {0}", value), conn); using (SqlDataReader reader = command.ExecuteReader()) { while (reader.Read()) {} } }
When you use sp_executesql and properties as shown in this code:
DECLARE @param VARCHAR(MAX) SET @param = '1; select ''hack''' DECLARE @SQL NVARCHAR(MAX) SET @SQL = 'SELECT TOP(5) name FROM sys.objects WHERE schema_id = @schema_id' PRINT @SQL EXEC sys.sp_executesql @SQL , N'@schema_id INT' , @schema_id = @param
It is not possible to add some information to the property.
In the code, you may see the following interpretation of the code:
using (SqlConnection conn = new SqlConnection()) { conn.ConnectionString = @"Server=.;Database=AdventureWorks2014;Trusted_Connection=true"; conn.Open(); SqlCommand command = new SqlCommand( "SELECT TOP(5) name FROM sys.objects WHERE schema_id = @schema_id", conn); command.Parameters.Add(new SqlParameter("schema_id", value)); ... }
Summary
Working with databases is not as simple as it may seem. There are a lot of points you should keep in mind when writing T-SQL queries.
Of course, it is not the whole list of pitfalls when working with SQL Server. Still, I hope that this article will be useful for newbies.