Archivo de Etiquetas de 'SQL'

Pinceladas de álgebra relacional.

Hoy algo de teoría. Como sabemos, el lenguaje estándar para realizar consultas en bases de datos es el SQL (Structured Query Language). Este lenguaje permite definir y manipular bases de datos relacionales, que son las bases de datos con las que solemos trabajar (como MySQL, Oracle, SQL Server y demás). Hay otro tipo de bases de datos que no son relacionales, pero su uso a día de hoy es tremendamente minoritario.

En una base de datos relacional la información está estructurada en forma de relaciones (ojo, no confundirse, en este ámbito ‘relación’ equivale a lo que habitualmente llamamos ‘tabla’) dando lugar a lo que se conoce como modelo relacional. En este modelo relacional las consultas se pueden llevar a cabo con lenguajes relacionales de dos tipos:

1) Lenguajes basados en álgebra relacional, que se inspira en la teoría de conjuntos para ir construyendo paso a paso el procedimiento a seguir para obtener los datos deseados. Estos lenguajes son lenguajes, pues, procedimentales.

2) Lenguajes basados en el cálculo relacional, que se basa en el cálculo de predicados de la lógica matemática. Estos lenguajes no describen un procedimiento y por tanto se les llama lenguajes declarativos (no procedimentales).

SQL es un lenguaje mixto, ya que incluye temas de álgebra relacional y otros de cálculo relacional, aunque predominan estos últimos, por lo que se considera un lenguaje declarativo. No obstante, hoy vamos a ver algunas pinceladas básicas de álgebra relacional.

Para especificar una consulta en álgebra relacional hay que ir siguiendo una serie de pasos que sirven para ir construyendo nuevas relaciones (recordemos: análogas a las tablas) a partir de las relaciones existentes. En estos pasos se utilizan las operaciones del álgebra relacional, que resumidas de un modo muy escueto son las siguientes.

0. Renombramiento.

Se trata de una operación que nos facilita trabajar con relaciones cambiándoles el nombre a ellas o a sus atributos. Así…

R (h) := RelacionConNombreOriginal (AtributoConNombreOriginal)

…renombra la relación a ‘R’ y uno de sus atributos a ‘h’ (del mismo modo que relaciones vienen a ser lo que habitualmente llamamos tablas, atributos vienen a ser lo que llamamos campos o columnas en esas tablas).

1. Unión.

Se trata de generar una nueva relación juntando todas las tuplas (tuplas = registros) de las dos relaciones que intervienen en la unión. Está claro que las dos relaciones deberán tener estructuras semejantes. Además, como en una relación (como en una tabla) no puede haber tuplas (registros) duplicadas, si al unirlas se da alguna duplicidad, se elimina directamente. Se denota con…

R := T U S

…donde ‘U’ representa el símbolo de unión y ‘T’ y ‘S’ son las relaciones de partida.

2. Intersección.

Se trata de generar una nueva relación con las tuplas que aparecen a la vez en las dos relaciones que intervienen en la intersección. También aquí las dos relaciones deberán tener estructuras semejantes. Se denota con…

R := T (U) S

…donde ‘(U)’ pretende ser el símbolo de intersección (’U’ invertida) que no sé cómo representar aquí en el blog y ‘T’ y ‘S’ son las relaciones de partida.

3. Diferencia.

Se trata de generar una nueva relación con las tuplas que aparecen en una primera relación pero no en una segunda. Nuevamente ambas relaciones deben tener estructuras semejantes. Se denota con…

R := T - S

…donde ‘T’ y ‘S’ son las relaciones de partida.

4. Producto cartesiano.

Se trata de generar una nueva relación combinando cada una de las tuplas de una primera relación con cada una de las tuplas de una segunda relación. Así pues la relación resultante tendrá tantos atributos como la suma de los atributos de cada una de las relaciones de partida. Y tendrá tantas tuplas como el producto de tuplas de ambas relaciones. Se denota con…

R := T x S

…donde ‘T’ y ‘S’ son las relaciones de partida.

5. Selección.

