2013/06/26

CTEs RECURSIVOS (1)

Que tal, hoy les explicaré como generar una tabla temporal de fechas que puede serles de mucha utilidad al momento de realizar ciertas consultas; en una ocasión anterior les mostré como crear un CTE ( Common TableExpression ), en esta ocasión nos apoyaremos en este recurso para poder realizar la recursión.

Que es lo que necesitamos, un inicio, que será la base y el comienzo a partir del cual nuestro CTE se ejecutará:
WITH miCTE AS (
       SELECT CAST('20130101' AS DATE) as fecha
)
SELECT * FROM miCTE
Si ejecutamos esto obtendremos:

Lo cual no se parece en nada a lo que necesitamos, ahora bien suponiendo que queremos obtener los primeros 10 días del mes de Enero,  el final de nuestra recursión será el día 2013-01-10 además de agregar una operación que vaya incrementando nuestra, por decirlo de alguna manera, variable ( en este caso sería valor de columna ) y no olvidemos llamar a nuestro CTE que estamos creando ya que si no lo hacemos, simplemente no será RECURSION, algo como esto:
WITH miCTE AS
(
       SELECT CAST('20130101' AS DATE) as fecha
       UNION ALL
       SELECT DATEADD( d , 1 ,fecha ) as fecha
       FROM miCTE
       WHERE fecha < CAST('20130110' AS DATE)
)
SELECT * FROM miCTE
Es muy importante la instrucción UNION ALL para la ejecución de este query, también debemos recordar que es muy importante que la cantidad de columnas en ambos SELECT debe ser el mismo, así como el tipo de datos. Ahora bien, ejecutamos nuestra consulta:
Tal vez se preguntarán, porque aparece el día 10 si el operador es un  < ( menor que )  y no un <= ( menor igual que ), ésta forma de hacer la recursión es como en algunos lenguajes de programación funciona el bucle DO WHILE, primero ejecuta todo el código contenido en él y después verifica si debe continuar dentro del bucle o salirse de él, en este caso primero realiza la suma de un día, antes de validar la condición.

