En este artículo, discutiremos los errores típicos que pueden enfrentar los desarrolladores novatos al diseñar el código T-SQL. Además, veremos las mejores prácticas y algunos consejos útiles que pueden ayudarlo cuando trabaje con SQL Server, así como soluciones alternativas para mejorar el rendimiento.
Contenido:
1. Tipos de datos
2. *
3. Alias
4. Orden de las columnas
5. NO EN VS NULO
6. Formato de fecha
7. Filtro de fecha
8. Cálculo
9. Convertir implícito
10. LIKE &Índice suprimido
11. Unicode frente a ANSI
12. COLABORAR
13. COLABORACIÓN BINARIA
14. Estilo de código
15. [var]char
16. Longitud de datos
17. ISNULL vs COALESCE
18. Matemáticas
19. UNIÓN vs UNIÓN TODOS
20. Vuelve a leer
21. Subconsulta
22. CASO CUANDO
23. Función escalar
24. VISTAS
25. CURSORES
26. STRING_CONCAT
27. Inyección SQL
Tipos de datos
El principal problema al que nos enfrentamos cuando trabajamos con SQL Server es una elección incorrecta de los tipos de datos.
Supongamos que tenemos dos tablas idénticas:
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')
Ejecutemos una consulta para comprobar cuál es la diferencia:
DECLARE @BirthDate DATE = '2012-09-01' SELECT * FROM @Employees1 WHERE BirthDate = @BirthDate SELECT * FROM @Employees2 WHERE BirthDate = @BirthDate
En el primer caso, los tipos de datos son más redundantes de lo que podrían ser. ¿Por qué deberíamos almacenar un valor de bit como SÍ/NO? ¿hilera? ¿Por qué debemos almacenar una fecha como una fila? ¿Por qué deberíamos usar BIGINT ? para empleados en la tabla, en lugar de INT ?
Conduce a los siguientes inconvenientes:
- Las tablas pueden ocupar mucho espacio en el disco;
- Necesitamos leer más páginas y poner más datos en BufferPool para manejar datos.
- Bajo rendimiento.
*
Me he enfrentado a una situación en la que los desarrolladores recuperan todos los datos de una tabla y luego, en el lado del cliente, usan DataReader para seleccionar solo los campos obligatorios. No recomiendo usar este enfoque:
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
Habrá una diferencia significativa en el tiempo de ejecución de la consulta. Además, el índice de cobertura puede reducir una cantidad de lecturas lógicas.
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
Vamos a crear una tabla:
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')
Supongamos que tenemos una consulta que devuelve la cantidad de filas idénticas en ambas tablas:
SELECT COUNT_BIG(*) FROM Sales.Currency WHERE CurrencyCode IN ( SELECT CurrencyCode FROM Sales.UserCurrency )
Todo funcionará como se esperaba, hasta que alguien cambie el nombre de una columna en Sales.UserCurrency tabla:
EXEC sys.sp_rename 'Sales.UserCurrency.CurrencyCode', 'Code', 'COLUMN'
A continuación, ejecutaremos una consulta y veremos que obtenemos todas las filas en Sales.Currency tabla, en lugar de 1 fila. Al crear un plan de ejecución, en la etapa de vinculación, SQL Server verificaría las columnas de Sales.UserCurrency, no encontrará CurrencyCode allí y decide que esta columna pertenece a Sales.Currency mesa. Después de eso, un optimizador eliminará el CurrencyCode =CurrencyCode condición.
Por lo tanto, recomiendo usar alias:
SELECT COUNT_BIG(*) FROM Sales.Currency c WHERE c.CurrencyCode IN ( SELECT u.CurrencyCode FROM Sales.UserCurrency u )
Orden de las columnas
Supongamos que tenemos una tabla:
IF OBJECT_ID('dbo.DatePeriod') IS NOT NULL DROP TABLE dbo.DatePeriod GO CREATE TABLE dbo.DatePeriod ( StartDate DATE , EndDate DATE )
Siempre insertamos datos allí según la información sobre el orden de las columnas.
INSERT INTO dbo.DatePeriod SELECT '2015-01-01', '2015-01-31'
Supongamos que alguien cambia el orden de las columnas:
CREATE TABLE dbo.DatePeriod ( EndDate DATE , StartDate DATE )
Los datos se insertarán en un orden diferente. En este caso, es una buena idea especificar explícitamente las columnas en la instrucción INSERT:
INSERT INTO dbo.DatePeriod (StartDate, EndDate) SELECT '2015-01-01', '2015-01-31'
Aquí hay otro ejemplo:
SELECT TOP(1) * FROM dbo.DatePeriod ORDER BY 2 DESC
¿Sobre qué columna vamos a ordenar los datos? Dependerá del orden de las columnas en una tabla. En caso de que uno cambie el orden, obtendremos resultados incorrectos.
NO DENTRO vs NULO
Hablemos del NO EN declaración.
Por ejemplo, debe escribir un par de consultas:devolver los registros de la primera tabla, que no existen en la segunda tabla y viceversa. Por lo general, los desarrolladores junior usan IN y NO EN :
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 primera consulta devolvió 2, la segunda, 1. Además, agregaremos otro valor en la segunda tabla:NULL :
INSERT INTO @t2 VALUES (1), (NULL)
Al ejecutar la consulta con NOT IN , no obtendremos ningún resultado. ¿Por qué IN funciona y NO IN no? La razón es que SQL Server usa TRUE , FALSO y DESCONOCIDO lógica al comparar datos.
Al ejecutar una consulta, SQL Server interpreta la condición IN de la siguiente manera:
a IN (1, NULL) == a=1 OR a=NULL
NO EN :
a NOT IN (1, NULL) == a<>1 AND a<>NULL
Al comparar cualquier valor con NULL, SQL Server devuelve DESCONOCIDO. Cualquiera 1=NULO o NULL=NULL – ambos resultan en DESCONOCIDO. Si tenemos AND en la expresión, ambos lados devuelven UNKNOWN.
Me gustaría señalar que este caso no es raro. Por ejemplo, marca una columna como NO NULO. Después de un tiempo, otro desarrollador decide permitir NULL para esa columna Esto puede conducir a la situación, cuando un informe de cliente deja de funcionar una vez que se inserta cualquier valor NULL en la tabla.
En este caso, recomendaría excluir valores NULL:
SELECT * FROM @t1 WHERE t1 NOT IN ( SELECT t2 FROM @t2 WHERE t2 IS NOT NULL )
Además, es posible utilizar EXCEPTO :
SELECT * FROM @t1 EXCEPT SELECT * FROM @t2
Alternativamente, puede usar NO EXISTS :
SELECT * FROM @t1 WHERE NOT EXISTS( SELECT 1 FROM @t2 WHERE t1 = t2 )
¿Qué opción es más preferible? La última opción con NO EXISTE parece ser el más productivo, ya que genera el desplazamiento de predicado más óptimo operador para acceder a los datos de la segunda tabla.
En realidad, los valores NULL pueden devolver un resultado inesperado.
Considérelo en este ejemplo particular:
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'
Como puede ver, no obtuvo el resultado esperado porque los valores NULL tienen operadores de comparación separados:
SELECT COUNT_BIG(*) FROM Production.Product WHERE Color IS NULL SELECT COUNT_BIG(*) FROM Production.Product WHERE Color IS NOT NULL
Aquí hay otro ejemplo con CHECK restricciones:
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')) )
Creamos una tabla con permiso para insertar solo colores blanco y negro:
INSERT INTO #temp VALUES ('Black') (1 row(s) affected)
Todo funciona como se esperaba.
INSERT INTO #temp VALUES ('Red') The INSERT statement conflicted with the CHECK constraint... The statement has been terminated.
Ahora, agreguemos NULL:
INSERT INTO #temp VALUES (NULL) (1 row(s) affected)
¿Por qué la restricción CHECK pasó el valor NULL? Bueno, la razón es que hay bastantes NO FALSO condición para hacer un registro. La solución consiste en definir explícitamente una columna como NOT NULL o use NULL en la restricción.
Formato de fecha
Muy a menudo, puede tener dificultades con los tipos de datos.
Por ejemplo, necesita obtener la fecha actual. Para hacer esto, puede usar la función GETDATE:
SELECT GETDATE()
Luego simplemente copie el resultado devuelto en una consulta requerida y elimine la hora:
SELECT * FROM sys.objects WHERE create_date < '2016-11-14'
¿Es eso correcto?
La fecha se especifica mediante una constante de cadena:
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
Todos los valores tienen una interpretación de un solo valor:
----------- ----------- ----------- ----------- 2016-12-05 2016-05-12 2016-05-12 2016-12-05
No causará ningún problema hasta que la consulta con esta lógica empresarial se ejecute en otro servidor donde la configuración puede diferir:
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
Sin embargo, estas opciones pueden dar lugar a una interpretación incorrecta de la fecha:
----------- ----------- ----------- ----------- 2016-05-12 2016-12-05 2016-12-05 2016-12-05
Además, este código puede generar un error tanto visible como latente.
Considere el siguiente ejemplo. Necesitamos insertar datos en una tabla de prueba. En un servidor de prueba todo funciona perfecto:
DECLARE @t TABLE (a DATETIME) INSERT INTO @t VALUES ('05/13/2016')
Aún así, en el lado del cliente, esta consulta tendrá problemas ya que la configuración de nuestro servidor es diferente:
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.
Entonces, ¿qué formato debemos usar para declarar constantes de fecha? Para responder a esta pregunta, ejecute esta consulta:
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
La interpretación de las constantes puede diferir según el idioma instalado:
----------- ----------- ----------- ----------- 2016-01-12 2016-01-12 2016-01-12 2016-01-12 ----------- ----------- ----------- ----------- 2016-12-01 2016-12-01 2016-01-12 2016-01-12
Por lo tanto, es mejor usar las dos últimas opciones. Además, me gustaría agregar que especificar explícitamente la fecha no es una buena 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.
Por lo tanto, si desea que las constantes con las fechas se interpreten correctamente, debe especificarlas en el siguiente formato YYYYMMDD.
Además, me gustaría llamar su atención sobre el comportamiento de algunos tipos de datos:
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 diferencia de DATETIME, DATE type se interpreta correctamente con varias configuraciones en un servidor:
---------- ---------- 2016-01-12 2016-01-12 ---------- ---------- 2016-01-12 2016-12-01
Filtro de fecha
Para continuar, consideraremos cómo filtrar datos de manera efectiva. Empecemos por ellos DATETIME/DATE:
USE AdventureWorks2014 GO UPDATE TOP(1) dbo.DatabaseLog SET PostTime = '20140716 12:12:12'
Ahora, intentaremos averiguar cuántas filas devuelve la consulta para un día específico:
SELECT COUNT_BIG(*) FROM dbo.DatabaseLog WHERE PostTime = '20140716'
La consulta devolverá 0. Al crear un plan de ejecución, el servidor SQL intenta convertir una constante de cadena al tipo de datos de la columna que necesitamos filtrar:
Crear un índice:
CREATE NONCLUSTERED INDEX IX_PostTime ON dbo.DatabaseLog (PostTime)
Hay opciones correctas e incorrectas para generar datos. Por ejemplo, debe eliminar la columna de tiempo:
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'
O necesitamos especificar un rango:
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'
Teniendo en cuenta la optimización, puedo decir que estas dos consultas son las más correctas. El punto es que todas las conversiones y los cálculos de las columnas de índice que se filtran pueden disminuir el rendimiento drásticamente y aumentar el tiempo de las lecturas lógicas:
Table 'DatabaseLog'. Scan count 1, logical reads 7, ... Table 'DatabaseLog'. Scan count 1, logical reads 2, ...
La hora de la publicación El campo no se había incluido en el índice antes, y no pudimos ver ninguna eficiencia en el uso de este enfoque correcto en el filtrado. Otra cosa es cuando necesitamos generar datos para un mes:
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'
Una vez más, la última opción es más preferible:
Además, siempre puede crear un índice basado en un campo calculado:
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)
En comparación con la consulta anterior, la diferencia en las lecturas lógicas puede ser significativa (si se trata de tablas grandes):
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, ...
Cálculo
Como ya se ha discutido, cualquier cálculo en las columnas de índice disminuye el rendimiento y aumenta el tiempo de las lecturas lógicas:
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, ...
Si observamos los planes de ejecución, en el primero, SQL Server ejecuta IndexScan :
Luego, cuando no haya cálculos en las columnas de índice, veremos IndexSeek :
Convertir implícito
Echemos un vistazo a estas dos consultas que filtran por el mismo valor:
USE AdventureWorks2014 GO SELECT BusinessEntityID, NationalIDNumber FROM HumanResources.Employee WHERE NationalIDNumber = 30845 SELECT BusinessEntityID, NationalIDNumber FROM HumanResources.Employee WHERE NationalIDNumber = '30845'
Los planes de ejecución proporcionan la siguiente información:
- Advertencia y IndexScan en el primer plan
- Buscar índices: en el segundo.
Table 'Employee'. Scan count 1, logical reads 4, ... Table 'Employee'. Scan count 0, logical reads 2, ...
El número de identificación nacional columna tiene el NVARCHAR(15) tipo de datos. La constante que usamos para filtrar datos se establece como INT lo que nos lleva a una conversión de tipo de datos implícita. A su vez, puede disminuir el rendimiento. Puede monitorearlo cuando alguien modifica el tipo de datos en la columna, sin embargo, las consultas no se modifican.
Es importante comprender que una conversión de tipo de datos implícita puede generar errores en tiempo de ejecución. Por ejemplo, antes de que el campo PostalCode fuera numérico, resultó que un código postal podía contener letras. Por lo tanto, se actualizó el tipo de datos. Aún así, si insertamos un código postal alfabético, la consulta anterior ya no funcionará:
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.
Otro ejemplo es cuando necesita usar EntityFramework en el proyecto, que por defecto interpreta todos los campos de fila como Unicode:
SELECT CustomerID, AccountNumber FROM Sales.Customer WHERE AccountNumber = N'AW00000009' SELECT CustomerID, AccountNumber FROM Sales.Customer WHERE AccountNumber = 'AW00000009'
Por lo tanto, se generan consultas incorrectas:
Para resolver este problema, asegúrese de que los tipos de datos coincidan.
LIKE &Índice suprimido
De hecho, tener un índice de cobertura no significa que lo usará de manera efectiva.
Vamos a comprobarlo en este ejemplo en particular. Supongamos que necesitamos mostrar todas las filas que comienzan 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%'
Obtendremos las siguientes lecturas lógicas y planes de ejecución:
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, ...
Por lo tanto, si hay un índice, no debe contener ningún cálculo o conversión de tipos, funciones, etc.
Pero, ¿qué hace si necesita encontrar la aparición de una subcadena en una cadena?
SELECT AddressLine1 FROM Person.[Address] WHERE AddressLine1 LIKE '%100%'v
Volveremos a esta pregunta más tarde.
Unicode frente a ANSI
Es importante recordar que existen los UNICODE y ANSI instrumentos de cuerda. El tipo UNICODE incluye NVARCHAR/NCHAR (2 bytes a un símbolo). Para almacenar ANSI cadenas, es posible usar VARCHAR/CHAR (1 byte a 1 símbolo). También hay TEXT/NTEXT , pero no recomiendo usarlos ya que pueden disminuir el rendimiento.
Si especifica una constante Unicode en una consulta, es necesario precederla con el símbolo N. Para comprobarlo, ejecute la siguiente consulta:
SELECT '文本 ANSI' , N'文本 UNICODE' ------- ------------ ?? ANSI 文本 UNICODE
Si N no precede a la constante, SQL Server intentará encontrar un símbolo adecuado en la codificación ANSI. Si no lo encuentra, mostrará un signo de interrogación.
COLOCAR
Muy a menudo, cuando se lo entrevista para el puesto de Desarrollador de base de datos intermedio/superior, un entrevistador suele hacer la siguiente pregunta:¿Esta consulta devolverá los datos?
DECLARE @a NCHAR(1) = 'Ё' , @b NCHAR(1) = 'Ф' SELECT @a, @b WHERE @a = @b
Depende. En primer lugar, el símbolo N no precede a una constante de cadena, por lo que se interpretará como ANSI. En segundo lugar, mucho depende del valor COLLATE actual, que es un conjunto de reglas, al seleccionar y comparar datos de cadena.
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
Esta instrucción COLLATE devolverá signos de interrogación ya que sus símbolos son iguales:
---- ---- ? ?
Si cambiamos la sentencia COLLATE por otra sentencia:
ALTER DATABASE test COLLATE Cyrillic_General_100_CI_AS
En este caso, la consulta no devolverá nada, ya que los caracteres cirílicos se interpretarán correctamente.
Por lo tanto, si una constante de cadena ocupa UNICODE, entonces es necesario establecer N antes de una constante de cadena. Aún así, no recomendaría configurarlo en todas partes por las razones que hemos discutido anteriormente.
Otra pregunta que se hará en la entrevista se refiere a la comparación de filas.
Considere el siguiente ejemplo:
DECLARE @a VARCHAR(10) = 'TEXT' , @b VARCHAR(10) = 'text' SELECT IIF(@a = @b, 'TRUE', 'FALSE')
¿Estas filas son iguales? Para verificar esto, necesitamos especificar explícitamente 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')
Como hay COLLATE que distingue entre mayúsculas y minúsculas (CS) y no distingue entre mayúsculas y minúsculas (CI) al comparar y seleccionar filas, no podemos decir con certeza si son iguales. Además, hay varios COLLATE tanto en un servidor de prueba como en el lado del cliente.
Existe un caso en el que COLLATE de una base de destino y tempdb no coinciden.
Cree una base de datos 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
Al crear una tabla, hereda COLLATE de una base de datos. La única diferencia para la primera tabla temporal, para la cual determinamos una estructura explícitamente sin COLLATE, es que hereda COLLATE de tempdb base de datos.
------ -------------------------- 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
Describiré el caso en el que COLLATE no coincide en el ejemplo particular con #t1.
Por ejemplo, los datos no se filtran correctamente, ya que COLLATE puede no tener en cuenta un caso:
SELECT * FROM #t1 WHERE c = 'A'
Alternativamente, podemos tener un conflicto para conectar tablas con COLLATE diferentes:
SELECT * FROM #t1 JOIN t ON [#t1].c = t.c
Todo parece funcionar perfectamente en un servidor de prueba, mientras que en un servidor cliente obtenemos un error:
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.
Para evitarlo, tenemos que establecer hacks en todas partes:
SELECT * FROM #t1 JOIN t ON [#t1].c = t.c COLLATE database_default
CLASIFICACIÓN BINARIA
Ahora, descubriremos cómo usar COLLATE para su beneficio.
Considere el ejemplo con la aparición de una subcadena en una cadena:
SELECT AddressLine1 FROM Person.[Address] WHERE AddressLine1 LIKE '%100%'
Es posible optimizar esta consulta y reducir su tiempo de ejecución.
Primero, necesitamos generar una tabla grande:
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
Cree columnas calculadas con COLLATE binarios e índices:
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)
Ejecutar el proceso de filtración:
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
Como puede ver, esta consulta devuelve el siguiente resultado:
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.
El punto es que el filtro basado en la comparación binaria toma menos tiempo. Por lo tanto, si necesita filtrar la ocurrencia de cadenas con frecuencia y rapidez, entonces es posible almacenar datos con COLLATE que termina en BIN. Sin embargo, se debe tener en cuenta que todos los COLLATE binarios distinguen entre mayúsculas y minúsculas.
Estilo de código
Un estilo de codificación es estrictamente individual. Aún así, este código debe ser mantenido simplemente por otros desarrolladores y cumplir con ciertas reglas.
Cree una base de datos separada y una tabla dentro:
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)
Luego, escribe la consulta:
select employeeid from employee
Ahora, cambie COLLATE a cualquiera que distinga entre mayúsculas y minúsculas:
ALTER DATABASE test COLLATE Latin1_General_CS_AI
Luego, intente ejecutar la consulta nuevamente:
Msg 208, Level 16, State 1, Line 19 Invalid object name 'employee'.
Un optimizador usa reglas para COLLATE actual en el paso de vinculación cuando verifica tablas, columnas y otros objetos y compara cada objeto del árbol de sintaxis con un objeto real de un catálogo del sistema.
Si desea generar consultas manualmente, debe usar siempre el caso correcto en los nombres de los objetos.
En cuanto a las variables, COLLATE se heredan de la base de datos maestra. Por lo tanto, también debe usar el caso correcto para trabajar con ellos:
SELECT DATABASEPROPERTYEX('master', 'collation') DECLARE @EmpID INT = 1 SELECT @empid
En este caso, no obtendrá un error:
----------------------- Cyrillic_General_CI_AS ----------- 1
Aún así, un caso de error puede aparecer en otro servidor:
-------------------------- Latin1_General_CS_AS Msg 137, Level 15, State 2, Line 4 Must declare the scalar variable "@empid".
[var]carácter
Como sabes, hay fijos (CHAR , NCHAR ) y variable (VARCHAR , NVARCHAR ) tipos de datos:
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')
Si una fila tiene una longitud fija, digamos 20 símbolos, pero ha escrito solo 4 símbolos, SQL Server agregará 16 espacios en blanco a la derecha de manera predeterminada:
--- --- ---- ---- ---------------------- ---------------------- 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
Sin embargo, no lo es. 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 puede ser usado. 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.