MySQL: Problemas con JOINs entre versión 4 y 5
11 de Abril de 2008 en Programación, SQL
Lecturas: 4,617

MySQLEste artículo puede sonar más bién a una llamada desesperada. Llevo unas semanas (por no decir un par de meses) liado con un proyecto que me consume más de lo que quisiera pero por respeto a la empresa no puedo desvelar casi nada. Y para colofón me aparece este problema que me está llevando de cabeza.

Resulta que tengo una query... sencilla en concepto, un par de joins cruzados... La construyo en local contra MySQL 4 y funciona perfectamente... y al subirla al servidor de producción con MySQL 5 simplemente me devuelve 0 rows. Estoy literalmente alucinando.

Explicación del escenario

Se trata de tres tablas con relación N:M, es decir, dos tablas principales y una que las une:

variables
---------
id (pk)
name

var_questions
-------------
id (pk)
id_variable (fk)
id_language (fk)
question

languages
----------
id (pk)
name
enabled

La tabla de variables contiene todas las variables registradas en el programa. La tabla languages contiene todos los idiomas disponibles en el programa. La tabla var_questions relaciona variables con languages y presenta el campo question que viene a ser el texto de la variable en el idioma determinado, es decir, para cada variable e idioma debe haber un texto.

Entonces yo necesito una query que me saque las inconsistencias (no las coincidencias), porque se trata de tablas MyISAM y en el proceso de desarrollo se puede haber creado un idioma sin haber generado ningún texto para él y para todas las variables. Mi query debe encontrar "lo que queda suelto".

La query

Usando la lógica (la mía, claro...) pensé: teniendo la lista de variables y la lista de idiomas, lo que debe presentar inconsistencias es la tabla intermedia. Entonces, podría hacer una query enlazando las variables con var_questions con un left join i la última con languages con un right join, para encontrar los nulls en la tabla intermedia var_questions. Luego le digo en el where que sólo quiero los nulls et violà.

SQL:
  1. SELECT
  2.   VARIABLES.name            AS var_name,
  3.   VARIABLES.id        AS var_id,
  4.   var_questions.question    AS question,
  5.   languages.name            AS lang
  6. FROM
  7.   VARIABLES
  8.     LEFT JOIN var_questions ON VARIABLES.id = var_questions.id_variable
  9.       RIGHT JOIN languages ON var_questions.id_language = languages.id
  10. WHERE
  11.     var_questions.question IS NULL
  12. AND languages.enabled = 1

La ejecuto en MySQL 4 y perfecto. Me saca justamente la información que necesito. Subo la query a producción, con un servidor MySQL 5 y me devuelve (con los mismos datos) 0 rows. Y justo ahí empieza mi odisea.

Investigando

La verdad es que poco más puedo investigar a parte de lo que haríamos todos: Googlear mucho, buscar problemas de compatibilidad, buscar documentación de JOINs, preguntar a cracks en el tema, dejar mensajes en foros, ...

Las versiones con las que juego son, en local, un MySQL 4.1.22-community-nt sobre Windows Vista, y en producción un MySQL 5.0.24a sobre Linux 2.6.9-023stab046.2-enterprise (Red Hat 3.4.5-2).

Durante las pruebas he encontrado una query que parece devolver los mismos resultados, pero no me gusta por el simple hecho que usa una CROSS JOIN que no podría explicar exactamente qué hace, pero parece hacer una producto cartesiano de las tablas enlazadas y no acepta un ON:

SQL:
  1. SELECT
  2.   VARIABLES.name            AS var_name,
  3.   var_questions.question    AS question,
  4.   languages.name            AS lang
  5. FROM
  6.   languages
  7.     CROSS JOIN VARIABLES
  8.       LEFT JOIN var_questions ON var_questions.id_language = languages.id
  9. WHERE
  10.     var_questions.question IS NULL
  11. AND languages.enabled = 1

Cómo probar?

Tengo unos datos preparados por si alguien quiere hacer algunas pruebas (que serán agradecidas profundamente):