Otro ejemplo claro de esto, es que en ocasiones es necesario los meses de cierto periodo, pero solo los meses, por ejemplo:
WITH miCTE AS
(
       SELECT CAST('20120601' AS DATE) as fecha
       UNION ALL
       SELECT DATEADD( m , 1 ,fecha ) as fecha
       FROM miCTE WHERE fecha < CAST('20130501' AS DATE)
)
SELECT * FROM miCTE
En este caso, queremos obtener los meses solamente del período 2012-06-01 hasta 2013-05-01, por lo tanto también deberemos cambiar el parametro de DATEADD para que nos sume los meses en lugar de días y obtendremos lo siguiente:
Si deseamos obtener las horas de un día, solo hacemos unos pequeños cambios como el tipo de dato y además el valor DATEADD que debe aplicar la operación:
WITH miCTE AS
(
       SELECT CAST('00:00:00' AS TIME) as hora
       UNION ALL
       SELECT DATEADD( HH , 1 ,hora ) as hora
       FROM miCTE WHERE hora < CAST('23:00:00' AS TIME)
)
SELECT * FROM miCTE
Otro ejemplo  es como generar todas las medias horas de un día, algo así:
WITH miCTE AS
(
       SELECT CAST('00:00:00' AS TIME) as hora
       UNION ALL
       SELECT DATEADD( MINUTE , 30 ,hora ) as hora
       FROM miCTE WHERE hora < CAST('23:30:00' AS TIME)
)
SELECT * FROM miCTE
Ahora bien, todo esto funciona de manera correcta siempre y cuando el proceso no deba hacer más de 100 recursiones ya que si lo hace, SQL SERVER mandará un error como el siguiente, por ejemplo si queremos obtener los días de todo un año:
WITH miCTE AS
(
       SELECT CAST('20130101' AS DATE) as fecha
       UNION ALL
       SELECT DATEADD( d , 1 ,fecha ) as fecha
       FROM miCTE WHERE fecha < CAST('20131231' AS DATE)
)
SELECT * FROM miCTE
Esto se debe a una clausula que en caso que no la especifiquemos, el motor solo hará 100 recursiones y al realizar la 101 marcará el error. Para esto debemos especificar la clausula OPTION(MAXRECURSION valorInt ) el valor puede ser entre 1 y 32767, si se especifica 0, la recursión no tiene límite.
WITH miCTE AS
(
       SELECT CAST('20130101' AS DATE) as fecha
       UNION ALL
       SELECT DATEADD( d , 1 ,fecha ) as fecha
       FROM miCTE WHERE fecha < CAST('20131231' AS DATE)
)
SELECT * FROM miCTE
OPTION(MAXRECURSION 0)
Una clara aplicación de esto es cuando queremos conocer, por ejemplo en la base de datos AdventureWorks2012, que día hicimos compras y aquellos días que no hayamos tenido compras reemplazar con 0:
SELECT CAST( OrderDate AS DATE) as fecha, COUNT( PurchaseOrderID ) as compras
FROM Purchasing.PurchaseOrderHeader
WHERE OrderDate >= '20080101' AND OrderDate < '20080121'
GROUP BY CAST( OrderDate AS DATE)
ORDER BY fecha
Como podemos observar no tenemos todos los días del período 2008-01-01 al 2008-01-20 pero queremos que aparezcan, como lo logramos? de la siguiente manera:
WITH miCTE AS
(
       SELECT CAST('20080101' AS DATE) as fecha
       UNION ALL
       SELECT DATEADD( d , 1 ,fecha ) as fecha
       FROM miCTE WHERE fecha < CAST('20080120' AS DATE)
)
SELECT *
FROM miCTE c
LEFT JOIN (
       SELECT CAST( OrderDate AS DATE) as fecha, COUNT( PurchaseOrderID ) as compras
       FROM Purchasing.PurchaseOrderHeader
       WHERE OrderDate >= '20080101' AND OrderDate < '20080121'
       GROUP BY CAST( OrderDate AS DATE)
) x
ON c.fecha = x.fecha
Y para lo cual solo bastaría hacer unos ajustes a la consulta:
WITH miCTE AS
(
       SELECT CAST('20080101' AS DATE) as fecha
       UNION ALL
       SELECT DATEADD( d , 1 ,fecha ) as fecha
       FROM miCTE WHERE fecha < CAST('20080120' AS DATE)
)
SELECT c.fecha,ISNULL( x.compras , 0 ) as compras
FROM miCTE c
LEFT JOIN (
       SELECT CAST( OrderDate AS DATE) as fecha, COUNT( PurchaseOrderID ) as compras
       FROM Purchasing.PurchaseOrderHeader
       WHERE OrderDate >= '20080101' AND OrderDate < '20080121'
       GROUP BY CAST( OrderDate AS DATE)
) x
ON c.fecha = x.fecha
Es muy útil la aplicación de CTEs recursivos, sobre todo en estos casos cuando queremos obtener cierta información que anteriormente la sacaríamos a través de establecer el valor a una celda en EXCEL y arrastrar el mouse hacia abajo. Este es uno de muchos ejemplos para los cuales se puede utilizar este tipo de recurso, más adelante les mostraré algunos otros.

SALUDOS!

2013/06/10

FILTRAR POR RANGO DE FECHAS

Que tal amigos, hoy les mostraré lo que algunos consideran un BUG al momento de filtrar datos por una columna tipo DATETIME.

Primero, creamos una tabla de prueba con algunos datos, les dejo un LINK con el script o el script directamente para copiar.
IF OBJECT_ID (N'pruebas', N'U') IS NOT NULL
DROP TABLE pruebas
CREATE TABLE pruebas( id INT IDENTITY( 1,1 ), fecha DATETIME )

