SQL-Server: Listar todas las tablas de una base de datos junto al número de filas que tienen.

A veces me tengo que enfrentar «a ciegas» a extraer datos de una BD para pasarlos a otra con una estructura distinta (por ejemplo, migraciones de un software a otro). Una de las complejidades de esto es saber qué hay que traspasar, a veces te encuentras una base de datos con 500 tablas y lógicamente no vas a revisarlas una por una. Hay varias formas de comprobar el estado de las mismas, yo suelo utilizar esta consulta que ya tengo guardada, donde usando el procedimiento almacenado del sistema sp_MSforeachtable puedo listar los nombres de todas las tablas y, junto a este, el número de filas que contiene, de esa forma ya descarto todas las tablas que no tengan datos:

--Ponemos en uso nuestra BD
USE NombreDeNuestraBD

--Variable de tabla que almacenará los datos
DECLARE @CuentaFilas TABLE ([TableName] VARCHAR(128), [RowCount] INT) ;

--Consulta sirviéndonos del procedimiento almacenado
INSERT INTO @CuentaFilas ([TableName], [RowCount])
EXEC sp_MSforeachtable 'SELECT ''?'' [TableName], COUNT(*) [RowCount] FROM ?' ;

--Visualizamos los datos, en mi caso los ordeno por cantidad de datos
--porque las que tengan 0 o 1 filas no me interesan. Ordenad como queráis.
SELECT [TableName], [RowCount]
FROM @CuentaFilas 
ORDER BY [RowCount] desc

Exportar datos de PostgreSQL a un fichero CSV

En algún momento ya habíamos hablado por aquí del formato CSV (comma separated values), un estándar para compartir información mecánicamente legible que, a pesar de algunas limitaciones con la codificación de caracteres, es bastante popular pues, por ejemplo, nos permite pasar datos de un sistema gestor de bases de datos a una hoja de cálculo o viceversa.

El sistema gestor de bases de datos PostgreSQL nos permite exportar datos a CSV de forma muy sencilla, y además tenemos dos posibilidades: Exportar una tabla entera o exportar el resultado de una consulta.

El comando básicamente sería COPY loquesea TO ficherodesalida DELIMITER ‘caracterdelimitador’ CSV HEADER; siendo el caracter delimitador generalmente una coma, pero puedes cambiarlo por otro si lo necesitas para tu exportación (puede darse el caso de que los datos que exportes tengan textos con coma, por lo que igual te interesa un caracter más exótico). El loquesea del ejemplo sería o el nombre de una tabla o una consulta, mientras que ficherodesalida sería la ruta donde queremos que se cree nuestro CSV.

Veamos dos ejemplos. Imaginemos que queremos expotar todos los datos de una tabla que se llama Pacientes:

COPY Pacientes TO 'Users/Donato/Documents/Pacientes.csv' DELIMITER ',' CSV HEADER;

Esa instrucción generaría un fichero con el nombre que le hemos definido en la ruta que le hemos ordenado, conteniendo todos los valores de la tabla pacientes separados por comas. Ahora imaginemos que solo queremos los nombres y apellidos de los pacientes del año 2021 (tenemos un campo con el año en el que se dieron de alta), podríamos exportarlos usando una consulta:

COPY (Select Nombre, Apellidos from Pacientes where Alta = 2021) TO 'Users/Donato/Documents/Pacientes.csv' DELIMITER ',' CSV HEADER;

De esta forma exportaríamos solo esos datos que nos interesan, filtrados a través de esa consulta. Ahora podríamos abrir esos datos en Excel, Googl Sheet o LibreOffice Math, por ejemplo.

Cómo abrir los puertos para la instancia predeterminada de SQL-Server y el SQL Server Browser en un servidor Windows

El título es largo, pero la entrada va a ser corta. ¿Cómo abrimos los puertos del firewall de un servidor Windows para poder acceder desde otro equipo a la base de datos SQL-Server y trabajar con ella? A veces nos encontramos con que el cortafuegos de Windows nos está bloqueando y solo podemos trabajar con la base de datos conectados al propio servidor. Pues con estos dos comandos desde el PowerShell podemos configurarlo para abrir los puertos predeterminados:

New-NetFirewallRule -DisplayName "SQLServer default instance" -Direction Inbound -LocalPort 1433 -Protocol TCP -RemoteAddress LocalSubnet -Action Allow

New-NetFirewallRule -DisplayName "SQLServer Browser service" -Direction Inbound -LocalPort 1434 -Protocol UDP -RemoteAddress LocalSubnet -Action Allow

Si queremos bloquear el acceso de nuevo solo tendríamos que cambiar el valor que le pasamos al parámetro –Action, poniendo Block en lugar de Allow. El parámetro -RemoteAddress está para limitar en este caso que solo se pueda acceder desde la red local, en el caso de querer bloquear podéis omitirlo para que bloquee el puerto desde cualquier ubicación remota.

SQL-Server 2008 R2: «El valor de la línea de comandos INSTALLSHAREDWOWDIR no es válido. Compruebe que la ruta de acceso especificada es válida y diferente de la ruta de acceso de INSTALLSHAREDDIR»

Pues llevo unas horas pegándome con este problema. Ayer un compañero se puso a instalar un SQL-Server 2008 y se le ocurrió poner la misma ruta para los recursos compartidos de la versión de x86 y para la de 64 bits. Cuando llegué hoy a restaurar la base de datos que tenía que ir en ese equipo me encontré con que no podía restaurarla.

Decidí que lo mejor sería desinstalar el motor de base de datos y volver a instalarlo, pero me saltó el error del título: «El valor de la línea de comandos INSTALLSHAREDWOWDIR no es válido. Compruebe que la ruta de acceso especificada es válida y diferente de la ruta de acceso de INSTALLSHAREDDIR«. Como el tiempo apretaba y no acababa de encontrar en google ninguna solución para cambiar esas rutas pensé que la solución rápida sería instalar otra instancia con otro nombre y usar esa, dejando la primera mal instalada apagada, pero me encontré con que el instalador tampoco me permitía cambiar la ruta duplicada al iniciar una nueva instalación.

Busqué en Google pero no había casi pistas, de hecho el ejemplo que encontraba no solía ser para esto problema sino para otro en el que uno de los directorios está definido y el otro no, que no era el caso. La cosa era que si el programa de instalación estaba abriendo ya con una ruta cargada esa ruta tenía que estar definida en algún sitio. Fiándome de un foro busqué un fichero que se llamase config.ini o ConfigurationFile.ini sin suerte. Al final encontré esta entrada en otro blog donde alguien había tenido ese problema cuando intentó actualizar de un 2008 a un 2014.

La cosa es que la ruta está guardada en el registro del sistema, en concreto en la clave HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-518\Components\0D1F366D0FE0E404F8C15EE4F1C15094 para INSTALLSHAREDDIR y en la clave HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\ Components\C90BFAC020D87EA46811C836AD3C507F para INSTALLSHAREDWOWDIR. Decidí modificar todos los valores y cambiarlos por la ruta por defecto que suele usar SQL-Server para no tener ya más quebraderos de cabeza. Tras el cambio no tuve ni que reiniciar, volví a abrir el programa de instalación y me permitió desinstalar y volver a instalar correctamente.

Common Table Expression (CTE): la sentencia WITH en PostgreSQL.

Hace tiempo escribí una entrada sobre el uso de CTE en SQL-Server de Microsoft y no hace mucho en dicha entrada me preguntaron si podría usarse también una CTE en PostgreSQL. La respuesta es que sí, existe este recurso también en ese sistema gestor de bases de datos.

Sintaxis básica de una CTE

Recuerda, una CTE es un artefacto que nos permite mantener en memoria el resultado de una consulta para poder utilizarlo como si se tratase de una tabla o vista más.

