Novedades de Transact-SQL 2008 (III)

Este es el último post sobre las novedades que presenta en lenguaje T-SQL para la versión 2008 de SQL Server.

Instrucción PIVOT

La instrucción PIVOT nos permitirá obtener un conjunto de resultados en un formato similar a una tabla dinámica de Excel, facilitando su análisis para la posterior toma de decisiones. Veamos un ejemplo, en el que obtendremos las ventas de productos por comercial, zona y año:

SELECT    soh.[SalesPersonID]
,c.[FirstName] + ' ' + COALESCE(c.[MiddleName], '') + ' '
+ c.[LastName] AS [FullName]
,e.[Title]
,st.[Name] AS [SalesTerritory]
,soh.[SubTotal]
,YEAR(DATEADD(m, 6, soh.[OrderDate])) AS [FiscalYear]
FROM [Sales].[SalesPerson] sp
INNER JOIN
[Sales].[SalesOrderHeader] soh
ON sp.[SalesPersonID] = soh.[SalesPersonID]
INNER JOIN
[Sales].[SalesTerritory] st
ON sp.[TerritoryID] = st.[TerritoryID]
INNER JOIN
[HumanResources].[Employee] e
ON soh.[SalesPersonID] = e.[EmployeeID]
INNER JOIN
[Person].[Contact] c
ON e.[ContactID] = c.ContactID

 Pivot01
En el resultado obtenido se muestran los datos deseados, pero… ¿Y si quisiéramos mostrar los ventas totales por comercial y año? Una forma rápida de resolverlo sería mediante el uso de la instrucción PIVOT, tal y como se muestra seguidamente:
 
SELECT * FROM 
(SELECT
soh.[SalesPersonID]
,c.[FirstName] + ' ' + COALESCE(c.[MiddleName], '') + ' '
+ c.[LastName] AS [FullName]
,e.[Title]
,st.[Name] AS [SalesTerritory]
,soh.[SubTotal]
,YEAR(DATEADD(m, 6, soh.[OrderDate])) AS [FiscalYear]
FROM [Sales].[SalesPerson] sp
INNER JOIN [Sales].[SalesOrderHeader] soh
ON sp.[SalesPersonID] = soh.[SalesPersonID]
INNER JOIN [Sales].[SalesTerritory] st
ON sp.[TerritoryID] = st.[TerritoryID]
INNER JOIN [HumanResources].[Employee] e
ON soh.[SalesPersonID] = e.[EmployeeID]
INNER JOIN [Person].[Contact] c
ON e.[ContactID] = c.ContactID
) AS source
PIVOT
(
SUM([SubTotal])
FOR [FiscalYear]
IN ([2002], [2003], [2004])
) as pvt

Pivot02
La transformación ha permitido poner en columnas los valores de los distintos años (2002, 2003 y 2004), y en las filas el comercial y su zona, mientras que en el área de valores hemos puesto las ventas. Cabe observar que aparecen valores NULL debido a que no todos los comerciales han realizado ventas durante alguno de los años.
 
Instrucción UNPIVOT

Mediante UNPIVOT podremos hacer el paso inverso al mostrado previamente. A partir de unos resultados mostrados en formato agrupado, podemos intentar mostrarlos en formato tabla. Para clarificar el ejemplo, en primer lugar guardaremos el resultado de la instrucción PIVOT (mostrado anteriormente) en una tabla (se llamará dbo.VentasComercial):

SELECT * 
INTO dbo.VentasComercial
FROM
(SELECT
soh.[SalesPersonID]
,c.[FirstName] + ' ' + COALESCE(c.[MiddleName], '') + ' '
+ c.[LastName] AS [FullName]
,e.[Title]
,st.[Name] AS [SalesTerritory]
,soh.[SubTotal]
,YEAR(DATEADD(m, 6, soh.[OrderDate])) AS [FiscalYear]
FROM [Sales].[SalesPerson] sp
INNER JOIN [Sales].[SalesOrderHeader] soh
ON sp.[SalesPersonID] = soh.[SalesPersonID]
INNER JOIN [Sales].[SalesTerritory] st
ON sp.[TerritoryID] = st.[TerritoryID]
INNER JOIN [HumanResources].[Employee] e
ON soh.[SalesPersonID] = e.[EmployeeID]
INNER JOIN [Person].[Contact] c
ON e.[ContactID] = c.ContactID
) AS source
PIVOT
(
SUM([SubTotal])
FOR [FiscalYear]
IN ([2002], [2003], [2004])
) as pvt
 
Ahora podemos ejecutar la instrucción UNPIVOT, accediendo a la información almacenada en la tabla dbo.VentasComercial.

