Está en la página 1de 43

Extrayendo información de una base de datos

En este tutorial 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.

Introducción

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.

-1-
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 |

-2-
+----+-------------+
| 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

-3-
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 |

-4-
| 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:

-5-
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 |
+----+-------------+---------------------+----------+

-6-
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

-7-
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.

Es verdadera cuando
Expresión Significado
...
E es verdadera y F es
E AND F EyF
verdadera
EoFo E es verdadera o F lo es
E OR F
ambos 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

-8-
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 |
+----+-------------+-----------------------+----------+

-9-
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.

- 10 -
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
5. mysql> SOURCE p/prueba.txt

o, si lo prefiere,

mysql> \. p/prueba.txt

6. 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:

- 11 -
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:

- 12 -
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.

- 13 -
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.

- 14 -
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');

- 15 -
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());

- 16 -
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:

- 17 -
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

- 18 -
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;


+----------+

- 19 -
| 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.

- 20 -
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);

- 21 -
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;

- 22 -
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;

- 23 -
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 (

- 24 -
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 |

- 25 -
| 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 |

- 26 -
+----+-------------+---------------------+----------+

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;

- 27 -
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:

- 28 -
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

- 29 -
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.

- 30 -
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

- 31 -
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

- 32 -
'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.

- 33 -
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.

- 34 -
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.

- 35 -
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. <BR
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;

- 36 -
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);

- 37 -
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í :

- 38 -
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.

- 39 -
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.

- 40 -
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.

- 41 -
• 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érmino Descripción
ALL Todos.
ANY Cualquier.
AS Como.
AVG Cualquier.
ANY Abreviatura de AVERAGE, promedio.
COUNT Contar.
Función SQL. Abreviatura de CURRENT DATE, fecha
CURDATE()
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.
Función SQL, concatenación de DAY OF WEEK, día de
DAYOFWEEK()
la semana.
Abreviatura de 'descending' , que puede traducirse
DESC como 'en sentido descendiente'. Se refiere a ordenar en
sentido descendente.
DISTINCT Distintos.
EXISTS Existe.
FROM De.
IN En.
IS Es.
Literalmente unión, zona de contacto. En SQL,
representa una manera especial de unir dos tablas.
JOIN
Matemáticamente, es un subconjunto de un producto
cartesiano..
LEFT JOIN Expresión usada en SQL. Literalmente, unión izquierda.
LIKE Parecido a.

- 42 -
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.
NULO, pero en un sentido especial, no se refiere al
NULL
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.
Mostrar, usado en SHOW TABLES (Mostrar tablas) y
SHOW
SHOW DATABASES (Mostrar bases de datos).
SUM Sumar.
TABLE Tabla.
UNION Unión.
WHERE Donde.
YEAR Año.

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.

- 43 -

También podría gustarte