miércoles, 7 de enero de 2009

Pasar filas a columnas con T-SQL: PIVOT() y las cosas que te alegran el día :)

English version

A veces las cosas que parecen complicadas tienen una solución muy fácil pero que no siempre es la primera que se nos ocurre (no es exactamente el principio de la navaja de Ockham, pero se le parece).

Veamos, el caso es que hay una tabla de logs en la que varios procesos van insertando el resultado de su paso por diferentes estados, algo como esto:



Proceso Estado Timestamp

Proceso1 10 2008/12/21 12:22:05.320
Proceso2 10 2008/12/21 12:22:05.358
Proceso1 11 2008/12/21 12:22:06.210
Proceso1 12 2008/12/21 12:22:06.588
Proceso2 11 2008/12/21 12:22:10.100
Proceso2 12 2008/12/21 12:22:10.048
Proceso3 10 2008/12/21 14:30:05.358
Proceso3 11 2008/12/21 14:30:20.052



Para poder hacer un seguimiento de los procesos y así identificar problemas de rendimiento en ciertos momentos: entran muchos procesos a la vez, se hacen copias de seguridad, el servidor se queda sin memoria, ... sacamos una consulta agrupando por proceso y hora.

SELECT proceso, count(*) as peticiones, datepart(hh,Timestamp) as hora
FROM MiLOG with (nolock)
GROUP BY proceso, datepart(hh,Timestamp)
ORDER BY 1,3

Con esta consulta teníamos un resultado parecido a este:


Proceso Peticiones Hora
Proceso1 10 12
Proceso1 2 13
Proceso2 5 12
Proceso2 3 13
Proceso3 25 14
Proceso3 4 15



Pero claro, esta salida es un poco complicada de tratar porque aunque estén agrupados por el proceso y la hora cuando hay muchos datos se vuelve muy farragoso comprobarlos, y acabamos llevando los resultados a una hoja de cálculo para que se viesen más claro. Entonces me pregunté, ¿no habrá una forma de hacerlo desde T-SQL para que me convierta las filas en columnas? Vamos, que haya una columna por cada hora y una única fila para cada proceso.

Como siempre la primera solución en una consulta complicada es una sub-consulta (no sé porqué pero es así, alguien tendría que estudiarlo :P) pero en este caso no se trataba de una sub-consulta, sino más bien de 24 sub-consultas, todas atacando a la misma tabla y todas usando la función datepart(hh,Timestamp). Seguro que con eso obtendríamos en resultado esperado, pero a costa de un rendimiento muy muy pobre.

Como esta situación es muy común tenía que haber una forma fácil que solucionarlo, así que en una primera búsqueda de Google no me sorprendió los primeros resultados mostrasen la misma solución de las sub-consultas (por el principio de que la primera solución en una consulta complicada es una sub-consulta :P) ya me veía haciendo una mega-consulta con sub-consultas, JOINs sobre la misma tabla y demás, pero ya en la primera página de resultados encontré una solución mucho más simple: la cláusula PIVOT (que es una novedad SQL Server 2005, así que no lo intentéis con versiones anteriores).

Así que con una consulta como esta:

WITH Procesos( proceso, peticiones, hora)
AS(
SELECT proceso, count(*) as peticiones, datepart(hh,Timestamp) as hora
FROM MiLOG with (nolock)
GROUP BY proceso, datepart(hh,Timestamp)
)
SELECT * FROM Procesos PIVOT(SUM(peticiones) FOR Hora IN ([0],[1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11],[12],[13],[14],[15],[16],[17],[18],[19],[20],[21],[22],[23])) AS Resultado
order by proceso

sí que tenemos el resultado esperado:


Proceso 0 1 2 ... 21 22 23

Proceso1 0 10 23 4 5 21
Proceso2 12 11 15 22 17 8
Proceso3 9 7 2 19 9 13


Como curiosidad decir que la cláusula PIVOT() únicamente acepta funciones de agregado (COUNT, SUM, MAX, AVG, ...), lo cual es totalmente lógico.

Otra cosa, igual os extraña ese

WITH Procesos( proceso, peticiones, hora) AS(...)

se trata de una expresión de tabla común (CTE) que es una forma de especificar un conjunto de resultados temporal con un nombre que puede ser usado en otras consultas.

Bueno, para empezar el año creo que está bien :D

Happy codding ;)



Bola extra: después de pensar en las sub-consultas y antes de descubrir la cláusula PIVOT, pensé en usar CUBE, que también podía servir, pero no era exactamente lo que buscaba, aquí tenéis un muy buen tutorial sobre CUBE.

miércoles, 31 de diciembre de 2008

Propósitos para 2009

Como siempre que se acaba algo es bueno echar la vista atrás y hacer un poco de reflexión de cómo ha sido el año, personalmente el 2008 ha sido decepcionante en algunos aspectos, vamos que no será un año que recuerde con nostalgia, pero no quiero ser pesimista (mi carácter me lo impide) y tengo que reconocer que este año he sacado unas cuantas buenas experiencias que merecieron la pena ser vividas :)

