2011-12-19 5 views
7

Предположим, у меня есть игра, в которую могут играть 2, 3 или 4 игрока. Я отслеживаю такую ​​игру в своей базе данных (MySQL 5.1) в трех таблицах, приведенных ниже. Я надеюсь, что поля самоочевидно:Левое соединение одной и той же таблицы несколько раз

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

[Два раза отслеживаемый в таблице игр являются временем начала и окончания]

Вот некоторые фиктивные данные для заполнения таблицы:

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

Теперь мне нужно подготовить отчет в следующем формате:

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] 

То есть, отчет, в котором показаны все игроки, которые играли в игру в том же ряду. Я также нужна их оценка и некоторая другая информация из таблицы пользователей, но это фаза 2. :-)

Я начал с этим:

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 

Это дает мне все игры, где было занято все четыре места (т. е. только ID игры 3 в приведенных выше фиктивных данных). Но это всего лишь подмножество данных, которые мне нужны.

Это моя вторая попытка:

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 

Это дает мне 14 строк с выше фиктивными данными. Я пытался устранить один источник ошибки, закрепляя Ug1 к входу для низшего 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 

Теперь я до 9 строк, но я до сих пор есть много ложных данных. Я вижу проблему - например, в игре 3, с ug1, привязанной к пользователю 102, есть еще три игрока, которым может быть привязан ug2. И так далее. Но я не могу найти способ решить эту загадку - как я могу в итоге достичь запроса, который будет выводить 4 строки с игроками в правильном порядке и количестве?

Это, по-видимому, должно быть решением проблемы в других контекстах. Полюбуется всякая помощь здесь.

+1

Я настоятельно советую вам * не * mix ',' и 'JOIN'. Просто используйте 'JOIN', это не 20 лет от даты ... – MatBailie

ответ

16

Одна из проблем заключается в том, что у вас нет полей, которые описывают пользователя как игрока 1, 2, 3 или 4. Тем не менее, вам необходимо убедиться, что к ЛЕВЫМ СОЕДИНЕНИЕМ присоединяется только один игрок.

Если добавить поле «player_id» в users_games, она становится тривиальным ...

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 

Там являются альтернативы, которые избегают всех левых соединений, но это примеры хорошо служит, как это является основой для следующего шага ...)


Если вы не можете добавить это поле, он становится более сложным. (SQL Server, Oracle и т. Д., Может проксировать это поле player_id, используя ROW_NUMBER(), MySQL не может.)

Вместо этого вам нужны коррелированные подзапросы для идентификации «следующего игрока».

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 РЕГИСТРИРУЙТЕСЬ бесплатную версию, предполагая наличие 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

Ничего себе, фантастический. Это, безусловно, решает мою проблему :-) Если вы можете дать метод для избежания всех левых объединений, мое образование на сегодня будет полным. – ObiObi

+0

@ObiObi - Ответ теста EugenRieck тоже. Это может быть быстрее, чем коррелированная версия подзапроса. – 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 

9999999 конечно должно быть максимально возможным идентификатор пользователя -1. Это обрабатывает подзапросы предыдущего ответа по большому запросу группировки.

Протестировано на MySQL 5.1 Ubuntu Lucid с вашими тестовыми данными.

+0

+1: Я бы предположил, что это действительно работает, лично я уклонился от него, потому что вы делали половину декартова продукта. (С 4-мя игроками вы получаете 4 * 3 * 2 * 1 = 24 записи, которые затем обрабатываете в группе, чтобы получить одну запись.) Затем вам нужно снова подключиться к таблице 'users_games' 4 раза, чтобы получить доступ каждого игрока Гол. ОДНАКО, коррелированные подзапросы в моем ответе также немного меньше, чем идеалы. Было бы в ваших интересах проверить оба подхода, чтобы увидеть, что вы предпочитаете с точки зрения производительности и элегантности. – MatBailie

+0

Вам действительно нужен IF()? Я не использую MySQL, но я бы хотел, если бы это было то же самое, поскольку MIN не возвращает NULL, если все значения не равны NULL? Что означало бы, что 'MIN (ugX.uid)' должно быть достаточно на его собственном из-за '>' предиката в вашей 'LEFT JOIN'? – MatBailie

+0

, рискуя нисходящим сигналом: если мне нужны оценки, я бы использовал что-то вроде «concat (ugx.uid, '.', Ugx.score '), отбросил это на float для них min и затем разложил его снова - на большинство DB-хостов IO намного дороже, чем некоторые циклы процессора. –

0

Не было бы проще просто .....

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 

И если вы хотите оценки, просто добавить функцию, то ...

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

И если вы хотите присоединиться к другим таблицам «Пользователи», чтобы получить метаданные пользователя и т. Д.? Если кто-то не сможет продемонстрировать, что альтернативы непригодны, я бы никогда не рекомендовал * конкатенировать несколько значений в одно поле. – MatBailie

Смежные вопросы