sábado, 5 de marzo de 2016

Consultas Agrupadas

Puede utilizar consultas agrupadas para definir grupos en sus datos, y luego puede realizar cálculos de análisis de datos por grupo. Agrupa los datos por un conjunto de atributos conocidos como un conjunto de agrupación. Las consultas T-SQL tradicionales definen un único conjunto de agrupación; es decir, agrupan los datos en una sola manera. Más recientemente, T-SQL introdujo soporte para características que le permiten definir varios conjuntos de agrupación en una sola consulta. Este post comienza cubriendo consultas que definen un único conjunto de agrupación, y luego se cubren consultas que definen varios conjuntos.

Trabajando con un Conjunto de Agrupación Simple

Con consultas agrupadas, puede organizar las filas que está consultando en grupos y aplicar cálculos de análisis de datos como funciones agregadas sobre estos grupos. Una consulta se convierte en una consulta agrupada cuando se utiliza una función de grupo, una cláusula GROUP BY, o ambas.
Una consulta que invoca una función de grupo, pero no tiene una cláusula GROUP BY explícita, organiza todas las filas en un grupo. Considere la siguiente consulta como un ejemplo.
USE TSQL2012;
SELECT COUNT(*) AS numpedidos
FROM Sales.Orders;
Esta consulta genera el siguiente resultado.
numpedidos
-----------
830
Debido a que no hay cláusula GROUP BY explícita, todas las filas consultadas de la tabla Sales.Orders están dispuestas en un grupo, y entonces la función COUNT (*) cuenta el número de filas en ese grupo. Las consultas agrupadas retornan una fila de resultado por grupo; y debido a que la consulta define solo un grupo, retorna solo una fila en el conjunto de resultado.
Usando una cláusula GROUP BY explícita, puede agrupar las filas basado en un conjunto de agrupación específico de expresiones. Por ejemplo, la siguiente consulta agrupa las filas por shipperid y cuenta el número de filas (pedidos, en este caso) por cada grupo distinto.
SELECT shipperid, COUNT(*) AS numpedidos
FROM Sales.Orders
GROUP BY shipperid;
Esta consulta genera la siguiente salida.
shipperid    numpedidos
----------- -----------
1            249
2            326
3            255
La consulta identifica tres grupos porque hay tres shipperid distintos.
El conjunto de agrupación puede estar hecho de varios elementos. Por ejemplo, la siguiente consulta agrupa las filas por shipperid y año de envío.
SELECT shipperid, YEAR(shippeddate) AS annoenvio,
   COUNT(*) AS numpedidos
FROM Sales.Orders
GROUP BY shipperid, YEAR(shippeddate);
Esta consulta genera el siguiente resultado.
shipperid   annoenvio   numpedidos
----------- ----------- -----------
1            2008         79
3            2008         73
1            NULL         4
3            NULL         6
1            2006         36
2            2007         143
2            NULL         11
3            2006         51
1            2007         130
2            2008         116
2            2006         56
3            2007         125
Observe que obtiene un grupo por cada combinación de shipperid distinto y de año de envío que existe en los datos, incluso cuando el año de envió es NULL. Recuerde que un NULL en la columna shippeddate representa pedidos no enviados; así un NULL en la columna annoenvio representa el grupo de pedidos no enviados, por el respectivo distribuidor.
Si necesita filtrar grupos enteros, necesita una opción de filtrado que es evaluada a nivel de grupo, a diferencia de la cláusula WHERE, que es evaluada a nivel de fila. Para esto, T-SQL proporciona la cláusula HAVING. Al igual que la cláusula WHERE, la cláusula HAVING utiliza un predicado pero evalúa el predicado por grupo en lugar de por fila. Esto significa que puede referirse a cálculos agregados porque los datos ya han sido agrupados.
Por ejemplo, suponga que necesita agrupar sólo los pedidos enviados por shipperid y el año de envío, y filtrar sólo los grupos que tienen menos de 100 pedidos. Puede utilizar la siguiente consulta para lograr esta tarea.
SELECT shipperid, YEAR(shippeddate) AS annoenvio,
   COUNT(*) AS numpedidos
