Curso de SQL usando MySQL PDF Imprimir E-mail
Escrito por Carlos Zaltzman   
Viernes, 07 de Abril de 2006 16:15
Indice del artículo
Curso de SQL usando MySQL
Páginas 2
Páginas 3
Páginas 4
Páginas 5
Páginas 6
Páginas 7
Páginas 8
Páginas 9
Páginas 10
Páginas 11
Páginas 12
Páginas 13
Páginas 14
Todas las páginas

En este artículo se explica el uso de la sentencia SELECT para realizar consultas a bases de datos MySQL. Se explican muchos conceptos relacionados al uso de la sentencia SELECT, pero sobre todo, se incluyen una gran cantidad de ejemplos y ejercicios.

 

Supongamos que usted ha instalado MySQL en una empresa. En ese caso los empleados tienen interés en poder realizar consultas a la base de datos. Pasado un tiempo, a un nivel gerencial medio se desarrolla un considerable interés en poder realizar consultas directamente. Es muy conveniente poder realizar consultas sin pasar por un mecanismo burocrático o incurrir en mayores costos.

Este tutorial puede servir de base para un curso. En general, los estudiantes deberían tener alguna experiencia previa como operadores. O quizás usted desarrolla aplicaciones y se encuentra con MySQL instalado. Quiere aprender rápidamente (por ejemplo, en un día) a realizar consultas en mysql, con peculiar atención a los problemitas que se generan con relación a los tildes, los formatos de fecha, etc.

En estos casos, confiamos en que este tutorial le resulte de ayuda.

Suponemos que tenemos a disposición un servidor MySQL versión 4.1.3b-beta o posterior. Versiones anteriores pueden servir para casi todo el curso, pero en algunas pueden no funcionar los subqueries.

  • Índice
  • Conociendo la base de datos.
  • Instrucciones SELECT elementales.
  • Manejo de fechas.
  • Funciones de agregados. (Aggregate functions).
  • Agrupando datos.
  • Vinculaciones entre tablas.
  • Búsqueda elemental de texto.
  • Unión de dos tablas de resultados.
  • Subqueries.
  • Salida a Excel.
  • El funcionamiento de SELECT.
  • Glosario.
  • Bibliografía.

El proceso por el que normalmente se extrae información de una base de datos, en SQL, es la instrucción SELECT.
Veremos a continuación varios problemas, con sus soluciones, relativos a SELECT.

En estos problemas utilizaremos una base de datos llamada empresa. Se dispone de una instalación de la base de datos para Windows. Baje el archivo crear_empresa.sql de aquí.

En lo que sigue suponemos que hay un directorio C:\mysql\bin donde tenemos el cliente mysql que se llama justamente mysql.exe. Si no es así, tendremos que averiguar en qué directorio se encuentra, y en todo lo que sigue se deberá sustituir C:\mysql\bin por el directorio donde se encuentra mysql.exe. Cree un directorio dentro del c:\mysql\bin con un nombre corto. Para lo que sigue supondremos que dicho directorio es c:\mysql\bin\p.

Copie el archivo crear_empresa.sql a este directorio.

Asegúrese que el servidor MySQL funciona. Si usted está trabajando directamente en el equipo que tiene instalado el servidor puede iniciarlo con el programa WinMySqlAdmin. Este programa se encuentra normalmente en el directorio c:\mysql\bin. Por otra parte, si está accediendo a un servidor en red, supondremos que el servidor está funcionando. De lo contrario deberá contactar al administrador de la red.

Ejecute el cliente mysql, según el siguientes procedimiento. En la mayoría de las instalaciones usted debería tener un nombre de usuario y un password (clave). Basta que cree una ventana MS-DOS (busque MS-DOS, o Símbolo de Sistema, en Inicio, Programas) y luego escriba

C:\WINDOWS > cd c:\mysql\bin  C:\mysql\bin > mysql -uUsuario –pClave  

donde Usuario debe sustituirse por su nombre de usuario y Clave por su clave. Si usted está accediendo desde el equipo donde está instalado el servidor, probablemente no necesite usuario ni clave. En ese caso, ingrese simplemente el comando mysql.

C:\MySQL\bin > mysql  

Debería aparecer, después de algunos mensajes, un prompt como este:

mysql>  

En ese prompt, escriba lo siguiente.

mysql>source p/crear_empresa.sql 

En este momento debería crearse la base de datos. Ya está listo para comenzar el curso. Si tiene dificultades, es posible que el administrador de base de datos no le haya dado suficientes "derechos". Quizás usted no tenga "derecho" a crear una base de datos, por ejemplo. Contáctelo y solicítele poder crear y tener todos los derechos sobre la base empresa.


Conociendo la base de datos

En primer lugar, antes de empezar a estudiar la instrucción SELECT exploraremos la base de datos. Suponemos que el cliente mysql está funcionando y tenemos el prompt mysql> a la vista. Si no es así, ejecute el cliente mysql.

Para acceder a la base de datos utilizamos la sentencia USE.

mysql> USE empresa; 

Recibimos un mensaje "Database changed" (base de datos cambiada). Una base de datos está formada por tablas, muy semejantes a las tablas de cualquier libro.

Para ver qué tablas tenemos escribimos la sentencia SHOW TABLES.

 

mysql> SHOW TABLES; +-------------------+ | Tables_in_empresa | +-------------------+ | articulos         | | clientes          | | conformes         | | empleados         | | facturas          | | grupos            | | lineas_factura    | | sucursales        | | ventas            | +-------------------+ 9 rows in set (0.00 sec) 

Para ver qué contiene alguna de esas tablas escribimos nuestra primera instrucción SELECT :

mysql> SELECT * FROM sucursales; +----+-------------+ | id | descripcion | +----+-------------+ |  1 | Centro      | |  2 | Unión       | |  3 | Malvín      | +----+-------------+ 

Esta tabla está formada por dos columnas: Id y Descripcion. Los nombres de columnas tampoco pueden llevar tildes. Debajo de los nombres están los valores. Tenemos una fila, por ejemplo, donde Id vale 2 y Descripcion es "Unión". Para averiguar más sobre esta tabla escribimos:

mysql> DESCRIBE sucursales; +-------------+------------------+------+-----+---------+----------------+ | Field       | Type             | Null | Key | Default | Extra          | +-------------+------------------+------+-----+---------+----------------+ | id          | int(10) unsigned |      | PRI | NULL    | auto_increment | | descripcion | varchar(15)      |      |     |         |                | +-------------+------------------+------+-----+---------+----------------+             

La instrucción "DESCRIBE sucursales" se podría traducir como "Realice una descripción de la tabla sucursales". Cada fila es una descripción de la columna cuyo nombre figura a la izquierda. En este caso, la 2º fila describe Id y la 3ª describe Descripcion. Esta descripción de la tabla sucursales contiene información más bien técnica, pero algunas partes son importantes:

El varchar(15) que se encuentra en la 3ª fila, 2ºcolumna significa que la columna Descripcion puede tener un número variable de caracteres, hasta 15. Análogamente el int(10) que se encuentra en la 2ª fila, 2ª columna indica que Id es un número entero.

La palabra PRI que se encuentra en la 2ªfila, 4ª columna significa que Id caracteriza de manera única una fila. Es decir, cada fila de sucursales va a tener un número que va a estar en la columna Id. Diferentes filas tendrán diferentes números. De manera que un valor de Id, si está en la tabla, determina una fila. PRI es una abreviatura de PRIMARY KEY , clave primaria. Esta es una expresión usada en computación para indicar una columna que se usa para identificar las filas. Es algo así como el número de documento de identidad para las personas. Si se quiere hacer una referencia a una sucursal en otra tabla se usa el Id.

Sugerimos que el estudiante explore, usando SELECT * FROM ... y DESCRIBE ..., sobre las diversas tablas de la base de datos empresa.


Tipos de datos

En una tabla cada columna es de un tipo de datos. Para nuestros fines hay 3 grandes tipos de datos: numéricos, cadenas de caracteres y fechas.

Los datos numéricos pueden ser enteros (int) o decimales (decimal). Los decimales tienen una parte entera y una fraccionaria. Por ejemplo, un decimal puede valer 120.40.

Las cadenas de caracteres representan texto, generalmente con un largo variable, hasta un máximo dado. Pueden contener letras, números y otros caracteres como ‘=’,’+’, etc.

Ya vimos un ejemplo con la columna Descripcion de la tabla sucursales. Las columnas de tipo fecha tienen, en su descripción, la palabra "date". Cualquier columna puede, en principio, tomar el valor NULL, que representa un valor desconocido o inexistente. Sin embargo, cuando se crearon las tablas el administrador de la base de datos normalmente especificó que la mayoría de las columnas no pueden tomar valores NULL. Las que sí pueden aparecen con la palabra YES en la columna NULL de la tabla que se obtiene haciendo el query "DESCRIBE nombre_de_tabla" . Vemos que por ejemplo en la tabla Sucursales la base de datos no aceptará un valor NULL ni en Id ni en Descripcion.


Instrucciones SELECT elementales

La instrucción SELECT

Para extraer información de una base de datos, ingresamos un texto llamado "query" (pronúnciese "cueri") en el cliente mysql. El query indica qué información queremos. El plural de "query" es "queries" (pronúnciese "cueris"). Se podría traducir query como consulta, pero hemos preferido mantener el término técnico. La información sale a pantalla, en un formato poco práctico cuando se manejan cantidadades importante de datos. En la sección 10 veremos cómo resolver este problema.

Por ahora, todos nuestros ejemplos se llevan a la práctica de la misma forma.

  1. El servidor MySQL debe estar en funcionamiento.
  2. Se debe haber iniciado el cliente mysql.
  3. Ya se ha seleccionado la base de datos empresa, con una instrucción "USE empresa".
  4. Se ingresa el query en el prompt mysql>.
  5. Los resultados salen a pantalla.

Para exhibir los valores de una columna determinada de una tabla, usamos SELECT Nombre_de_columna FROM Nombre_de_tabla;

Ejemplo :

mysql> SELECT descripcion FROM sucursales; +-------------+ | descripcion | +-------------+ | Centro      | | Unión       | | Malvín      | +-------------+  

Para seleccionar más de una columna, SELECT Nombre_de_columna_1, Nombre_de_columna_2,... FROM Nombre_de_tabla;

Ejemplo:

mysql> SELECT nombre, direccion FROM clientes; +-------------+---------------------+ | nombre      | direccion           | +-------------+---------------------+ | Matt Design | NULL                | | Diana Pérez | Brito del Pino 1120 | | John Smith  | Zum Felde 2024      | +-------------+---------------------+ 

Para seleccionar todas las columnas de una tabla, SELECT * FROM Nombre_de_tabla;

Ejemplo:

mysql> SELECT * FROM ventas; +----+---------+----------+---------+----------+----------+----------+------------+ | id | cliente | vendedor | importe | articulo | cantidad | sucursal | fecha      | +----+---------+----------+---------+----------+----------+----------+------------+ |  1 |       1 |        1 |  178.50 |        1 |        1 |        1 | 2004-10-01 | |  2 |       2 |        1 |  195.00 |        2 |        1 |        2 | 2004-10-05 | |  3 |       3 |        1 |  178.50 |        1 |        1 |     NULL | 2004-10-07 | |  4 |       1 |        1 |   16.00 |        7 |        2 |        2 | 2004-11-01 | |  5 |       1 |        2 |  220.00 |        3 |        1 |        1 | 2004-12-02 | |  6 |       1 |        1 |    5.50 |        5 |        1 |        1 | 2004-10-02 | |  7 |       1 |        2 |   11.00 |        5 |        2 |        1 | 2004-10-03 | |  8 |       1 |        2 |    5.50 |        5 |        1 |        1 | 2005-01-01 | |  9 |       2 |        1 |  200.00 |        2 |        1 |        2 | 2004-10-12 | +----+---------+----------+---------+----------+----------+----------+------------+ 

Obsérvese que los clientes, vendedores, artículos y sucursales son identificados por su Id. El importe representa el total de la venta, no el precio unitario. También se pueden hacer cálculos con una columna numérica.

Ejemplos:

mysql> SELECT importe, 0.10*importe FROM ventas; +---------+--------------+ | importe | 0.10*importe | +---------+--------------+ |  178.50 |        17.85 | |  195.00 |        19.50 | |  178.50 |        17.85 | |   16.00 |         1.60 | |  220.00 |        22.00 | |    5.50 |         0.55 | |   11.00 |         1.10 | |    5.50 |         0.55 | |  200.00 |        20.00 | +---------+--------------+ 

Se pueden agregar a la salida columnas constantes, que no tienen relación con las tablas