Se trata de generar una nueva relación con un subconjunto de las tuplas de la relación de partida. Para ello es preciso expresar alguna condición que las tuplas deberán cumplir para formar parte de la nueva relación. Se denota con…

R := T(C)

…donde ‘T’ es la relación de partida y ‘C’ la condición (puede ser compuesta) que deben cumplir las tuplas de ‘T’ para formar parte de ‘R’.

6. Proyección.

Se trata de generar una nueva relación con sólo algunos de los atributos de los que consta la relación de partida. Se denota con…

R := T [A1, A2, A3...]

…donde ‘T’ es la relación de partida y las ‘A’ representan los atributos que se quieren trasladar a la relación resultante ‘R’.

7. Combinación.

Se trata de generar una nueva relación a partir de un producto cartesiano entre dos relaciones de partida, con la salvedad que sólo formarán parte de la nueva relación las tuplas resultantes del producto que cumplan unas determinadas condiciones de igualdad entre atributos. Dios, suena fatal… pero es simplemente un JOIN. Se denota con…

R := T [B] S

…donde ‘T’ y ‘S’ son las relaciones de partida y ‘B’ es el conjunto de condiciones.

8. Combinación natural.

Igual que la combinación, sólo que no es necesario especificar las condiciones ‘B’ puesto que se asume que los atributos que se quieren igualar son los que se llaman igual en las dos relaciones de partida. Se denota con…

R := T * S

…donde ‘T’ y ‘S’ son las relaciones de partida.

Expresado así todo ello suena bastante complejo, la verdad. Pero si conocemos algo de SQL y hemos ido identificando las instrucciones veremos que es mucho más sencillo de lo que parece. Los renombramientos vienen a ser alias, las uniones vienen a ser UNIONs, las selecciones son SELECT * con
cláusulas WHERE, las proyecciones son SELECT con una selección de campos, etc.

Vamos a ver ahora un ejemplo interesante de cómo se construye una consulta con álgebra relacional. Imaginemos que tenemos una relación con las siguientes tuplas (voy a representarlo como tabla con registros para que resulte más familiar):

+-----------------+
|   biblioteca    |
+-------+---------+
| libro | paginas |
+-------+---------+
|   AAA |     125 |
|   BBB |     250 |
|   CCC |     300 |
|   DDD |     105 |
+-------+---------+

Si en SQL quisiéramos saber cuál es el menor número de páginas nos bastaría con hacer…

SELECT MIN(paginas) 
FROM biblioteca;

Y si quisiéramos saber cuál es el libro con ese menor número de páginas tendríamos que hacer…

SELECT libro 
FROM biblioteca 
WHERE paginas = (SELECT MIN(paginas) FROM biblioteca);

¿Pero cómo conseguimos encontrar esto con álgebra relacional donde NO tenemos una maravillosa función MIN()? Tenemos que hacerlo por pasos, más o menos así:

Paso 1.

Obtener una relación sólo con los números de páginas.

R1 := biblioteca [paginas]

+---------+
| paginas |
+---------+
|     125 |
|     250 |
|     300 |
|     105 |
+---------+
Paso 2.

Renombrar esta relación para poder llevar a cabo el siguiente paso. Podríamos haberlo hecho directamente en el paso 1, pero así queda más claro.

R2 (pags) := R1 (paginas)

+------+
| pags |
+------+
|  125 |
|  250 |
|  300 |
|  105 |
+------+
Paso 3.

Llevar a cabo una combinación entre ‘biblioteca’ y la recién obtenida ‘R2′ estableciendo como condición que ‘paginas’ sea mayor que ‘pags’.

R3 := biblioteca [paginas > pags] R2

+-------+---------+------+
| libro | paginas | pags |
+-------+---------+------+
|   AAA |     125 |  105 |
|   BBB |     250 |  125 |
|   BBB |     250 |  105 |
|   CCC |     300 |  125 |
|   CCC |     300 |  250 |
|   CCC |     300 |  105 |
+-------+---------+------+