FROM Sales.Orders
WHERE shippeddate IS NOT NULL
GROUP BY shipperid, YEAR(shippeddate)
HAVING COUNT(*) < 100;
Esta consulta genera la siguiente salida.
shipperid    annoenvio  numpedidos
----------- ----------- -----------
1            2008         79
3            2008         73
1            2006         36
3            2006         51
2            2006         56
Observe que la consulta filtra sólo los pedidos enviados en la cláusula WHERE. Este filtro es aplicado a nivel de fila conceptualmente antes que los datos sean agrupados. Luego la consulta agrupa los datos por shipperid y año de envío. Entonces la cláusula HAVING filtra sólo los grupos que tienen un conteo de filas (pedidos) que sea menor que 100. Por último, la cláusula SELECT retorna el shipperid, año de envío, y el conteo de pedidos por cada grupo hallado.
T-SQL soporta un número de funciones agregadas. Estos incluyen COUNT(*) y algunas funciones de conjunto generales (ya categorizados por el SQL estándar) como COUNT, SUM, AVG, MIN y MAX. Las funciones de conjunto generales son aplicadas a una expresión e ignoran los NULLs.
La siguiente consulta invoca la función COUNT(*), además de un número de funciones de conjunto generales, incluyendo COUNT.
SELECT shipperid,
  COUNT(*) AS numpedidos,
  COUNT(shippeddate) AS pedidosenviados,
  MIN(shippeddate) AS fechaprimerpedido,
  MAX(shippeddate) AS fechaultimopedido,
  SUM(val) AS valortotal
FROM Sales.OrderValues
GROUP BY shipperid;
Esta consulta genera la siguiente salida (fechas formateadas para facilitar la lectura).
shipperid numpedidos pedidosenviados fechaprimerpedido fechaultimopedido valortotal
--------- ---------- --------------- ----------------- ---------------- -----------
3          255       249             2006-07-15              2008-05-01    383405.53
1          249       245             2006-07-10              2008-05-04    348840.00
2          326       315             2006-07-11              2008-05-06    533547.69
Note la diferencia entre los resultados de COUNT(shippeddate) y COUNT(*). El primero ignora NULLs en la columna shippeddate, y por lo tanto los conteos son menores o iguales que, a los producidos por el último.
Con funciones de conjunto generales, puede trabajar con ocurrencias distintas por especificar una cláusula DISTINCT antes de la expresión, de la siguiente manera.
SELECT shipperid, COUNT(DISTINCT shippeddate) AS numfechasenvio
FROM Sales.Orders
GROUP BY shipperid;
Esta consulta genera la siguiente salida.
shipperid    numfechasenvio
----------- -----------------
1            188
2            215
3            198
Note que la opción DISTINCT está disponible no sólo en la función COUNT, sino también para otras funciones de conjunto generales. Sin embargo, es más común utilizarlo con COUNT.
Desde una perspectiva de procesamiento de consulta lógico, la cláusula GROUP BY es evaluada después de las cláusulas FROM y WHERE, y antes de las cláusulas HAVING, SELECT y ORDER BY. Así que las últimas tres cláusulas ya trabajan con una tabla agrupada, y por lo tanto las expresiones que soportan son limitadas. Cada grupo está representado por solo una fila de resultado; por lo tanto, todas las expresiones que aparecen en tales cláusulas deben garantizar un solo valor de resultado por grupo. No hay problema en referenciar directamente a los elementos que aparecen en la cláusula GROUP BY, porque cada uno de estos, retorna sólo un valor distinto por grupo. Pero si desea referirse a elementos de las tablas originarias que no aparecen en la lista GROUP BY, debe aplicarles una función agregada. Así es como se puede estar seguro de que la expresión retorna un solo valor por grupo. Como ejemplo, la siguiente consulta no es válida.
SELECT S.shipperid, S.companyname, COUNT(*) AS numorders
FROM Sales.Shippers AS S
  JOIN Sales.Orders AS O
    ON S.shipperid = O.shipperid
GROUP BY S.shipperid;
Esta consulta genera el siguiente error.
Mens. 8120, Nivel 16, Estado 1, Línea 1
La columna 'Sales.Shippers.companyname' de la lista de selección no es válida, porque no está contenida en una función de agregado ni en la cláusula GROUP BY.
A pesar de que sabe que no puede haber más de un nombre de compañía distinto por cada shipperid distinto, T-SQL no lo sabe. Debido a que la columna S.companyname nunca aparece en la lista GROUP BY ni es contenida en una función agregada, no está permitida en las cláusulas HAVING, SELECT y ORDER BY.
Puede utilizar un número de soluciones. Una solución es agregar la columna S.companyname a la lista GROUP BY, de la siguiente manera.
SELECT S.shipperid, S.companyname, COUNT(*) AS numpedidos
FROM Sales.Shippers AS S
  INNER JOIN Sales.Orders AS O
    ON S.shipperid = O.shipperid