INSERT INTO pruebas( fecha )
VALUES ('2012-12-01 00:04:40'), ('2012-12-01 05:35:29'), ('2012-12-01 08:54:10'),('2012-12-01 09:04:34'), 
('2012-12-01 09:37:48'), ('2012-12-01 09:39:55'), ('2012-12-01 10:28:14'),('2012-12-01 11:46:13'), 
('2012-12-01 12:01:40'), ('2012-12-01 13:22:10'), ('2012-12-01 13:27:37'),('2012-12-01 13:35:01'), 
('2012-12-01 14:40:25'), ('2012-12-01 15:01:15'), ('2012-12-01 15:36:44'),('2012-12-01 16:39:27'), 
('2012-12-01 17:21:07'), ('2012-12-01 17:46:49'), ('2012-12-01 17:54:16'),('2012-12-01 18:10:10'), 
('2012-12-01 19:58:17'), ('2012-12-01 20:05:47'), ('2012-12-01 20:31:41'),('2012-12-01 22:19:10'), 
('2012-12-02 09:02:21'), ('2012-12-02 10:45:44'), ('2012-12-02 11:58:16'),('2012-12-02 14:21:06'), 
('2012-12-02 17:07:40'), ('2012-12-02 17:44:29'), ('2012-12-02 20:50:20'),('2012-12-03 07:47:26'), 
('2012-12-03 08:06:27'), ('2012-12-03 08:29:54'), ('2012-12-03 08:42:01'), ('2012-12-03 09:24:39'), 
('2012-12-03 09:26:37'), ('2012-12-03 09:30:15'), ('2012-12-03 09:48:14'), ('2012-12-03 09:55:21'), 
('2012-12-03 10:16:13'), ('2012-12-03 10:48:05'), ('2012-12-03 10:55:11'), ('2012-12-03 10:55:23'), 
('2012-12-03 10:56:33'), ('2012-12-03 10:57:39'), ('2012-12-03 11:02:07'), ('2012-12-03 11:08:28'),
('2012-12-03 11:21:47'), ('2012-12-03 11:22:33'), ('2012-12-03 11:26:58'), ('2012-12-03 11:36:33'),
('2012-12-03 11:52:40'), ('2012-12-03 12:12:28'), ('2012-12-03 12:20:56'), ('2012-12-03 12:28:10'),
('2012-12-03 12:42:23'), ('2012-12-03 12:52:23'), ('2012-12-03 13:11:18'), ('2012-12-03 13:23:37'),
('2012-12-03 13:23:46'), ('2012-12-03 13:32:52'), ('2012-12-03 13:33:00'), ('2012-12-03 13:49:14'),
('2012-12-03 14:30:19'), ('2012-12-03 14:46:38'), ('2012-12-03 15:14:41'), ('2012-12-03 15:31:47'),
('2012-12-03 16:03:05'), ('2012-12-03 17:19:39'), ('2012-12-03 17:41:06'), ('2012-12-03 17:54:31'),
('2012-12-03 18:33:24'), ('2012-12-03 21:32:30'), ('2012-12-04 08:22:47'), ('2012-12-04 08:27:28'),
('2012-12-04 08:43:58'), ('2012-12-04 08:59:31'), ('2012-12-04 09:22:33'), ('2012-12-04 09:28:17'),
('2012-12-04 09:35:36'), ('2012-12-04 10:04:03'), ('2012-12-04 10:53:45'), ('2012-12-04 11:07:15'),
('2012-12-04 11:12:53'), ('2012-12-04 11:26:23'), ('2012-12-04 11:30:38'), ('2012-12-04 11:42:46'),
('2012-12-04 12:32:57'), ('2012-12-04 12:40:16'), ('2012-12-04 12:59:17'), ('2012-12-04 13:00:09'),
('2012-12-04 13:08:13'), ('2012-12-04 13:09:56'), ('2012-12-04 13:23:39'), ('2012-12-04 13:26:42'),
('2012-12-04 13:51:10'), ('2012-12-04 14:57:20'), ('2012-12-04 15:20:42'), ('2012-12-04 15:37:18'),
('2012-12-04 16:14:04'), ('2012-12-04 16:16:06'), ('2012-12-04 17:21:32'), ('2012-12-04 17:28:54'),
('2012-12-04 17:42:00'), ('2012-12-04 18:04:24'), ('2012-12-04 18:05:48'), ('2012-12-04 19:13:49'),
('2012-12-04 19:23:38'), ('2012-12-04 21:27:58'), ('2012-12-04 22:42:20'), ('2012-12-05 08:48:58'),
('2012-12-05 09:00:59'), ('2012-12-05 09:24:29'), ('2012-12-05 10:09:48'), ('2012-12-05 10:35:41'),
('2012-12-05 10:39:00'), ('2012-12-05 11:21:44'), ('2012-12-05 11:35:34'), ('2012-12-05 12:28:42'),
('2012-12-05 12:31:30'), ('2012-12-05 12:54:57'), ('2012-12-05 13:30:31'), ('2012-12-05 13:39:06'),
('2012-12-05 13:43:25'), ('2012-12-05 13:47:54'), ('2012-12-05 13:59:07'), ('2012-12-05 14:19:53'),
('2012-12-05 14:34:23'), ('2012-12-05 14:42:55'), ('2012-12-05 15:13:39'), ('2012-12-05 15:52:14'),
('2012-12-05 15:58:17'), ('2012-12-05 16:13:00'), ('2012-12-05 16:17:26'), ('2012-12-05 18:02:29'),
('2012-12-05 19:21:14'), ('2012-12-05 19:21:53'), ('2012-12-05 21:13:34'), ('2012-12-05 22:16:08')
En total son 140 filas, agrupando por día, tenemos como resultado lo siguiente:
SELECT CAST( fecha AS DATE ) as dia, COUNT( id ) as total
FROM pruebas
GROUP BY CAST( fecha AS DATE )
Como podemos observar, para el día 1 y 2 de Diciembre tenemos 24 y 7 filas( 31 en total ), ahora ejecutemos la siguiente consulta:
SELECT *
FROM pruebas
WHERE fecha BETWEEN '20121201' AND '20121202'
Como podemos observar solo arroja 24 filas, y que pasó con las 7 filas restantes??? Si observamos detenidamente la consulta:
SELECT *
FROM pruebas
WHERE fecha BETWEEN '20121201' AND '20121202'
La columna fecha es de tipo DATETIME, pero que tipo de dato son los filtros que estamos aplicando? Ejecutemos la siguiente consulta:
SELECT CAST( '20121201' AS DATETIME ) as fecha1
, CAST( '20121202' AS DATETIME ) as fecha2
Sorpresa? No, porque está es la forma en que el motor maneja las fechas, observemos y ejecutemos la siguiente consulta:
DECLARE @hoy AS DATETIME
SET @hoy = GETDATE()
SELECT @hoy AS fechaDATETIME
, CAST( @hoy AS DATE ) AS fechaDATE
, CAST( CAST( @hoy AS DATE ) AS DATETIME ) AS fecha
Mostramos la fecha hora actual que es de tipo DATETIME, lo convertimos a DATE y nuevamente a DATETIME, como podemos observar la hora actual no es la misma.

