Archivo Mensual de Noviembre, 2008

MySQL no admite TRANSFORM PIVOT (pero se pueden obtener resultados parecidos).

Estos últimas días he estado mirando un tema que me preocupaba porque iba a necesitarlo en la aplicación que ando desarrollando… y tras no haber encontrado nada, finalmente me he tenido que poner a fondo con ello hasta sacarlo de una u otra manera. El tema consiste en hacer una consulta de referencias cruzadas en MySQL y, como digo, tras bastante investigar he descubierto que en realidad MySQL no admite esta clase de consultas. Para explicar mejor lo que pretendía hacer voy a basarme en un ejemplo consistente en la siguiente tabla sfc_salefrcast:

+---------+---------+------------+---------+
| sfc_cus | sfc_mat | sfc_mth    | sfc_qty |
+---------+---------+------------+---------+
| 12345   |       1 | 2008-11-01 |    1200 |
| 12345   |       3 | 2008-11-01 |    1500 |
| 54321   |       2 | 2008-11-01 |    2500 |
| 54321   |       3 | 2008-12-01 |    3500 |
| 54321   |       3 | 2009-03-01 |    4500 |
| 54321   |       3 | 2009-07-01 |    4500 |
| 99999   |       4 | 2009-02-01 |    2000 |
| 99999   |       4 | 2009-04-01 |    4000 |
| 99999   |       4 | 2009-06-01 |    6000 |
+---------+---------+------------+---------+

Es una tabla de previsiones de ventas en donde sfc_cus es el código del cliente, sfc_mat el código del material, sfc_mth el mes representado por su primer día y sfc_qty la cantidad que prevemos que vamos a venderle de dicho material a dicho cliente dicho mes (y dichosos nosotros si acertamos).

Bien, pues a partir de esta tabla pretendía conseguir una tabla (o vista… un conjunto de registros, vamos) en la que en cada registro se me mostrara una combinación de cliente y material, y a partir de aquí una columna para cada mes con la cantidad correspondiente. Algo del estilo de lo siguiente…

+---------+---------+------+------+------+------+------+------+
| sfc_cus | sfc_mat | M0   | M1   | M2   | M3   | M4   | M5   |
+---------+---------+------+------+------+------+------+------+
| 12345   |       1 | 1200 |    0 |    0 |    0 |    0 |    0 |
| 12345   |       3 | 1500 |    0 |    0 |    0 |    0 |    0 |
| 54321   |       2 | 2500 |    0 |    0 |    0 |    0 |    0 |
| 54321   |       3 |    0 | 3500 |    0 |    0 | 4500 |    0 |
| 99999   |       4 |    0 |    0 |    0 | 2000 |    0 | 4000 |
+---------+---------+------+------+------+------+------+------+

…en donde M0 es el mes actual (2008-11-01), M1 es el mes próximo, M2 el siguiente, etc.

Pues bien, si estuviéramos trabajando en Microsoft Access podríamos hacer algo simple como…

TRANSFORM SUM(sfc_qty) 
    SELECT sfc_cus, sfc_mat
    FROM sfc_salefrcast
    GROUP BY sfc_cus, sfc_mat
PIVOT sfc_mth;

…y obtendríamos algo muy parecido al resultado deseado. Simplemente no nos mostraría las columnas donde no haya ningún valor que mostrar (como M2), pero sería un problema muy menor que solventaríamos sin dificultad. En Microsoft Excel también sería sencillísimo obtener esos resultados a través de una tabla dinámica.

No obstante, trabajo con MySQL. Y por desgracia MySQL no admite TRANSFORM-PIVOT. De hecho no he encontrado ninguna alternativa atractiva para resolver mi problema, de modo que he tenido que rascar un poquito y crear una a medida, que me ha quedado así:

SELECT
    sfc_cus,
    sfc_mat,
    SUM(IF(MID(sfc_mth, 1, 7) = MID(NOW(), 1, 7), sfc_qty, 0)) AS M0,
    SUM(IF(MID(sfc_mth, 1, 7) = 
           MID(DATE_ADD(NOW(),INTERVAL 1 MONTH), 1, 7), 
           sfc_qty, 0))                                        AS M1,
    SUM(IF(MID(sfc_mth, 1, 7) = 
           MID(DATE_ADD(NOW(),INTERVAL 2 MONTH), 1, 7), 
           sfc_qty, 0))                                        AS M2,
    SUM(IF(MID(sfc_mth, 1, 7) = 
           MID(DATE_ADD(NOW(),INTERVAL 3 MONTH), 1, 7), 
           sfc_qty, 0))                                        AS M3,
    SUM(IF(MID(sfc_mth, 1, 7) = 
           MID(DATE_ADD(NOW(),INTERVAL 4 MONTH), 1, 7), 
           sfc_qty, 0))                                        AS M4,
    SUM(IF(MID(sfc_mth, 1, 7) = 
           MID(DATE_ADD(NOW(),INTERVAL 5 MONTH), 1, 7), 
           sfc_qty, 0))                                        AS M5
FROM
    sfc_salefrcast
GROUP BY 
    sfc_cus, sfc_mat;

(Ver actualización 1.)

Al no utilizar directamente fechas sino estar jugando con NOW() y la función DATE_ADD, obtenemos siempre resultados para el mes corriente y los siguientes N meses, independientemente de cuando solicitemos esa información a la base de datos.

Ignoro si hay alguna manera más directa de hacerlo (y si la hay y la conoces, estaré encantadísimo de leerla), pero por lo pronto lo he solucionado así…

Actualización.

1. Siguiendo el consejo -uno de ellos- de Luís Medel en los comentarios, he cambiado la versión original que había publicado para dejar esta más simplificada al realizar en un solo paso el sumatorio y el filtrado. Además, Luís propone utilizar CASE WHEN THEN ELSE END en lugar de IF.

2. Pedro Cambra facilita en los comentarios un link a la página de Roland Bouman donde se expone un método para hacerlo de forma dinámica utilizando procedimientos almacenados.

3. Pedro Cambra comenta también que MySQL dispone de la función EXTRACT con el parámetro YEAR_MONTH que efectivamente se podría utilizar para mejorar el apartado del MID(fecha,1,7). Más información en el manual de referencia de MySQL.

ReDim Preserve para cambiar más de una dimensión en .NET.

Recientemente en grupos de .NET salió el tema de cómo se pueden redimensionar las dos dimensiones de una matriz de dos dimensiones sin perder los valores que ya se tienen almacenados en dicha matriz.

Si fuera una matriz de una dimensión no habría ningún problema, ya que la opción Preserve nos permite hacer precisamente eso:

Dim myArray(3) As Int32
myArray(0) = 2
myArray(1) = 4
myArray(2) = 6
myArray(3) = 8
ReDim Preserve myArray(5)
myArray(4) = 10
myArray(5) = 12

Así, este código no da ningún problema. Y tampoco lo da este otro:

Dim myArray(3, 0) As Int32
myArray(0, 0) = 2
myArray(1, 0) = 4
myArray(2, 0) = 6
myArray(3, 0) = 8
ReDim Preserve myArray(3, 1)
myArray(0, 1) = 10
myArray(1, 1) = 12
myArray(2, 1) = 14
myArray(3, 1) = 1

Ya que aunque es una matriz de dos dimensiones, sólo estamos redimensionando la dimensión situada más a la derecha. En cambio si intentamos hacer esto que sigue:

Dim myArray(3, 0) As Int32
myArray(0, 0) = 2
ReDim Preserve myArray(4, 1)
myArray(4, 1) = 10

Nos dará una excepción de tipo ArrayTypeMismatchException y nos dirá que…

‘ReDim’ sólo puede cambiar la dimensión situada más a la derecha

…porque un ReDim Preserve en una matriz de dos dimensiones sólo puede actuar sobre la última dimensión.

