sql >> Base de Datos >  >> RDS >> Database

Gemas T-SQL pasadas por alto

Mi buen amigo Aaron Bertrand me inspiró a escribir este artículo. Me recordó cómo a veces damos las cosas por sentadas cuando nos parecen obvias y no siempre nos molestamos en comprobar la historia completa detrás de ellas. La relevancia para T-SQL es que a veces asumimos que sabemos todo lo que hay que saber sobre ciertas características de T-SQL y no siempre nos molestamos en consultar la documentación para ver si hay más. En este artículo, cubro una serie de características de T-SQL que a menudo se pasan por alto por completo o que admiten parámetros o capacidades que a menudo se pasan por alto. Si tiene ejemplos propios de gemas T-SQL que a menudo se pasan por alto, compártalos en la sección de comentarios de este artículo.

Antes de comenzar a leer este artículo, pregúntese qué sabe acerca de las siguientes características de T-SQL:EOMONTH, TRANSLATE, TRIM, CONCAT y CONCAT_WS, LOG, variables de cursor y MERGE con OUTPUT.

En mis ejemplos, usaré una base de datos de muestra llamada TSQLV5. Puede encontrar el script que crea y completa esta base de datos aquí, y su diagrama ER aquí.

EOMONTH tiene un segundo parámetro

La función EOMONTH se introdujo en SQL Server 2012. Mucha gente piensa que solo admite un parámetro que contiene una fecha de entrada y que simplemente devuelve la fecha de fin de mes que corresponde a la fecha de entrada.

Considere una necesidad un poco más sofisticada para calcular el final del mes anterior. Por ejemplo, suponga que necesita consultar la tabla Ventas.Pedidos y devolver los pedidos que se realizaron al final del mes anterior.

Una forma de lograr esto es aplicar la función EOMONTH a SYSDATETIME para obtener la fecha de fin de mes del mes actual y luego aplicar la función DATEADD para restar un mes del resultado, así:

USE TSQLV5; 
 
SELECT orderid, orderdate
FROM Sales.Orders
WHERE orderdate = EOMONTH(DATEADD(month, -1, SYSDATETIME()));

Tenga en cuenta que si realmente ejecuta esta consulta en la base de datos de muestra TSQLV5, obtendrá un resultado vacío ya que la última fecha de pedido registrada en la tabla es el 6 de mayo de 2019. Sin embargo, si la tabla tenía pedidos con una fecha de pedido que cae en el último día del mes anterior, la consulta los habría devuelto.

Lo que mucha gente no se da cuenta es que EOMONTH admite un segundo parámetro en el que indica cuántos meses sumar o restar. Aquí está la sintaxis [totalmente documentada] de la función:

EOMONTH ( start_date [, month_to_add ] )

Nuestra tarea se puede lograr de manera más fácil y natural simplemente especificando -1 como el segundo parámetro de la función, así:

SELECT orderid, orderdate
FROM Sales.Orders
WHERE orderdate = EOMONTH(SYSDATETIME(), -1);

TRANSLATE es a veces más simple que REPLACE

Mucha gente está familiarizada con la función REEMPLAZAR y cómo funciona. Lo usa cuando desea reemplazar todas las apariciones de una subcadena con otra en una cadena de entrada. Sin embargo, a veces, cuando tiene varios reemplazos que necesita aplicar, usar REPLACE es un poco complicado y da como resultado expresiones complicadas.

Como ejemplo, suponga que recibe una cadena de entrada @s que contiene un número con formato español. En España usan un punto como separador para grupos de miles y una coma como separador decimal. Debe convertir la entrada al formato de EE. UU., donde se usa una coma como separador para grupos de miles y un punto como separador decimal.

Usando una llamada a la función REEMPLAZAR, puede reemplazar solo todas las apariciones de un carácter o subcadena con otro. Para aplicar dos reemplazos (puntos a comas y comas a puntos) necesita anidar llamadas a funciones. La parte complicada es que si usa REEMPLAZAR una vez para cambiar los puntos a comas, y luego una segunda vez contra el resultado para cambiar las comas a puntos, termina solo con puntos. Pruébalo:

DECLARE @s AS VARCHAR(20) = '123.456.789,00';
 
SELECT REPLACE(REPLACE(@s, '.', ','), ',', '.');

Obtienes el siguiente resultado:

123.456.789.00

Si desea seguir usando la función REEMPLAZAR, necesita tres llamadas de función. Uno para reemplazar puntos con un carácter neutral que sabe que normalmente no puede aparecer en los datos (por ejemplo, ~). Otro contra el resultado de reemplazar todas las comas con puntos. Otro contra el resultado de reemplazar todas las apariciones del carácter temporal (~ en nuestro ejemplo) con comas. Aquí está la expresión completa:

DECLARE @s AS VARCHAR(20) = '123.456.789,00';
SELECT REPLACE(REPLACE(REPLACE(@s, '.', '~'), ',', '.'), '~', ',');

Esta vez obtienes el resultado correcto:

123,456,789.00

Es un poco factible, pero resulta en una expresión larga y enrevesada. ¿Qué pasaría si tuviera más reemplazos para aplicar?

Mucha gente no sabe que SQL Server 2017 introdujo una nueva función llamada TRANSLATE que simplifica mucho estos reemplazos. Esta es la sintaxis de la función:

TRANSLATE ( inputString, characters, translations )

La segunda entrada (caracteres) es una cadena con la lista de caracteres individuales que desea reemplazar, y la tercera entrada (traducciones) es una cadena con la lista de los caracteres correspondientes con los que desea reemplazar los caracteres de origen. Naturalmente, esto significa que el segundo y el tercer parámetro deben tener el mismo número de caracteres. Lo importante de la función es que no realiza pases separados para cada uno de los reemplazos. Si lo hiciera, potencialmente habría resultado en el mismo error que en el primer ejemplo que mostré usando las dos llamadas a la función REEMPLAZAR. En consecuencia, el manejo de nuestra tarea se convierte en una obviedad:

DECLARE @s AS VARCHAR(20) = '123.456.789,00';
SELECT TRANSLATE(@s, '.,', ',.');

Este código genera el resultado deseado:

123,456,789.00

¡Eso es bastante bueno!

TRIM es más que LTRIM(RTRIM())

SQL Server 2017 introdujo soporte para la función TRIM. Mucha gente, incluido yo mismo, inicialmente asume que no es más que un simple acceso directo a LTRIM (RTRIM (entrada)). Sin embargo, si revisa la documentación, se da cuenta de que en realidad es más poderoso que eso.

Antes de entrar en detalles, considere la siguiente tarea:dada una cadena de entrada @s, elimine las barras diagonales iniciales y finales (hacia atrás y hacia adelante). Como ejemplo, suponga que @s contiene la siguiente cadena:

//\\ remove leading and trailing backward (\) and forward (/) slashes \\//

La salida deseada es:

 remove leading and trailing backward (\) and forward (/) slashes 

Tenga en cuenta que la salida debe conservar los espacios iniciales y finales.

Si no conocía todas las capacidades de TRIM, esta es una forma en que podría haber resuelto la tarea:

DECLARE @s AS VARCHAR(100) = '//\\ remove leading and trailing backward (\) and forward (/) slashes \\//';
 