Ejemplo:

mysql> SELECT "Impuesto", 0.10*importe FROM ventas; +----------+--------------+ | Impuesto | 0.10*importe | +----------+--------------+ | Impuesto |        17.85 | | Impuesto |        19.50 | | Impuesto |        17.85 | | Impuesto |         1.60 | | Impuesto |        22.00 | | Impuesto |         0.55 | | Impuesto |         1.10 | | Impuesto |         0.55 | | Impuesto |        20.00 | +----------+--------------+ 

En general es deseable no tener títulos de columna complicados como "0.10*Importe". Para esos casos se usa la instrucción AS ( que significa, entre otras cosas, "como").

Ejemplo:

mysql> SELECT 0.10*importe AS impuesto FROM ventas; +----------+ | impuesto | +----------+ |    17.85 | |    19.50 | |    17.85 | |     1.60 | |    22.00 | |     0.55 | |     1.10 | |     0.55 | |    20.00 | +----------+ 

Se puede solicitar que no haya repeticiones en las filas seleccionadas. Basta agregar DISTINCT después del SELECT.

Ejemplo:

mysql> SELECT DISTINCT cliente FROM ventas; +---------+ | cliente | +---------+ |       1 | |       2 | |       3 | +---------+ 

Los resultados de cálculos, si no se toma alguna medida al respecto, a veces salen con demasiados decimales. En esos casos, conviene usar la función ROUND. Esta función redondea los números a la cantidad deseada de decimales después del punto. Por ejemplo, ROUND(2.343,2) produce el resultado 2.34. Análogamente ROUND(2.347,2) produce 2.35, y ROUND(2.245,2) produce 2.34. Para redondear a un entero alcanza con escribir ROUND(número). Por ejemplo, ROUND(2.345) produce 2.

Ejemplo:

mysql> SELECT ROUND(importe/3,2) FROM ventas; +--------------------+ | ROUND(importe/3,2) | +--------------------+ |              59.50 | |              65.00 | |              59.50 | |               5.33 | |              73.33 | |               1.83 | |               3.67 | |               1.83 | |              66.67 | +--------------------+ 

En el último punto hemos visto por primera vez una función. Una función consta de un nombre, seguido de paréntesis, entre los cuales según los casos puede no haber nada, o haber una o más cantidades. La función realiza ciertos cálculos con las cantidades y genera otra cantidad, llamada el valor de la función. Una consideración práctica es que no se pueden dejar espacios entre el nombre de la función y el primer paréntesis después del mismo. Siempre escribiremos paréntesis después del nombre de una función.


La cláusula WHERE.

Esta cláusula sirve para seleccionar filas, dentro de las columnas seleccionadas. WHERE significa "donde". Se pueden seleccionar filas donde una columna tiene un valor determinado.

Ejemplo:

mysql> SELECT * FROM clientes WHERE id=1; +----+-------------+-----------+----------+ | id | nombre      | direccion | vendedor | +----+-------------+-----------+----------+ |  1 | Matt Design | NULL      |        1 | +----+-------------+-----------+----------+ 

Se puede seleccionar filas donde una columna tiene un valor mayor (o menor) que uno dado.

Ejemplo:

mysql> SELECT * FROM clientes WHERE id < 3; +----+-------------+---------------------+----------+ | id | nombre      | direccion           | vendedor | +----+-------------+---------------------+----------+ |  1 | Matt Design | NULL                |        1 | |  2 | Diana Pérez | Brito del Pino 1120 |     NULL | +----+-------------+---------------------+----------+ 

Las igualdades y desigualdades también se aplican a cadenas de caracteres. Una cadena es menor que otra cuando es previa en orden alfabético. Por ejemplo "azzl" < "baa" . No se distingue entre mayúsculas y minúsculas (aunque, si se lo desea, es posible configurar el servidor MySQL para que sí distinga). Es decir, "A" < "b" < "c" < "D" y "a"="A".

Ejemplo:

mysql> SELECT * FROM clientes WHERE nombre < "i"; +----+-------------+---------------------+----------+ | id | nombre      | direccion           | vendedor | +----+-------------+---------------------+----------+ |  2 | Diana Pérez | Brito del Pino 1120 |     NULL | +----+-------------+---------------------+----------+ 

Este SELECT listará los clientes con nombre empezando en una letra anterior a "i" en el alfabeto.

También se aplican a fechas la igualdad y desigualdad. Para referirse a una fecha, se le escribe como "Año con 4 cifras-Mes-Día". Por ejemplo, "2004-12-20" indica el 20 de Diciembre de 2004.

Ejemplo:

mysql> SELECT * FROM ventas WHERE fecha < '2004-12-20'; +----+---------+----------+---------+----------+----------+----------+------------+ | id | cliente | vendedor | importe | articulo | cantidad | sucursal | fecha      | +----+---------+----------+---------+----------+----------+----------+------------+ |  1 |       1 |        1 |  178.50 |        1 |        1 |        1 | 2004-10-01 | |  2 |       2 |        1 |  195.00 |        2 |        1 |        2 | 2004-10-05 | |  3 |       3 |        1 |  178.50 |        1 |        1 |     NULL | 2004-10-07 | |  4 |       1 |        1 |   16.00 |        7 |        2 |        2 | 2004-11-01 | |  5 |       1 |        2 |  220.00 |        3 |        1 |        1 | 2004-12-02 | |  6 |       1 |        1 |    5.50 |        5 |        1 |        1 | 2004-10-02 | |  7 |       1 |        2 |   11.00 |        5 |        2 |        1 | 2004-10-03 | |  9 |       2 |        1 |  200.00 |        2 |        1 |        2 | 2004-10-12 | +----+---------+----------+---------+----------+----------+----------+------------+ 

Para determinar si un dato es NULL se usa la condición ‘IS NULL’. Para saber si no es NULL, se usa la condición ‘IS NOT NULL’.

Ejemplo:

mysql> SELECT * FROM ventas WHERE sucursal IS NULL; +----+---------+----------+---------+----------+----------+----------+------------+ | id | cliente | vendedor | importe | articulo | cantidad | sucursal | fecha      | +----+---------+----------+---------+----------+----------+----------+------------+ |  3 |       3 |        1 |  178.50 |        1 |        1 |     NULL | 2004-10-07 | +----+---------+----------+---------+----------+----------+----------+------------+ 

No debe usarse <>NULL. Produce resultados equivocados. Usar IS NOT NULL.

Llamamos condiciones simples a las siguientes

Expresión Significado
a = b a es igual a b
a = b a es igual a b
a <> b a es distinto de b
a < b a es menor que b
a > b a es mayor que b
a <= b a es menor o igual a b
a >= b a es mayor o igual a b
a IS NULL a es NULL
a IS NOT NULL a no es NULL

Las cantidades a y b pueden ser números, cadenas de caracteres o fechas, en todos los casos. En una cláusula WHERE se puede usar cualquier condición simple. La cláusula WHERE selecciona aquellas filas en que la condición es verdadera.

Las condiciones simples pueden aparecer combinadas por operadores lógicos. Los operadores lógicos son AND, OR y NOT. Aquí E y F representan condiciones.

ExpresiónSignificadoEs verdadera cuando ...
E AND F E y F E es verdadera y F es verdadera
E OR F E o F o ambos E es verdadera o F lo es o ambas
NOT E No E E es falsa

NOTA : El operador NOT requiere paréntesis. Es decir se debe escribir WHERE NOT (salario >50) mientras que es incorrecto WHERE NOT salario > 50. Se entiende que buscamos filas con salarios no mayores que 50.

Ejemplos:

Listar los empleados cuya fecha de ingreso sea anterior al 2004, o cuyo salario sea mayor que 50 ( o ambas cosas).

mysql> SELECT * FROM empleados WHERE fecha_ingreso < '2004-1-' OR salario > 50; +----+-----------------+---------------+---------+ | id | nombre          | fecha_ingreso | salario | +----+-----------------+---------------+---------+ |  1 | Carlos Zaltzman | 1999-12-10    |   10000 | |  2 | Juan Fernández  | 2000-01-03    |   12000 | +----+-----------------+---------------+---------+ 

Listar los clientes cuyos nombres empiecen con una letra entre ‘c’ y ‘l’.

mysql> SELECT * FROM clientes WHERE "C" <= nombre AND nombre < "M"; +----+-------------+---------------------+----------+ | id | nombre      | direccion           | vendedor | +----+-------------+---------------------+----------+ |  2 | Diana Pérez | Brito del Pino 1120 |     NULL | |  3 | John Smith  | Zum Felde 2024      |        1 | +----+-------------+---------------------+----------+ 

Obsérvese que la condición apropiada para obtener los nombre que empiezan con ‘l’ es nombre <’M’. Si escribiésemos nombre < =‘L’ no obtendríamos un nombre como ‘Luis Zúñiga’ porque ‘L’<’Luis Zúñiga’. Se pueden usar incluso condiciones más complicadas. Por ejemplo, supongamos que ahora queremos listar los empleados cuya fecha de ingreso es anterior al 2004, o cuyo salario sea mayor que 50, pero no ambas cosas a la vez. Entonces, debemos escribir :

mysql> SELECT * FROM empleados WHERE (fecha_ingreso < '2004-1-1' OR salario > 50)     -> AND NOT (fecha_ingreso < '2004-1-1' AND salario > 50); Empty set (0.00 sec) 

El resultado es "Empty set", es decir, no hay filas que cumplan la condición. En el caso de condiciones más complicadas se recomienda un amplio uso de paréntesis.


La cláusula ORDER BY

La cláusula ORDER BY produce una ordenación de las filas de salida del query. Se puede ordenar por una columna seleccionada.

mysql> SELECT cliente, articulo FROM ventas ORDER BY cliente; +---------+----------+ | cliente | articulo | +---------+----------+ |       1 |        1 | |       1 |        7 | |       1 |        3 | |       1 |        5 | |       1 |        5 | |       1 |        5 | |       2 |        2 | |       2 |        2 | |       3 |        1 | +---------+----------+ 

También se puede ordenar por varias columnas.

mysql> SELECT cliente, vendedor, articulo FROM ventas ORDER BY cliente, vendedor; +---------+----------+----------+ | cliente | vendedor | articulo | +---------+----------+----------+ |       1 |        1 |        1 | |       1 |        1 |        7 | |       1 |        1 |        5 | |       1 |        2 |        3 | |       1 |        2 |        5 | |       1 |        2 |        5 | |       2 |        1 |        2 | |       2 |        1 |        2 | |       3 |        1 |        1 | +---------+----------+----------+ 

Cuando se ordena por varias columnas, por ejemplo 3, el procedimiento es básicamente el que sigue : Se ordena por la primera columna. Si hay valores repetidos en la primera columna, para cada grupo de valores repetidos se ordenan las filas por el valor de la 2ª columna. Si hay valores repetidos de las dos primeras columnas en conjunto, se ordenan las filas correspondientes por la 3ª columna. Ejemplo: Supongamos una tabla con las siguientes columnas y valores:

   +-----+-----------------------+------------+----------+ | a   | b                     | c          | d        | +-----+-----------------------+------------+----------+ | 125 | Diana Pérez           | 1/1/2004   | 12313    | | 486 | Alejandro Bentancourt | 30/12/2005 | 45646    | | 222 | Jorge Rodríguez       | 2/5/2004   | 78987    | | 125 | Diana Pérez           | 3/8/2004   | 654654   | | 222 | Adriana Salgado       | 1/3/2002   | 12312    | +-----+-----------------------+------------+----------+  

Si ordenamos esta tabla por las columnas a, b y c, obtenemos

+-----+-----------------------+------------+----------+ | a   | b                     | c          | d        | +-----+-----------------------+------------+----------+ | 125 | Diana Pérez           | 1/1/2004   | 12313    | | 125 | Diana Pérez           | 3/8/2004   | 654654   | | 222 | Adriana Salgado       | 1/3/2002   | 12312    | | 222 | Jorge Rodríguez       | 2/5/2004   | 78987    | | 486 | Alejandro Bentancourt | 30/12/2005 | 45646    | +----+-------------+-----------------------+----------+ 

Por último, se puede ordenar por una cantidad calculada a partir de una o varias columnas.

  mysql> SELECT articulo, importe, cantidad FROM ventas     -> ORDER BY articulo, importe/cantidad; +----------+---------+----------+ | articulo | importe | cantidad | +----------+---------+----------+ |        1 |  178.50 |        1 | |        1 |  178.50 |        1 | |        2 |  195.00 |        1 | |        2 |  200.00 |        1 | |        3 |  220.00 |        1 | |        5 |    5.50 |        1 | |        5 |   11.00 |        2 | |        5 |    5.50 |        1 | |        7 |   16.00 |        2 | +----------+---------+----------+