Como vemos, hemos obtenido una relación donde aparecen todas las combinaciones en que ‘paginas’ tiene un valor superior a algún valor de ‘pags’. En esa relación tenemos pues todos los valores de ‘paginas’ que son mayores que algún otro valor de ‘pags’, y por tanto sólo faltan los valores de ‘paginas’ que no son mayores a ningún otro valor de ‘pags’ (o sea, los valores mínimos).

Paso 4.

Hacer una proyección para quedarnos sólo con los valores de ‘paginas’.

R4 := R3 [paginas]

+---------+
| paginas |
+---------+
|     125 |
|     250 |
|     300 |
+---------+
Paso 5.

Efectuar una diferencia entre los números de páginas de la relación original (lo tenemos en ‘R1′) y los que acabamos de obtener.

R5 := R4 - R1

+---------+
| paginas |
+---------+
|     105 |
+---------+

¡Bingo! Ya tenemos el valor mínimo de páginas… vamos ahora a terminar encontrando el título del libro con el menor número de páginas…

Paso 6.

Realizar una combinación natural entre la relación original y esta última.

R6 := biblioteca * R5

+-------+---------+
| libro | paginas |
+-------+---------+
|   DDD |     105 |
+-------+---------+
Paso 7.

Y para obtener el título del libro… una última proyección.

R7 := R6 [libro]

+-------+
| libro |
+-------+
|   DDD |
+-------+

Así pues, vemos que para llevar a cabo consultas con álgebra relacional debemos seguir una técnica procedimental en que vamos marcando los pasos que hay que ir siguiendo (haz esto y hazlo así). En cambio con SQL no necesitamos hacerlo de ese modo, ya que como hemos comentado al principio del post SQL es un lenguaje declarativo (haz esto, no me importa cómo lo hagas).

Nunca está de más tener algunas nociones de teoría de bases de datos, ¿no? ;-)

Claves foráneas vs UNIQUE en MySQL.

Cuando diseñamos una tabla en MySQL (y en general en cualquier otro SGBD) solemos prestar mucha más importancia a las claves primarias que a las foráneas. Sin embargo las claves foráneas son también muy importantes principalmente por un doble motivo. Por un lado permiten mantener la integridad de la base de datos ante posibles modificaciones o eliminación de datos. Por otro lado aceleran enormemente determinadas combinaciones (joins) de tablas. No obstante, si estamos utilizando MySQL este segundo tema no depende estrictamente de las claves foráneas sino de la unicidad de las claves. Son temas parecidos pero no iguales, ya que para que una campo pueda ser referenciado por la clave foránea de otra tabla debe cumplir el criterio de unicidad, pero que lo cumpla no obliga a que alguna clave foránea tenga que referenciarlo. De esto es de lo que va a tratar este post.

Como el tema va a ser algo complicado de explicar sin unas tablas de datos a las que sujetarnos, voy a partir de tres tablas con las que desarrollaré toda la explicación. Estas tres tablas son las que paso a explicar a continuación.

Tabla de materiales vendidos.
mysql> DESCRIBE mat_materialsx;
+---------+----------------------+------+-----+---------+-------+
| Field   | Type                 | Null | Key | Default | Extra |
+---------+----------------------+------+-----+---------+-------+
| mat_ped | varchar(6)           | NO   | PRI |         |       |
| mat_pos | smallint(5) unsigned | NO   | PRI | 0       |       |
| mat_typ | varchar(6)           | YES  |     | NULL    |       |
| mat_fin | varchar(2)           | YES  |     | NULL    |       |
| mat_qty | double unsigned      | YES  |     | NULL    |       |
+---------+----------------------+------+-----+---------+-------+
5 rows in set (0.01 sec)

Es una tabla que recoge todos los materiales que se han vendido en algún momento a lo largo de la trayectoria de una empresa. Los campos mat_ped y mat_pos son la clave primaria e identifican el número de pedido de venta y la posición dentro de dicho pedido. No afectarán en nada a nuestro desarrollo, así que podemos olvidarnos de ellos. En cambio los restantes campos serán importantes: mat_typ indica el tipo de material, mat_fin indica el acabado de ese material y mat_qty indica el número de kilos vendidos en ese pedido y posición.

Quedará más claro viendo algunos datos que puede contener esta tabla:

mysql> SELECT * FROM mat_materialsx LIMIT 5;
+---------+---------+---------+---------+---------+
| mat_ped | mat_pos | mat_typ | mat_fin | mat_qty |
+---------+---------+---------+---------+---------+
| 100023  |       1 | 430     | XP      |     800 |
| A09085  |       1 | 430     | BA      |    3000 |
| A09086  |       1 | 430     | BA      |    7380 |
| A09138  |       1 | 409     | 2B      |     538 |
| A09139  |       1 | 409     | 2B      |     539 |
+---------+---------+---------+---------+---------+
5 rows in set (0.00 sec)

Lo he limitado a 5 resultados pero en realidad en esta tabla es donde estará el enorme volumen de datos, ya que la tabla entera contendrá…

mysql> SELECT COUNT(*) FROM mat_materialsx;
+----------+
| COUNT(*) |
+----------+
|   177047 |
+----------+
1 row in set (0.06 sec)

El objetivo final que querremos conseguir (para lo cual necesitaremos llevar a cabo una consulta) es una agrupación de esos 177.047 materiales vendidos. Para ello agruparemos tanto los tipos de material como los acabados a través de unas tablas de parametrización en las que le diremos qué tipo de material o qué acabado agrupado corresponden a cada tipo de material o acabado original. Estas dos tablas son las que ahora vemos. Primero la de tipos de material.

Tabla de agregación de tipos de material.
mysql> DESCRIBE typ_alltypesx;
+---------+------------+------+-----+---------+-------+
| Field   | Type       | Null | Key | Default | Extra |
+---------+------------+------+-----+---------+-------+
| typ_bas | varchar(6) | NO   | PRI |         |       |
| typ_agg | varchar(6) | NO   |     |         |       |
+---------+------------+------+-----+---------+-------+
2 rows in set (0.01 sec)

Es una tabla extremadamente simple donde sólo consta el tipo de material básico (el original, el que aparece en los pedidos) y el tipo de material agrupado que le corresponde a cada tipo de material básico.

Algunos datos de los que consta son los siguientes:

mysql> SELECT * FROM typ_alltypesx LIMIT 5;
+---------+---------+
| typ_bas | typ_agg |
+---------+---------+
| 14462   | 524     |
| 202     | 202     |
| 301PK   | 524     |
| 304     | 304     |
| 304/L   | 304     |
+---------+---------+
5 rows in set (0.00 sec)

Así, los materiales 14462 y 301PK quedarán agrupados como material 524. Los 304 y 304/L quedarán como 304. Y así para un total de unos 70 materiales.

Tabla de agregación de acabados.
mysql> DESCRIBE fin_allfinish;
+---------+------------+------+-----+---------+-------+
| Field   | Type       | Null | Key | Default | Extra |
+---------+------------+------+-----+---------+-------+
| fin_bas | varchar(2) | NO   | PRI |         |       |
| fin_agg | varchar(2) | NO   |     |         |       |
+---------+------------+------+-----+---------+-------+
2 rows in set (0.01 sec)

También es una tabla extremadamente simple donde sólo consta el acabado básico (el original, el que aparece en los pedidos) y el acabado agrupado que le corresponde a cada acabado básico.

Algunos datos de los que consta son los siguientes:

mysql> SELECT * FROM fin_allfinish LIMIT 5;
+---------+---------+
| fin_bas | fin_agg |
+---------+---------+
| 1       | 1       |
| 1D      | 1       |
| 1P      | K3      |
| 2B      | 2B      |
| 2C      | 2B      |
+---------+---------+
5 rows in set (0.00 sec)

Así, los acabados 1 y 1D quedarán agrupados como acabado 1. El 2B y el 2C quedarán agrupados como acabado 2B. Y así para un total de 94 acabados.

Primera consulta de agrupación.

Tenemos pues las tres tablas presentadas. Vamos a llevar a cabo una primera ejecución de la consulta de combinación y agrupación que necesitaremos para obtener el resultado deseado y evaluaremos cuánto tiempo ha tardado MySQL en devolvernos el resultado:

mysql> SELECT typ_agg AS type, 
    -> fin_agg AS finish, 
    -> SUM(mat_qty) AS quantity
    -> FROM 
    -> ((mat_materialsx LEFT JOIN typ_alltypesx ON mat_typ = typ_bas) 
    -> LEFT JOIN fin_allfinish ON mat_fin = fin_bas)
    -> GROUP BY typ_agg, fin_agg;
+-------+--------+-----------+
| type  | finish | quantity  |
+-------+--------+-----------+
| 202   | 2B     |      7217 |
| 202   | BA     |     31140 |
| 430   | 1      |    994298 |
| 430   | 2B     |  11621394 |
| 430   | BA     |  22678569 |
| 430   | K3     |    546858 |
| 441   | 2B     |   2862123 |
| 441   | K3     |     55160 |
| 524   | 1      |   4930442 |
| 524   | 2B     |  23379844 |
| 524   | BA     |   4499020 |
| 524   | K3     |    652620 |
+-------+--------+-----------+
12 rows in set (0.95 sec)

No está nada mal. En apenas un segundo ha hecho los correspondientes joins y las agregaciones de los 177.047 registros para devolvernos lo que deseábamos obtener. Entonces… ¿si estoy defendiendo la importancia de las claves foráneas y sobretodo de la unicidad, no es un contrasentido haber obtenido unos resultados tan buenos? No, no lo es. Tiene una explicación. Fijémonos que estamos haciendo las combinaciones con los cambos typ_bas y fin_bas que son claves principales en sus respectivas tablas y que por tanto son campos únicos en ellas. Dicho de otro modo, MySQL no necesita estrictamente que existan claves foráneas (en este caso serían mat_typ y mat_fin) referenciando a estas claves primarias para ejecutar la consulta rápidamente. Lo que sí necesita es que exista ese unicidad.

Bien, vamos a complicarlo y se entenderá mejor -espero-. Imaginemos que no estamos trabajando con datos de una sola empresa sino que formamos parte de un pequeño grupo de empresas que comparten base de datos. Así, en las tablas de parametrización de tipos de material y acabados, la clave principal no es únicamente el tipo de material y el acabado, sino que incorporamos también el código de la empresa en cuestión, ya que el material 304/L en una empresa se agrupará como 304 mientras que en otra se agrupará como 524 (es un decir).

De este modo las dos tablas quedarán ahora así.

Tabla de agregación de tipos de material.
mysql> describe typ_alltypesx;
+---------+------------+------+-----+---------+-------+
| Field   | Type       | Null | Key | Default | Extra |
+---------+------------+------+-----+---------+-------+
| typ_cpy | varchar(3) | NO   | PRI |         |       |
| typ_bas | varchar(6) | NO   | PRI |         |       |
| typ_agg | varchar(6) | NO   |     |         |       |
+---------+------------+------+-----+---------+-------+
3 rows in set (0.01 sec)

Con unos datos como estos (AMG es el código de una de las empresas, concretamente de la que nos interesará, ya que pondremos absolutamente todos los datos con este código de empresa):

mysql> SELECT * FROM typ_alltypesx LIMIT 5;
+---------+---------+---------+
| typ_cpy | typ_bas | typ_agg |
+---------+---------+---------+
| AMG     | 14462   | 524     |
| AMG     | 202     | 202     |
| AMG     | 301PK   | 524     |
| AMG     | 304     | 304     |
| AMG     | 304/L   | 304     |
+---------+---------+---------+
5 rows in set (0.00 sec)
Tabla de agregación de acabados.
mysql> describe fin_allfinish;
+---------+------------+------+-----+---------+-------+
| Field   | Type       | Null | Key | Default | Extra |
+---------+------------+------+-----+---------+-------+
| fin_cpy | varchar(3) | NO   | PRI |         |       |
| fin_bas | varchar(2) | NO   | PRI |         |       |
| fin_agg | varchar(2) | NO   |     |         |       |
+---------+------------+------+-----+---------+-------+
3 rows in set (0.01 sec)

Con unos datos como estos:

mysql> SELECT * FROM fin_allfinish LIMIT 5;
+---------+---------+---------+
| fin_cpy | fin_bas | fin_agg |
+---------+---------+---------+
| AMG     | 1       | 1       |
| AMG     | 1D      | 1       |
| AMG     | 1P      | K3      |
| AMG     | 2B      | 2B      |
| AMG     | 2C      | 2B      |
+---------+---------+---------+
5 rows in set (0.00 sec)
Segunda consulta de agrupación.

Tenemos pues de nuevo las tres tablas presentadas, sólo que ahora las dos últimas han sufrido un pequeño cambio, aunque aparentemente no demasiado importante puesto que sólo hemos añadido un campo adicional, pero no hemos tocado los que ya había y siguen siendo claves primarias los que lo eran anteriormente. Vamos a llevar a cabo la segunda ejecución de la consulta de combinación y agrupación y evaluaremos cuánto tiempo ha tardado esta vez MySQL en devolvernos el resultado:

mysql> SELECT typ_agg AS type, 
    -> fin_agg AS finish, 
    -> SUM(mat_qty) AS quantity
    -> FROM 
    -> ((mat_materialsx LEFT JOIN typ_alltypesx ON mat_typ = typ_bas) 
    -> LEFT JOIN fin_allfinish ON mat_fin = fin_bas)
    -> GROUP BY typ_agg, fin_agg;
+-------+--------+-----------+
| type  | finish | quantity  |
+-------+--------+-----------+
| 202   | 2B     |      7217 |
| 202   | BA     |     31140 |
| 430   | 1      |    994298 |
| 430   | 2B     |  11621394 |
| 430   | BA     |  22678569 |
| 430   | K3     |    546858 |
| 441   | 2B     |   2862123 |
| 441   | K3     |     55160 |
| 524   | 1      |   4930442 |
| 524   | 2B     |  23379844 |
| 524   | BA     |   4499020 |
| 524   | K3     |    652620 |
+-------+--------+-----------+
12 rows in set (21.02 sec)

Nada menos que 21 segundos. Una barbaridad. De acuerdo que tiene que combinar y agrupar 177.047 registros, pero tampoco son tantos… ¿qué pasará cuando sean millones? El problema que nos ha aparecido es ni más ni menos que la unicidad. Antes los campos typ_bas y fin_bas que se utilizan para los joins eran únicos. Ahora ya no lo son. Vamos a intentar solucionarlo.

Establecimiento de unicidad.

Para ello ejecutaremos las siguientes sentencias:

mysql> ALTER TABLE typ_alltypesx 
    -> CHANGE typ_bas typ_bas VARCHAR(6) UNIQUE;
Query OK, 70 rows affected (0.03 sec)
Records: 70  Duplicates: 0  Warnings: 0

mysql> DESCRIBE typ_alltypesx;
+---------+------------+------+-----+---------+-------+
| Field   | Type       | Null | Key | Default | Extra |
+---------+------------+------+-----+---------+-------+
| typ_cpy | varchar(3) | NO   | PRI |         |       |
| typ_bas | varchar(6) | NO   | PRI |         |       |
| typ_agg | varchar(6) | NO   |     |         |       |
+---------+------------+------+-----+---------+-------+
3 rows in set (0.01 sec)

mysql> ALTER TABLE fin_allfinish 
    -> CHANGE fin_bas fin_bas VARCHAR(2) UNIQUE;
Query OK, 94 rows affected (0.03 sec)
Records: 94  Duplicates: 0  Warnings: 0

mysql> DESCRIBE fin_allfinish;
+---------+------------+------+-----+---------+-------+
| Field   | Type       | Null | Key | Default | Extra |
+---------+------------+------+-----+---------+-------+
| fin_cpy | varchar(3) | NO   | PRI |         |       |
| fin_bas | varchar(2) | NO   | PRI |         |       |
| fin_agg | varchar(2) | NO   |     |         |       |
+---------+------------+------+-----+---------+-------+
3 rows in set (0.01 sec)