Para solventar esto podemos utilizar la siguiente función:

'--------------------------------------------------------------------
' Author:      Albert Mata (www.albertmata.net)
' Date:        20081118
' Description: Simulates a ReDim Preserve action on 2-dimensions
'              arrays, allowing to change not only the last dimension
'              but both.
'--------------------------------------------------------------------
Public Function ReDimPreserve(ByVal M As Array, _
ByVal NewLimit0 As Integer, ByVal NewLimit1 As Integer) As Array
    If NewLimit0 >= M.GetUpperBound(0) _
    And NewLimit1 >= M.GetUpperBound(1) Then
        Dim NewArray(NewLimit0, NewLimit1) As [Int32]
        For i As Integer = 0 To M.GetUpperBound(0)
            For j As Integer = 0 To M.GetUpperBound(1)
                NewArray.SetValue(M.GetValue(i, j), i, j)
            Next
        Next
        Return NewArray
    Else
        Return M
    End If
End Function

De tal manera que ahora ante el siguiente código, donde creamos una matriz inicialmente de dimensiones (2,3) para luego redimensionarla a (4,5) y por tanto cambiando las dos dimensiones de la matriz, sin perder los valores que ya teníamos almacenados…

Debug.Print("Creating array with dimensions [2,3]")
Dim myArray(2, 3) As [Int32]
Debug.Print("Storing value 12 in position [0,2]")
myArray.SetValue(12, 0, 2)
Debug.Print("Storing value 15 in position [2,3]")
myArray.SetValue(15, 2, 3)
Debug.Print("Upper bound for first dimension = " _
            & (myArray.GetUpperBound(0)))
Debug.Print("Upper bound for second dimension = " _
            & (myArray.GetUpperBound(1)))
Debug.Print("Value in position [0,2] = " _
            & myArray.GetValue(0, 2).ToString)
Debug.Print("Value in position [2,3] = " _
            & myArray.GetValue(2, 3).ToString)
Try
    myArray.SetValue(24, 3, 4)
    Debug.Print("I can store values in position [3,4]")
Catch ex As Exception
    Debug.Print("I can't store values in position [3,4]")
End Try

Debug.Print("Changing array dimensions to [4,5]")
myArray = DirectCast(Me.ReDimPreserve(myArray, 4, 5), Integer(,))
Debug.Print("Upper bound for first dimension = " _
            & (myArray.GetUpperBound(0)))
Debug.Print("Upper bound for second dimension = " _
            & (myArray.GetUpperBound(1)))
Debug.Print("Value in position [0,2] = " _
            & myArray.GetValue(0, 2).ToString)
Debug.Print("Value in position [2,3] = " _
            & myArray.GetValue(2, 3).ToString)
Try
    myArray.SetValue(24, 3, 4)
    Debug.Print("I can store values in position [3,4]")
Catch ex As Exception
    Debug.Print("I can't store values in position [3,4]")
End Try

…obtenemos esta salida en la Ventana Inmediato:

Creating array with dimensions [2,3]
Storing value 12 in position [0,2]
Storing value 15 in position [2,3]
Upper bound for first dimension = 2
Upper bound for second dimension = 3
Value in position [0,2] = 12
Value in position [2,3] = 15
*** 'System.IndexOutOfRangeException' EXCEPTION ***
I can't store values in position [3,4]
Changing array dimensions to [4,5]
Upper bound for first dimension = 4
Upper bound for second dimension = 5
Value in position [0,2] = 12
Value in position [2,3] = 15
I can store values in position [3,4]

La función propuesta no es ni mucho menos perfecta. Tendría que hacerse más genérica, permitir redimensionar no dos sino N dimensiones, controlar mejor posibles errores y demás. Pero puede ser una primera aproximación para resolver situaciones de este tipo…

PD. También es posible utilizar el método Array.Copy en lugar de iterar por las dos dimensiones de la matriz, aunque en este caso lo he hecho así para que resulte más evidente el proceso.