Perfecto, ya quedó comprendido, debo utilizar un tipo de dato DATETIME para hacer el filtro de la siguiente manera:
SELECT *
FROM pruebas
WHERE fecha BETWEEN '20121201 00:00' AND '20121202 23:59'
Claro, quedó solucionado, ya podemos filtrar por fechas, pero... que pasa si agregamos una fila con la hora 23:59:59
INSERT INTO pruebas VALUES( '2012-12-02 23:59:59' )
Y ejecutamos nuevamente la consulta de agrupación para observar cuantas filas tenemos por día.
SELECT CAST( fecha AS DATE ) as dia, COUNT( id ) as total
FROM pruebas
GROUP BY CAST( fecha AS DATE )
Observemos que ya tenemos 8 filas en el día 2 y apliquemos el query para extraer las filas del día 1 y 2 nuevamente:
SELECT *
FROM pruebas
WHERE fecha BETWEEN '20121201 00:00' AND '20121202 23:59'
Y ahora que paso? si ya aplicamos la consulta con los filtros fecha-hora... Ok, esto es cierto, pero recordemos que el tipo DATETIME en SQL SERVER almacena hasta milésimas de segundo, por lo tanto en la consulta debemos especificar hasta la ultima milésima de segundo del día 2, en este caso solo manejamos hasta segundos:
SELECT fecha
FROM pruebas
WHERE fecha BETWEEN '20121201 00:00' AND '20121202 23:59:59'
Ahora, hagamos la prueba agregando una fila con milésimas de segundo:
INSERT INTO pruebas VALUES( '2012-12-02 23:59:59.997' )
Ejecutamos la consulta de agrupación:
SELECT CAST( fecha AS DATE ) as dia, COUNT( id ) as total
FROM pruebas
GROUP BY CAST( fecha AS DATE )
Y ahora ya tenemos 9 filas en el día 2 y para poder obtener esas filas es necesario especificar el ultimo milisegundo del día 2 de la siguiente manera:
SELECT fecha
FROM pruebas
WHERE fecha BETWEEN '20121201 00:00' AND '20121202 23:59:59.999'
Ok, comprendido, pero también esto lo consigo entonces filtrando de la siguiente manera???:
SELECT fecha
FROM pruebas
WHERE fecha BETWEEN '20121201' AND '20121203'
Ya que 2012-12-03 es igual a 2012-12-03 00:00:00.000 porque SQL SERVER así lo interpreta cuando se comparan DATE y un DATETIME.
ERROR, esto funcionará siempre y cuando no tengamos una fila con fecha 2012-12-03 00:00:00.000
INSERT INTO pruebas VALUES( '2012-12-03 00:00:00.000' )
Si ejecutamos el mismo query, obtenemos lo siguiente:
Esta bien, ya comprendí, entonces esto lo puedo lograr convirtiendo la columna a tipo DATE y filtrando solo por el día sin horas, minutos y segundos??