La cláusula LIMIT

La preparación de un query complicado implica normalmente un proceso de prueba y error. Aunque no se cometan errores, siempre se empieza escribiendo queries que sólo realizan una parte de lo que se desea alcanzar. Luego, se van mejorando gradualmente hasta llegar al objetivo buscado.

Cuando se trabaja con tablas auténticas con muchos miles de filas, puede ser demasiado engorroso ir obteniendo repetidas salidas con cientos o miles de filas. Es obvio que no se pueden observar en la pantalla del cliente mysql. Más adelante, veremos cómo usar Microsoft Excel para observar las salidas de los queries. Por otra parte, en su instalación puede haber otros clientes que operen con MySQL.

De todas maneras, interesa una cláusula sencilla que limite el número de filas que produce el SELECT. Esa es la función de LIMIT. Si, por ejemplo, escribimos

mysql> SELECT articulo, cliente FROM ventas LIMIT 3; +----------+---------+ | articulo | cliente | +----------+---------+ |        1 |       1 | |        2 |       2 | |        1 |       3 | +----------+---------+ 

entonces sólo vemos 3 filas en la salida, a pesar que hay varias más en Ventas.


El orden de las cláusulas.

Las cláusulas mencionadas, SELECT...FROM.. , WHERE, ORDER BY, y LIMIT deben escribirse, si aparecen, en ese orden. SELECT siempre aparece y va en primer lugar. Las otras 3 son optativas.


Cómo ingresar queries.

Hasta ahora hemos venido escribiendo a continuación del prompt mysql> del cliente mysql.

Este procedimiento es inconveniente apenas se empiezan a complicar los queries. Un procedimiento sencillo para Windows, es el siguiente:

  1. Inicie el programa Bloc de Notas. Suele estar en Inicio, Programas, Accesorios.
  2. Escriba en él su query.
  3. Guárdelo en nuestro directorio de trabajo : C:\MySQL\bin\p. con un nombre sencillo que le parezca apropiado como prueba.txt. Nota : El Bloc de Notas agrega automáticamente .txt al final del nombre de archivo así que sólo escriba prueba, sin punto al final. ¡Si escribe prueba. se guardará como prueba..txt, con 2 puntos!
  4. Vaya al cliente mysql y escriba
    mysql> SOURCE p/prueba.txt  
    o, si lo prefiere,
    mysql> \. p/prueba.txt  
  5. Si el query no dio el resultado esperado, ahora no es preciso escribir todo de nuevo. Simplemente corríjalo en el Bloc de Notas. y vuelva a guardarlo. Repita los pasos d) y e) hasta obtener el resultado buscado. Si lo desea, ahora puede archivar el query con un nombre más descriptivo, usando la opción Guardar como del menú Archivo del Bloc de Notas.

Nota sobre la arquitectura Cliente-Servidor

A esta altura, más de un lector se hará la pregunta de para qué existe la separación entre el servidor MySQL y el cliente mysql. ¿ No podría ser todo un solo programa? La respuesta es que normalmente se va a trabajar en red con un servidor y muchos clientes.

Los clientes son programas relativamente sencillos que se pueden programar para que estén adaptados a las necesidades del usuario. En cambio, los servidores son programas complejos que realmente no se pueden adaptar a las necesidades de un usuario individual (aunque son configurables, por ejemplo, pueden emitir mensajes en un idioma seleccionado por el administrador de la base de datos).


Problemas resueltos
  1. Obtener todos los datos de la tabla Empleados.
    Solución :
    El problema es obtener todos las columnas de todas las filas de la tabla Empleados.
    Query :
              SELECT * FROM Empleados; 
  2. Obtener los nombres y las fechas de ingreso de los empleados.
    Solución :
    Formalmente, queremos obtener las columnas Nombre, Fecha_ingreso de todas las filas de la tabla Empleados. El query es:
              SELECT  Nombre, Fecha_ingreso FROM Empleados; 
  3. Obtener los datos de los empleados que ganan más de $ 10500,50.
    Solución:
    Queremos obtener todas las columnas de las filas de Empleados para las cuales Salario es mayor que 10500,50. El query es:
              SELECT * FROM Empleados           WHERE Salario > 10500.50; 
    Obsérvese el uso del punto decimal. Las comas no se pueden utilizar ni en lugar del punto decimal ni para separar los miles (en un query).

  4. Queremos obtener los datos del funcionario Carlos Zaltzman.
    Solución:
    Se trata de obtener una fila de Empleados, aquella en la cual el nombre del funcionario es Carlos Zaltzman. El query es:
              SELECT  * FROM  Empleados           WHERE Nombre = 'Carlos Zaltzman'; 
    Este método no es muy práctico. En primer lugar el nombre debe ser escrito en el query casi exactamente como está en la tabla. Con todo, los diseñadores de MySQL han tenido el sentido común de incluir en un select como este a filas (si existiesen) con Nombre ‘carlos zaltzman’ o ‘Carlos Zaltzman ‘. Es decir: no se tienen en cuenta mayúsculas y minúsculas, ni blancos al final.

    Sin embargo, sigue siendo muy poco práctico. Además, si hay dos empleados con el mismo nombre, no hay manera de distinguirlos, si no se usa otra columna. Por eso todas las empresas tienen números de funcionario.

  5. ¿Qué empleados ingresaron el 3 de enero del 2000?.
    Solución:
    Se trata de obtener filas de Empleados, aquellas en las cuales la fecha de ingreso sea '2000-1-3'. El formato apropiado para la fecha es Año-Mes-Día. El query es :
              SELECT  * FROM Empleados           WHERE Fecha_ingreso = '2000-1-3'; 
  6. ¿Qué empleados ingresaron en fecha posterior al 1º de enero del 2000?
    Solución:
    Queremos obtener las filas de Empleados con fecha de ingreso mayor que '2000-1-1'. El query es :
              SELECT  * FROM Empleados           WHERE Fecha_ingreso>'2000-1-1'; 
  7. ¿Qué empleados ingresaron el 5 de enero del 2000 o en una fecha posterior?
    Solución:
    La fecha de ingreso debe ser mayor o igual a '2001-1-5'. El query es :
              SELECT  * FROM Empleados           WHERE Fecha_ingreso>='2000-1-5'; 
  8. Sacar una lista de los nombres de los clientes de los cuales no se tiene dirección.
    Solución:
    Se trata de obtener la columna Nombre de las filas de la tabla Clientes cuya dirección es NULL. El query es :
              SELECT Nombre FROM Clientes           WHERE Direccion IS NULL; 
  9. Sacar una lista de los nombres y direcciones de los clientes de los cuales sí se tiene la dirección.
    Solución:
    Se trata de obtener la columnas Nombre, Direccion de las filas de la tabla Clientes que tienen direcciones que no sean NULL. El query es:
              SELECT  Nombre,Direccion FROM Clientes           WHERE Direccion IS NOT NULL; 
  10. Obtener una lista de los diferentes salarios que se pagan en la empresa.
    Solución:
              SELECT DISTINCT Salario FROM Empleados; 
  11. Sacar una lista ordenada alfabéticamente de los nombre y direcciones de los clientes, ordenados por nombre.
    Solución:
              SELECT Nombre,Direccion FROM Clientes           ORDER BY Nombre; 
  12. Sacar una lista de todos los datos de los empleados ordenados por nombre.
    Solución:
              SELECT * FROM Empleados           ORDER BY Nombre; 
  13. Sacar una lista de los datos de los empleados ordenados por salario en forma descedente. Los salarios deben ser formateados con comas cada 3 dígitos y dos decimales después del punto y alineados a la derecha , por ejemplo, 1,200,340.50.
    Solución:
              SELECT Id,                  Nombre,                  Fecha_ingreso,                  LPAD( FORMAT(Salario,2) , 12 , ' ' ) AS Salario            FROM Empleados           ORDER BY Salario DESC; 
  14. Igual al anterior pero se quiere que los números salgan formateados con puntos cada 3 dígitos y coma decimal, en vez de punto. Por ejemplo, 1.200.340,50 .
    Solución:
              SELECT Id,                  Nombre,                   Fecha_ingreso,                  REPLACE(                  REPLACE(                  REPLACE(                          LPAD(FORMAT(Salario,2),12,' '),                          '.','!'),                          ',','.'),                          '!',',')                  AS Salario            FROM Empleados           ORDER BY Salario DESC; 

Si bien es un procedimiento complicado, para usarlo en otro caso sólo hay que copiar la parte que comienza con REPLACE y termina en el paréntesis antes de AS Salario y hacer los siguientes cambios:

  1. Cambiar la columna Salario por la que se vaya a usar.
  2. Eventualmente cambiar el número de decimales después de la coma, que figura dentro de la función FORMAT, de 2 al valor que se desee.
  3. Si es conveniente, variar el ancho de la columna. Este ancho aparece dentro de la función LPAD. En este ejemplo vale 12. El ancho debe ser suficiente para poder escribir todos los valores que aparezcan en la columna, con el número de decimales que se haya solicitado.


Manejo de fechas

SELECT como calculadora

Se puede usar la cláusula SELECT para hacer cálculos aritméticos.

Ejemplos:

          SELECT 1+1;           SELECT 4.5*(3.86+2.34) 

Se debe usar punto decimal y no coma.

Se puede usar la función POW ( por power, potencia) para hacer cálculos relativos a intereses.

POW(a,b) calcula a elevado a la potencia b. Por ejemplo, POW(10,3) produce el valor 1000.

También se pueden hacer cálculos trigonométricos, etc, pero, en general, estos cálculos no relacionados con bases de datos se hacen mejor con una calculadora.