GROUP BY S.shipperid, S.companyname;
Esta consulta genera la siguiente salida.
shipperid   companyname    numpedidos
----------- -------------- -----------
1            Shipper GVSUA  249
2            Shipper ETYNR  326
3            Shipper ZHISN  255
Otra solución es aplicar una función agregada como MAX a la columna, de la siguiente manera.
SELECT S.shipperid,
  MAX(S.companyname) AS empresaenvio,
  COUNT(*) AS numorders
FROM Sales.Shippers AS S
  INNER JOIN Sales.Orders AS O
    ON S.shipperid = O.shipperid
GROUP BY S.shipperid;
En este caso, la función agregada es artificial, porque no puede haber más de un nombre de compañía distinto por cada shipperid distinto. La primera solución, sin embargo, tiende a producir los planes más óptimos, y también parece ser la solución más natural.
La tercera solución es agrupar y agregar las filas de la tabla Orders primero, definir una expresión de tabla basado en la consulta agrupada, y luego unir la expresión de tabla con la tabla Shippers para obtener los nombres de compañías de envíos. Aquí está el código de la solución.
WITH C AS
(
  SELECT shipperid, COUNT(*) AS numpedidos
  FROM Sales.Orders
  GROUP BY shipperid
)
SELECT S.shipperid, S.companyname, numpedidos
FROM Sales.Shippers AS S  
  INNER JOIN
    ON S.shipperid = C.shipperid;
SQL Server generalmente optimiza la tercera solución, como lo hace con la primera. La primera solución podría ser preferible, ya que implica mucho menos código.

Trabajando con Varios Conjuntos de Agrupación