Aparentemente las tablas no han cambiado. Al menos no nos muestra cambios en el DESCRIBE. Sin embargo sí que lo han hecho, ya que le hemos establecido que los campos typ_bas y fin_bas además de formar parte de claves primarias, son en sí mismos únicos. Si quisiéramos que claves foráneas de otras tablas hicieran referencia a estos campos ahora podríamos hacerlo (antes no habríamos podido ya que MySQL nos habría devuelto un error), pero en realidad para evaluar la velocidad de la consulta no nos es estrictamente necesario. Ya he comentado antes que para la velocidad a la hora de combinar y agrupar MySQL requiere unicidad, no necesariamente claves foráneas. Vamos a verlo.

Tercera consulta de agrupación.

Respecto a la ejecución anterior sólo hemos cambiado que ahora las dos tablas de parametrización han visto como sus campos clave utilizados en los joins se han hecho únicos. Veamos cómo va la tercera ejecución de la consulta de combinación y agrupación y evaluaremos cuánto tiempo ha tardado ahora MySQL en devolvernos el resultado:

mysql> SELECT typ_agg AS type, 
    -> fin_agg AS finish, 
    -> SUM(mat_qty) AS quantity
    -> FROM 
    -> ((mat_materialsx LEFT JOIN typ_alltypesx ON mat_typ = typ_bas) 
    -> LEFT JOIN fin_allfinish ON mat_fin = fin_bas)
    -> GROUP BY typ_agg, fin_agg;
+-------+--------+-----------+
| type  | finish | quantity  |
+-------+--------+-----------+
| 202   | 2B     |      7217 |
| 202   | BA     |     31140 |
| 430   | 1      |    994298 |
| 430   | 2B     |  11621394 |
| 430   | BA     |  22678569 |
| 430   | K3     |    546858 |
| 441   | 2B     |   2862123 |
| 441   | K3     |     55160 |
| 524   | 1      |   4930442 |
| 524   | 2B     |  23379844 |
| 524   | BA     |   4499020 |
| 524   | K3     |    652620 |
+-------+--------+-----------+
12 rows in set (1.43 sec)

Genial. Hemos pasado de 21 segundos a apenas uno y medio. La mejora es espectacular… ¿Qué ocurriría ahora si aprovechamos que esos campos ya son únicos para hacer que también sean el destino de claves foráneas en la tabla principal? Veámoslo.

Establecimiento de claves foráneas.
mysql> ALTER TABLE mat_materialsx 
    -> ADD FOREIGN KEY(mat_typ) REFERENCES typ_alltypesx(typ_bas);
Query OK, 177047 rows affected (3.71 sec)
Records: 177047  Duplicates: 0  Warnings: 0

mysql> ALTER TABLE mat_materialsx 
    -> ADD FOREIGN KEY(mat_fin) REFERENCES fin_allfinish(fin_bas);
Query OK, 177047 rows affected (4.10 sec)
Records: 177047  Duplicates: 0  Warnings: 0

Cuarta consulta de agrupación.

¿Habremos mejorado algo al incorporar estas claves foráneas? Veamos, veamos…

mysql> SELECT typ_agg AS type, 
    -> fin_agg AS finish, 
    -> SUM(mat_qty) AS quantity
    -> FROM 
    -> ((mat_materialsx LEFT JOIN typ_alltypesx ON mat_typ = typ_bas) 
    -> LEFT JOIN fin_allfinish ON mat_fin = fin_bas)
    -> GROUP BY typ_agg, fin_agg;
+-------+--------+-----------+
| type  | finish | quantity  |
+-------+--------+-----------+
| 202   | 2B     |      7217 |
| 202   | BA     |     31140 |
| 430   | 1      |    994298 |
| 430   | 2B     |  11621394 |
| 430   | BA     |  22678569 |
| 430   | K3     |    546858 |
| 441   | 2B     |   2862123 |
| 441   | K3     |     55160 |
| 524   | 1      |   4930442 |
| 524   | 2B     |  23379844 |
| 524   | BA     |   4499020 |
| 524   | K3     |    652620 |
+-------+--------+-----------+
12 rows in set (1.45 sec)

