12 febrero 2013

TALLY TABLE: una excelnte alternativa a ciclos en SQL

Este post lo voy a dedicar a una alternativa poco conocida a la utilización de los ciclos en SQL, la TALLY TABLE. Como siempre en mis post no me gusta dar toda la información, en la red hay demasiada información como para repetir lo mismo una y otra vez. La intención es dar a conocer el uso de este recurso y fomentar la investigación y la imaginación.

Ciclos; en algún momento de nuestro trabajo debemos de utilizarlos ya que son una estructura de programación básica. Pero en consultas SQL no siempre son la mejor opción al momento de codificar una rutina, podrían ralentizar nuestro proceso, tal vez en escenarios con pocos datos todo marche sobre ruedas, pero al momento de realizar pruebas con altos volúmenes de información puede afectarnos gravemente. Alternativas, bueno pues en realidad depende mucho de lo que se vaya a realizar, pero una muy buena son las TALLY TABLES.

Bueno, y a todo esto que son las TALLY TABLE. Las TALLY TABLE son tablas que cuentan con una sola columna, la cual se recomienda indexar. En esta columna se ingresan números en forma ordenada a partir de 0 o 1, hasta un número que nos sea de utilidad. Ejemplo: si trabajaremos con una rutina que necesite recorrer todos los caracteres de un campo varchar, deberíamos de llenarla del 1 hasta el 8000, que es el número máximo de valores que puede ser ingresado en este campo con este tipo de datos, no es recomendable llenarla de más.

Basta de rollo y vamos a la acción.

Antes de ejemplificar como emplear estas tablas debemos de crearlas y llenarlas. En Internet hay muchos recursos en los cuales están documentados como crear de manera muy eficiente estas tablas, aquí les dejo una, igual y se les ocurre una forma mejor.


Para llenarla podríamos utilizar un ciclo, pero ¿qué no es lo que estamos tratando de evitar? por lo tanto vamos a llenarla utilizando un CROSS JOIN

SELECT TOP 8000 --cantidad de caracteres máximo en un campo varchar
     IDENTITY(INT,1,1) AS N
INTO dbo.Tally
FROM Master.dbo.SysColumns sc1,
    Master.dbo.SysColumns sc2

Agregamos el indice cluster a la tabla.

ALTER TABLE dbo.Tally
ADD CONSTRAINT PK_Tally_N
PRIMARY KEY CLUSTERED (N) WITH FILLFACTOR = 100


Listo, ya tenemos nuestra tabla llena, vamos a ponerla en práctica y a compararla con un ciclo normal.

Ejemplo 1

Obtener las posiciones de un caracter en especifico en una cadena

Primero hagamoslo con un ciclo normal


DECLARE @Variable VARCHAR(8000)
SET @Variable = ',Elemento01,Elemento02,Elemento03,Elemento04,Elemento05,'
DECLARE @N INT
SET @N = 1      

WHILE @N <= LEN(@Variable)
  BEGIN
      IF SUBSTRING(@Variable,@N,1) = ','
         BEGIN
               SELECT @N, SUBSTRING(@Variable,@N,1)
         END
           SET @N = @N + 1
  END


Si analizamos el proceso veremos que el select se ejecuta 6 veces, una vez por cada coma que se encuentre en la cadena. Ahora hagamos el ejemplo con una TALLY TABLE.


DECLARE @Variable VARCHAR(8000)
SET @Variable = ',Elemento01,Elemento02,Elemento03,Elemento04,Elemento05,'

 SELECT N, SUBSTRING(@Variable,N,1)
   FROM dbo.Tally
  WHERE N <= LEN(@Variable)
    AND SUBSTRING(@Variable,N,1) = ','
  ORDER BY N


Con esta solución solamente ejecutamos el select una sola vez. La tabla se utiliza para recorrer cada uno de los caracteres del parámetro y despliega las filas que contienen el carácter buscado.

Ejemplo 2

Hagamos con esto algo más interesante, realicemos el split de la cadena separada por comas.

DECLARE @Variable VARCHAR(8000)
SET @Variable = 'Elemento01,Elemento02,Elemento03,Elemento04,Elemento05'
set @Variable = ',' + @Variable + ','

SELECT SUBSTRING(@Variable,N+1,CHARINDEX(',',@Variable,N+1)-N-1)
FROM dbo.Tally
WHERE N < LEN(@Variable)
AND SUBSTRING(@Variable,N,1) = ',' 

Ejemplo 3

Si con este ejemplo no quedamos convencidos les invito a hacer uno con más registros y veremos la magia.

Les voy a ayudar un poco, les dejo el ejemplo con la TALLY TABLE, les queda hacer el ejemplo con un While, que no creo que batallen y comparen el tiempo


DECLARE @Variable VARCHAR(8000)
SET @Variable = 'Elemento01,Elemento02,Elemento03,Elemento04,Elemento05,'
SET @Variable = REPLICATE('Element01,Element02,Element03,Element04,Element05,',159)
set @Variable = ',' + @Variable 
SELECT SUBSTRING(@Variable,N+1,CHARINDEX(',',@Variable,N+1)-N-1)
FROM dbo.Tally
WHERE N < LEN(@Variable)
AND SUBSTRING(@Variable,N,1) = ',' 


¿Muy impresionante no? No tarda ni un segundo, la TALLY TABLE realmente ayuda a realizar más rápido este tipo de procesos. ¿Tienen alguna aplicación para una TALLY TABLE? ponganlo en los comentarios y compartan con los demás.

Espero les haya gustado este post y les sea de utilidad.

Si les gusta compartenlo.

Hasta la próxima semana.