Estadística de visitas (200810).

Venga, una vez más, publico las estadísticas de visitas a este blog del mes recién cerrado. Como siempre, si hay alguien interesado en realizar el seguimiento, puede hacerlo a través del tema habitual visitas, que aglutina todos estos posts. Este mes los resultados han sido especialmente buenos (no me preguntéis por qué) y me da que marcarán un listón difícil de superar por meses venideros.

Octubre 2008.

Número de visitas totales:       3339     (+ 88,75%)
Páginas individuales vistas:     4215     (+ 83,82%)
Promedio de tiempo en el sitio:  00:01:06 (-  8,33%)
Porcentaje de rebote:            85,12%   (+  1,40%)
Porcentaje de visitas nuevas:    82,42%   (+  0,96%)

El día con más visitas del mes ha sido el 28 de octubre con un total de 173.

Y los tres posts que durante el mes han sido más visitados son los siguientes:

1. Informe en .NET con Crystal Reports y base de datos MySQL. (9,21%)
2. Pasando parámetros al informe en .NET con Crystal Reports. (6,81%)
3. Crear librería .dll en .NET… ¡y utilizarla! (6,50%)

Lo cual me gusta porque los posts más visitados venían siendo siempre los mismos y andaba yo ya con la sensación de que no lograba aportar nada que tuviera el mismo interés de nuevo, pero el que en octubre ha sido el segundo más visitado es un post del, precisamente, 1 de octubre.

Y por último… este mes recupero la sección Premio Google del Mes. No es que haya habido una búsqueda especialmente absurda que ha venido a parar a este blog, pero sí ha habido una que me ha resultado curiosa, así que proclamo ganador de esta nueva edición y por tanto merecedor del…

Premio Google del Mes de Octubre del 2008

…a quien hizo la búsqueda en Google:

5 horas para ejecutar un archivo bat

Sorprendente. No quiero ni pensar qué hará dicho archivo (ni cómo)… :-)

Correspondencia entre tipos de datos en MySQL, en VisualBasic.NET y en .NET Framework.

Estos últimos días he estado liado programando una clase que tira intensamente de reflexión (me refiero a System.Reflection, no a que haya estado reflexionando intensamente, que también) para relacionar un objeto de una clase determinada (la que sea) con una tabla en la base de datos. Así, utilizando esa clase auxiliar podemos decirle que cargue un objeto a partir de un registro de una tabla o que lo guarde en ella. Y sirve para cualquier clase que tenga una tabla relacionada en la base de datos. Quizá otro día cuelgo esa clase… Pero el caso es que mientras lo desarrollaba he tenido algunos problemillas por no encajarme exactamente los tipos de datos que me devolvía MySQL y los que esperaba .NET, así que tras haber estado buscando cuáles son las equivalencias exactas, paso a exponer la tabla de correspondencias entre tipos de datos tanto en MySQL como en VisualBasic.NET como en el .NET Framework.

+--------------------+----------+----------------+
|       MYSQL        |  VB.NET  | .NET Framework |
+--------------------+----------+----------------+
| TINYINT            | SByte    | SByte          |
| TINYINT UNSIGNED   | Byte     | Byte           |
| SMALLINT           | Short    | Int16          |
| SMALLINT UNSIGNED  | UShort   | UInt16         |
| MEDIUMINT          | Integer  | Int32          |
| MEDIUMINT UNSIGNED | UInteger | UInt32         |
| INT                | Integer  | Int32          |
| INT UNSIGNED       | UInteger | UInt32         |
| BIGINT             | Long     | Int64          |
| BIGINT UNSIGNED    | ULong    | UInt64         |
+--------------------+----------+----------------+

Teniendo en cuenta estas relaciones, todo funciona a las mil maravillas… :-)




Creative Commons License
El blog de Albert Mata by Albert Mata is licensed under a Creative Commons Reconocimiento-Compartir bajo la misma licencia 2.5 España License.