SELECT  SalesPersonID,
Title,
SalesTerritory,
FiscalYear,
Sales
FROM
(SELECT SalesPersonID,
FullName,
Title,
SalesTerritory,
[2002],
[2003],
[2004]
FROM dbo.VentasComercial) as pvt
UNPIVOT (Sales FOR FiscalYear IN ([2002],[2003],[2004])) AS unpvt;

Unpivot01

Finalmente podemos observar como tenemos resultados similares al primer conjunto de resultados mostrado en este artículo, aunque en este caso sólo obtenemos un registro por comercial y año.

Funciones RANK y DENSE_RANK

Entre las distintas funciones de ranking que presenta SQL Server, vamos a ver en primer lugar la función DENSE_RANK. Mediante esta instrucción podemos obtener en qué posición se encuentra una fila respecto al conjunto total de resultados. En el ejemplo que se muestra seguidamente se realiza un ranking de productos según la cantidad que existe de cada uno de ellos en los distintos almacenes:

SELECT  i.ProductID, 
p.Name,
i.LocationID,
i.Quantity
,DENSE_RANK() OVER
(PARTITION BY i.LocationID ORDER BY i.Quantity) AS 'RANK'
FROM Production.ProductInventory i
INNER JOIN
Production.Product p
ON i.ProductID = p.ProductID
ORDER BY i.LocationID,i.Quantity

Rank01

Es importante destacar que se pueden producir empates, cuando existe la misma cantidad de productos distintos en un mismo almacén.

Así, también disponemos de la función RANK, que es similar a la anterior, aunque en este caso los números asignados en el ranking no son contiguos.

SELECT  i.ProductID, 
p.Name,
i.LocationID,
i.Quantity
,RANK() OVER
(PARTITION BY i.LocationID ORDER BY i.Quantity) AS 'RANK'
FROM Production.ProductInventory i
INNER JOIN
Production.Product p
ON i.ProductID = p.ProductID
ORDER BY i.LocationID,i.Quantity

Rank02

En este caso, como se produce un empate en el ranking 7, el siguiente numero asignado es el 9, hecho que no se producia con la función DENSE_RANK.

Funciones de Fecha y Hora

En la versión de SQL Server 2008 han aparecido nuevos tipos de datos para gestionar fechas y horas: datetime2, time, date y datetimeoffset , que permiten almacenar y manipular fechas en un formato más preciso que el tipo datetime. En el siguiente ejemplo vamos a mostrar como usar los distintos tipos de datos y las funciones SYSUTCDATETIME, SYSDATETIME y SYSDATETIMEOFFSET:

SELECT  SYSUTCDATETIME() AS UTC, 
SYSDATETIME() AS FECHA_HORA,
SYSDATETIMEOFFSET() AS DIF_HORARIA,
CONVERT(DATE,SYSDATETIME()) AS FECHA,
CONVERT(TIME,SYSDATETIME()) AS HORA

Date01

Podemos comprobar como podemos convertir un dato del tipo datetime2 (el que nos devuelve la función SYSDATETIME) a un tipo date o time para obtener unicamente la fecha o la hora.

Aunque existen más novedades, explicaré algunas de ellas en artículos más concretos.

Espero que todo lo que he contado os sirva… ¡Y enjoy T-SQL!

Tags: , , , , , ,

Novedades de Transact-SQL 2008 (II)

En este post seguiré explicando algunas de las novedades que presenta el lenguaje T-SQL en SQL Server 2008.

Extensión GROUPING SETS

Grouping Sets es una extensión de la instrucción Group By que nos permitirá obtener resultados agregados mediante una agrupación múltiple en única sentencia. Ciertamente la definición parece un poco complicada, y por eso más vale un ejemplo que mil palabras.

Mediante la siguiente instrucción obtendremos el precio medio de los productos, agrupados por año y subcategoría:

SELECT   ProductSubcategoryID,
YEAR(SellStartDate) AS Year,
AVG(ListPrice) AS AvgPrice
FROM Production.Product
WHERE ProductSubcategoryID IS NOT NULL
GROUP BY GROUPING SETS((ProductSubcategoryID),(YEAR(SellStartDate)))
ORDER BY ProductSubcategoryID,YEAR(SellStartDate)

GroupingSets01

 

 

 

 

 

 

Vemos como obtenemos los totales por año y por subcategoría de producto. Sin la extensión Grouping Sets, y para obtener el mismo resultado, hubiéramos tenido que crear una instrucción con dos consultas unidas mediante el operador UNION.