SELECT
  TRANSLATE(TRIM(TRANSLATE(TRIM(TRANSLATE(@s, ' /', '~ ')), ' \', '^ ')), ' ^~', '\/ ')
    AS outputstring;

La solución comienza usando TRANSLATE para reemplazar todos los espacios con un carácter neutral (~) y barras inclinadas con espacios, luego usa TRIM para recortar los espacios iniciales y finales del resultado. Este paso esencialmente recorta las barras diagonales iniciales y finales, usando temporalmente ~ en lugar de los espacios originales. Este es el resultado de este paso:

\\~remove~leading~and~trailing~backward~(\)~and~forward~( )~slashes~\\

Luego, el segundo paso usa TRANSLATE para reemplazar todos los espacios con otro carácter neutral (^) y barras inclinadas hacia atrás con espacios, luego usa TRIM para recortar los espacios iniciales y finales del resultado. Este paso esencialmente recorta las barras inclinadas hacia atrás iniciales y finales, utilizando temporalmente ^ en lugar de espacios intermedios. Este es el resultado de este paso:

~remove~leading~and~trailing~backward~( )~and~forward~(^)~slashes~

El último paso usa TRANSLATE para reemplazar espacios con barras invertidas, ^ con barras inclinadas y ~ con espacios, generando el resultado deseado:

 remove leading and trailing backward (\) and forward (/) slashes 

Como ejercicio, intente resolver esta tarea con una solución compatible anterior a SQL Server 2017 donde no puede usar TRIM y TRANSLATE.

Volviendo a SQL Server 2017 y versiones posteriores, si se molestó en consultar la documentación, habría descubierto que TRIM es más sofisticado de lo que pensó inicialmente. Esta es la sintaxis de la función:

TRIM ( [ characters FROM ] string )

Los caracteres FROM opcionales part le permite especificar uno o más caracteres que desea recortar desde el principio y el final de la cadena de entrada. En nuestro caso, todo lo que necesita hacer es especificar '/\' como esta parte, así:

DECLARE @s AS VARCHAR(100) = '//\\ remove leading and trailing backward (\) and forward (/) slashes \\//';
 
SELECT TRIM( '/\' FROM @s) AS outputstring;

¡Es una mejora bastante significativa en comparación con la solución anterior!

CONCAT y CONCAT_WS

Si ha estado trabajando con T-SQL por un tiempo, sabe lo incómodo que es lidiar con NULL cuando necesita concatenar cadenas. Como ejemplo, considere los datos de ubicación registrados para los empleados en la tabla HR.Employees:

SELECT empid, country, region, city
FROM HR.Employees;

Esta consulta genera el siguiente resultado:

empid       country         region          city
----------- --------------- --------------- ---------------
1           USA             WA              Seattle
2           USA             WA              Tacoma
3           USA             WA              Kirkland
4           USA             WA              Redmond
5           UK              NULL            London
6           UK              NULL            London
7           UK              NULL            London
8           USA             WA              Seattle
9           UK              NULL            London

Tenga en cuenta que, para algunos empleados, la parte de la región es irrelevante y una región irrelevante se representa como NULL. Suponga que necesita concatenar las partes de la ubicación (país, región y ciudad), usando una coma como separador, pero ignorando las regiones NULL. Cuando la región es relevante, desea que el resultado tenga la forma <coutry>,<region>,<city> y cuando la región es irrelevante, desea que el resultado tenga la forma <country>,<city> . Normalmente, concatenar algo con NULL produce un resultado NULL. Puede cambiar este comportamiento desactivando la opción de sesión CONCAT_NULL_YIELDS_NULL, pero no recomendaría habilitar un comportamiento no estándar.

Si no sabía de la existencia de las funciones CONCAT y CONCAT_WS, probablemente habría usado ISNULL o COALESCE para reemplazar un NULL con una cadena vacía, así:

SELECT empid, country + ISNULL(',' + region, '') + ',' + city AS location
FROM HR.Employees;

Aquí está el resultado de esta consulta:

empid       location
----------- -----------------------------------------------
1           USA,WA,Seattle
2           USA,WA,Tacoma
3           USA,WA,Kirkland
4           USA,WA,Redmond
5           UK,London
6           UK,London
7           UK,London
8           USA,WA,Seattle
9           UK,London

SQL Server 2012 introdujo la función CONCAT. Esta función acepta una lista de entradas de cadenas de caracteres y las concatena y, al hacerlo, ignora los valores NULL. Entonces, usando CONCAT puedes simplificar la solución de esta manera:

SELECT empid, CONCAT(country, ',' + region, ',', city) AS location
FROM HR.Employees;

Aún así, debe especificar explícitamente los separadores como parte de las entradas de la función. Para hacernos la vida aún más fácil, SQL Server 2017 introdujo una función similar llamada CONCAT_WS en la que comienza indicando el separador, seguido de los elementos que desea concatenar. Con esta función, la solución se simplifica aún más así:

SELECT empid, CONCAT_WS(',', country, region, city) AS location
FROM HR.Employees;

El siguiente paso es, por supuesto, leer la mente. El 1 de abril de 2020, Microsoft planea lanzar CONCAT_MR. La función aceptará una entrada vacía y descubrirá automáticamente qué elementos desea que concatene al leer su mente. La consulta se verá así:

SELECT empid, CONCAT_MR() AS location
FROM HR.Employees;

LOG tiene un segundo parámetro

Al igual que la función EOMONTH, muchas personas no se dan cuenta de que a partir de SQL Server 2012, la función LOG admite un segundo parámetro que le permite indicar la base del logaritmo. Antes de eso, T-SQL admitía la función LOG(entrada) que devuelve el logaritmo natural de la entrada (usando la constante e como base) y LOG10(entrada) que usa 10 como base.

No ser consciente de la existencia del segundo parámetro de la función LOG, cuando la gente quería calcular Logb (x), donde b es una base distinta de e y 10, a menudo lo hacían de la manera más larga. Podría confiar en la siguiente ecuación:

Registrob (x) =Registroa (x)/Registroa (b)

Como ejemplo, para calcular Log2 (8), se basa en la siguiente ecuación:

Registro2 (8) =Registroe (8)/Iniciar sesióne (2)

Traducido a T-SQL, aplica el siguiente cálculo:

DECLARE @x AS FLOAT = 8, @b AS INT = 2;
SELECT LOG(@x) / LOG(@b);

Una vez que te das cuenta de que LOG admite un segundo parámetro donde indicas la base, el cálculo simplemente se convierte en:

DECLARE @x AS FLOAT = 8, @b AS INT = 2;
SELECT LOG(@x, @b);

Variables de cursor

Si ha estado trabajando con T-SQL por un tiempo, probablemente tuvo muchas oportunidades de trabajar con cursores. Como sabe, cuando trabaja con un cursor, normalmente utiliza los siguientes pasos:

  • Declarar el cursor
  • Abre el cursor
  • Iterar a través de los registros del cursor
  • Cierra el cursor
  • Desasignar el cursor

Como ejemplo, suponga que necesita realizar alguna tarea por base de datos en su instancia. Usando un cursor, normalmente usaría un código similar al siguiente:

DECLARE @dbname AS sysname;
 
DECLARE C CURSOR FORWARD_ONLY STATIC READ_ONLY FOR
  SELECT name FROM sys.databases;
 
OPEN C;
 
FETCH NEXT FROM C INTO @dbname;
 
WHILE @@FETCH_STATUS = 0
BEGIN
  PRINT N'Handling database ' + QUOTENAME(@dbname) + N'...';
  /* ... do your thing here ... */
  FETCH NEXT FROM C INTO @dbname;
END;
 
CLOSE C;
DEALLOCATE C;

El comando CLOSE libera el conjunto de resultados actual y libera bloqueos. El comando DEALLOCATE elimina una referencia de cursor y, cuando se desasigna la última referencia, libera las estructuras de datos que componen el cursor. Si intenta ejecutar el código anterior dos veces sin los comandos CLOSE y DEALLOCATE, obtendrá el siguiente error:

Msg 16915, Level 16, State 1, Line 4
A cursor with the name 'C' already exists.
Msg 16905, Level 16, State 1, Line 6
The cursor is already open.

Asegúrese de ejecutar los comandos CLOSE y DEALLOCATE antes de continuar.

Muchas personas no se dan cuenta de que cuando necesitan trabajar con un cursor en un solo lote, que es el caso más común, en lugar de usar un cursor normal, pueden trabajar con una variable de cursor. Como cualquier variable, el alcance de una variable de cursor es solo el lote donde se declaró. Esto significa que tan pronto como finaliza un lote, caducan todas las variables. Usando una variable de cursor, una vez que finaliza un lote, SQL Server lo cierra y lo desasigna automáticamente, ahorrándole la necesidad de ejecutar los comandos CLOSE y DEALLOCATE explícitamente.

Aquí está el código revisado usando una variable de cursor esta vez:

DECLARE @dbname AS sysname, @C AS CURSOR;
 
SET @C = CURSOR FORWARD_ONLY STATIC READ_ONLY FOR
  SELECT name FROM sys.databases;
 
OPEN @C;
 
FETCH NEXT FROM @C INTO @dbname;
 
WHILE @@FETCH_STATUS = 0
BEGIN
  PRINT N'Handling database ' + QUOTENAME(@dbname) + N'...';
  /* ... do your thing here ... */
  FETCH NEXT FROM @C INTO @dbname;
END;

Siéntase libre de ejecutarlo varias veces y observe que esta vez no obtiene ningún error. Simplemente es más limpio y no tiene que preocuparse por mantener los recursos del cursor si olvidó cerrar y desasignar el cursor.

COMBINAR con SALIDA

Desde el inicio de la cláusula OUTPUT para declaraciones de modificación en SQL Server 2005, resultó ser una herramienta muy práctica siempre que deseaba devolver datos de filas modificadas. Las personas usan esta función regularmente para fines como archivar, auditar y muchos otros casos de uso. Sin embargo, una de las cosas molestas de esta función es que si la usa con declaraciones INSERT, solo puede devolver datos de las filas insertadas, prefijando las columnas de salida con inserted . No tiene acceso a las columnas de la tabla de origen, aunque a veces necesita devolver columnas del origen junto con columnas del destino.

Como ejemplo, considere las tablas T1 y T2, que crea y completa ejecutando el siguiente código:

DROP TABLE IF EXISTS dbo.T1, dbo.T2;
GO
 
CREATE TABLE dbo.T1(keycol INT NOT NULL IDENTITY PRIMARY KEY, datacol VARCHAR(10) NOT NULL);
 
CREATE TABLE dbo.T2(keycol INT NOT NULL IDENTITY PRIMARY KEY, datacol VARCHAR(10) NOT NULL);
 
INSERT INTO dbo.T1(datacol) VALUES('A'),('B'),('C'),('D'),('E'),('F');

Observe que se usa una propiedad de identidad para generar las claves en ambas tablas.

Suponga que necesita copiar algunas filas de T1 a T2; digamos, aquellos en los que keycol % 2 =1. Desea utilizar la cláusula OUTPUT para devolver las claves recién generadas en T2, pero también desea devolver junto con esas claves las claves de origen respectivas de T1. La expectativa intuitiva es usar la siguiente instrucción INSERT:

INSERT INTO dbo.T2(datacol)
    OUTPUT T1.keycol AS T1_keycol, inserted.keycol AS T2_keycol
  SELECT datacol FROM dbo.T1 WHERE keycol % 2 = 1;

Desafortunadamente, como se mencionó, la cláusula OUTPUT no le permite hacer referencia a las columnas de la tabla de origen, por lo que obtiene el siguiente error:

Mensaje 4104, nivel 16, estado 1, línea 2
El identificador de varias partes "T1.keycol" no se pudo vincular.

Muchas personas no se dan cuenta de que, curiosamente, esta limitación no se aplica a la instrucción MERGE. Entonces, aunque es un poco incómodo, puede convertir su declaración INSERT en una declaración MERGE, pero para hacerlo, necesita que el predicado MERGE sea siempre falso. Esto activará la cláusula WHEN NOT MATCHED y aplicará la única acción INSERT admitida allí. Puede usar una condición falsa ficticia como 1 =2. Aquí está el código convertido completo:

MERGE INTO dbo.T2 AS TGT
USING (SELECT keycol, datacol FROM dbo.T1 WHERE keycol % 2 = 1) AS SRC 
  ON 1 = 2
WHEN NOT MATCHED THEN
  INSERT(datacol) VALUES(SRC.datacol)
OUTPUT SRC.keycol AS T1_keycol, inserted.keycol AS T2_keycol;

Esta vez, el código se ejecuta correctamente y produce el siguiente resultado:

T1_keycol   T2_keycol
----------- -----------
1           1
3           2
5           3

Con suerte, Microsoft mejorará la compatibilidad con la cláusula OUTPUT en las otras declaraciones de modificación para permitir también la devolución de columnas de la tabla de origen.

Conclusión

¡No asumas y RTFM! :-)