Cálculos con fechas
Donde sí resulta realmente interesante el poder de cálculo de MySQL, es con los cálculos relativos a fechas. Para esos cálculos, necesitaremos unas cuantas funciones. Para los ejemplos, supondremos que hoy es 1/1/2005.
  • CURDATE(). Esta función da la fecha del día (¡si el sistema tiene la fecha correcta!). Ejemplo:
    SELECT CURDATE();  
    Obtenemos 2005-01-01, en el formato de fecha ‘Año-Mes-Día’ propio de MySQL.

  • DAYNAME(). Dada una fecha, esta función da el nombe ( en inglés) del día de la semana correspondiente. Ejemplo:
    SELECT DAYNAME(CURDATE());  
    Obtenemos Saturday, es decir, Sábado.

  • DAYOFWEEK(). Es complementaria de la anterior. En vez de darnos el nombre del día de la semana nos da un código numérico de 1 a 7. El código 1 representa el Domingo, el 2 el Lunes, y así hasta el 7 que representa el Sábado. Ejemplo:
    SELECT DAYOFWEEK(CURDATE());  
    Obtenemos un 7,que indica el Sábado.

  • DATE_FORMAT(). Nos permite presentar las fechas en otros formatos. Los formatos que usaremos son '%d/%m/%y' y '%d/%m/%Y'. Ejemplo:
    SELECT DATE_FORMAT(CURDATE(),’%d/%m/%Y');  
    Produce 01/01/2005.

  • DATE_ADD(). Esta función nos permite agregar a una fecha cierto número de días ( o meses y años) Ejemplo: ¿Cuál es la fecha de dentro de 15 días?
    SELECT DATE_ADD(CURDATE(), INTERVAL 15 DAY);  
    o mejor :
    SELECT DATE_FORMAT(DATE_ADD(CURDATE(),INTERVAL 15 DAY) ,’%d/%m/%Y');  
    Produce 16/01/2005.
  • DATE_SUB(). Esta función le quita cierto número de días (o meses y años) a una fecha. Ejemplo: ¿Cuál es la fecha de hace 15 días?
    SELECT DATE_FORMAT(DATE_SUB(CURDATE(),INTERVAL 15 DAY),’%d/%m/%Y');  
    Produce 17/12/2004.

  • DATEDIFF(). Esta función obtiene la diferencia, en días, entre dos fechas. Ejemplo :
    SELECT DATEDIFF(‘2005-4-1’,’2004-5-30’);  
    Este query produce 306, lo que significa que del 30/5/2004 al 1/4/2005 van 306 días.

  • YEAR(), MONTH() ,DAY() extraen de una fecha el año, el mes y día correspondientes. Ejemplo:
    SELECT MONTH(‘2005-4-1’); produce 4, el número del mes.  

Problemas resueltos
  1. ¿A qué fecha estamos?
    Solución:
              SELECT CURDATE(); 
    La instrucción SELECT se puede usar para hacer diversas operaciones que no tienen relación con una base de datos. En este caso, nos da la fecha. Esta fecha sale en el formato año-mes-día que puede resultar difícil de leer para muchas personas. Por eso, es mejor
              SELECT DATE_FORMAT(CURDATE(),'%d / %m / %y'); 
    con lo que se obtiene el formato día/mes/año usual. El uso de blancos antes y después de los signos / es opcional.

    En general la función DATE_FORMAT( .........., '%d / %m / %y') nos sirve para presentar cualquier fecha en un formato legible.

  2. ¿Qué día de la semana es hoy?
    Solución:
    Si nos conformamos con un resultado en inglés alcanza con
              SELECT DAYNAME(CURDATE()); 
    Para obtener el día de la semana en español hay que escribir un poco más :
              SELECT              CASE DAYOFWEEK(CURDATE())                   WHEN 1 THEN 'Domingo'                   WHEN 2 THEN 'Lunes'                   WHEN 3 THEN 'Martes'                   WHEN 4 THEN  CONCAT('Mi',CHAR(130),'rcoles')                   WHEN 5 THEN 'Jueves'                   WHEN 6 THEN 'Viernes'                   WHEN 7 THEN  CONCAT('S',CHAR(160),'bado')              END AS Dia_de_la_Semana; 
    Este mismo método puede aplicarse a cualquier fecha sustituyendo el CURDATE() de la primera línea por la fecha dada.

  3. Obtener una lista de los empleados que ingresaron después del 15/12/1999, con sus nombres y fechas de ingreso en el formato usual día/mes/año (el año con 4 cifras).
    Solución:
              SELECT Nombre,DATE_FORMAT(Fecha_ingreso,'%d/%m/%Y') AS                   Fecha_de_Ingreso           FROM Empleados           WHERE Fecha_ingreso > '1999-12-15'; 
    La %Y en el formato de la fecha produce el año con 4 cifras.

  4. ¿Qué ventas se han realizado en los últimos 25 días?
    Solución:
    Incluimos el día de hoy entre los 25 días mencionados.
              SELECT * FROM Ventas           WHERE Fecha > DATE_SUB(CURDATE(), INTERVAL 25 DAY); 
  5. ¿ Qué ventas se han realizado en el corriente mes?
    Solución:
              SELECT * FROM Ventas           WHERE Fecha > DATE_SUB(CURDATE(),                 INTERVAL DAYOFMONTH(CURDATE()) DAY); 
    ¿Cómo funciona este query?

    Primero se evalúa CURDATE() dando la fecha del día. DAYOFMONTH separa de esa fecha el día del mes. Supongamos que se trata de un día 3. Entonces DATE_SUB le resta a su primer parámetro CURDATE(), el número de día del mes. Pero 3 días antes del 3 de mes está el último día del mes anterior. Una fecha mayor que el último día del mes anterior es lo mismo que una fecha del mes actual.

  6. ¿Qué conformes a pagar se vencen durante los próximos 18 días?
    Solución:
              SELECT * FROM CONFORMES           WHERE Vencimiento<=DATE_ADD(CURDATE(),INTERVAL 18 DAY); 
  7. Realizar una tabla con los datos de los conformes, pero agregando una columnas donde se indica cuántos días faltan para el vencimiento de cada uno.
    Solución:
              SELECT *, DATEDIFF(Vencimiento,CURDATE()) AS Dias_para_Vencimiento            FROM Conformes; 
  8. Obtener una lista de los conformes con sus fechas de vencimiento en la forma , por ejemplo, 3 de Febrero de 2004 , ordenados por fecha de vencimiento de manera que el conforme que se vence primero aparezca primero en la lista.
    Solución:
    En inglés las fechas se formatean con facilidad:
              SELECT Acreedor,                  DATE_FORMAT(Vencimiento,'%M %D, %Y') AS Vencimiento,                  Importe            FROM Conformes           ORDER BY Vencimiento; 
    Para obtener un resultando semejante en español hay que escribir:
              SELECT Acreedor,                  CONCAT(                         DAYOFMONTH(Vencimiento),                         ' de ',                         CASE MONTH(Vencimiento)                              WHEN 1 THEN 'Enero'                              WHEN 2 THEN 'Febero'                              WHEN 3 THEN 'Marzo'                              WHEN 4 THEN 'Abril'                              WHEN 5 THEN 'Mayo'                              WHEN 6 THEN 'Junio'                              WHEN 7 THEN 'Julio'                              WHEN 8 THEN 'Agosto'                              WHEN 9 THEN 'Setiembre'                              WHEN 10 THEN 'Octubre'                              WHEN 11 THEN 'Noviembre'                              WHEN 12 THEN 'Diciembre'                         END,                         ' de ',                         YEAR(Vencimiento)) AS Vencimiento,                         Importe           FROM Conformes; 
  9. Obtener una lista de los empleados con el número de años que llevan en la empresa al día de hoy. Se cuentan únicamente años enteros. Por ejemplo, si un empleado lleva 5 años y 10 meses en la empresa, contamos únicamente 5 años.
    Solución:
              SELECT *,                  YEAR(CURDATE()) - YEAR(Fecha_ingreso) - (RIGHT(CURDATE(),5)                   < RIGHT(Fecha_ingreso,5)) AS Antiguedad            FROM Empleados; 
    Veamos cómo funciona este query, que es bastante complejo. En primer lugar, aparece un valor booleano RIGHT(CURDATE(),5) < RIGHT(Fecha_ingreso,5). Para entender este valor, en primer lugar debemos darnos cuenta que RIGHT(CURDATE(),5) representa los 5 caracteres más a la derecha de CURDATE(). Es decir, los dos dígitos del mes de hoy, un guión, y los dos dígitos del día de hoy.

    El signo de < que separa los dos RIGHT representa en realidad una pregunta : ¿ el primero da un resultado alfabéticamente anterior al segundo ? Esto es equivalente a preguntar si la fecha de hoy es anterior al día en que se cumplen años de la fecha de ingreso. Ahora bien, el hecho que una condición sea verdadera se representa en el computador por un valor 1 mientras que si la condición es falsa se guarda un valor 0.

    Entonces, si la desigualdad es verdadera la expresión (RIGHT(CURDATE(),5) < RIGHT(Fecha_ingreso,5)) vale 1. En caso contrario, vale 0.

    Veamos un ejemplo completo. Si un empleado hubiese ingresado el 20 de diciembre del año y hoy es 10 de octubre, entonces todavía no cumplió un año. Obtenemos un resultado correcto porque YEAR(CURDATE()) - YEAR(Fecha_ingreso) vale 1 y (RIGHT(CURDATE(),5) < RIGHT(Fecha_ingreso,5)) también vale 1. La diferencia, cero, indica que el empleado todavía no tiene un año en la empresa.

    Cuando llegue el 20 de diciembre, la expresión (RIGHT(CURDATE(),5) < RIGHT(Fecha_ingreso,5)) pasa a valer 0 y la diferencia YEAR(CURDATE()) - YEAR(Fecha_ingreso) - (RIGHT(CURDATE(),5) < RIGHT(Fecha_ingreso,5)) pasa a valer 1. Es decir, ahora el empleado ha cumplido un año en la empresa.

    Este truco es útil porque muchas reglamentaciones sobre licencias y primas dependen de los años enteros que tenga el empleado.


Funciones de agregados. (Aggregate functions)

A menudo queremos realizar operaciones sobre un conjunto de filas de una tabla. Por ejemplo, queremos sumar los contenidos de una columna Importe en toda la tabla, o sólo en las filas que cumplen cierta condición. Para eso existen ciertas funciones llamadas funciones de agregados (Aggregate functions). 

Veremos algunas de estas funciones:
  • SUM(). Esta función totaliza una columna, dentro de las filas que cumplen una condición, o de todas las filas si no es especifica ninguna condición. En lo que sigue abreviaremos esta expresión y diremos simplemente ‘de las filas especificadas’. Ejemplos: Sumar todos los importes de Ventas.
    mysql> SELECT SUM(importe) FROM ventas; +--------------+ | SUM(importe) | +--------------+ |      1010.00 | +--------------+ 
    Sumar todos los importes de Ventas, con fecha del año 2003.
    mysql> SELECT SUM(importe) FROM ventas WHERE YEAR(fecha)=2003; +--------------+ | SUM(importe) | +--------------+ |         NULL | +--------------+ 
  • COUNT(*). Cuenta todas las filas especificadas. Por ejemplo, el siguiente query produce el número de filas de la tabla lineas_factura.
    mysql> SELECT COUNT(*) FROM lineas_factura; +----------+ | COUNT(*) | +----------+ |        5 | +----------+ 
  • COUNT(Nombre_de_columna). Cuenta cuántos valores no NULL hay de la columna especificada dentro de las filas especificadas. Por ejemplo, el siguiente query produce el número de direcciones de clientes que tenemos registradas.
    mysql> SELECT COUNT(direccion) FROM clientes; +------------------+ | COUNT(direccion) | +------------------+ |                2 | +------------------+ 
  • COUNT ( DISTINCT Nombre_de_columna). Cuenta cuántos valores no NULL diferentes hay de la columna especificada, en las filas especificadas. El siguiente ejemplo produce el número de clientes distintos a los cuales se les han registrado ventas.
    mysql> SELECT COUNT(DISTINCT(cliente)) FROM ventas; +--------------------------+ | COUNT(DISTINCT(cliente)) | +--------------------------+ |                        3 | +--------------------------+ 
  • MAX(). Produce el máximo valor de una columna dentro de las filas especificadas.
  • MIN(). Produce el mínimo valor de una columna en las filas especificadas.
  • AVG(). Produce el promedio de los valores de una columna numérica, contando sólo las filas especificadas.

Introducción a las subqueries
Un subquery es una instrucción SELECT dentro de otra instrucción. Los subqueries que vamos a estudiar en esta sección son llamado subqueries escalares. Producen una sola cantidad que como de costumbre puede ser numérica, una cadena de caracteres o una fecha. 

Un subquery siempre debe estar rodeado de paréntesis. Usaremos subqueries para crear condiciones en cláusulas WHERE.

Problemas resueltos
  1. Obtener la suma de los salarios pagos en la Empresa.
    Solución:
    Debemos obtener la suma de la columna Salario en la tabla Empleados.
              SELECT SUM(Salario) FROM Empleados; 
  2. Obtener la suma de los salarios mayores a 11000.
    Solución:
    Debemos obtener la suma de la columna Salario de aquellas filas de Empleados que tienen Salario mayor a 11000.
              SELECT SUM(Salario) FROM Empleados           WHERE Salario > 11000; 
  3. Contar cuántos empleados tiene la empresa.
    Solución:
    Debemos obtener el número de filas en la tabla Empleados.
              SELECT COUNT(*) FROM Empleados; 
  4. Contar cuánto empleados tienen salario menor de 11000.
    Solución:
    Debemos obtener el número de filas en la tabla Empleados con Salario <11000.
              SELECT COUNT(*) FROM Empleados           WHERE Salario < 11000; 
  5. Contar cuántas direcciones de clientes se tienen.
    Solución:
    Debemos obtener el número de filas de Clientes cuya columna Direccion no es NULL.
              SELECT COUNT(Direccion) FROM Clientes; 
    Observar que como se especificó COUNT(Direccion) las direcciones NULL no se cuentan. En cambio SELECT COUNT(*) FROM Clientes cuenta todas las filas, independientemente que tengan o no dirección (o algúna otra columna) NULL.

  6. ¿Cuál es el salario promedio de la empresa?
    Solución:
              SELECT AVG(Salario) FROM Empleados; 
  7. ¿Cuál es el salario máximo pago por la empresa?
    Solución:
              SELECT MAX(Salario) FROM Empleados; 
  8. ¿Y el salario mínimo?
    Solución:
              SELECT MIN(Salario) FROM Empleados; 
  9. ¿Cuál son los empleados más antiguos de la empresa?
    Solución:
    Vamos a seleccionar todas las filas de la tabla Empleados con Fecha de Ingreso igual a la fecha de ingreso mínima.
              SELECT * FROM Empleados           WHERE Fecha_ingreso =                       (SELECT MIN(Fecha_ingreso) FROM Empleados); 
  10. Hallar los nombres de los empleados con salario máximo en la empresa.
    Solución:
              SELECT Nombre FROM Empleados           WHERE Salario =                       (SELECT MAX(Salario) FROM Empleados); 
  11. ¿Cuántos salarios distintos se pagan en la empresa?
    Solución:
              SELECT COUNT(DISTINCT Salario) FROM Empleados; 

 


Agrupando datos

La cláusula GROUP BY.

A menudo interesa agrupar las filas según algún criterio, que suele ser el valor de una columna, o varias. Una cláusula SELECT con un GROUP BY produce una sola fila por cada grupo. Los valores de las columnas especificadas en el SELECT deberían ser iguales dentro de cada grupo. De lo contrario los resultados son imprevisibles.

También aparecen usualmente funciones de agregado que ahora se calculan sobre cada grupo. Los resultados, si no especifica otra cosa con un ORDER BY, salen ordenados por las columnas del GROUP BY.

Ejemplo: ¿Cuántas líneas tiene cada una de las facturas? Veamos la tabla Lineas_factura:

 

+----+---------+-------+ | id | factura | venta | +----+---------+-------+ |  1 |       1 |     1 | |  2 |       1 |     6 | |  3 |       1 |     7 | |  4 |       2 |     2 | |  5 |       3 |     3 | +----+---------+-------+ 

Veamos el resultado de efectuar el siguiente query.

mysql> SELECT factura, COUNT(*) FROM lineas_factura GROUP BY factura; +---------+----------+ | factura | COUNT(*) | +---------+----------+ |       1 |        3 | |       2 |        1 | |       3 |        1 | +---------+----------+ 

Es decir, que tenemos una factura 1 con 3 líneas y otras, 2 y 3, con una línea cada una.


La cláusula HAVING.

Después de un GROUP BY, todavía se pueden volver a seleccionar filas con una cláusula HAVING. Por ejemplo, podemos estar interesados, hipotéticamente, en las facturas con más de una línea. Entonces deberíamos usar

          SELECT Factura, COUNT(*) FROM Lineas_Factura           GROUP BY Factura           HAVING COUNT(*) > 1; 

Si no nos interesa ver el número de líneas podemos usar

          SELECT Factura  FROM Lineas_Factura           GROUP BY Factura           HAVING COUNT(*) > 1; 

Otra variante más legible

mysql> SELECT factura, COUNT(*) AS cantidad FROM lineas_factura     -> GROUP BY factura HAVING cantidad > 1; 

El resultado es:

+---------+----------+ | factura | cantidad | +---------+----------+ |       1 |        3 | +---------+----------+ 

El orden de las cláusulas vistas hasta ahora es SELECT.....FROM...., WHERE, GROUP BY, HAVING, ORDER BY, LIMIT.

Hay dos momentos en que se seleccionan filas, una con el WHERE antes de la agrupación y otro con el HAVING, después de la misma.


Problemas resueltos
  1. Cuánto totalizan las ventas de cada sucursal?
    Solución:

    En la tabla Ventas tenemos una columna Sucursal. Esa columna contiene un código numérico que por ahora utilizaremos tal como está. ( Más adelante, le haremos corresponder a cada código numérico su descripción del tipo 1=Centro, 2=Unión, etc. Estos son barrios de la bella ciudad de Montevideo, donde es nacido el autor). Un valor NULL de Sucursal puede representar una venta no asociada a una región determinada, por ejemplo una exportación inusual o una venta al Estado. Queremos para cada Sucursal totalizar los Importes correspondientes.
              SELECT Sucursal, SUM(Importe) FROM Ventas           GROUP BY Sucursal; 
    Una presentación algo mejor se logra con:
              SELECT Sucursal, SUM(Importe) as Ventas_Totales            FROM Ventas           GROUP BY Sucursal; 
  2. ¿Cuántas ventas se efectuaron en cada región?
    Solución:
              SELECT Sucursal, COUNT(*) as Numero_Ventas FROM Ventas           GROUP BY Sucursal; 
  3. ¿Cuántas ventas se efectuaron en cada región, por cada vendedor? Sólo se van a incluir en este informe las ventas con datos completos. No admitimos Sucursal o Vendedor NULL.
    Solución:
              SELECT Sucursal, Vendedor, COUNT(*) AS Numero_Ventas            FROM Ventas           WHERE Sucursal IS NOT NULL                  AND Vendedor IS NOT NULL           GROUP BY Sucursal, Vendedor; 
  4. ¿Cuál fue el importe promedio de las ventas en cada región?
    Solución:
              SELECT Sucursal, AVG(Importe) AS Venta_Promedio FROM Ventas           GROUP BY Sucursal; 
    Este query genera una salida un tanto confusa debido a que los promedios tienen demasiados decimales. Por ejemplo, uno de los promedios aparece como 137.000000 . Para evitar esto, es recomendable utilizar
              SELECT Sucursal, ROUND(AVG(Importe),2) AS Venta_Promedio            FROM Ventas           GROUP BY Sucursal; 
    La expresión ROUND(AVG(Importe),2) indica a MySQL que promedie los importes. redondee el resultado a 2 dígitos después de la coma. Si no queremos decimales podemos usar
              SELECT Sucursal, ROUND(AVG(Importe)) AS Venta_Promedio            FROM Ventas           GROUP BY Sucursal; 
  5. Ahora queremos totalizar los importes de las ventas de cada vendedor en cada región.
    Solución:
    Le vamos a pedir a MySQL que sume los importes agrupando por vendedor y región.
              SELECT Vendedor, Sucursal, SUM(Importe) AS Total            FROM Ventas           GROUP BY Vendedor, Sucursal; 
  6. Queremos totalizar los importes de las ventas de cada vendedor y además el total general de las ventas.
    Solución:
              SELECT Vendedor, SUM(Importe) AS Total            FROM Ventas           GROUP BY Vendedor WITH ROLLUP; 
    El total está en la última fila. Aparecen dos filas con Vendedor NULL. La primera corresponde a las ventas para las que no se tiene un vendedor en la tabla. La última contiene el total general, como es fácil verificar. Este total general aparece debido a que se agregó WITH ROLLUP al GROUP BY.

  7. Queremos totalizar los importes de las ventas por vendedor y región pero además obtener subtotales por vendedor y un total general.
    Solución:
              SELECT Vendedor, Sucursal,SUM(Importe) AS Total            FROM Ventas           GROUP BY Vendedor, Sucursal WITH ROLLUP; 
    Las presencia de filas con Vendedor y/o Sucursal NULL complica la lectura. Es preferible usar este método con agrupación por columnas que no presenten valores NULL.

  8. Queremos el promedio de las ventas de cada vendedor.
    Solución :
              SELECT ROUND(AVG(Total)) FROM (              SELECT Vendedor, SUM(Importe) AS Total               FROM Ventas               GROUP BY Vendedor               HAVING Vendedor IS NOT NULL)               AS Totales; 
    Hay varios puntos interesantes y novedosos en este query. Estamos usando un subquery para crear la tabla temporaria Totales. Esa tabla contiene el nombre del Vendedor y el Total de sus ventas. Se agrega una cláusula HAVING para no incluir el Vendedor NULL (es decir las ventas en las que no intervino un vendedor directamente).

    Después del subquery es imprescindible agregar "AS Totales" para darle un nombre ("Totales") a la tabla temporaria . Es decir, si el subquery se usa para generar una tabla temporaria de la cual se realiza un SELECT...FROM, es decir, de la cual se seleccionan filas, entonces es preciso asignarle un nombre a la tabla temporaria. El nombre se le asigna usando "AS".

    De nuevo usamos ROUND para evitar las cifras después de la coma.



Vinculaciones entre tablas

Supongamos que hiciéramos todas las combinaciones entre las filas de 2 o más tablas. Obtendríamos una nueva tabla con un número de filas igual al producto de los números de filas de las tablas consideradas. Esta tabla es el JOIN (se puede traducir como zona de unión, el verbo to join significa unir) de las tablas dadas. 

Por ejemplo, supongamos que hacemos esta operación con las tablas T1 y T2
T1  +------+------+ | a    | b    | +------+------+ | 1    | 4    | | 2    | 9    | | 3    | 7    | +------+------+  T2 +------+------+ | c    | d    | +------+------+ | AB   | 4    | | CD   | 10   | | EF   | 9    | +------+------+ 
Entonces el JOIN de T1 y T2 es : 
+------+------+------+------+ | a    | b    | c    | d    | +------+------+------+------+ | 1    | 4    | AB   | 4    | | 2    | 9    | AB   | 4    | | 3    | 7    | AB   | 4    | | 1    | 4    | CD   | 10   | | 2    | 9    | CD   | 10   | | 3    | 7    | CD   | 10   | | 1    | 4    | EF   | 9    | | 2    | 9    | EF   | 9    | | 3    | 7    | EF   | 9    | +------+------+------+------+ 

La cláusula ON
Generalmente, no nos interesa obtener todas las combinaciones de filas, sino únicamente aquellas que cumplen cierta condición. Esa condición se especifica en la cláusula ON. Por ejemplo, si efectuásemos el query 
          SELECT * FROM T1 JOIN T2           ON T1.b=T2.d; 
obtendríamos únicamente las filas del JOIN donde b y d coinciden, es decir: 
+------+------+------+------+ | a    | b    | c    | d    | +------+------+------+------+ | 1    | 4    | AB   | 4    | | 2    | 9    | EF   | 9    | +------+------+------+------+ 
Supongamos ahora, que queremos hacer un listado donde figure cada fila de T1 combinada con cada fila de T2 que tenga T1.b = T2.d, y además, la fila de T1 seguida de una fila de T2 llena de NULL si no hay ninguna fila de T2 que verifique la condición del ON con la fila de T1 dada. Esto es lo que se llama un LEFT JOIN. El query 
          SELECT * FROM T1 LEFT JOIN T2           ON T1.b=T2.d; 
produce la tabla 
+------+------+------+------+ | a    | b    | c    | d    | +------+------+------+------+ | 1    | 4    | AB   | 4    | | 2    | 9    | EF   | 9    | | 3    | 7    | NULL | NULL | +------+------+------+------+ 
La última fila aparece debido a la inexistencia de valores de d iguales a b=7 en la tabla T2. 

Si, al revés, queremos que toda fila de T2 aparezca precedida de las filas de T1 que le corresponden, si las hay, y si no, una fila llena de NULL, entonces tenemos que hacer un RIGHT JOIN. El query
          SELECT * FROM T1 RIGHT JOIN T2           ON T1.b=T2.d; 
produce 
+------+------+------+------+ | a    | b    | c    | d    | +------+------+------+------+ | 1    | 4    | AB   | 4    | | NULL | NULL | CD   | 10   | | 2    | 9    | EF   | 9    | +------+------+------+------+ 
Para prepararnos para los problemas resueltos, veamos las siguientes tablas: 

Clientes
+----+-------------+---------------------+----------+ | id | nombre      | direccion           | vendedor | +----+-------------+---------------------+----------+ |  1 | Matt Design | NULL                |        1 | |  2 | Diana Pérez | Brito del Pino 1120 |     NULL | |  3 | John Smith  | Zum Felde 2024      |        1 | +----+-------------+---------------------+----------+ 
Empleados 
+----+-----------------+---------------+---------+ | id | nombre          | fecha_ingreso | salario | +----+-----------------+---------------+---------+ |  1 | Carlos Zaltzman | 1999-12-10    |   10000 | |  2 | Juan Fernández  | 2000-01-03    |   12000 | +----+-----------------+---------------+---------+ 
Algunos clientes son atendidos por un vendedor de la empresa. El vendedor que atiende a un clientes es identificado en la columna Vendedor por su código. El 1 que aparece en la primera fila de Clientes significa que Matt Design es atendido por el empleado Carlos Zaltzman, que tiene código 1. Sin entrar en detalles, MySQL "sabe" que la columna Vendedor de Clientes se refiere a la columna Codigo de Empleados. 

El NULL de la segunda fila de Clientes significa que no es atendido por ningún vendedor. La columna Vendedor de Clientes es lo que se llama una clave extranjera ("foreign key"). La columna Id de Empleados es una clave primaria (identifica completamente a un empleado). La clave extranjera Vendedor de Clientes es una referencia a la clave primaria Id de Empleado.

También tenemos las primeras filas y columnas de la tabla Ventas.
+----+---------+----------+---------+ | id | cliente | vendedor | importe | +----+---------+----------+---------+ |  1 |       1 |        1 |  178.50 | |  2 |       2 |        1 |  195.00 | |  3 |       3 |        1 |  178.50 | |  4 |       1 |        1 |   16.00 | +----+---------+----------+---------+ 
La primera columna Id identifica la venta (es una clave primaria). La segunda columna Cliente es una clave extranjera que se refiere a la clave primaria Id de Clientes. La columna Vendedor es una clave extranjera que se refiere a la clave primaria Id de Empleados. 

Por ejemplo, la primera fila significa que la venta identificada por el número 1 fue realizada al cliente Matt Design por el vendedor Carlos Zaltzman, por un importe de 178.50.
Problemas resueltos
  1. Se busca hacer una tabla con los nombres de los clientes que son atendidos por un vendedor y el nombre del correspondiente vendedor.
    Solución:
    Formamos una tabla con el Nombre de los Clientes y de los Empleados que tengan el Vendedor del Cliente igual al Código del Empleado.
              SELECT Clientes.Nombre, Empleados.Nombre            FROM Clientes JOIN Empleados           ON Clientes.Vendedor = Empleados.Id; 
  2. Ahora queremos que aparezcan en la tabla también los clientes que no son atendidos por ningún vendedor. El nombre del vendedor aparecerá como NULL.
    Solución:
              SELECT Clientes.Nombre, Empleados.Nombre           FROM Clientes LEFT JOIN Empleados           ON Clientes.Vendedor = Empleados.Id; 
    También :
              SELECT Clientes.Nombre,Empleados.Nombre           FROM Empleados RIGHT JOIN Clientes           ON Clientes.Vendedor = Empleados.Id; 
  3. En este caso queremos hacer una tabla con los nombres de los empleados y al lado de ellos los nombres de los clientes. Si un empleado atiende varios clientes aparecerá en varias filas. Si no atiende ninguno aparecerá una sola vez con un NULL en la columna de cliente.
    Solución:
              SELECT Empleados.Nombre, Clientes.Nombre           FROM Empleados LEFT JOIN Clientes           ON Clientes.Vendedor = Empleados.Id; 
  4. Hacer una tabla en la cual figuren para cada venta el nombre del cliente, el nombre del vendedor si lo hubo y el importe de la venta.
    Solución:
              SELECT clientes.nombre AS cliente,                   empleados.nombre AS vendedor,                   ventas.importe           FROM clientes                   JOIN                 ventas ON clientes.id=ventas.cliente                  LEFT JOIN                 empleados ON ventas.vendedor=empleados.id; 
    La salida es :
    +-------------+-----------------+---------+ | cliente     | vendedor        | importe | +-------------+-----------------+---------+ | Matt Design | Carlos Zaltzman |  178.50 | | Diana Pérez | Carlos Zaltzman |  195.00 | | John Smith  | Carlos Zaltzman |  178.50 | | Matt Design | Carlos Zaltzman |   16.00 | | Matt Design | Juan Fernández  |  220.00 | | Matt Design | Carlos Zaltzman |    5.50 | | Matt Design | Juan Fernández  |   11.00 | | Matt Design | Juan Fernández  |    5.50 | | Diana Pérez | Carlos Zaltzman |  200.00 | +-------------+-----------------+---------+ 

    La primera columna es realmente Clientes.Nombre que aparece como Cliente debido al "AS Cliente" en el query. La segunda columna, análogamente es Empleados.Nombre, pero aparece como Vendedor.

    Si no se hubiesen agregado los "AS ..." las dos primeras columnas llevarían el título de "Nombre" lo que es confuso.

    Es de hacer notar que hemos realizado un JOIN de 3 tablas, o si se prefiere un JOIN de Cliente con Ventas, al cual se le ha hecho un LEFT JOIN con Empleados. Este último tenía que ser un LEFT JOIN porque pueden existir ventas que no tienen un empleado correspondiente (Vendedor es NULL). Si hubiésemos hecho un JOIN esas ventas sin empleado correspondiente no figurarían en el resultado.

  5. Igual al anterior salvo que sólo deben aparecer las ventas con importes mayores a $ 50.
    Solución:
              SELECT Clientes.Nombre AS Cliente,                  Empleados.Nombre AS Vendedor,                   Ventas.Importe           FROM Clientes                   JOIN                 Ventas ON Clientes.Id=Ventas.Cliente                  LEFT JOIN                 Empleados ON Ventas.Vendedor=Empleados.Id           WHERE Ventas.Importe > 50; 
  6. Considerando ahora todas las ventas queremos totalizar cuánto le vendió cada vendedor a cada cliente.
    Solución:
              SELECT clientes.nombre AS cliente,                   empleados.nombre AS vendedor,                   SUM(ventas.importe) AS total           FROM clientes                   JOIN                 ventas ON clientes.id=ventas.cliente                   LEFT JOIN                 empleados ON ventas.vendedor=empleados.id           GROUP BY cliente,vendedor; 
    La salida es
    +-------------+-----------------+--------+ | cliente     | vendedor        | total  | +-------------+-----------------+--------+ | Diana Pérez | Carlos Zaltzman | 395.00 | | John Smith  | Carlos Zaltzman | 178.50 | | Matt Design | Carlos Zaltzman | 200.00 | | Matt Design | Juan Fernández  | 236.50 | +-------------+-----------------+--------+ 
  7. Este problema es una combinación de los dos anteriores. Queremos totalizar cuánto le vendió cada vendedor a cada cliente, salvo que sólo totalizaremos las ventas de importe que superen los.
    Solución:
              SELECT Clientes.Nombre AS Cliente,                   Empleados.Nombre AS Vendedor,                   SUM(Ventas.Importe) AS Total           FROM Clientes                   JOIN                 Ventas ON Clientes.Id=Ventas.Cliente                  LEFT JOIN                 Empleados ON Ventas.Vendedor=Empleados.Id           WHERE Ventas.Importe > 50           GROUP BY Cliente,Vendedor; 
    El WHERE debe ir antes del GROUP BY.

  8. Ahora queremos totalizar cuánto le vendió cada vendedor a cada cliente, pero queremos listar únicamente los TOTALES de ventas que superen los 0.
    Solución:
              SELECT clientes.nombre AS cliente,                   empleados.nombre AS vendedor,                   SUM(ventas.importe) AS total           FROM clientes                   JOIN                 ventas ON clientes.id=ventas.cliente                  LEFT JOIN                 empleados ON ventas.vendedor=empleados.id           GROUP BY cliente,vendedor           HAVING total > 200; 
    La salida es
    +-------------+-----------------+--------+ | cliente     | vendedor        | total  | +-------------+-----------------+--------+ | Diana Pérez | Carlos Zaltzman | 395.00 | | Matt Design | Juan Fernández  | 236.50 | +-------------+-----------------+--------+ 
    Obsérvese que la cláusula HAVING va después del GROUP, al revés que el WHERE del problema anterior. Esto es coherente con el orden en que se realizan las operaciones. En el problema anterior, MySQL primero hace el JOIN, después selecciona las filas con Importe mayor que 50, y luego agrupa por Cliente y Vendedor. En cambio, en este problema, después de hacer el JOIN agrupa, y luego selecciona las filas con Total mayor que 200.

  9. Formar una tabla que represente las ventas con los nombres de clientes, los vendedores, los importes y los artículos vendidos.
    Solución:
              SELECT Clientes.Nombre AS Cliente,                   Empleados.Nombre AS Vendedor,                   Ventas.Importe,                   Articulos.Nombre as Artículo           FROM Clientes                   JOIN                 Ventas ON Clientes.Id=Ventas.Cliente                  LEFT JOIN                 Empleados ON Ventas.Vendedor=Empleados.Id                  JOIN                 Articulos ON Ventas.Articulo=Articulos.Id; 
    Se pueden hacer joins con cualquier número de tablas.

  10. Tenemos un archivo de Facturas. La legislación, con fines impositivos, requiere que estén registrados todos los números de factura consecutivamente. Queremos comprobar si es así.
    Solución:
              SELECT F1.Numero+1 AS Falta            FROM Facturas F1                   LEFT JOIN                 Facturas F2 ON F2.Numero=F1.Numero+1           WHERE F2.Numero IS NULL                  AND F1.NUMERO < (SELECT MAX(Numero) FROM Facturas); 

    Esta solución es un ejemplo de lo que se llama un "theta join", que es simplemente un join con un cláusula ON que no se refiere a la igualdad de dos columnas. En este caso, hacemos todas las combinaciones de una factura de F1 (una copia de Facturas) y otra de F2 (otra copia) con el número siguiente. Como se trata de un LEFT JOIN, de no haber una factura con el número siguiente, en el join va la factura de F1 seguida de columnas NULL. Pero de esas combinaciones de filas únicamente seleccionamos el número de factura de F1 más 1 cuando el de F2 es NULL.

    Es decir, cuando no hay número siguiente seleccionamos el que debería haber. La última parte del WHERE sirve para que no se agregue a la lista de faltantes el siguiente al último número de factura del archivo.

    Esta solución tiene limitaciones. Algunas son inherentes al problema planteado: no tenemos manera de saber si faltan números de factura antes del primer número registrado o después del último. Pero la solución propuesta únicamente encuentra el primer número de cada intervalo de números faltantes.

    Por ejemplo, si los números registrados son ...,1033, 1034, 1037, 1038, 1039, 1040,.... sólo se listará 1035. No es difícil completar manualmente el trabajo, pero se puede resolver esta dificultad completamente como se verá en el problema 8-3.

  11. Clientes a quienes se les ha vendido durante 3 meses consecutivos y en qué mes comienza el trimestre en que se efectuaron dichas ventas.
    Solución:
              SELECT V1.Cliente,                  CONCAT(MONTH(V1.Fecha) ,'-',YEAR(V1.Fecha)) AS Comienzo           FROM Ventas V1                   JOIN                 Ventas V2 ON V1.Cliente=V2.Cliente                              AND V1.Fecha <= V2.Fecha                             AND YEAR(V2.Fecha)*12+MONTH(V2.Fecha)                                 < YEAR(V1.Fecha)*12+MONTH(V1.Fecha)+3           GROUP BY V1.Cliente,YEAR(V1.Fecha),MONTH(V1.Fecha)           HAVING COUNT(DISTINCT MONTH (V2.Fecha))=3; 

    En primer lugar se combinan todas las filas de V1 (copia de Ventas) con filas de V2 (otra copia) cuando ambas filas se refieren al mismo cliente, la fecha de la primera fila es igual o anterior a la segunda, y el mes de la segunda fecha es o igual al de la primera o uno de los dos siguientes.

    Nota: La manera de saber si un mes de un determinado año es anterior o posterior a otro y cuántos meses de diferencia hay entre ellos es usar la expresión YEAR(UnaFecha)*12+MONTH(Una Fecha).

    Aquí Una Fecha representa una de las dos fechas que estamos comparando. Por ejemplo, consideremos el mes de marzo (3) del 2004 y el mes de diciembre(12) de 2003.

    2004*12 + 3 = 24051 2003*12 + 12 = 24048

    Como 24051 - 24048 = 3, concluimos que diciembre de 2003 es 3 meses antes que marzo de 2004. Este es un "theta join". Luego se agrupan estas combinaciones según el cliente y el mes y año de la primera fecha y por último, se cuentan cuantos meses distintos hay en cada grupo.

    Cuando hay 3 meses distintos esto significa que figuran por lo menos una vez el mes de la primera fecha y los dos siguientes. O sea que el cliente tiene ventas en 3 meses consecutivos.

    Esta solución tiene una peculiaridad : supongamos que un cliente ha efectuado compras en 4 meses consecutivos. Por ejemplo, de enero a abril de 2004. Entonces, en la salida va a figurar el cliente con una fecha del mes de enero y otra de febrero. Esto es correcto, en la medida que son ciertas dos afirmaciones

    Se realizaron ventas enero, febero y marzo. Se realizaron ventas en febrero, marzo y abril. Hay varios puntos interesantes más en este query: hemos usado la función CONCAT que concatena, es decir escribe uno después de otro los resultados de varias expresiones; en este caso el mes en que comienza el trimestre, un guión y el año en que comienza dicho trimestre de nuevo hemos usado COUNT (DISTINCT ....) para contar cuántos elementos distintos hay en una expresión calculada en base a una columna.



 

Búsqueda elemental de texto

A menudo tenemos columnas que son cadenas de caracteres, y queremos buscar las cadenas que contienen cierta palabra. Esto se realiza a través de un nuevo tipo de condición: Nombre_de_columna LIKE cadena_de_caracteres.

Por ejemplo, Nombre LIKE ‘Carlos’.

Estas condiciones pueden usarse, como todas, en una cláusula WHERE o en una cláusula HAVING. La condición Nombre_de_columna LIKE cadena_de_caracteres (donde cadena_de_caracteres no contiene ni ‘%’ ni ‘_’ ) es verdadera cuando el valor de Nombre_de_columna coincide con cadena_de_caracteres (salvo que no se distingue entre mayúsculas y minúsculas).

Veamos los valores de la condición A LIKE ’Carlos’ donde A es una columna:

Valor de la columna A
'Carlos' Verdadera
'carlos' Verdadera
'Carlos ' Falsa
'Juan' Falsa
'Juan Carlos' Falsa


Un % dentro de cadena_de_caracteres representa cualquier cadena de caracteres, incluso una sin caracteres. Entonces A LIKE ‘Carlos%’ tiene los siguientes valores:

Valor de la columna A
'Carlos' Verdadera
'carlos' Verdadera
'Carlos Zaltzman' Verdadera
'Carlos ' Verdadera
'Juan' Falsa
'Juan Carlos' Falsa


Valores de A LIKE ‘%Carlos’:
Valor de la columna A
'Carlos' Verdadera
'carlos' Verdadera
'Juan carlos' Verdadera
'Juan Carlos Rodríguez' Falsa
'Juan Carlos ' Falsa


Valores de A LIKE ‘%Carlos%’:
Valor de la columna A
'Carlos' Verdadera
'carlos' Verdadera
' carlos ' Verdadera
'Juan Carlos Rodríguez' Verdadera
'Juan' Falsa


El carácter '_' representa un carácter cualquiera. A diferencia de ‘%’ representa un carácter y uno sólo. Luego dos’_’ seguidos representan dos caracteres cualquiera,etc.

Valores de A LIKE ‘__Carlos’
Valor de la columna A
'Carlos' Falsa
'12Carlos' Verdadera
'xxCarlos' Verdadera
' Carlos' Verdadera
'xxcarlos' Verdadera
'Juan Carlos' Falsa


NOT LIKE es simplemente la negación de LIKE: es verdadera cuando LIKE es falsa y recíprocamente.


Problemas resueltos
  1. Listar el artículo cuyo código es 'mon20'.
    Solución:
              SELECT * FROM Articulos WHERE Codigo LIKE ‘mon20’; 
  2. Hacer una lista de todos los datos de los artículos en cuyo nombre figura la palabra ‘monitor’.
    Solución:
              SELECT  * FROM  Articulos WHERE  Nombre LIKE ‘%monitor%’; 
    Obsérvese que aunque la palabra monitor esté en la columna Nombre con mayúscula, igualmente aparece en el resultado del query.

  3. Listar todos los datos de los artículos cuyo código comience por ‘mon’.
    Solución:
              SELECT * FROM Articulos WHERE Codigo LIKE ‘mon%’; 
  4. Listar los artículos cuyo código termine en ‘20’.
    Solución:
              SELECT * FROM Articulos WHERE Codigo LIKE ‘%20’; 
  5. Listar los artículos cuyo código tenga exactamente dos caracteres.
    Solución:
              SELECT * FROM Articulos WHERE Codigo LIKE "__"; 
    No hay ninguno. Entonces MySQL responde "Empty set", es decir, "Conjunto vacío".

  6. Listar todos los artículos en cuyo nombre NO figure la palabra "monitor".
    Solución:
              SELECT * FROM Articulos WHERE Nombre NOT LIKE "%monitor%"; 



 

Unión de dos tablas de resultados

UNION ALL indica que se haga la unión de dos resultados, simplemente escribiendo una tabla debajo de la otra. Por ejemplo, si queremos los artículos y cantidades de las ventas a los clientes 1 y 2, basta escribir

          (SELECT Articulo, Cantidad FROM Ventas            WHERE Cliente=1)           UNION ALL           (SELECT Articulo, Cantidad FROM Ventas            WHERE Cliente=2); 

UNION realiza una tarea más compleja que UNION ALL: además de unir los resultados de los queries, suprime las filas duplicadas.


Problemas resueltos
  1. Listar juntas las ventas con Sucursal = 1 y las de Sucursal = 3.
    Solución:
              (SELECT * FROM Ventas            WHERE  Sucursal = 1)           UNION ALL           (SELECT * FROM Ventas            WHERE Sucursal = 3); 
    Obsérvese que como no es posible que una venta corresponda a las dos Sucursales, entonces es mejor especificar UNION ALL, con lo que se evita el trabajo de ordenar las filas, para suprirmir las duplicaciones.

  2. Listar los conformes que se vencen en los próximos 30 días y los que son de más de $ 1000 y todavía no se han vencido, sin duplicaciones.
    Solución:
              (SELECT * FROM Conformes            WHERE Vencimiento<= DATE_ADD( CURDATE(), INTERVAL 30 DAY)                  AND Vencimiento>=CURDATE())            UNION           (SELECT * FROM Conformes            WHERE  Importe > 1000                   AND Vencimiento >= CURDATE() ); 
  3. Volver a resolver el problema 6-10, de los números de factura faltantes, de manera que se indique para cada intervalo faltante de cuál a cuál valor no están registradas las facturas.
    Solución:
             (SELECT 'Desde' AS Limite, F1.Numero+1 AS Falta            FROM Facturas F1                   LEFT JOIN                 Facturas F2 ON F2.Numero=F1.Numero+1           WHERE F2.Numero IS NULL                  AND F1.NUMERO < (SELECT MAX(Numero) FROM Facturas))          UNION ALL          (SELECT 'Hasta' AS Limite, F2.Numero - 1 AS Falta            FROM Facturas F1                   RIGHT JOIN                  Facturas F2 ON F1.Numero=F2.Numero - 1            WHERE F1.Numero IS NULL                   AND F2.NUMERO > (SELECT MIN(Numero) FROM Facturas))           ORDER BY Falta, Limite; 

    El query antes del UNION ALL es el mismo del problema 6-10, salvo que se agregó una columna fija llamada Limite que siempre tiene el valor ‘Desde’, porque este query encuentra "desde" qué número faltan facturas. El segundo query es análogo al primero. Encuentra números, después del primero, que no tienen el inmediato anterior registrado. Esta vez la columna Limite vale ‘Hasta’.

    El UNION ALL junta todas las filas obtenidas. Luego se ordenan, primero por número faltante y luego por Limite. Esto hace que si falta una factura sola el Desde correspondiente figure antes del Hasta. Por ejemplo, dada la sucesión de facturas 1020,1022,1025, la salida es :

    +---------+-------+ | Limite  | Falta | +---------+-------+ | Desde   | 1021  |  | Hasta   | 1021  | | Desde   | 1023  | | Hasta   | 024   | +---------+-------+ 



 

Subqueries

Subqueries escalares

Ya antes, en la sección 4, usamos subqueries escalares. Un subquery escalar produce un sólo valor, que se usa en una condición WHERE o HAVING.

Ejemplo: Listar los empleados que tienen salarios mayores que la mitad del salario más alto.

         SELECT * FROM Empleados          WHERE Salario > (SELECT MAX(Salario) FROM Empleados)/2; 


IN y NOT IN

Una manera de preguntar si un valor si el valor de una columna está en una lista es usando IN.

Ejemplo: Listar las ventas efectuadas por los empleados con código 1,2.

         SELECT * FROM Ventas          WHERE Vendedor IN (1,2); 

La lista que figura después de un IN puede obtenerse de un subquery.

         SELECT * FROM Clientes          WHERE Id IN (SELECT Cliente FROM Ventas); 

Este query lista los Clientes a los cuáles se les han vendido algo.

NOT IN es la negación de IN. Si queremos la lista de los clientes a los que no se les han vendido nada en los últimos 30 días, escribimos

         SELECT * FROM Clientes          WHERE Id NOT IN (                          SELECT Cliente FROM Ventas                          WHERE Fecha>=DATE_SUB(CURDATE(),                                           INTERVAL 30 DAY)); 


ALL y ANY

Podemos seleccionar filas que cumplen una condición con todas las filas de un subquery.

Ejemplo: ¿Qué conformes se vencieron después de la última factura?

         SELECT * FROM Conformes          WHERE Vencimiento > ALL (SELECT Fecha FROM Facturas); 

También podemos seleccionar filas que cumplen una condición con alguna de las filas de un subquery.

Ejemplo: ¿Qué conformes se vencieron antes de la primera factura?

         SELECT * FROM Conformes          WHERE Vencimiento < ANY (SELECT Fecha FROM Facturas); 

Nota: Se recomienda tener cuidado con el uso de ALL. Puede producir inestabilidad del sistema si la columna que figura antes del ALL se escribe incorrectamente y por lo tanto no existe.


Subqueries correlacionados

Un subquery se llama correlacionado cuando hace referencia a una tabla que no figura en su cláusula FROM (aunque sí debe figurar en un query que lo contiene).

Ejemplo: Hacer una tabla de las ventas cuyo importe es máximo dentro del artículo considerado.

         SELECT Articulo, Importe FROM Ventas V1          WHERE Importe = (SELECT MAX(Importe) FROM Ventas V2                           WHERE V2.Articulo = V1.Articulo)          ORDER BY Articulo; 

Claro que sería más sencillo, en este caso, escribir

         SELECT Articulo, MAX(Importe) AS Maximo            FROM Ventas          GROUP BY Articulo; 

Los subqueries correlacionados tienden a ser ineficientes con tablas grandes, aunque a menudo MySQL reformula internamente el query y lo hace eficiente.


EXISTS y NOT EXISTS

A menudo lo único que interesa de un subquery correlacionado es saber si tiene alguna fila o no. Para eso tenemos EXISTS y NOT EXISTS.

Por ejemplo, si queremos seleccionar todas las Sucursales en las que se hizo alguna venta,

           SELECT * FROM Sucursales          WHERE EXISTS (SELECT 1 FROM Ventas                        WHERE Sucursal = Sucursales.Id); 

El SELECT 1 es para enfatizar que no nos interesa el contenido de las filas del subquery sino si existen o no. Si ejecutamos el subquery por sí mismo, con un valor apropiado en vez de Sucursales.Id, obtenemos una columnas de unos. Esta tabla tiene tantas filas como filas de Ventas hay con la Sucursal indicada.

En este caso también hubiese podido evitarse el subquery correlacionado con :

         SELECT DISTINCT Sucursales.*           FROM Sucursales                 JOIN               Ventas ON Sucursales.Id = Ventas.Sucursal; 

Si queremos las Sucursales donde no se hizo una venta, podemos usar

         SELECT * FROM Sucursales          WHERE NOT EXISTS (SELECT 1 FROM Ventas                            WHERE Sucursal = Sucursales.Id); 

o también

         SELECT DISTINCT Sucursales.*           FROM Sucursales                  LEFT JOIN               Ventas ON Sucursales.Id = Ventas.Sucursal          WHERE  Ventas.Id IS NULL; 


Problemas resueltos
  1. Listar las Ventas que hayan sido facturadas;
    Solución:
             SELECT * FROM Ventas           WHERE Id  IN (SELECT Venta                         FROM Lineas_Factura); 
  2. Listar las Ventas que falte facturar.
    Solución:
             SELECT * FROM Ventas           WHERE Id NOT IN (SELECT Venta                            FROM Lineas_Factura); 
  3. Ventas posteriores a la última factura.
    Solución:
             SELECT * FROM  Ventas           WHERE Fecha > ALL (SELECT Fecha FROM  Facturas); 
    Es equivalente a
             SELECT * FROM  Ventas           WHERE Fecha > (SELECT MAX(Fecha )                            FROM  Facturas); 
    De hecho, aunque se solicite el primer query, MySQL, internamente, ejecuta el 2º., que es mucho más eficiente.

  4. Artículos que no se han vendido, para cada región, en el último año.
    Solución:
             SELECT Articulos.Nombre, Sucursales.Descripcion          FROM   Articulos,  Sucursales          WHERE NOT EXISTS (SELECT 1 FROM Ventas                            WHERE Sucursal = Sucursales.Id                                   AND Articulo = Articulos.Id                                  AND Fecha > DATE_SUB(CURDATE(), INTERVAL 365 DAY))          ORDER BY  1, 2; 

    Este es un ejemplo de un subquery correlacionado. En efecto, dos columnas del query exterior, Sucursales.Id y Articulos.Id, figuran en el subquery. MySQL va combinando todas las Sucursales con todos los artículos y seleccionando aquellas combinaciones para las cuales no hay ventas correspondientes.

    El SELECT 1 del subquery no tiene importancia, lo que realmente hace MySQL es averiguar si hay alguna fila de Ventas que cumpla la cláusula WHERE, para darle un valor al NOT EXISTS. Si hay una fila que cumple el WHERE Sucursal..., entonces el NOT EXISTS es falso, etc.

    El SELECT 1 puede sustituirse por cualquier otro SELECT, que quizás resulte más intuitivo. El funcionamiento será el mismo.

  5. Clientes a los que se les han hecho menos de tres ventas en los últimos 365 días.
    Solución:
             SELECT * FROM Clientes          WHERE 3 > (SELECT COUNT(*)                        FROM Ventas                         WHERE Fecha > DATE_SUB(CURDATE(), INTERVAL 365 DAY)                              AND Cliente=Clientes.Id); 
    Otro ejemplo de subquery correlacionado. El subquery calcula el número de ventas que se le ha hecho a cada cliente en el último año. Cuando el número es menor que 3, entonces se selecciona la fila de clientes.

  6. Artículos que se han vendido en todas las sucursales.
    Solución:
             SELECT Nombre          FROM Articulos          WHERE NOT EXISTS( SELECT 1 FROM Sucursales                             WHERE NOT EXISTS (SELECT 1                                               FROM Ventas                                              WHERE Sucursal=Sucursales.Id                                               AND Articulo=Articulos.Id))          ORDER BY 1; 
    Lo que se trata de lograr podría expresarse así :
             SELECCIONAR Nombre          DE Articulos          CUANDO PARA TODA Sucursal          EXISTE Venta de Articulo en Sucursal. 
    El problema es " CUANDO PARA TODA Sucursal..." que no tiene una traducción directa a MySQL, ni a ningún SQL que yo conozca. Pero esta expresión es equivalente a "NO EXISTE Sucursal TAL QUE NO...". Es decir, afirmar que algo se cumple para todas las Sucursales equivale a afirmar que no existe una región para la que no se cumpla. Resultaría :
             SELECCIONAR Nombre          DE Articulos          CUANDO NO EXISTE  Sucursal TAL QUE          NO EXISTE Venta de Articulo en Sucursal. 
    El query es traducción directa a MySQL de este seudocódigo.

  7. Vendedores que hayan vendido todos los artículos a la venta en el último año (los últimos 365 días).
    Solución:
             SELECT * FROM Empleados           WHERE NOT EXISTS (SELECT 1 FROM Articulos                             WHERE NOT EXISTS (                                  SELECT 1 FROM Ventas                                   WHERE Ventas.Vendedor=Empleados.Id                                   AND Ventas.Articulo=Articulos.Id                                   AND Fecha> DATE_SUB(CURDATE(),                                                 INTERVAL 365 DAY))); 
    Este problema es análogo al anterior. Se ha incluido con el propósito que el lector repita el análisis del problema anterior.

  8. Vendedores que hayan vendido por lo menos un artículo de cada grupo en el último año.
    Solución:
             SELECT * FROM Empleados           WHERE NOT EXISTS (                    SELECT 1 FROM Grupos                     WHERE NOT EXISTS (                              SELECT  1 FROM Articulos                               WHERE Grupo=Grupos.Id                               AND EXISTS (                                  SELECT 1 FROM Ventas                                   WHERE Articulo=Articulos.Id                                   AND  Vendedor=Empleados.Id                                   AND Fecha>DATE_SUB(CURDATE(),                                                INTERVAL 365 DAY))));))); 
    Podríamos escribir el siguiente seudocódigo :
             SELECCIONAR *          DE Empleados          DONDE PARA TODO Grupo          EXISTE Articulo en Grupo          TAL QUE EXISTE Venta          DEL Empleado y Articulo EN ultimo año. 
    Como no tenemos una manera directa de representar el PARA TODO, usamos la equivalencia de los problemas anteriores. Resulta,
             SELECCIONAR *          DE Empleados          DONDE NO EXISTE Grupo TAL QUE NO          EXISTE Articulo en Grupo          TAL QUE EXISTE Venta          DEL Empleado y Articulo EN ultimo año. 
    El resto es una traducción directa.

  9. Grupos de los cuales no se vendieron artículos en el último año, para cada vendedor, ordenado alfabéticamente por vendedor y grupo.
    Solución:
             SELECT Empleados.Nombre,                  Grupos.Descripcion           FROM Empleados, Grupos           WHERE NOT EXISTS(                    SELECT 1 FROM Ventas                    WHERE Vendedor = Empleados.Id                     AND Fecha>DATE_SUB(CURDATE(), INTERVAL 365 DAY)                    AND EXISTS (                        SELECT 1 FROM Articulos                         WHERE Id = Ventas.Articulo                         AND Grupo = Grupos.Id))          ORDER BY 1, 2; 
    Seudocódigo :
             SELECCIONAR Vendedor, Grupo           DE Empleados, Grupos          DONDE  NO EXISTE Venta          TAL QUE Venta corresponde a Vendedor                  Y es del último año                  Y el  Articulo de la Venta es del Grupo. 



 