SELECT   NULL,
YEAR(SellStartDate) AS Year,
AVG(ListPrice) AS AvgPrice
FROM Production.Product
WHERE ProductSubcategoryID IS NOT NULL
GROUP BY YEAR(SellStartDate)
UNION
SELECT ProductSubcategoryID,
NULL AS Year,
AVG(ListPrice) AS AvgPrice
FROM Production.Product
WHERE ProductSubcategoryID IS NOT NULL
GROUP BY ProductSubcategoryID

Extensiones COMPUTE, ROLLUP y CUBE

Mediante la extensión COMPUTE de la instrucción SELECT es posible obtener un valor total a partir de una consulta y la operación especificada en la extensión. En el siguiente ejemplo obtendremos el precio medio de los productos agrupados por su color y los días que tardan en fabricarse. Adicionalmente obtendremos el precio medio de todos los productos listados:

SELECT   Color,
DaysToManufacture,
AVG(ListPrice) AS AvgPrice
FROM Production.Product
WHERE Color IS NOT NULL
AND DaysToManufacture IS NOT NULL
GROUP BY Color,DaysToManufacture
COMPUTE AVG(AVG(ListPrice))

Compute01

Aunque esta consulta puede sernos muy útil, no lo será tanto si ésta se debe ser llamada desde una aplicación, ya que nos devolverá dos conjuntos de resultados distintos. Además, como bien indica la ayuda sobre la instrucción COMPUTE, se mantiene por un tema de compatibilidad, pero en futuras versiones no será soportada y en su lugar se deberá utilizar la extensión ROLLUP. El siguiente ejemplo utiliza esta extensión:

SELECT   Color,
DaysToManufacture,
AVG(ListPrice) AS AvgPrice,
GROUPING(DaysToManufacture) AS Grouped
FROM Production.Product
WHERE Color IS NOT NULL
AND DaysToManufacture IS NOT NULL
GROUP BY Color,DaysToManufacture WITH ROLLUP
Order By Color,DaysToManufacture

Rollup01
 
Podemos observar como mediante la instrucción ROLLUP se realizan todas las combinaciones de valores de la columna color con el resto de columnas. En aquellas columnas donde se muestra el valor NULL significa que es un campo de valor agregado o total. Aun así, se puede utilizar la función GROUPING para saber si un valor es totalizado o no por esa columna.
 
Por último, también podemos utilizar la extensión CUBE, que realiza una función similar a la extensión ROLLUP, con la diferencia de que la primera realiza todas las combinaciones posibles entre los distintos valores de las columnas especificadas en la consulta, y no con una única columna. Veamos un ejemplo:
 
SELECT   Color,
DaysToManufacture,
AVG(ListPrice) AS AvgPrice,
GROUPING(Color) AS GroupedColor,
GROUPING(DaysToManufacture) AS GroupedDays
FROM Production.Product
WHERE Color IS NOT NULL
AND DaysToManufacture IS NOT NULL
GROUP BY Color,DaysToManufacture WITH CUBE
ORDER BY Color,DaysToManufacture


Cube01
 
A diferencia del ejemplo anterior, en este caso obtenemos el precio medio de los productos segun los días de fabriación, sin tener en cuenta el color. Así mismo, introducimos dos columnas GROUPING, para saber si el resultado obtenido es un valor agrupado por alguna de las dos columnas (color, DaysToManufacture).
 
Ciertamente, con la instrucciones usando ROLLUP y CUBE no hemos obtenido exactamente el mismo resultado que con la instrucción COMPUTE. En esta última instrucción obteníamos el precio medio para todos los productos, independientemente del color y de los días de fabricación (obtuvimos el valor de 607,5081).
 
Con la instrucción ROLLUP el valor obtenido es de 847,2869 (el valor obtenido cuando las columnas color y DaysToManufacture tienen el valor NULL). Esto es debido a que en la instrucción ROLLUP también se tiene en cuenta el valor del precio medio del producto cuando la columna DaysToManufacture es NULL, cuando esto no debería ser así. Por ese mismo motivo, deberíamos escribir una nueva consulta utilizando las CTE (Expresiones Comunes de Tablas, o Common Table Expressions), instrucción que ya apareció en la versión 2005 de SQL Server.
 
WITH Resumen (Precio,Color,Dias)
AS
(
SELECT AVG(ListPrice),
Color,
DaysToManufacture
FROM Production.Product
WHERE Color IS NOT NULL
AND DaysToManufacture IS NOT NULL
GROUP BY Color,DaysToManufacture WITH ROLLUP
)
SELECT AVG(Precio) FROM Resumen WHERE Dias IS NOT NULL

Espero haberos aportado un poco de luz sobre el tema. Igualmente os recomiendo ejecutar las consultas y analizar los resultados obtenidos para comprender definitivamente las diferencias entre las distintas instrucciones.