Carlos E. Rodríguez

02 febrero 2013

CROSS APPY SQL 2005 Y 2008

El día de hoy comparto con ustedes una instrucción de SQL Server 2005 que me ha sido de mucha ayuda al momento de crear tablas compuestas: CROSS APPLY.

La finalidad de este articulo es mostrar algunas de sus aplicaciones, dando oportunidad al lector investigar a fondo la instrucción. Al final del articulo dejo algunas ligas de interés con más información al respecto.

Como habremos de imaginar por su nombre, la instrucción CROSS APPLY nos permite crear una tabla compuesta (un set de resultados combinando columnas de dos o más tablas ).

La principal caracteristica de esta instrucción a diferencia de las instrucciones JOIN (CROSS JOIN, INNER JOIN, OUTER JOIN) es que nos permite generar una tabla compuesta a partir de una tabla y una función de tipo tabla.

¿Y esto para que me sirve dirían muchos?, pues de bastante, pongo un ejemplo bastante sencillo que creo que se demostrará la capacidad de esta instrucción.

Ejemplo


Tabla: ModificacionesFactura
Factura FechaCreacion FechaCorte
1001 10/10/2012 10/10/2011
1001 11/10/2012 10/10/2013
1003 12/10/2012 10/10/2012

Supongamos que necesitamos obtener la fecha menor entre el campo FechaCreacion y el campo FechaCorte:

Para lograrlo podríamos hacer algo así


select Factura,
case when FechaCreacion < FechaCorte then FechaCreacion
else FechaCorte end FechaMenor
From ModificacionesFactura
Hasta aquí todo bien, sin problemas. Ahora a poner una pequeña traba, ¿qué pasa si queremos hacer algo más con la FechaMenor que acabamos de obtener?, como por ejemplo saber la diferencia de días entre el día de hoy y la fecha menor. En este caso tendríamos que repetir la instrucción para obtener de nuevo la fecha menor y obtendríamos algo como esto:


select Factura
case when FechaCreacion &lt FechaCorte then FechaCreacion
else FechaCorte end FechaMenor,
DATEDIFF(dd,case when FechaCreacion < FechaCorte then FechaCreacion
else FechaCorte end FechaMenor, GETDATE()) Diferencia
From ModificacionesFactura

Alternativa con CROSS APPLY

Con el uso de CROSS APPLY podríamos reducir la consulta anterior de esta manera:


SELECT Factura,
FechaMenor,
DATEDIFF(dd, A.FechaMenor, GETDATE()) Diferencia
FROM ModificacionesFactura
CROSS APPLY
(
     SELECT case when FechaCreacion < FechaCorte then FechaCreacion            
     else FechaCorte end FechaMenor
) A

De esta manera queda más limpio nuestro SELECT ya que pasamos al cálculo de la fecha menor al CROSS APPLY. Incluso podemos utilizar el campo A.FechaMenor en la clausula Group By directamente lo cual, de otra manera nos llevaría muchas más lineas de código.

La instrucción CROSS APPLY ejecuta para cada  registro de la tabla, la función de tipo tabla que especificamos. En este caso la función regresa un solo registro para cada registro de la tabla, pero podría darse el caso en el que regrese más de uno. Incluyo una liga donde se puede ver el ejemplo fácilmente:

http://www.sqlteam.com/article/using-cross-apply-in-sql-server-2005


Limpiando todavía más la consulta

Esta instrucción puede quedar todavía más limpia si pasamos la lógica para obtener la fecha menor a una función de tipo tabla.

La cual podría quedar de la siguiente manera:


CREATE FUNCTION dbo.ObtenerFechaMenor(@Fecha1 as datetime, @Fecha2 as datetime)
RETURNS Table
AS
RETURN
SELECT case when @Fecha1 < @Fecha2 then @Fecha1
             else @Fecha2 end FechaMenor
GO 

Nuestro query podría quedar de la siguiente manera:


SELECT Factura,
            FechaMenor,
            DATEDIFF(dd, A.FechaMenor, GETDATE()) Diferencia
FROM ModificacionesFactura
CROSS APPLY dbo.ObtenerFechaMenor(FechaCreacion, FechaCorte) A

Alguien podría decir, ¿para que uso funciones definidas por el usuario si no son muy óptimas al momento de consultar grandes cantidades de registros?, y esto es verdad, pero un detalle interesante de la instrucción CROSS APPLY es que, las instrucciones dentro de la función se toman al generar el plan de ejecución de nuestro query, lo cual aumenta la velocidad de nuestra consulta en la mayoria de los casos. Cabe aclarar que siempre debemos realizar pruebas de desempeño comparando diversas opciones para que tengamos argumentos de porque realizamos la consulta de tal o cual manera.

Espero que les haya gustado este breve artículo y les haya dado curiosidad para investigar más acerca de esta instrucción. Para mi ha sido muy útil al momento de reutilizar y reducir código, algo que a veces en SQL Server es un poco complicado de hacer. 

Si les gusta compartan este articulo y dejen sus puntos de vista.

Hasta luego.

Carlos E. Rodríguez

ligas de interés:

http://geeks.ms/blogs/lfranco/archive/2011/09/26/funciones-escalares-en-tsql-joins-cross-apply-y-la-madre-que-pari-243-al-topo.aspx

http://social.msdn.microsoft.com/Forums/en/transactsql/thread/796821b0-ba48-49b4-ba42-1783061d8c41 

http://amby.net/2010/03/29/ejemplo-de-empleo-de-clausula-cross-aply-en-t-sql-2005-y-superior/ 

http://msdn.microsoft.com/es-es/library/ms175156%28v=sql.105%29.aspx