En PostgreSQL usaría la misma sintaxis que en SQL-Server: tras la instrucción WITH definimos el nombre de la CTE y podemos opcionalmente agregar también los nombres que queramos dar a las columnas de la tabla que devolverá como resultado (si no lo hacemos entonces se usarán por defecto los nombres de las columnas que traigamos en la consulta). Dentro del cuerpo de la instrucción WITH definimos la consulta que queramos usar en la CTE. Finalmente ya podremos usar nuestra CTE dentro de una consulta. En fin, que lo básico sería tal que así:

WITH cte_name (column_list) AS (
    Select * from TablaQueNecesitemos
)

El mismo ejemplo que usé en la entrada sobre SQL-Server nos vale para aquí, una CTE sobre dos tablas (vendedores y ventas) para ver el total de ventas de cada uno ese año:

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
)  

Después podríamos usar esa CTE como una tabla más en cualquier consulta de SELECT, INSERT,UPDATE o DELETE. Recordad que como en SQL-Server no podremos usar cláusulas coo ORDER BY o FOR BROWSE, algo que tampoco tendría mucho sentido por otra parte.

¿Por qué usar una CTE?

La misma pregunta que nos hacíamos en el primer artículo. Hay motivos varios para usar una CTE con PostrgreSQL:

  • Hacer más legible y más entendible una consulta compleja, permitiéndonos prescindir de variables de tabla y subconsultas, consiguiendo un código más ordenado y limpio sin penalizar el rendimiento.
  • La posibilidad de crear consultas recursivas (un tema del que prometí escribir hace dos años y se me pasó)
  • La posibilidad de usar funciones de agregado o funciones ventana y poder operar con ellas después.

SQL-Server: Generar un número aleatorio entre dos valores

¿Cómo genero un número aleatorio en SQL-Server? Con la función RAND(), que nos devuelve un valor float pseudoaleatorio entre 0 y 1.

¿Y cómo puedo acotar esto entre dos valores? Pues redondeando el valor de multiplicar el resultado de la función RAND() por la diferencia entre el valor más alto y más bajo que queremos usar como límites y sumándole finalmente el valor más bajo. Que así puesto solo como texto parece más lioso de lo que es, la fórmula es muy sencilla y sería:

SELECT FLOOR(RAND()*(valorHasta-valorDesde)+valorDesde);

Siendo valorHasta el valor más alto y valorDesde el más bajo entre los que queremos obtener el valor aleatorio.

Imagina que queremos un valor aleatorio entre 100 y 200:

SELECT FLOOR(RAND()*(200-100)+100);

Operadores de conjunto en SQL-Server: UNION, INTERSECT y EXCEPT

Los operadores de conjunto UNION, INTERSECT y EXCEPT nos permite combinar en una misma salida el resultado de distintas consultas SELECT, construyendo así una consulta más compleja, lo que se llama una consulta compuesta. Para poder combinar dos consultas con estos operadores necesitamos que se cumplan dos requisitos:

  • Que ambas consultas devuelvan el mismo número de columnas.
  • Que estas columnas contengan el mismo tipo de datos, o al menos tipos de datos que se puedan convertir de forma implícita.

Estos tres operadores se incorporaron a SQL-Server a partir de la versión 2008 y están también disponibles en la base de datos SQL de la plataforma Azure. Los tres operadores aceptan además el parámetro ALL, que modificará ligeramente los resultados ¿Cómo funcionan y qué diferencia hay entre ellas?

EXCEPT:

Este operador encuentra la diferencia entre las dos consultas y devuelve las filas que pertenecen únicamente a la primera consulta. Es decir, si una tupla aparece tanto en la consulta de la izquierda como en la de la derecha no será incluida en el resultado final. Si aparece solo en la de izquierda y en la de la derecha no, entonces será devuelta una vez.

Si añadimos ALL al EXCEPT notaremos una pequeña diferencia. Al igual que con el EXCEPT a secas el operador buscará la diferencia entre las dos consultas, pero los datos devueltos cambian. En este caso si una tupla aparece un número m de veces en la primera consulta, y la misma tupla aparece un número n veces en la segunda consulta, entonces esa tupla aparece m – n veces en la respuesta de salida, si dicha resta es mayor que 0.