Y como cada vez que se empieza algo, también es bueno tener un plan, por pequeño que sea, de cómo nos gustaría hacer las cosas, así que hay van mis propósitos para 2009:

En lo personal, intentar ser feliz, que con la que está cayendo creo que ya es bastante. Y disfrutar un poco más de la vida, ser un poco más irresponsable y hacer las cosas que me apetece hacer antes de las que “debería” hacer.


En lo profesional:

  • Darle un poco de caña al tema web, que lo tengo bastante abandonado desde hace tiempo y siempre es bueno cambiar de chip de vez en cuando y actualizarse un poco.
  • Poner en práctica las cosas nuevas que he aprendido este año, como SCRUM.
  • Mantenerme al día de las novedades que vayan apareciendo en .NET (.NET 4.0 y VS 2010 ya están a la vuelta de la esquina), pero sin volverme loco, que para estar al corriente de todo hay que ser un super-desarrollador :P
  • Intentar escribir cosas más interesantes en el blog, que lo tengo un poco abandonado pero ganas no me faltan.
  • Seguir aprendiendo cosas nuevas, que aunque no tengan que ver directamente con el trabajo, siempre aportan algo.

Seguro que cuando vaya avanzando el 2009 mis prioridades irán cambiando, como siempre lo afronto con ilusión y ganas, a ver qué tal sale todo :)

FELIZ AÑO A TOD@S y happy codding ;)

martes, 30 de diciembre de 2008

Problema (y solución) al instalar .NET Framework 3.5 (V)

Seguimos con errores al instalar el .NET Framework 3.5 SP1, esta vez se trata de un Windows XP SP2 que, aparentemente, no tienen ningún .NET Framework instalado:


[XX/XX/08,16:24:15] RGB Rast: [2] Error: Installation failed for component RGB Rast. MSI returned error code 1625
[XX/XX/08,16:24:30] WapUI: [2] DepCheck indicates RGB Rast is not installed.

Intentamos instalar manualmente el componente RGB RAST pero se produce el siguiente error:

MSI (s) (9C:AC) [16:26:53:437]: Machine policy value 'DisableUserInstalls' is 0
MSI (s) (9C:AC) [16:26:53:453]: File will have security applied from OpCode.
MSI (s) (9C:AC) [16:26:53:453]: SOFTWARE RESTRICTION POLICY: Verifying package --> 'C:\dotnetfx30\RGB9RAST_x86.msi' against software restriction policy
MSI (s) (9C:AC) [16:26:53:453]: SOFTWARE RESTRICTION POLICY: C:\dotnetfx30\RGB9RAST_x86.msi has a digital signature
MSI (s) (9C:AC) [16:26:53:531]: SOFTWARE RESTRICTION POLICY: SaferComputeTokenFromLevel reported failure. Assuming untrusted. . . (GetLastError returned 1260)
MSI (s) (9C:AC) [16:26:53:531]: La instalación de C:\dotnetfx30\RGB9RAST_x86.msi no está permitida debido a un error en el proceso de directiva de restricción del software. No hay confianza en el objeto.
MSI (s) (9C:AC) [16:26:53:531]: Note: 1: 1718 2: C:\WINDOWS\Installer\1b5ea04.msi
MSI (s) (9C:AC) [16:26:53:531]: MainEngineThread is returning 1625
MSI (c) (74:88) [16:26:53:640]: Decrementing counter to disable shutdown. If counter >= 0, shutdown will be denied. Counter after decrement: -1
MSI (c) (74:88) [16:26:53:640]: MainEngineThread is returning 1625


Como os podéis imaginar, en las políticas de restricción de software del equipo no había ABOSLUTAMENTE NADA!!! Tenía permisos de administrador pero por algún motivo no me dejaba instalar un componente de la propia Microsoft.


Buscando un poco encuentro la solución en esta página: http://support.microsoft.com/kb/925336 que consiste en:


1. Haga clic en Inicio y en Ejecutar, escriba regedit y haga clic en Aceptar.


2. En el Editor del Registro, busque la clave siguiente y después haga clic en ella:
HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\Safer\CodeIdentifiers

Nota: antes de modificar la clave, recomendamos que haga un copia de seguridad. Para ello, haga clic con el botón secundario en CodeIdentifiers y, a continuación, haga clic en Exportar. Guarde el archivo en una ubicación donde pueda buscarlo en el equipo.

3. Cambie el valor del Registro PolicyScope. Para ello, haga doble clic en PolicyScope y, a continuación, cambie la configuración de 0 a 1.

4. Cierre el Editor del Registro.

5. Haga clic en Inicio y en Ejecutar, escriba cmd y, a continuación, haga clic en Aceptar para abrir una ventana de símbolo del sistema.

6. En el símbolo del sistema, escriba el comando siguiente y presione ENTRAR:

net stop msiserver