Salida a Excel

El programa mysql, en el que ingresamos los queries, no permite ver resultados grandes claramente. Los problemas vistos tenían soluciones con pocas filas y columnas. ¿ Qué hacemos si tenemos 1000 filas y 30 columnas?

Una solución, no muy buena, es pasar la salida del query a texto. Esto se realiza agregando después de la cláusula SELECT (exterior, si hay subqueries) una cláusula INTO OUTFILE path.

Por ejemplo, para que el resultado se guarde en un archivo c:\MySQL\bin\p\salida.txt (c:\MySQL\bin\p es el directorio donde hemos supuesto que guardaron los problemas resueltos) debemos escribir

         SELECT ...          INTO OUTFILE 'c:/MySQL/bin/p/salida.txt'          LINES TERMINATED BY '\r\n'          FROM ... 

La última línea es característica de Windows, donde el final de cada línea se indica por dos caracteres (invisibles) mientras que en Unix, el final de cada línea se indica por un solo carácter.

Obsérvese que el camino (path) del archivo va con barras : / . Mientras que la cláusula LINES TERMINATED lleva contrabarras : \ .

Para que el procedimiento funcione es imprescindible que el directorio donde se va a guardar la salida exista. MySQL crea el archivo pero no el directorio. También es imprescindible que el archivo no exista. Si existe, aparecerá un mensaje de error. Si se observa el texto obtenido con cualquier procesador (Bloc de notas, WordPad, Word) se verá que hay problemas : salvo en el Bloc de notas las columnas no aparecen bien alineadas ( y el bloc de notas tiene limitaciones en lo que se refiere al tamaño del archivo que puede manejar).
Las vocales con tilde se cambian por otros caracteres.