En este caso la respuesta es que no. Nos hemos quedado exactamente igual. No obstante con todo esto no quiero decir que las claves foráneas no sean necesarias, ya que como he dicho al comenzamiento del post, nos sirven para mantener la integridad de la base de datos (¡aspecto fundamental!). Lo único que pretendo con este post es dejar claro que MySQL no las necesita estrictamente para combinar eficientemente distintas tablas, ya que es capaz de hacerlo igual de bien simplemente con que los campos que utilizamos para los joins sean únicos.

Nota importante.

A lo largo de todo el ejemplo es básico ir borrando la caché de consultas de MySQL para no obtener resultados distorsionados. Para ello he utilizado la sentencia:

mysql> RESET QUERY CACHE;
Query OK, 0 rows affected (0.00 sec)

De lo contrario nos encontraremos con tiempos de respuesta maravillosos de 0.00 sec… ;-)

Simulando DLookup fuera de Access (III).

En los dos posts anteriores he mostrado maneras de simular un DLookup en una base de datos MySQL mediante stored procedures y stored functions (primera aproximación y versión mejorada). No obstante, ha quedado claro que hacerlo así aunque puede resultar útil tiene sus limitaciones. Es por ello que normalmente prefiero utilizar un DLookup llevado a cabo desde la parte de la aplicación. En mi caso esto equivale a realizarlo en VisualBasic.NET. Y sin más preámbulos veamos el código de la función:

'--------------------------------------------------------------------
' Author:      Albert Mata (www.albertmata.net)
' Date:        20080530
' Description: Function to simulate Microsoft Access DLookup. 
'--------------------------------------------------------------------
Public Function DLookup(ByVal Field As String, _
                        ByVal Table As String, _
               Optional ByVal Condition As String = "TRUE") _
                        As Object
    Try
        'Creating SQL string.
        Dim SQL As String
        SQL = "SELECT " & Field _
            & " FROM " & Table _
            & " WHERE " & Condition
        'Filling dataset with desired value. 
        Dim DS As New DataSet
        Dim DA As New MySqlDataAdapter(SQL, CONNECTION_STRING)
        DA.Fill(DS, "anyname")
        'Returning value or Null.
        If Not IsDBNull(DS.Tables("anyname").Rows(0).Item(0)) Then
            Return DS.Tables("anyname").Rows(0).Item(0)
        Else
            Dim X As Object = Convert.DBNull
            Return X
        End If
    Catch
        'If error happens, returning Null.
        Dim X As Object = Convert.DBNull
        Return X
    End Try
End Function

Cabe aclarar que CONNECTION_STRING será una constante definida en algún punto de la aplicación que recogerá la cadena de conexión para nuestra base de datos. En mi caso es algo como:

Database = nombre_bbdd; _
Data Source = localhost; _
User ID = root; _
Password = mi_password

Relacionado con cadenas de conexión, recomendado darse un paseo por ConnectionStrings.com.

Yo estoy trabajando con una base de datos MySQL, por eso el DataAdapter es en realidad un MySqlDataAdapter, pero el funcionamiento sería análogo con cualquier otra base de datos y utilizando un objeto DataAdapter convencional. Lo único es que si se trabaja con MySQL es necesario previamente haber agregado la referencia correspondiente al proyecto, y para mayor comodidad en temas de nomenclatura importar el espacio de nombres correspondiente en la parte superior de la clase:

Imports MySql.Data.MySqlClient

Respecto a la función, poco que explicar. Pasarle el campo y la tabla deseados y de manera opcional una condición en formato SQL sin el WHERE. Si no se desea condición se pueden informar sólo dos argumentos, en esta ocasión no es necesario enviar una cadena vacía ni nada por el estilo. Además, el valor que nos devuelve la función es de tipo Object, así que no tendremos ningún problema con el tipo de dato devuelto, nos admitirá cualquier tipo de campo que quiera que sea el campo en cuestión de la tabla en cuestión. En caso de producirse algún error (nombre de campo o tabla mal escritos, conexión a base de datos fallida…) o si no se halla ningún registro, la función nos devolverá un valor nulo, con lo cual desde código podremos recogerlo sin problemas y actuar en consecuencia. :-)




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