12 may 2013

Concatenar varios registros en una sola columna

Supongamos que, en SQL Server, tenemos una tabla de proveedores y una tabla aparte donde se almacenan los números de teléfonos de dichos proveedores como registros separados. De modo que tendríamos algo similar a lo siguiente:

Proveedor:
codProveedor desRazonSocial ...
1 Proveedor de Prueba 1 ...
2 Proveedor de Prueba 2 ..

ProveedorTelefono:
codProveedor desTelefono
1 88881685
1 89000000
1 98012012
2 78912000
2 12345678

Supongamos que una de las necesidades de nuestro cliente es que mostremos, en un grid o tabla, cada proveedor con todos sus teléfonos en una sola columna separados por comas.

Como para con casi todo en la vida, hay varias formas de hacerlo; podríamos traernos todo en dos consultas separadas (o en una sola por medio de joins, con la información de los proveedores repetida) y ya en nuestra aplicación, por medio de un bucle, concatenar los teléfonos en un solo campo, o usar un store procedure con un cursor y hacer mas o menos lo mismo que en el punto anterior pero en el motor de base de datos o tantas otras aproximaciones.

También se puede utilizar la combinación de una función y una consulta de la siguiente manera:

Primero creamos una función para devolver los teléfonos concatenados por proveedor:
CREATE FUNCTION [dbo].[fnTelefonosProveedor] (@codProveedor int)
RETURNS varchar(600)
AS
BEGIN 
 DECLARE @listaTelefonos varchar(600)
 SET @listaTelefonos = null
 
 SELECT  @listaTelefonos = COALESCE(@listaTelefonos, '') +
 	CASE
   		WHEN @listaTelefonos IS NULL THEN ''
  	ELSE
   		', '
  	END
  + T.desTelefono
 FROM ProveedorTelefono T
 WHERE T.codProveedor = @codProveedor

 RETURN @listaTelefonos 
END
Esta función que recibe por parámetro el código del proveedor y retorna los teléfonos concatenados en campo varchar de hasta 600 caracteres. La combinación de la sentencia SELECT con el COALESCCE [si el primer parámetro es nulo devuelve el segundo parámetro] hace la magia de concatenar los teléfonos en una sola variable (@listaTelefonos) separados por coma.

Finalmente utilizamos nuestra función dentro de nuestra consulta:
SELECT P.conCodigoProveedor, P.nomRazonSocial, 
	dbo.fnTelefonosProveedor(P.codProveedor) AS desTelefonos  
FROM Proveedor P
Obteniendo algo similar a

codProveedor desRazonSocial desTelefonos
1 Proveedor de Prueba 1 88881685, 89000000, 98012012
2 Proveedor de Prueba 2 78912000, 12345678

Y ya con esto concatenamos varios registros en una sola columna...y es fácil de traducir a otros motores de base de datos.

1 may 2013

Count Incorrecto

Alguna que otra ves encontramos comportamientos extraños en algunas aplicaciones. A veces es un poco de desconocimiento, en otras malas prácticas y en otras algo de ambas.

Me encontré una aplicación en que en una de sus características simplemente debía retornar la cantidad de registros dado un campo de la tabla como filtro. Y el dato devuelto estaba erróneo. ¿Cómo algo tan simple y básico puede estar equivocado? 

Veamos un ejemplo: supongamos que tengo una Tabla con los siguientes registros:

Ahora realicemos un simple count(*) de los registros cuya Ubicacion sea 'CA'.


Cinco registros, esto es correcto. Pero todos sabemos que no es una buena práctica utilizar el asterisco(*) en una consulta por lo ineficiente que resulta la resolución de la misma. Entonces elegimos un campo para realizar el mismo count, digamos Descripcion.
Y el resultado es... erróneo. 

¿Por qué? Simplemente por que la implementación del count excluye de la cuenta los valores nulos resultantes de evaluar la expresión que estemos usando como parámetro para cada columna resultante del filtro. Así bien, existen dos registros con Ubicación 'CA' cuya Descripcion (la expresión a evaluar) es null y éstos no son tomados en cuenta en el resultado.

La solución es simple, utilicemos una columna en el que no haya posibilidad de valores nulos. En este particular el campo llave. O También podemos usar una expresión, por ejemplo 1, la cual evaluada por cada columna siempre será distinta de null.

Una ultima reflexión si somos un poco más inquisitivos y nos preguntamos ¿Cual de las tres sentencias que arrojan el resultado correcto es la más óptima? Podemos referirnos al plan de ejecución de cada sentencia.

Como podemos notar los tres planes de ejecución son exactamente iguales, entonces, podemos decir que las tres sentencias son equivalentes, por lo que, en este caso en particular, el usar el count(*) no significa mayor perdida de eficiencia de la consulta.

En lo personal prefiero el count(1) ya que no queda sujeta a ninguna columna de la tabla y su evaluación no depende de la implementación del motor de la base de datos.