Para resolver estos problemas se puede usar Microsoft Excel. Usando Excel 97 empezamos abriendo el archivo (teniendo cuidado de seleccionar ‘Archivos de texto’ en ‘Tipo de Archivo’ ). A continuación aparecen varios cuadros de diálogo.

  • El primero tiene título ‘Asistente para importar texto – paso 1 de 3’. A media altura, a la derecha, donde dice ‘Origen del archivo:’ se debe seleccionar ‘DOS u OS/2 (PC-8)’. Si no están seleccionados, seleccionar además ‘Delimitados’ y ‘ Comenzar a importar en la fila : 1’. Hacer click en el botón ‘Siguiente>’.
  • En el segundo cuadro de diálogo asegurarse que estén seleccionados ‘Tabulación’ en ‘Separadores’ y ‘’ en ‘Calificador de texto’. Luego hacer click en el botón ‘Siguiente>’.
  • En el tercer cuadro de diálogo asegurarse que esté seleccionado ‘General’ en ‘Formato de los datos en columnas’ y por último hacer click en el botón ‘Terminar’.

Para disponer de una planilla Excel del tipo usual ir al menú ‘Archivo’ y seleccionar ‘Guardar como’.

En el cuadro de diálogo que aparece seleccionar en ‘Guardar como tipo: ‘ la opción ‘Libro de Microsoft Excel (*.xls)’. Luego hacer click en el botón ‘Guardar’.

