2011-12-19 18 views
7

Supponiamo di avere un gioco che può essere giocato da 2, 3 o 4 giocatori. Traccio un gioco del genere nel mio database (MySQL 5.1) in tre tabelle, riportate di seguito. Spero che i campi sono auto-esplicativo:Left-join dello stesso tavolo più volte

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

[I due volte cingolati nella tabella giochi sono l'ora di inizio e di fine]

Ecco alcuni dati fittizi per popolare le tabelle:

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

Ora, ho bisogno di produrre un report nel seguente 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] 

Cioè, un rapporto che mostra tutti i giocatori che hanno giocato una partita nella stessa fila. Ho anche bisogno i loro punteggi e alcune altre informazioni dalla tabella degli utenti, ma che è la fase 2. :-)

ho iniziato con questo:

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 

Questo mi ha tutti i giochi in cui sono stati occupati tutti i quattro posti dà (cioè, solo l'ID di gioco 3 nei dati fittizi sopra). Ma questo è solo un sottoinsieme dei dati di cui ho bisogno.

Questo è il mio secondo tentativo:

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 

Questo mi dà 14 righe con i dati fittizi di cui sopra. Ho cercato di eliminare una fonte di errore ancorando UG1 alla voce per il giocatore più basso-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 

Ora sono fino a 9 righe, ma ho ancora un sacco di dati spuri. Riesco a vedere il problema - che per esempio nel gioco 3, con ug1 ancorato all'utente 102, ci sono ancora tre giocatori ai quali si può ancorare ug2. E così via. Ma non riesco a capire un modo per risolvere questo enigma: come posso ottenere in definitiva una query che produrrà 4 righe con i giocatori nell'ordine e nel numero corretti?

Questo mi sembra dovrebbe essere un problema risolto in altri contesti. Apprezzerò tutto l'aiuto qui.

+1

Io vi consiglio vivamente a * non * mix ',' e 'JOIN' sintassi. Basta usare 'JOIN', non è 20 anni non aggiornato ... – MatBailie

risposta

16

Un problema è che non ci sono campi che descrivono un utente come Player 1, 2, 3 o 4. Eppure, è necessario assicurarsi che solo un giocatore sia unito per LEFT JOIN.

Se si aggiunge un campo "player_id" per users_games, diventa banale ...

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 

Ci sono alternative che evitano tutti i join SINISTRA, ma questi esempi serve bene in quanto è la base per il passo successivo ...)


Se non è possibile aggiungere questo campo, diventa più complesso. (SQL Server, Oracle, ecc. Possono eseguire il proxy di questo campo player_id utilizzando ROW_NUMBER(), MySQL non possibile.)

Al contrario, sono necessarie sottoclassi correlate per identificare il "giocatore successivo".

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) 


EDIT JOIN versione gratuita, assumendo presenza di 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, fantastico. Questo certamente risolve il mio problema :-) Se puoi dare il metodo per evitare tutti i join di sinistra, la mia formazione per oggi sarebbe completa. – ObiObi

+0

@ObiObi - Prova anche la risposta di EugenRieck. Potrebbe essere più veloce della versione di sottoquery correlata. – 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 

ofcourse 9999999 dovrebbe essere il massimo id utente possibile -1. Questo negozia le subquery della risposta precedente contro una grande query di raggruppamento.

Testato su MySQL 5.1 Ubuntu Lucid con i dati di test.

+0

+1: Immagino che questo funzioni, personalmente ho evitato di farlo perché stai facendo un mezzo prodotto cartesiano. (Con 4 giocatori ottieni 4 * 3 * 2 * 1 = 24 record, che poi elaborerai in un gruppo per ottenere un record.) Devi quindi ricongiungerti alla tabella 'users_games' 4 volte per ottenere il punteggio di ogni giocatore Punto. TUTTAVIA, le sottoquery correlate nella mia risposta sono anche un po 'meno ideali. Sarebbe nel tuo interesse testare entrambi gli approcci per vedere quale preferisci in termini di prestazioni ed eleganza. – MatBailie

+0

Hai davvero bisogno dell'IF()? Non uso MySQL, ma avrei comunque lo stesso, in quanto MIN non restituisce NULL a meno che tutti i valori siano NULL? Il che significherebbe che 'MIN (ugX.uid)' dovrebbe essere sufficiente a causa del predicato '>' nel tuo 'LEFT JOIN's? – MatBailie

+0

mentre rischia un downvote: se ho bisogno dei punteggi, userei qualcosa come "concat (ugx.uid, '.', Ugx.score '), lo castiamo a un float per loro min e poi lo decomponiamo di nuovo - su la maggior parte degli host DB un IO è molto più costosa di alcuni cicli della CPU –

0

Non sarebbe più semplice da solo .....

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 

E se si vuole colonne sonore, basta aggiungere una funzione, allora ...

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

E poi se vuoi unirti ad altre tabelle 'Utenti' per ottenere meta-dati Utente, etc? A meno che qualcuno non dimostri che le alternative non sono adatte, io non raccomanderei mai * di concatenare più valori in un singolo campo. – MatBailie

Problemi correlati