Con T-SQL, puede definir varios conjuntos de agrupación en la misma consulta. En otras palabras, puede utilizar una consulta para agrupar los datos en más de una forma. T-SQL soporta tres cláusulas que permiten definir varios conjuntos de agrupamiento: GROUPING SETS, CUBE y ROLLUP. Los utilizará en la cláusula GROUP BY.
Puede utilizar la cláusula GROUPING SETS para listar todos los conjuntos de agrupación que desea definir en la consulta. Como ejemplo, la siguiente consulta define cuatro conjuntos de agrupación.
SELECT shipperid, YEAR(shippeddate) AS annoenvio, COUNT(*) AS numpedidos
FROM Sales.Orders
GROUP BY GROUPING SETS
(
  (shipperid, YEAR(shippeddate)   ),
  (shipperid                      ),
  (YEAR(shippeddate)              ),
  (                               )
);
Lista los conjuntos de agrupación separados por comas en el par exterior de paréntesis, pertenecientes a la cláusula GROUPING SETS. Se utiliza un par interior de paréntesis para encerrar cada conjunto de agrupación. Si no indica un par interior de paréntesis, cada elemento individual es considerado un conjunto de agrupación separado.
Esta consulta define cuatro conjuntos de agrupación. Uno de ellos es el conjunto de agrupación vacío, que define un grupo con todas las filas para el cálculo de grandes agregados. La consulta genera la siguiente salida.
shipperid   annoenvio   numpedidos
----------- ----------- -----------
1            NULL         4
2            NULL         11
3            NULL         6
NULL         NULL         21
1            2006         36
2            2006         56
3            2006         51
NULL         2006         143
1            2007         130
2            2007         143
3            2007         125
NULL         2007         398
1            2008         79
2            2008         116
3            2008         73
NULL         2008         268
NULL         NULL         830
1            NULL         249
2            NULL         326
3            NULL         255
La salida combina los resultados de agrupación y agregación de los datos de cuatro conjuntos de agrupación diferentes. Como puede ver en la salida, los NULLs son utilizados como marcadores de posición en filas donde un elemento no es parte del conjunto de agrupaciones. Por ejemplo, en las filas de resultados que están asociados con el conjunto de agrupación (shipperid), la columna de resultado annoenvio se establece a NULL. Del mismo modo, en las filas que están asociados con el conjunto de agrupación (YEAR(shippeddate)), la columna shipperid se establece a NULL.
Podría conseguir el mismo resultado escribiendo cuatro consultas agrupadas separadas, cada una definiendo solo un único conjunto de agrupación, y unificando sus resultados con un operador UNION ALL. Sin embargo, esta solución implicaría mucho más código y no estaría optimizada tan eficientemente como la consulta con la cláusula GROUPING SETS.
T-SQL soporta dos cláusulas adicionales llamadas CUBE y ROLLUP, que se pueden considerar como abreviaciones de la cláusula GROUPING SETS. La cláusula CUBE acepta una lista de expresiones como entradas y define todos los conjuntos de agrupación posibles que pueden ser generados de las entradas, incluyendo el conjunto de agrupación vacío. Por ejemplo, la siguiente consulta es un equivalente lógico de la consulta previa que utiliza la cláusula GROUPING SETS.
SELECT shipperid, YEAR(shippeddate) AS shipyear, COUNT(*) AS numorders
FROM Sales.Orders
GROUP BY CUBE(shipperid, YEAR(shippeddate));
La cláusula CUBE define los cuatro posibles conjuntos de agrupación de las dos entradas:
1.    (shipperid, YEAR(shippeddate))
2.    (shipperid)
3.    (YEAR(shippeddate))
4.    ( )
La cláusula ROLLUP es también una abreviación de la cláusula GROUPING SETS, pero lo utiliza cuando hay una jerarquía formada por los elementos de entrada. En tal caso, sólo un subconjunto de los posibles conjuntos de agrupación es realmente interesante. Considere, por ejemplo, una jerarquía de ubicación hecha de los elementos shipcountry, shipregion y shipcity, en este orden. Solo es interesante cubrir los datos en una sola dirección, calculando agregados para los siguientes conjuntos de agrupación:
1.    (shipcountry, shipregion, shipcity)
2.    (shipcountry, shipregion)
3.    (shipcountry)
4.    ( )
Los otros conjuntos de agrupación simplemente no son interesantes. Por ejemplo, a pesar de que el mismo nombre de ciudad puede aparecer en diferentes lugares del mundo, no es interesante agregar todas las ocurrencias, independientemente de la región y el país.
Así que, cuando los elementos forman una jerarquía, se utiliza la cláusula ROLLUP y de esta manera evita calcular agregados innecesarios. He aquí un ejemplo de una consulta utilizando la cláusula ROLLUP basada en la jerarquía antes mencionada.
SELECT shipcountry, shipregion, shipcity, COUNT(*) AS numpedidos
FROM Sales.Orders
GROUP BY ROLLUP(shipcountry, shipregion, shipcity);
Esta consulta genera la siguiente salida (que se muestra aquí en forma abreviada).
shipcountry    shipregion       shipcity        numpedidos
--------------- --------------- --------------- -----------
Argentina       NULL             Buenos Aires        16
Argentina       NULL             NULL                16
Argentina       NULL             NULL                16
...
USA             AK               Anchorage           10
USA             AK               NULL                10
USA             CA               San Francisco       4
USA             CA               NULL                4
USA             ID               Boise               31
USA             ID               NULL                31
...
USA             NULL             NULL                122
...
NULL            NULL             NULL                830
Como se ha mencionado, los NULLs son utilizados como marcadores de posición, cuando un elemento no es parte del conjunto de agrupación. Si todas las columnas agrupadas no aceptan NULLs en la tabla originaria, puede identificar las filas que están asociados con un único conjunto de agrupación, basado en una combinación única de NULLs y no NULLs en estas columnas. Un problema surge en la identificación de las filas que están asociadas con un único conjunto de agrupación, cuando una columna agrupada permite NULLs, como es el caso con la columna shipregion. ¿Cómo saber si un NULL en el resultado representa un marcador de posición (significando "todas las regiones") o un NULL original de la tabla (significando "región inaplicable")? T-SQL proporciona dos funciones para ayudar a resolver este problema: GROUPING y GROUPING_ID.
La función GROUPING acepta un solo elemento como entrada y retorna 0 cuando el elemento es parte del conjunto de agrupación y 1 cuando no lo es. La siguiente consulta muestra el uso de la función de agrupación.
SELECT
  shipcountry, GROUPING(shipcountry) AS grplocation,
  shipregion, GROUPING(shipregion) AS grplocation,
  shipcity, GROUPING(shipcity) AS grplocation,
  COUNT(*) AS numpedidos