Se creará una planilla Excel con el mismo nombre del archivo de texto, pero con terminación ‘xls’. En este caso, sería ‘salida.xls’.

Esta vez tenemos una columna de Excel por cada columna del query y las vocales con tilde se ven correctamente.

Lo que sí hay que agregar manualmente son los títulos de las columnas.

Nota : los NULL aparecen como ‘\N’.



 

El funcionamiento de SELECT

Ahora que hemos visto unos cuantos queries, podemos preguntarnos cómo funciona SELECT. Es un proceso formado por varias etapas sucesivas :

  • FROM: Aquí se realizan los JOINS o un subquery.
  • WHERE: De las filas resultantes del FROM se separan las que cumplen las condiciones especificadas en el WHERE.
  • GROUP BY: Las filas se ordenan y agrupan según las columnas especificadas.
  • SELECT : Ahora a partir de las filas que han resultado de los pasos anteriores, se construyen otras según las expresiones que figuren en el SELECT. Si hay un GROUP BY sólo se construye una fila por cada grupo de filas del GROUP BY. Cuidado :en caso que haya valores distintos de una columna seleccionada dentro de un grupo, se selecciona un valor cualquiera al azar.
  • Si hay un DISTINCT, se eliminan las duplicaciones de filas.
  • HAVING: Vuelven a descartarse filas que no cumplan con la condición de esta cláusula.
  • ORDER BY : Se ordenan las filas, por el criterio indicado.
  • LIMIT : Si hay más filas de las indicadas por esta cláusula, se descartan.
  • UNION : Se unen los resultados de varios queries. Puede haber un ORDER BY y un LIMIT después del UNION.