SQL:
  1. CREATE TABLE `variables` (
  2.   `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  3.   `name` varchar(100) collate latin1_spanish_ci NOT NULL DEFAULT '',
  4.   PRIMARY KEY  (`id`)
  5. ) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_spanish_ci;
  6.  
  7. CREATE TABLE `languages` (
  8.   `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  9.   `name` varchar(10) collate latin1_spanish_ci NOT NULL DEFAULT '',
  10.   `enabled` tinyint(1) NOT NULL DEFAULT '0',
  11.   PRIMARY KEY  (`id`)
  12. ) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_spanish_ci;
  13.  
  14. CREATE TABLE `var_questions` (
  15.   `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  16.   `id_variable` int(11) UNSIGNED NOT NULL DEFAULT '0',
  17.   `id_language` int(11) UNSIGNED NOT NULL DEFAULT '0',
  18.   `question` text collate latin1_spanish_ci,
  19.   PRIMARY KEY  (`id`)
  20. ) ENGINE=MyISAM AUTO_INCREMENT=163 DEFAULT CHARSET=latin1 COLLATE=latin1_spanish_ci;
  21.  
  22. INSERT INTO `languages` (`id`, `name`, `enabled`) VALUES (2, 'es', 1);
  23. INSERT INTO `languages` (`id`, `name`, `enabled`) VALUES (4, 'ca', 1);
  24. INSERT INTO `languages` (`id`, `name`, `enabled`) VALUES (5, 'en', 0);
  25.  
  26. INSERT INTO `variables` (`id`, `name`) VALUES (56, 'var_1');
  27. INSERT INTO `variables` (`id`, `name`) VALUES (60, 'var_2');
  28. INSERT INTO `variables` (`id`, `name`) VALUES (61, 'var_3');
  29.  
  30. INSERT INTO `var_questions` (`id`, `id_variable`, `id_language`, `question`) VALUES (157, 56, 2, 'Variable 1');
  31. INSERT INTO `var_questions` (`id`, `id_variable`, `id_language`, `question`) VALUES (158, 60, 2, 'Variable 2');
  32. INSERT INTO `var_questions` (`id`, `id_variable`, `id_language`, `question`) VALUES (159, 61, 2, 'Variable 3');

El resultado correcto debería ser el siguiente:

var_name var_id question lang
var_1 56 null ca
var_2 60 null ca
var_3 61 null ca

Colofón

Vale. No soy capaz de encontrar el problema. Será un problema de algún bug en la versión que uso para producción? Está mal planteada la query? A alguien se le ocurre algo?

Una cervecita para quien dé con la solución ;)

Salu10

Tag:
 Enviar a Fresqui

Leer los Comentarios

[ # 19625 ] Comment desde franci [11 de Abril de 2008, 04:08]

Usar un valor null provocado por la inexistencia de tuplas en un left join o right join no creo que sea muy aconsejable (excepto si se utiliza desde otra sentencia que usa el resultado como tabla. Desconozco como sería en MySQL pero en derby, SQLServer y varios más podría ser así:

SELECT
V.name var_name,
V.id var_id,
‘null’ question,
L.name lang
FROM
VARIABLES V, languages L
WHERE
L.enabled = 1 and
not exists
(
select *
from
var_questions VQ
where
VQ.id_variable = V.id and
VQ.id_language = L.id and
VQ.question IS not NULL
)

al menos en derby (es lo que tengo en casa) me da el resultado que pides.

Por cierto ¿Me cambias la cervecita por un café o una horchata? :-)

[ # 19626 ] Comment desde Jurguen [11 de Abril de 2008, 04:38]

No es problema de Mysql o del Join, es solo que no vas a poder obtener ese tipo de resultado, realizando left o right join a menos que utilizes cross joins ¿y no veo el problema en usarlo?

Te envio una consulta que retorna el resultado que deseas:

select variables.name, variables.id, NULL as question, languages.name from variables,languages
where languages.id not in (select id_language from var_questions)
and languages.enabled=1

Es obvio que el campo question este en null, pero no se porque deseas mostrarlo.

Espero te sirva, nos leemos.

[ # 19627 ] Comment desde Chema [11 de Abril de 2008, 04:56]

En una mysql 5.0.54 (en linux gentoo con 2.6.23), el resultado que tu comentas resulta igual. A ver si te sirve de algo.

Un saludo,

[ # 19628 ] Comment desde Chema [11 de Abril de 2008, 04:59]

Pero ojo, no me devuelve 0 rows, sino mas bien esto:

var_name var_id question lang
NULL NULL NULL ca

[ # 19629 ] Comment desde Bellz [11 de Abril de 2008, 05:20]

He probado con un MySQL 5.051a y me retorna una fila con las columnas con valor nulo, exceptuando languages.name.

El resultado que esperas

var_name var_id question lang
var_1 56 null ca
var_2 60 null ca
var_3 61 null ca

no se puede dar nunca, al menos en esa forma.

El left join de var_questions retorna las filas coincidentes por identificador de variable y te devolverá las columnas de la selección correctamente. En cuanto aplicas el right join con languages te devolverá el lenguaje para las variables resultado del left join anterior y una fila (solo una) por lenguaje no coincidente y exceptuando los valores de languages el resto de columnas de esa fila tendrán valor nulo, por ejemplo

SELECT *
FROM variables
left JOIN var_questions ON (variables.id = var_questions.id_variable)
right JOIN languages ON (languages.id=var_questions.id_language)

retornará

id|name|id|id_variable|id_language|question|id|name|enabled
56|var_1|157|56|2|Variable 1|2|es|1
60|var_2|158|60|2|Variable 2|2|es|1
61|var_3|159|61|2|Variable 3|2|es|1
\N|\N|\N|\N|\N|\N|4|ca|1
\N|\N|\N|\N|\N|\N|5|en|0

Creo que el problema no es MySQL 5 sino MySQL 4, que no optimiza los resultados de los JOIN y aplica el último join a cada fila de resultado del join anterior, por ejemplo, la misma query anterior en un MySQL 4 produce éste resultado

id|name|id|id_variable|id_language|question|id|name|enabled
56|var_1|157|56|2|Variable 1|2|es|1
60|var_2|158|60|2|Variable 2|2|es|1
61|var_3|159|61|2|Variable 3|2|es|1
56|var_1|\N|\N|\N|\N|4|ca|1
60|var_2|\N|\N|\N|\N|4|ca|1
61|var_3|\N|\N|\N|\N|4|ca|1
56|var_1|\N|\N|\N|\N|5|en|0
60|var_2|\N|\N|\N|\N|5|en|0
61|var_3|\N|\N|\N|\N|5|en|0

Puede que esta query te sirva

SELECT variables.id, variables.name, languages.name FROM variables, var_questions, languages
where not languages.id = var_questions.id_language
and languages.enabled = 1
group by variables.id, languages.id order by variables.id

Un saludo.

[ # 19632 ] Comment desde Patxi Echarte [11 de Abril de 2008, 08:03]

Una forma sencilla de hacerlo es con consultas anidadas:

SELECT v.*, l.name
FROM variables AS v, languages AS l
WHERE l.enabled=1 AND v.id NOT
IN (
SELECT id_variable
FROM var_questions
WHERE id_language = l.id
)

[ # 19633 ] Comment desde Patxi Echarte [11 de Abril de 2008, 08:18]

Otra opción es tal como dices utilizando un join normal:

SELECT v.*, l.name
FROM
variables AS v CROSS JOIN languages AS l
LEFT JOIN var_questions AS vq ON v.id = vq.id_variable
AND vq.id_language=l.id
WHERE l.enabled=1 AND vq.id IS NULL

El cross join tal como dices se corresponde con el producto cartesiano de las tablas, (el join básico) en este caso de languages y variables. Esto produce que se obtengan todas las combinaciones de variables y lenguajes existentes. A partir de esto es suficiente con utilizar un left join para buscar los elementos que falten.

[ # 19636 ] Comment desde Biel Frontera [11 de Abril de 2008, 09:16]

Hola!
A mi me funciona la select en la versión 5.0.30-Debian (después de cambiar VARIABLES por variables).

De todas maneras, podrías plantear la select de otra manera (no sé si es más pesada o no… pero también tiene que funcionar). Puedes hacer una lectura de todas las parejas lenguaje-variable que no encuentre ningún registro en la tabla var_questions (con un count).

Sería:
SELECT
variables.name AS var_name,
languages.name AS lang
FROM languages, variables
WHERE
languages.enabled =1
AND (
SELECT count( 1 )
FROM var_questions
WHERE
var_questions.id_language = languages.id
AND var_questions.id_variable = variables.id
) = 0

[ # 19763 ] Comment desde Xavi [14 de Abril de 2008, 08:43]

@Franci, Jurguen, Patxi y Biel: Se me olvidó de decir que quería no utilizar subquerys… Hace tiempo hicimos unas pruebas con un amigo y descubrimos que la subconsulta se re-ejecuta a cada iteración de la consulta principal, con lo que se vuelve mucho mucho más pesada. De todas formas, gracias ;) Y no es que desee mostrar el campo NULL, es simplemente para ver que realmente es null (mientras desarrollo la query).

@Chema: No entiendo ése resultado… De acuerdo que faltan todas las variables con el lenguage “ca”, pero deberían salir con su nombre… juer

@Bellz: Tienes mi cerveza. ;) Por un lado te diré que mi query si que me funciona en MySQL 4. Entiendo tu explicación y entiendo también que el fallo realmente es de MySQL 4 en aplicar el right join a todos los resultados del left join (aunque podría pensarse en otros casos que éso pudiera ser correcto). He probado tu query en MySQL y no ha funcionado. En cambio, en MySQL 5 ha funcionado de forma esperada y me da los resultados correctos. Hasta me da vergüenza ver lo simple que és, y yo liándome con cross joins… Gracias majo!

@Patxi: gracias por la explicación del CROSS JOIN

A todos, gracias por responder. Da gusto ver que aún hay gente dispuesta a echar una mano.

Salu10!

[ # 19781 ] Comment desde Bellz [14 de Abril de 2008, 02:09]

De nada Xavi ;)

Gracias por la cerve :)

Escribe un Comentario





Estadísticas