FROM Sales.Orders
GROUP BY ROLLUP(shipcountry, shipregion, shipcity);
Esta consulta genera la siguiente salida (que se muestra aquí en forma abreviada).
shipcountry  grplocation shipregion  grplocation shipcity      grplocation numpedidos
------------ ---------- ------------ ---------- -------------- ---------- ---------
Argentina    0           NULL      0          Buenos Aires      0         16
Argentina    0           NULL      0          NULL              1         16
Argentina    0           NULL      1          NULL              1         16
...
USA          0           AK        0          Anchorage         0         10
USA          0           AK        0          NULL              1         10
USA          0           CA        0          San Francisco     0         4
USA          0           CA        0          NULL              1         4
USA          0           ID        0          Boise             0         31
USA          0           ID        0          NULL              1         31
...
USA          0           NULL      1          NULL              1         122
...
NULL         1           NULL      1          NULL              1         830
Ahora puede identificar un conjunto de agrupación por la búsqueda de 0s en los elementos que son parte del conjunto de agrupación y 1s en el resto.
Otra función que puede utilizar para identificar los conjuntos de agrupación es GROUPING_ID. Esta función acepta la lista de columnas agrupadas como entradas y retorna un entero representando un bitmap. El bit más a la derecha representa la entrada más a la derecha. El bit es 0 cuando el elemento respectivo es parte del conjunto de agrupación y 1 cuando no lo es. Cada bit representa 2 elevado a la potencia de la posición del bit menos 1; por lo que el bit más a la derecha representa 1, el de su izquierda 2, luego 4, a continuación, 8, y así sucesivamente. El entero resultante es la suma de los valores que representan elementos que no son parte del conjunto de agrupación debido a que sus bits están activados. He aquí una consulta que demuestra el uso de esta función.
SELECT GROUPING_ID(shipcountry, shipregion, shipcity) AS grp_id,
shipcountry, shipregion, shipcity,
COUNT(*) AS numpedidos
FROM Sales.Orders
GROUP BY ROLLUP(shipcountry, shipregion, shipcity);
Esta consulta genera la siguiente salida (mostrada aquí en forma abreviada).
grp_id       shipcountry   shipregion        shipcity       numpedidos
----------- --------------- --------------- --------------- -----------
0            Argentina      NULL              Buenos Aires        16
1            Argentina      NULL              NULL                16
3            Argentina      NULL              NULL                16
...
0            USA            AK                Anchorage           10
1            USA            AK                NULL                10
0            USA            CA                San Francisco       4
1            USA            CA                NULL                4
0            USA            ID                Boise               31
1            USA            ID                NULL                31
...
3            USA            NULL              NULL                122
...
7            NULL           NULL              NULL                830
La última fila de esta salida representa el conjunto de agrupación vacía, ninguno de los tres elementos es parte del conjunto de agrupaciones. Por lo tanto, los bits respectivos (valores 1, 2, y 4) están activados. La suma de los valores que esos bits representan es 7.
Algebra de Conjuntos de Agrupación
Puede especificar varias cláusulas GROUPING SETS, CUBE, ROLLUP en la cláusula GROUP BY separadas por comas. De esta manera, consigue un efecto multiplicador. Por ejemplo, la cláusula CUBE(a, b, c) define ocho conjuntos de agrupación y la cláusula ROLLUP(x, y, z) define cuatro conjuntos de agrupación. Al especificar una coma entre los dos, como en CUBE (a, b, c), ROLLUP (x, y, z), son multiplicados y obtiene 32 conjuntos de agrupación.

Ejercicio 1: Agregar Información Acerca de Pedidos de Clientes

