Common Table Expression (CTE): la sentencia WITH de SQL-Server

Las expresiones de tabla común (common table expression o CTE) fueron añadidas por Microsoft a SQL-Server a partir de la versión 2008. Cuando hablamos de CTE hablamos de un artefacto que nos mantiene en memoria el resultado de una consulta, que podremos llamar luego dentro de esa misma consulta. Esta cláusula también se puede utilizar en una instrucción CREATE VIEW como parte de la instrucción SELECT que la define.

Sintaxis de una CTE

La sintaxis básica de una CTE sería la siguiente:

WITH nombreDeNuestra_CTE (Columna1, Columna2, Columna3)  
AS  
-- Aquí definimos la consulta que la crea.  
(  
    SELECT Columna1, Columna2, Columna3
    FROM BaseDatos.dbo.Tabla
    Where Condicion=Condicion
)  

Por ejemplo, imagina que tenemos una tabla con vendedores y otra con sus ventas, donde el Id del vendedor actúa como clave externa en cada línea de ventas. Podemos usar una CTE para tener un resultado temporal con su total de ventas de 2018. Algo así:

WITH Ventas_CTE (IdVendedor, NombreVendedor, TotalVendido)  
AS  
(  
    SELECT 
      v.Id as IdVendedor, 
      v.Nombre as NombreVendedor, 
      SUM(vt.Importe) AS TotalVentas 
    FROM Vendedores v inner join ventas vt on v.Id = vt.IdVendedor
    WHERE YEAR(vt.Fecha)=2018
)  

Ahora podríamos usar la CTE del ejemplo de arriba en una consulta como si de una tabla normal se tratase. Por ejemplo, podríamos sacar todos los departamentos donde haya vededores que hayan superado los 100.000 euros en ventas haciendo un join contra una tabla de departamentos:

SELECT Distinct
  d.Nombre
FROM
  Departamentos d
inner join
  Departamentos_Vendedores vd
on
  d.Id = vd.Idep
inner join
  Ventas_CTE v
on
  v.IdVendedor = vd.IdVend
where
  vd.TotalVendido > 100000

Hay que recordar que las cláusulas INTO, ORDER BY, FOR BROWSE y OPTION no están permitidas en una consulta de definición de una CTE. Sí permite el uso de operadores de conjuntos como UNION ALL, UNION, INTERSECT o EXCEPT y también permite referencias a tablas externas, e incluso a tablas situadas en servidores remotos.

En el ejemplo hemos usado un SELECT después de crear nuestra CTE, pero podríamos haber usado una instrucción INSERT, DELETE o UPDATE también. Incluso la podemos utilizar para definir un cursor.

¿Por qué usar una CTE?

Aparte del potencial que tienen las CTE por permitir crear una CTE recursiva, cosa de la que ya hablaremos más adelante en otro artículo, las CTE principalmente nos permiten sustituir a subconsultas y a variables de tabla.

En el caso de las subconsultas realmente la CTE no nos da ninguna ventaja en el rendimiento, pero sí nos permitirá tener un código más ordenado y más manejable, lo que facilita la legibilidad del mismo y las tareas de mantenimiento.

En el caso de la variables de tipo tabla ahí sí que las CTE nos dan un rendimiento mejor en la consulta, por lo que siempre serán una opción más recomendable.

También uno podría pensar en cambiar una Vista por una CTE. Si se trata de conjuntos de datos muy grandes, con muchas líneas, la Vista tendrá un rendimiento mayor por tratarse de un objeto creado en la base de datos que no permite definir índices (ya hablamos aquí sobre cuándo esto es recomendable y cuándo no).

Anuncios

SQL-Server: Usar CASE en una cláusula ORDER BY

La ordenación de resultados en una consulta SQL suele ralentizar la respuesta de la consulta, aunque en muchos casos necesitamos tener nuestro resultado ordenado. Hoy me veía con este caso particular, en una aplicación que conecta con una base de datos de SQL-Server: en una pantalla se muestran una serie de líneas de deuda, algunas haciendo referencia a tratamientos ya realizados y otras a tratamientos pendientes. En caso de que estén realizados habría que ordenarlos por fecha de realización, en caso de que no estén realizados sería por fecha de creación.

Para eso podemos utilizar una sentencia CASE en la cláusula ORDER BY:

SELECT 
  FechaCrea,   
  Concepto,
  Precio,
  ImportePagado,
  Realizado,
  FechaRealizado 
FROM 
  vistaPagos  
WHERE 
  Precio > 0 
ORDER BY 
 CASE Realizado WHEN True THEN FechaRealizado  
 ELSE FechaCrea END;  

Veamos ahora otra posibilidad de uso del CASE: cuando queremos que la consulta reciba un parámetro con el campo por el que ordenar los resultados:

SELECT 
  FechaCrea,   
  Concepto,
  Precio,
  ImportePagado,
  Realizado,
  FechaRealizado 
