2011-12-19 24 views
7

Supongamos que tengo un juego que puede jugar 2, 3 o 4 jugadores. Rastreo tal juego en mi base de datos (MySQL 5.1) en tres tablas, que figuran a continuación. Tengo la esperanza de que los campos no necesitan explicación:Unir a la izquierda la misma mesa varias veces

create table users (id int, login char(8)); 
create table games (id int, stime datetime, etime datetime); 
create table users_games (uid int, gid int, score int); 

[Las dos veces seguidas en la tabla de juegos son el tiempo de inicio y final]

aquí tienes datos ficticios para rellenar las tablas:

insert into games values 
(1, '2011-12-01 10:00:00', '2011-12-01 13:00:00'), 
(2, '2011-12-02 11:00:00', '2011-12-01 14:00:00'), 
(3, '2011-12-03 12:00:00', '2011-12-01 15:00:00'), 
(4, '2011-12-04 13:00:00', '2011-12-01 16:00:00'); 

insert into users_games values 
(101, 1, 10), 
(102, 1, 11), 
(101, 2, 12), 
(103, 2, 13), 
(104, 2, 14), 
(102, 3, 15), 
(103, 3, 16), 
(104, 3, 17), 
(105, 3, 18), 
(102, 4, 19), 
(104, 4, 20), 
(105, 4, 21); 

Ahora, necesito para producir un informe en el siguiente formato:

gid  p1 p2 p3 p4 started ended 
1  101 102    [g1] [g1] 
2  101 103 104   [g2] [g2] 
3  102 103 104 105 [g3] [g3] 
4  102 104 105   [g4] [g4] 

es decir, un informe que muestra todos los jugadores que jugaron un juego en la misma fila. También necesito sus puntuaciones y alguna otra información de la tabla de usuarios, pero que es la fase 2. :-)

empecé con esto:

select g.id, g.stime, g.etime, ug1.uid, ug2.uid, ug3.uid, ug4.uid 
from games g, users_games ug1, users_games ug2, users_games ug3, users_games ug4 
where 
g.id = ug1.gid and 
ug1.gid = ug2.gid and 
ug1.uid < ug2.uid and 
ug2.gid = ug3.gid and 
ug2.uid < ug3.uid and 
ug3.gid = ug4.gid and 
ug3.uid < ug4.uid 

Esto me todos los juegos en los que se ocuparon los cuatro asientos da (es decir, solo el juego ID 3 en los datos ficticios anteriores). Pero eso es solo un subconjunto de los datos que necesito.

Este es mi segundo intento:

select g.id, g.stime, g.etime, ug1.uid, ug2.uid, 
    ifnull(ug3.uid, ''), ifnull(ug4.uid, '') 
from (games g, users_games ug1, users_games ug2) 
left join users_games ug3 on ug2.gid = ug3.gid and ug2.uid < ug3.uid 
left join users_games ug4 on ug3.gid = ug4.gid and ug3.uid < ug4.uid 
where 
g.id = ug1.gid and 
ug1.gid = ug2.gid and 
ug1.uid < ug2.uid 

Esto me da 14 filas con los datos ficticios anteriores. He intentado eliminar una fuente de error mediante el anclaje UG1 a la entrada para el jugador de menor-UID:

select g.id, g.stime, g.etime, ug1.uid, ug2.uid, 
    ifnull(ug3.uid, ''), ifnull(ug4.uid, '') 
from 
(games g, users_games ug1, users_games ug2, 
    (select gid as g, min(uid) as u from users_games group by g) as xx 
) 
left join users_games ug3 on ug2.gid = ug3.gid and ug2.uid < ug3.uid 
left join users_games ug4 on ug3.gid = ug4.gid and ug3.uid < ug4.uid 
where 
g.id = xx.g and 
ug1.uid = xx.u and 
g.id = ug1.gid and 
ug1.gid = ug2.gid and 
ug1.uid < ug2.uid 

Ahora estoy abajo a 9 filas, pero todavía tengo un montón de datos espurios. Puedo ver el problema, por ejemplo, en el juego 3, con ug1 anclado al usuario 102, todavía hay tres jugadores a los que se puede anclar ug2. Y así. Pero no puedo encontrar una manera de resolver este enigma: ¿cómo puedo finalmente lograr una consulta que genere 4 filas con los jugadores en el orden y número correctos?

Esto me parece debería ser un problema resuelto en otros contextos. Apreciaremos toda la ayuda aquí.

+1

Yo les aconsejamos a * no * mezcla la sintaxis ',' y 'JOIN'. Simplemente use 'JOIN', no está 20 años fuera de fecha ... – MatBailie

Respuesta

16

Uno de los problemas que tiene es que no tiene campos que describan a un usuario como Jugador 1, 2, 3 o 4. Sin embargo, debe asegurarse de que solo un jugador se une por PARTE IZQUIERDA.

Si añade un campo "player_id" a users_games, se vuelve trivial ...

SELECT 
    * 
FROM 
    games 
LEFT JOIN 
    users_games  AS p1 
    ON p1.gid = games.id 
    AND p1.player_id = 1 
LEFT JOIN 
    users_games  AS p2 
    ON p2.gid = games.id 
    AND p2.player_id = 2 
LEFT JOIN 
    users_games  AS p3 
    ON p3.gid = games.id 
    AND p3.player_id = 3 
LEFT JOIN 
    users_games  AS p4 
    ON p4.gid = games.id 
    AND p4.player_id = 4 

Hay son alternativas que eviten todo se une a la izquierda, pero estos ejemplos sirven bien, ya que es la base para el siguiente paso ...)