Respuesta: SI, pero no es recomendable. Al hacer esto afectaremos la búsqueda del query, supongamos que nuestra tabla está indexada por este campo:
CREATE NONCLUSTERED INDEX IXfecha ON pruebas (fecha);
Después activamos la Inclusión del Plan de Ejecución en el resultado de la consulta, a través de CTRL + M  o en el botón que describe la siguiente imagen:
Y ejecutamos la consulta:
SELECT fecha
FROM pruebas
WHERE fecha BETWEEN '20121201 00:00' AND '20121202 23:59:59.999'
Como podemos observar, hay una pestaña más en nuestro resultado que es el Plan de Ejecución y además vemos una gráfica del proceso que hizo el motor para obtener el resultado; Al ver un Index Seek no nos debemos preocupar, esto quiere decir que el motor está buscando o filtrando por un Índice, lo cual es correcto. Pero que pasa si convertimos la columna en tipo DATE para poder lograr lo que unos pasos antes comentabamos:
SELECT fecha
FROM pruebas
WHERE CAST( fecha AS DATE)  BETWEEN '20121201' AND '20121202'
Si, también obtendremos las mismas 33 filas pero a que costo, observemos el plan de ejecución de la consulta nuevamente:
Hizo más pasos para obtener las mismas filas, entonces esto quiere decir que nuestra consulta no está del todo bien, más adelante les explicaré que significa tantas imágenes en el plan de ejecución.
Esto sucede porque al aplicar una función sobre una columna que se encuentra indexada, esta pierde su propiedad y estaremos buscando sobre una columna NO INDEXADA.

Ok ok, ya comprendí, pero entonces que puedo hacer para obtener esos 33 registros sin afectar el rendimiento del query???

Respuesta: Volver a los operadores mayor o igual que( >= ) y menor ( < ) de la siguiente manera:
SELECT fecha
FROM pruebas
WHERE fecha >= '20121201' AND fecha < '20121203'
Observemos el Plan de Ejecución ( CTRL + M
De esta forma estaremos tomando las filas que tengan 2012-12-01 00:00:00.000 y además aquellas que sean menores a 2012-12-03 00:00:00.000 como 2012-12-02 23:59:59.999.

En resumen, debemos de conocer cómo funciona el tipo de dato DATETIME para poder aplicar un BETWEEN, en caso contrario lo mejor siempre será utilizar los operadores mayor o igual que( >= ) y menor ( < ).

SALUDOS!

2013/06/05

SCRIPTS DE CREACION DE TABLAS Y DATOS.

Un tema ya muy comentado y publicado es este, Como puedo generar los scripts de creación de tablas y los datos en forma de sentencias INSERTs ?? pues bien, les explicaré un poco, además que es muy necesario al momento de plantear un problema en algún foro para poder reproducir el escenario.

1.- Click derecho sobre el nombre de la Base de datos en el Explorador de Objetos, Tasks-> Generate Scripts...
2.- Aparece la introducción donde podemos indicar que no se muestre la próxima ocasión que hagamos está operación.

3.- Seleccionamos los objetos de nuestra base de datos( Tablas, vistas, procedimientos almacenados, UDFs, etc ... )
Para el ejemplo solo seleccionaremos una tabla.
4.- Podemos optar por Guardar los scripts en una ruta específica o bien, publicarlos en un Web Service, seleccionamos la primera.
Para el ejemplo, seleccionaremos la opción Save to new query window. Además que es muy importante el botón Advanced, es donde podemos seleccionar los objetos de nuestra tabla que queremos generar, por mencionar algunos: La versión del Script, Restricciones, Llaves foráneas, índices así como los datos de nuestra tabla, que es lo que requerimos.
Debemos seleccionar Schema and data.

5.- Posteriormente nos mostrará una pantalla Resumen que nos permitirá verificar los pasos que previamente realizamos.
6.- Una vez terminado el proceso, nos mostrará el resultado de este.
Así como el Script de nuestra tabla con los datos.
Es muy importante que al hacer una pregunta en un foro, se proporcione toda la información posible para poder reproducir el escenario, de está manera se obtendrá una respuesta más rápida y concreta, si el script generado contiene demasiadas líneas, podemos apoyarnos de herramientas como Google Drive, Drop Box o SkyDrive para colgar el archivo.

SALUDOS!