/*consultamos una tabla de Productos 
y sacamos todos los resultados únicos 
que no que existen en la consulta sobre la tabla Almacén*/

Select Descripcion, Codigo from Productos
EXCEPT
Select Descripcion, Codigo from Almacen

/*Con ALL si la tupa Descripción,Codigo existiese tres veces
en el resultado de la primera consulta y una vez en la segunda
entonces en el resultado final saldría dos veces*/
Select Descripcion, Codigo from Productos
EXCEPT ALL
Select Descripcion, Codigo from Almacen

INTERSECT:

Este operador combina los resultados de dos consultas en un único resultado que comprime todas las filas comunes para ambas consultas. Es decir, funcionaría como un AND lógico: devuelve solo las ocurrencias existentes en ambas consultas.

Si añadimos ALL a este operador el resultado también cambiará. En ese caso si una tupla aparece un número m de veces en el resultado de la primera consulta y la misma tupla aparece n veces en la segunda, entonces esa tupla aparece el menor número de entre m o n en la respuesta de salida.

/*consultamos una tabla de Productos 
y sacamos todos los resultados únicos 
que también existen en la consulta sobre la tabla Almacén*/

Select Descripcion, Codigo from Productos
INTERSECT
Select Descripcion, Codigo from Almacen

/*Con ALL si la tupa Descripción,Codigo existiese tres veces
en el resultado de la primera consulta y una vez en la segunda
entonces en el resultado final saldría una vez solo*/
Select Descripcion, Codigo from Productos
INTERSECT ALL
Select Descripcion, Codigo from Almacen

UNION:

Finalmente vamos con UNION. Si antes os decía que INTERSECT funciona como un operador lógico AND entonces UNION funcionaría como un operador lógico OR. Devuelve las filas únicas que existen o en la consulta de la izquierda o en la de la derecha.

En este caso el operador ALL lo que hará será modificar el resultado del UNION de forma que en lugar de recibir solo las filas únicas recibamos tantas filas como haya en la primera consulta y en la segunda, un poco la operación contraria a la que realiza EXCEPT ALL. En este caso si una tupla aparece un número m de veces en la primera consulta, y la misma tupla aparece un número n veces en la segunda consulta, entonces esa tupla aparece m + n veces en la respuesta de salida.

/*consultamos una tabla de Productos 
y la tabla Almacén y sacamos los resultados únicos, distintos*/

Select Descripcion, Codigo from Productos
UNION
Select Descripcion, Codigo from Almacen

/*Con ALL si la tupa Descripción,Codigo existiese tres veces
en el resultado de la primera consulta y una vez en la segunda
entonces en el resultado final saldría cuatro veces*/
Select Descripcion, Codigo from Productos
UNION ALL
Select Descripcion, Codigo from Almacen

La función lógica CHOOSE() en SQL-Server

La función lógica CHOOSE() se añadió a SQL-Server desde la versión 2012, y su funcionamiento es similar a recuperar valores de un array. Es una función que recibe al menos tres valores: el primero, que será un valor índice, y tras él una serie de valores separados por comas, requeríendose al menos dos. La función devolverá el valor de la lista que coincida posicionalmente con el valor del índice.

Veámoslo con un ejemplo simple:

--Esto devolvería "Pringao" por ser el tercer valor:
SELECT CHOOSE ( 3, 'Jefazo', 'Jefecillo', 'Pringao', 'Becario' ) AS TuPuesto  

Lógicamente el primer valor no tiene por qué ser una constante, puede salir de una consulta. Veámoslo en un ejemplo similar al de arriba:

--Si el IdPuesto es 1 devuelve "Jefazo", si es 4 devuelve "Becario"
SELECT IdPuesto, CHOOSE ( IdPuesto, 'Jefazo', 'Jefecillo', 'Pringao', 'Becario' ) AS NombrePuesto from Plantilla  

O de una variable:

SELECT @Puesto, CHOOSE ( @Puesto, 'Jefazo', 'Jefecillo', 'Pringao', 'Becario' ) AS NombrePuesto from Plantilla 


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).

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.