Si no se puede agregar este campo, se hace más compleja. (SQL Server, Oracle, etc., pueden proxy este campo player_id usando ROW_NUMBER(), MySQL no puede).

En su lugar, necesita subconsultas correlacionadas para identificar al 'siguiente jugador'.

SELECT 
    * 
FROM 
    games 
LEFT JOIN 
    users_games  AS p1 
    ON p1.gid = games.id 
    AND p1.uid = (SELECT MIN(uid) FROM users_games WHERE gid = games.id) 
LEFT JOIN 
    users_games  AS p2 
    ON p2.gid = games.id 
    AND p2.uid = (SELECT MIN(uid) FROM users_games WHERE gid = games.id AND uid > p1.uid) 
LEFT JOIN 
    users_games  AS p3 
    ON p3.gid = games.id 
    AND p3.uid = (SELECT MIN(uid) FROM users_games WHERE gid = games.id AND uid > p2.uid) 
LEFT JOIN 
    users_games  AS p4 
    ON p4.gid = games.id 
    AND p4.uid = (SELECT MIN(uid) FROM users_games WHERE gid = games.id AND uid > p3.uid) 


EDITAR JOIN versión libre, suponiendo presencia de un campo player_id ...

SELECT 
    games.id, 
    MAX(CASE WHEN users_games.player_id = 1 THEN users_games.uid END) AS p1_id, 
    MAX(CASE WHEN users_games.player_id = 2 THEN users_games.uid END) AS p2_id, 
    MAX(CASE WHEN users_games.player_id = 3 THEN users_games.uid END) AS p3_id, 
    MAX(CASE WHEN users_games.player_id = 4 THEN users_games.uid END) AS p4_id 
FROM 
    games 
LEFT JOIN 
    users_games 
    ON users_games.gid = games.id 
GROUP BY 
    games.id 
+0

Wow, fantástico. Eso definitivamente resuelve mi problema :-) Si puede dar el método para evitar todas las combinaciones de la izquierda, mi educación para hoy estaría completa. – ObiObi

+0

@ObiObi - Pruebe la respuesta de EugenRieck también. Puede ser más rápido que la versión de sub-consulta correlacionada. – MatBailie

4
SELECT games.*, 
IF(min(ifnull(ug1.uid,9999999))=9999999,null,ug1.uid) AS user1, 
IF(min(ifnull(ug2.uid,9999999))=9999999,null,ug2.uid) AS user2, 
IF(min(ifnull(ug3.uid,9999999))=9999999,null,ug3.uid) AS user3, 
IF(min(ifnull(ug4.uid,9999999))=9999999,null,ug4.uid) AS user4 
FROM games 
LEFT JOIN users_games AS ug1 ON ug1.gid=games.id 
LEFT JOIN users_games AS ug2 ON ug2.gid=games.id AND ug2.uid>ug1.uid 
LEFT JOIN users_games AS ug3 ON ug3.gid=games.id AND ug3.uid>ug2.uid 
LEFT JOIN users_games AS ug4 ON ug4.gid=games.id AND ug4.uid>ug3.uid 
GROUP BY games.id 

supuesto 9.999.999 debería ser la máxima posible ID de usuario -1. Esto intercambia las subconsultas de la respuesta anterior con una consulta de agrupación grande.

Probado en MySQL 5.1 Ubuntu Lucid con sus datos de prueba.

+0

+1: supongo que esto funciona, personalmente me rehusé a hacerlo porque estás haciendo un producto medio cartesiano. (Con 4 jugadores obtienes 4 * 3 * 2 * 1 = 24 registros, que luego procesas en un grupo para obtener un registro). Luego, también debes volver a la mesa 'users_games' 4 veces para obtener el puntaje de cada jugador. Puntuación. SIN EMBARGO, las sub-consultas correlacionadas en mi respuesta también son un poco menos que ideales. Te interesaría probar ambos enfoques para ver cuál prefieres en términos de rendimiento y elegancia. – MatBailie

+0

¿Realmente necesita el IF()? No uso MySQL, pero hubiera pensado que es lo mismo, ¿en que MIN no devuelve NULL a menos que todos los valores sean NULL? Lo que significaría que 'MIN (ugX.uid)' debería ser suficiente por sí mismo debido al predicado '>' en su 'LEFT JOIN's? – MatBailie

+0

arriesgándote a un voto negativo: si necesito los puntajes, usaría algo como 'concat (ugx.uid,'. ', Ugx.score'), lo fundiré en minúsculo y luego lo descompondré nuevamente - en la mayoría de los hosts de DB un IO es mucho más costoso que algunos ciclos de CPU –

0

¿No sería más sencillo simplemente .....

SELECT g.id, GROUP_CONCAT(u.login ORDER BY u.login), g.stime, g.etime 
FROM games g, 
users u, 
users_games ug 
WHERE ug.gid=g.id 
AND ug.uid=u.id 
GROUP BY g.id, g.stime, g.etime 

Y si quieres resultados, basta con añadir una función, entonces ...

SELECT g.id, GROUP_CONCAT(
    CONCAT(u.login, '=', get_score(u.login, g.id)) ORDER BY 1 
    ), g.stime, g.etime 
FROM games g, 
users u, 
users_games ug 
WHERE ug.gid=g.id 
AND ug.uid=u.id 
GROUP BY g.id, g.stime, g.etime 
+0

Y luego, ¿desea unirse a otras tablas de 'Usuarios' para obtener metadatos de usuario, etc.? A menos que alguien pueda demostrar que las alternativas son inadecuadas, nunca * recomendaría * concatenar múltiples valores en un solo campo. – MatBailie

Cuestiones relacionadas