¡Enjoy (again) T-SQL!

Tags: , , , ,

Novedades de Transact-SQL 2008 (I)

En este post voy a explicar las principales novedades o funcionalidades que incorpora SQL Server 2008 desde un punto de vista de programación mediante el lenguaje Transact-SQL (T-SQL).

Os recomiendo instalar la base de datos de ejemplo AdventureWorks2008, que se encuentra aquí.

Declaración de variables e inicializaciones

Ya es posible declarar una variable y asignarle al mismo tiempo un valor:

DECLARE @intNumero INT=0
 
En este caso declaramos una variable de tipo entero y se le asigna el valor 0.

Constructores de fila

Mediante los constructores de fila es posible insertar distintas filas en una tabla mediante una única sentencia. El ejemplo se basa en una tabla creada previamente, que se incluye en el script:

--CREACIÓN DE LA TABLA
CREATE TABLE dbo.Paises
(
IDPais VARCHAR(2) PRIMARY KEY,
Descripcion VARCHAR(500),
FechaModif DATETIME
)

--INSERT MEDIANTE CONSTRUCTORES DE FILA
INSERT INTO dbo.Paises
VALUES ('MA','Malta',SYSDATETIME()),
('IR','Ireland',SYSDATETIME()),
('FR','France',SYSDATETIME())

En este caso creamos una tabla que guarda información de paises, y en una sola instrucción insert agregamos 3 registros en la tabla.

Cabe indicar que el número máximo de filas a insertar en una instrucción es de 1.000, y que la instrucción se ejecuta como una única transacción (si alguno de los insert no es correcto, no se realiza ninguno de ellos).

Instrucción OUTPUT

Mediante la instrucción OUTPUT podemos acceder a los registros afectados por una operación de insert, update, delete o merge. Es importante notar que esta instrucción ya estaba disponible en SQL Server 2005.

--DECLARAMOS UNA VARIABLE DE TIPO TABLA
DECLARE @tblEliminados TABLE(Codigo VARCHAR(2),Nombre VARCHAR(500))

--ELIMINAMOS TODOSO LOS REGISTROS DE LA TABLA Y LOS GUARDAMOS EN LA VARIABLE
DELETE FROM dbo.Paises
OUTPUT deleted.IDPais, deleted.Descripcion INTO @tblEliminados

--MOSTRAMOS LOS REGISTROS ELIMINADOS
SELECT * FROM @tblEliminados

En este ejemplo podemos comprobar como solo guardamos los datos de las columnas IDPais y Descripción sobre la variable de tipo tabla creada previamente (en las columnas codigo y nombre).

Instrucción MERGE

Mediante esta instrucción es posible definir qué acción se debe llevar a cabo sobre los registros de una tabla cuando éstos se encuentran (match) o no sobre los registros de una tabla origen distinta.

En el siguiente ejemplo añadiremos paises a nuestra tabla a partir de una tabla origen (person.CountryRegion de la base de datos AdventureWorks), siempre y cuando no existan previamente. Si se produce este último caso, entonces unicamente actualizaremos su fecha de modificacion.

MERGE dbo.Paises AS target
USING
(
SELECT CountryRegionCode,
Name
FROM Person.CountryRegion
) AS source (codigo,nombre)
ON target.IDPais=source.codigo
WHEN MATCHED THEN UPDATE SET FechaModif=SYSDATETIME()
WHEN NOT MATCHED THEN INSERT VALUES (codigo,nombre,SYSDATETIME());
 
Es importante observar la sintaxis de la instrucciones update e insert, ya que en ninguna de ambas se especifica la tabla donde se realiza la operación (se supone que es la tabla de destino o target, que en nuestro caso es la tabla dbo.Paises).
 
Esta instrucción merge también se puede usar conjuntamente con la instrucción output, de forma que podamos capturar la información de los registros afectados por la operación.
 
MERGE dbo.Paises AS target
USING
(
SELECT CountryRegionCode,
Name
FROM Person.CountryRegion
) AS source (codigo,nombre)
ON target.IDPais=source.codigo
WHEN MATCHED THEN UPDATE SET FechaModif=SYSDATETIME()
WHEN NOT MATCHED THEN INSERT VALUES (codigo,nombre,SYSDATETIME())
OUTPUT $action, Inserted.IDPais, Inserted.Descripcion,
Inserted.FechaModif, Deleted.FechaModif;

En este caso, tenemos acceso a una columna especial llamada $action, que nos permite saber qué operación se ha realizado (insert, update o delete). En la tabla inserted accedermos a los registros añadidos o actualizados (valor nuevo) a la tabla destino, mientras que en la tabla Deleted accederemos a los registros eliminados o actualizados (valor antiguo).
 