FROM 
  vistaPagos  
WHERE 
  Precio > 0 
ORDER BY 
CASE @OrdenaPor 
   WHEN 'Fecha' THEN FechaCrea
   WHEN 'FReal' THEN FechaRealizado       
 END,
CASE @OrdenaPor 
   WHEN 'Precio' THEN Precio
   WHEN 'Pendiente' THEN ImportePagado   
 END
;  

¿Por qué hay dos CASE separados en el segundo ejemplo? Bueno, CASE necesita que los tipos devueltos en la expresión sean compatibles. En el primer caso devolverá fechas, en el segundo devolverá importes. En caso de que no se cumpla ninguna de las condiciones devolverá un null, así que no debería hacer fallar la consulta.

Algunos consejos para optimizar consultas en SQL-Server

Vamos con una serie de pequeños trucos para optimizar nuestras consultas en SQL-Server y conseguir un mejor rendimiento, como ya habíamos hecho en el pasado con MySQL.

  • Añadir al nombre de la tabla el propietario y el esquema hará más rápida nuestra consulta. Si no lo ponemos SQL-Server buscará en todos los esquemas hasta encontrar el objeto.
  • Un clásico: No usar el comodín * en las consultas, poner el nombre de las columnas que queremos traer ayudará a ahorrar tiempo y memoria.
  • Como ya dijimos en un artículo anterior, NOT EXISTS tiene un mejor rendimiento que NOT IN.
  • No uses el prefijo sp_ para nombrar tus procedimientos almacenados. Aunque suene raro, al ser el prefijo que SQL-Server usa para los procedimientos almacenados por defecto siempre que se invoque uno que empieza por sp_ primero lo buscará en la base de datos maestra.
  • Comprueba que tus índices sean eficientes y que no estén demasiado saturados. Recuerda usar las claves primarias y externas de forma adecuada y tener la bases de datos bien normalizadas
  • Recuerda que las vistas con índices, como ya comentamos en otro artículo, mejoran la velocidad de consulta pero penalizan las operaciones de borrado, lectura y escritura.
  • No uses variables de tipo table en los joins de las consultas. Una tabla temporal o una expresión de tabla común (CTE) te darán un mejor rendimiento.
  • No utilices las cláusulas Distinct, Group By y Order By si no es indispensable. Consumen mucha memoria.
  • SQL-Server siempre devuelve la cuenta del número de filas afectado por las consultas de INSERT, DELETE, UPDATE y SELECT. Utilizar la cláusula SET NOCOUNT ON evitará esto ahorrando memoria y tiempo. En una consulta simple apenas se nota, pero en consultas con muchos joins o subconsultas ahorra mucho tiempo.
  • Las funciones son uno de los puntos débiles de SQL-Server, ralentizan enormemente las consultas. La ventaja que aportan es la reutilización de código, pero tienes que ver si te compensa su uso por lo que penalizan el rendimiento. En muchos casos una subconsulta o una tabla temporal serán más rápidas.

Borrar un trigger solo si este existe, en SQL-Server

El otro día vimos cómo hacerlo con una tabla, hoy vamos a ver cómo borrar un trigger de una base de datos, pero solo en caso de que exista (para evitar mensajes de error en nuestros scripts).

De nuevo, como en el ejemplo anterior, antes de SQL-Server 2016 se hacía de una forma y a partir de esa versión tenemos un sintaxis simplificada:

Versiones anteriores a SQL-Server 2016:

Tendríamos que hacer una consulta para ver si existe el trigger y luego lo eliminaríamos:

IF EXISTS (SELECT * FROM sys.triggers WHERE name = 'trelTriggerQueBorraremos')  DROP TRIGGER trelTriggerQueBorraremos

De SQL-Server 2016 en adelante:

DROP TRIGGER IF EXISTS trelTriggerQueBorraremos

Borrar una tabla solo si esta existe, en SQL-Server

El caso que vamos a tratar es el siguiente. Estamos creando un script para SQL-Server y queremos que si existe una tabla esta se borre. ¿Cómo lo hacemos? Dependiendo de la versión de nuestro SGBD lo haremos de una forma u otra:

SQL-Server anterior a la versión 2016:

Si trabajamos con una versión de SQL-Server anterior a 2016 (yo normalmente trabajo con SQL-Server 2008 R2 en la mayoría de los clientes de mi empresa) la sintaxis más correcta sería la siguiente:

IF OBJECT_ID('dbo.TablaQueQueremosBorrar', 'U') IS NOT NULL 
  DROP TABLE dbo.TablaQueQueremosBorrar; 

Como puedes ver, comprobamos si existe el nombre de la tabla a borrar y si la consulta devuelve algo distinto de NULL ejecutamos el borrado.

De SQL-Server 2016 en adelante:

Para SQL-Server 2016 se simplificó este paso con una nueva instrucción, que podríamos resumir como DIE (Drop If Exists)