- Nota final sobre eficiencia: Para bases de datos chicas, con algunos miles de filas, normalmente no hay grandes problemas de eficiencia. Con todo, si algún query es muy lento, eso no quiere decir que el problema sea intrínsecamente demasiado complicado para el computador. Los tiempos en que se ejecutan los queries varían muchísimo según los detalles de la base de datos y los detalles de los queries mismos. O sea que si hay problemas de velocidad, es recomendable consultar con un programador con experiencia en SQL.

 





 

Glosario

Como el SQL es a menudo comprensible aún para personas que no lo han estudiado, pero que hablan inglés, parece útil tener un pequeño diccionario.

TérminoDescripción
ALL Todos.
ANY Cualquier.
AS Como.
AVG Cualquier.
ANY Abreviatura de AVERAGE, promedio.
COUNT Contar.
CURDATE() Función SQL. Abreviatura de CURRENT DATE, fecha de hoy.
DATE Fecha.
DATE_ADD() Función SQL. Literalmente, fecha sumar.
DATE_SUB() Función SQL. Literalmente, fecha restar.
DAY Día.
DAYNAME() Función SQL, de DAY, día y NAME, nombre.
DAYOFWEEK() Función SQL, concatenación de DAY OF WEEK, día de la semana.
DESC Abreviatura de 'descending' , que puede traducirse como 'en sentido descendiente'. Se refiere a ordenar en sentido descendente.
DISTINCT Distintos.
EXISTS Existe.
FROM De.
IN En.
IS Es.
JOIN Literalmente unión, zona de contacto. En SQL, representa una manera especial de unir dos tablas. Matemáticamente, es un subconjunto de un producto cartesiano..
LEFT JOIN Expresión usada en SQL. Literalmente, unión izquierda.
LIKE Parecido a.
MAX Abreviatura de MAXIMUM, máximo.
MIN Abreviatura de MINIMUM, mínimo.
MONTH Mes.
NAME Nombre.
NOT No.
NOT IN No en, no pertenece a.
NULL NULO, pero en un sentido especial, no se refiere al número 0, sino a un valor desconocido.
ON Sobre. En SQL indica una condición en un JOIN.
ORDER BY Ordenar por.
RIGHT JOIN Expresión usada en SQL. Literalmente, unión derecha.
RIGHT() Funcion SQL. Right significa derecha.
SELECT Seleccionar.
SHOW Mostrar, usado en SHOW TABLES (Mostrar tablas) y SHOW DATABASES (Mostrar bases de datos).
SUM Sumar.
TABLE Tabla.
UNION Unión.
WHERE Donde.
YEAR Año.



Autor: Carlos Zaltzman
Fecha publicación: 30 Enero del 2005

Fuente: MySQL Hispano. http://www.mysql-hispano.org/page.php?id=31&pag=14


Bibliografía

 

  1. Rudy Limeback, Ask the Expert, http://searchdatabase.techtarget.com/originalContent/0,289142,sid13_gci950920,00.html
    Muchos problemas de este tutorial se basan en ideas del Sr. Limeback.
  2. Reference Manual for version 5.0.1-alfa, MySQL AB, http://dev.mysql.com/doc. En esta página encontrará las instrucciones para conseguir el manual.
  3. J. Benavides Abajo, J.M. Olaizola Bartolomé, E. Rivero Cornelio, SQL Para usuarios y programadores, Paraninfo S.A., Madrid, 1991.
  4. MySQL Internals Manual for version 4.1.7, MySQL AB. Para obtener este manual deberá obtener una distribución de MySQL con fuentes (source version) y encontrará el archivo internals.texi en el directorio Docs de la distribución. Para obtener esa distribución seguir las instrucciones de http://www.mysql.com/downloads. Advertencia: este manual es altamente técnico y requiere Unix para ser leído con facilidad.


Última actualización el Domingo, 24 de Febrero de 2008 01:03
 
Joomla template by a4joomla