En el próximo post presentaré otras novedades que creo que os pueden ser de mucha utilidad.
 
¡Enjoy T-SQL!

Tags: , , , , ,

DTS vs Integration Services (no me lo puedo creer…)

Esta es la frase que me pasó por la cabeza despúes de estar trabajando unas cuántas horas en un proyecto de migración de los antiguos DTS (Data Transformation Services) de SQL Server 2000, a los nuevos paquetes de Integration Services de SQL Server 2008. La razón es muy simple… Los procesos de extracción, transformación y carga (también conocidos como procesos ETL) funcionan mucho más rápido en la primera plataforma que en la segunda.

¿Cómo puede ser que un producto realizado hace más de 9 años funcione mejor que un producto nuevo? ¡Pregunten a Microsoft! Ciertamente el diseño y la productividad en la creación de paquetes ha mejorado mucho respecto a la versión del 2000, con nuevas herramientas de desarrollo y depuración, que facilitan enormemente estas tareas. ¿Pero qué pasa con el rendimiento de los paquetes?

En el caso concreto que nos ocupó, los paquetes extraían datos de Oracle y de un DB2 (sobre un AS400), sobre los que se aplican algunas transformaciones y cálculos agregados, y que finalmente se guardan en una base de datos en SQL Server 2008. El proceso de extracción de datos del DB2 (6M de registros) pasó de 3 minutos a 1 hora. No comment…

Investigando un poco, descubrí que este es un problema conocido del producto, y que ya existen productos de terceros que mejoran el rendimiento en cuanto a la conectividad y extracción de datos de otras plataformas. Algunos productos que podemos destacar son:

logo_attunity

  • DataDirect Connect64 for SSIS: Un poco complicado para configurar, pero puede ser muy útil si tenéis plataformas de 64 bits. Desconozco el precio.

progress_dd_logo

  • SSIS Oracle Bulk Load Connectors: Una opción interesante (mirad las comparativas de rendimiento) si trabajáis con Oracle. Tampoco se informa del precio en la web.

Persistent

Para finalizar este post, solo comentaros que mi compañero José Antonio encontró una solución salomónica para mejorar el rendimiento del paquete que se conectaba a DB2. Consistió en crear un fichero Access, vincular las tablas del DB2, y crear las consultas necesarias sobre ellas. Posteriormente, creamos un paquete que se conecta al fichero Access, y usa las consultas creadas para seleccionar los datos que se desean importar. ¿Increíble, verdad? De esta manera el paquete es más rápido que extrayendo los datos directamente del DB2.

Pasan los años, aparecen nuevos productos, y resulta que los antiguos tienen mejor rendimiento que los nuevos. De nuevo, no me lo puedo creer…

¡Alguien tiene experiencias similares? ¡No dudéis en compartirlas!

¡Espero que os sirva!

PD: Me dejé de incluir un vínculo al artículo How to Choose the Best Connectors for SSIS, donde se explica de forma clara y concisa qué criterios debemos seguir para elegir el mejor conector para Integration Services (se encuentra en la web sqlservercentral.com, que requiere registro, pero totalmente gratuito).

Tags: , , , ,

SQL Profiler para versiones SQL Express

En el último proyecto de desarrollo en el que estoy involucrado me he encontrado con la necesidad de comprobar si las sentencias SQL generadas desde la aplicación llegaban correctamente al servidor de base de datos (SQL Server 2005 Express Edition) para su posterior ejecución.

En las versiones Express de SQL Server no se incluye la herramienta SQL Profiler, que nos permite analizar lo que está sucediendo en el servidor en distintos momentos. De esta manera podemos analizar, por ejemplo, las sentencias o procedimientos almacenados que se están ejecutando, el tiempo que tardan en ejecutarse o los usuarios que lanzan esos comandos, entre otros.

Para resolver este problema estuve buscando un poco y finalmente encontré una aplicación llamada Profiler for Microsoft SQL Server 2005/2008 Express Edition, que proporciona la misma funcionalidad que SQL Profiler, ¡y de forma totalmente gratuita! Además es un proyecto con el código fuente abierto, con lo que podemos modificar la aplicación a nuestro gusto o necesidades, además de comprobar como está hecha.

Según mi punto de vista, una aplicación muy útil y recomendable si no disponemos de SQL Profiler.

¡Espero que os sirva!

PD: En otro post os explicaré las utilidades de SQL Profiler y como sacar el máximo de esta herramienta, no tan conocida por los programadores como lo debería ser.

Tags: ,