En este ejercicio, agrupa y agrega datos que implican clientes y pedidos. Cuando se le da una tarea, prueba primero con su propia solución de consulta antes de mirar a la consulta proporcionada.
1.    Abra el SSMS y conéctese a la base de datos de muestra TSQL2012.
2.    Escriba una consulta que calcule el número de pedidos por cada cliente para los clientes de España.
Para lograr esta tarea, primero necesita unir las tablas Sales.Customers y Sales.Orders basados en coincidencias entre el custid del cliente y el custid del pedido. Entonces, filtrar sólo las filas donde el país del cliente es España. Entonces agrupar las filas restantes por custid. Porque hay una columna custid en ambas tablas de entrada, necesita dar prefijo a la columna con el origen de tabla. Por ejemplo, si prefiere utilizar la tabla Sales.Customers, y darle alias de C a esa tabla, necesita especificar C.custid en la cláusula GROUP BY. Por último, retornar el custid y el conteo de filas en la lista SELECT. A continuación mostramos la consulta completa.
USE TSQL2012;
SELECT C.custid, COUNT(*) AS numpedidos
FROM Sales.Customers AS C
  INNER JOIN Sales.Orders AS O
    ON C.custid = O.custid
WHERE C.country = N'Spain'
GROUP BY C.custid;
Esta consulta genera la siguiente salida.
custid       numpedidos
----------- -----------
8            3
29           5
30           10
69           5
3.    Agregue la información de la ciudad en la salida de la consulta. En primer lugar, trate de solo agregar C.city a la lista SELECT, como sigue.
SELECT C.custid, C.city, COUNT(*) AS numpedidos
FROM Sales.Customers AS C
  INNER JOIN Sales.Orders AS O
    ON C.custid = O.custid
WHERE C.country = N'Spain'
GROUP BY C.custid;
Obtiene el siguiente error.
Mens. 8120, Nivel 16, Estado 1, Línea 1
La columna 'Sales.Customers.city' de la lista de selección no es válida, porque no está contenida en una función agregada ni en la cláusula GROUP BY.
4.    Encontrar una solución que podría retornar la ciudad también.
Una posible solución es agregar la ciudad a la cláusula GROUP BY, como sigue.
SELECT C.custid, C.city, COUNT(*) AS numpedidos
FROM Sales.Customers AS C
  INNER JOIN Sales.Orders AS O
    ON C.custid = O.custid
WHERE C.country = N'Spain'
GROUP BY C.custid, C.city;
Esta consulta genera la siguiente salida.
custid       city             numpedidos
----------- --------------- -----------
8            Madrid           3
29           Barcelona        5
30           Sevilla          10
69           Madrid           5
Ejercicio 2: Definir Varios Conjuntos de Agrupación
En este ejercicio, se definen varios conjuntos de agrupación.
·         Su punto de partida es la consulta que escribió en el paso 4 del Ejercicio 1. Además de los conteos por cliente retornado por esta consulta, también incluye en la misma salida el conteo total. Necesita que la salida muestre primero los conteos por cliente y luego el conteo total.
Puede utilizar la cláusula GROUPING SETS para definir dos conjuntos de agrupación: uno para (C.custid, C.city), y otro para el conjunto de agrupación vacío (). Para ordenar los conteos de clientes antes del conteo total, ordenar los datos por GROUPING(C.custid). Aquí está la consulta completa.
SELECT C.custid, C.city, COUNT(*) AS numpedidos
FROM Sales.Customers AS C
  INNER JOIN Sales.Orders AS O
    ON C.custid = O.custid
WHERE C.country = N'Spain'
GROUP BY GROUPING SETS((C.custid, C.city),())
ORDER BY GROUPING(C.custid);
Esta consulta genera el siguiente resultado.
custid       city             numpedidos
----------- --------------- -----------
8            Madrid           3
29           Barcelona        5
30           Sevilla          10
69           Madrid           5
NULL         NULL             23

2 comentarios:

  1. Este post trata sobre consultas agrupadas, trabajando con una consulta y varias consultas agrupadas. Espero les sea de utilidad.

    ResponderBorrar
  2. Don Narcizo gracias por el Tema de Agrupamiento asi como los Operadores GROUPING SETS, ROLLUP Y CUBE para incluir filas de resumen para grupos y subgrupos, son muy interesantes.

    Corrijame si estoy mal, pero por lo que pude notar es que estos Operadores GROUPING SETS, ROLLUP Y CUBE son muy parecidos a las funciones de ventana OVER(PARTITION BY ORDEN BY ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) ya que en ambos puedo agrupar y sacar resumenes sin ocultar el DETALLE.

    Por lo demas gracias por compartir su conocimiento

    ResponderBorrar