DROP TABLE IF EXISTS dbo.TablaQueQueremosBorrar


Conexión con base de datos SQLite en el lenguaje R

El lenguaje de programación R es tremendamente popular entre los matemáticos por estar desarrollado enfocado al análisis estadístico y por ser software libre (licencia GNU/GPL). Y siendo un lenguaje orientado a la estadística la lógica nos dicta que tiene que ser posible conectar con una base de datos.

Por suerte se han desarrollado librerías que sirven como interfaz para trabajar con la mayoría de los sistemas gestores de bases de datos más populares del mercado: SQL-Server, MySQL, PosgreSQL, Oracle y el caso que hoy nos ocupa, SQLite. Principalmente dispones de tres librerías para estas conexiones: ODBC (un standar desarrollado por Microsoft), DBI (basado en DBI de Perl y adaptado de forma nativa para R) y dplyr (otra aproximación nativa profundamente integrada). En este artículo vamos a ver cómo hacerlo con DBI, puesto que es la solución que me han recomendado más programadores de R veteranos.

El primer paso, claro, será instalar los paquetes de la librerías necesarias:

install.packages(c("DBI", "RSQLite"))

Y una vez instalados lo siguiente simplemente es añadir las librerías a nuestro código y configurar una conexión:

library(DBI)
library(RSQLite)

# Definimos el driver
driver <- dbDriver("SQLite")

#realizamos la conexión con dbConnect()
#Esta función recibe como primer parámetro el driver o un objeto
#de conexión ya existente, y como segundo parámetro todos los
#parámetros de conexión que requiere nuestra base de datos.
#SQLite solo necesita la ruta al fichero de la base de datos
#Otros SGBD necesitarán más
archivo_sqlite <- system.file("home/database.sqlite")

conexion <- dbConnect(driver, archivo_sqlite)

#Pdemos ejecutar consultas con dbSendQuery()
#la función recibe el objeto conexión y una consulta SQL.
resultado <- dbSendQuery(conexion, "SELECT * FROM baseDatosEjemplo")

#Para cerrar la conexión con la base de datos
dbDisconnect(conexion)

MySQL: Formateando fechas con DATE_FORMAT()

La función DATE_FORMAT() de MySQL, que fue incorporada en la versió 4.0 del conocido SGBD, nos permite dentro de un select definir el formato de salida de una fecha. La función recibe dos parámetros: una fecha o campo de fecha y una cadena de texto con el formato.

Los siguientes valores pueden ser usados en el formateo de fecha:

%aNombre del día abreviado (Sun a Sat)
%bNombre de mes abreviado (Jan a Dec)
%cMes en formato número (0 a 12)
%DDía del mes en formato numérico seguido de sufijo numeral (1st, 2nd, 3rd, …)
%dDía del mes en formato numérico, forzando dos dígitos  siempre ( de 01 a 31)
%eDía del mes en formato numérico, sin forzar dos dígitos (1 a 31)
%fMicrosegundos (000000 to 999999)
%HHora en formato 24 horas, con dos dígitos
%hHora en formato 12 horas, dos dígitos
%IHora en formato 12 horas, dos dígitos
%iMinutos
%jDía del año (001 a 366)
%kHora en formato 24 horas, sin forzar los dos dígitos
%lHora en formato 12 horas, sin forzar los dos dígitos
%MNombre del mes (January a December)
%mMes en formato numérico, forzando dos dígitos.
%pAM o PM
%rHora en formato 12 horas AM o PM (hh:mm:ss AM/PM)
%SSegundos (00 a 59)
%sSegundos (00 a 59)
%THora en formato 24 horas (hh:mm:ss)
%USemana, tomando el domingo como primer día (00 a 53)
%uSemana, tomando el lunes como primer día (00 to 53)
%VSemana, tomando el domingo como primer día (01 a 53). Usado con %X
%vSemana, tomando el lunes como primer día (01 a 53). Usado con %X
%WNombre del día completo (Sunday to Saturday)
%wNúmero del día en la semana, siendo el domindo 0 y el sábado 6.
%XAño para la semana, tomando el domingo como primer día. Usado con %V
%xAño para la semana, tomando el lunes como primer día. Usado con %V
%YAño en formato de cuatro dígitos
%yAño en formato de dos dígitos

Veamos unos ejemplos si quieres probarlo en casa:

Select DATE_FORMAT("2018-11-20", "%d/%m/%Y"); 
#Pintaría 20/11/2018, el formato europeo
Select DATE_FORMAT("2018-11-20", "%V %X"); 
#Pintaría 46 2018
Select DATE_FORMAT("2018-11-20", "%j-%y"); 
#Pintaría 324-2018
Select DATE_FORMAT("2018-11-20", "%W %d %M"); 
#Pintaría Tuesday 20 November

En el ejemplo le hemos pasado una cadena con una fecha como primer parámetro, pero puedes probar una consulta sobre un campo de una tabla que almacena alguna fecha.