Este comando detiene el servicio Windows Installer si se está ejecutando actualmente en segundo plano. Cuando el servicio se haya detenido, cierre la ventana de símbolo del sistema y, a continuación, vaya al paso 7.

Nota: si recibe el mensaje siguiente en el símbolo del sistema, cierre la ventana de símbolo del sistema y, a continuación, vaya al paso 7: El servicio Windows Installer no se inicia

7. Instale el paquete que estaba intentando instalar cuando recibió el mensaje de error que se menciona en la sección "Síntomas".

8. Después de instalar el paquete, repita los pasos 1 y 2. A continuación, vuelva a establecer el valor del Registro PolicyScope en 0.

9. Si desconectó el equipo de un dominio, vuelva a unirse al dominio y, a continuación, reinicie el equipo.Nota: si no desconectó el equipo de un dominio, no tiene que reiniciarlo.

Lo curioso del tema es la posible explicación que dan, que dice algo como: “Si un archivo de paquete .msi o .msp es demasiado grande para caber en una parte contigua de memoria virtual, Windows Installer no puede comprobar que el paquete es correcto.”

Y claro, en vez de decir que no puede instalarlo porque no tiene memoria virtual suficiente, pues nos dice que no tenemos permisos porque hay una política de restricción de software que lo impide, todo muy coherente, sí señor ¬¬


Volviendo al tema, una vez modificada la entrada en el registro, se intenta la instalación, pero vuelve a fallar, esta vez con el error :


[09/03/08,11:30:32] Microsoft .NET Framework 2.0a: [2] Error: Installation failed for component Microsoft .NET Framework 2.0a. MSI returned error code 1603
[09/03/08,11:32:03] WapUI: [2] DepCheck indicates Microsoft .NET Framework 2.0a is not installed.

Este ya es un error conocido, y lo solucioné como otras veces: Problema (y solución) al instalar .NET Framework 3.5 (II)

Relacionado:
Problema (y solución) al instalar .NET Framework 3.5 (I)
Problema (y solución) al instalar .NET Framework 3.5 (II)
Problema (y solución) al instalar .NET Framework 3.5 (III)
Problema (y solución) al instalar .NET Framework 3.5 (IV)

jueves, 6 de noviembre de 2008

Try ...Catch y RAISERROR en T-SQL: ¿es un bug o una feature?

No voy a entrar a explicar cómo se usan los bloques Try...Catch en T-SQL, que para eso ya está el MSDN: http://msdn.microsoft.com/es-es/library/ms179296(SQL.90).aspx

si no que os voy a contar un caso curioso que nos ha pasado estos últimos días:

Tenemos una aplicación en VB 6.0 (sí, tiene que ser en VB 6.0 por narices, así que es lo que hay) que tiene que llamar a store procedures de una base de datos SQL Server 2005 usando ADO (sí, el clásico no el .NET), bueno, el caso es que en algunos store procedures tenemos algo parecido a esto:


CREATE PROCEDURE Mi_Procedure
AS
BEGIN
BEGIN TRY
... Código antes del RAISERROR
RAISERROR(...) -- Este RAISERROR provoca que se vaya al bloque CATCH
... Código después del RAISERROR que no se ejecuta
END TRY
BEGIN CATCH
Exec Otro_Procedure @parametro1, @parametro2
RAISERROR(...) -- Este RAISERROR debería propagar el error al que ha llamado al store procedure
END CATCH
END


Bien, es algo bastante básico, pero lo curioso del tema es que tal y como está, desde ADO no se captura el error, ni se dispara un error interceptable ni está en la colección de errores de la conexión.

Investigando un poco descubrí que en Otro_Procedure que se llama desde aquí había una llamada a sp_executesql y esto provoca que por algún motivo se genere un entorno de ejecución diferente, lo que a su vez provoca que el ADO no capture el error.

¿Cuál puede ser la solución?
Teniendo en cuenta que no podíamos obviar la llamada a Otro_Procedure y tampoco a sp_executesql se me ocurrió la idea de darle la vuelta al bloque CATCH y hacer el RAISERROR antes de la llamada a Otro_Procedure. La idea parece un poco absurda si pensamos que el bloque TRY...CATCH funcionase como, por ejemplo, en C#, pero aquí viene el origen del post, el bloque TRY...CATCH en T-SQL te permite ejecutar cualquier instrucción después de hacer un RAISERROR.

O_o

¿¿¿ Eing???

Pues sí, la lógica dice que en un CATCH cuando hagas un RAISERROR este se propagará hacia arriba hasta llegar al llamador del procedure, pero la implementación en T-SQL hace que esto no sea así y que no se finalice la ejecución hasta llegar al END CATCH, con lo que después del RAISERROR podemos poner la/s instrucciones que queramos que se van a ejecutar.

Por supuesto, haciendo la misma llamada al store procedure desde .NET con ADO.NET siempre se intercepta el error, esté el RAISERROR del CATCH antes o después de la llamada a Otro_Procedure, con lo que +1 para ADO.NET :)

Happy coding ;)