Alexander Farber Alexander Farber - 3 months ago 7
SQL Question

Select all entries from a table and the LATEST entries from another "logging" table

I have tried to prepare an SQL Fiddle for my problem -

In a multiplayer word game active games are stored in the table

words_games
:

CREATE TABLE words_games (
gid SERIAL PRIMARY KEY, /* game id */
created timestamptz NOT NULL,

player1 integer REFERENCES words_users(uid) ON DELETE CASCADE NOT NULL,
player2 integer REFERENCES words_users(uid) ON DELETE CASCADE,

played1 timestamptz,
played2 timestamptz,

score1 integer NOT NULL CHECK(score1 >= 0),
score2 integer NOT NULL CHECK(score2 >= 0),

hand1 varchar[7] NOT NULL,
hand2 varchar[7] NOT NULL,
pile varchar[116] NOT NULL,

letters varchar[15][15] NOT NULL,
values integer[15][15] NOT NULL,
bid integer NOT NULL REFERENCES words_boards ON DELETE CASCADE
);


And it is easy to select all games in which for example a player with id
1
participates:

SELECT * FROM words_games WHERE player1 = 1 OR player2 = 1;


But now I have also added a table
words_moves
, which acts as a logging journal of player actions:

CREATE TYPE words_action AS ENUM ('play', 'skip', 'swap', 'resign');

CREATE TABLE words_moves (
mid SERIAL PRIMARY KEY, /* move id */
action words_action NOT NULL,
gid integer NOT NULL REFERENCES words_games ON DELETE CASCADE,
uid integer NOT NULL REFERENCES words_users ON DELETE CASCADE,
played timestamptz NOT NULL,
tiles jsonb NULL,
score integer NULL CHECK(score > 0) /* score awarded in that move */
);


Now, when a user connects to my game server, I would like not only to send her all active games, but also the latest action (with the highest
mid
) for each game.

How to run such a join (or CTE) in one query please?

I have tried the following INNER JOIN but it returns all moves, while I only need the latest move in each game:

SELECT
g.gid,
EXTRACT(EPOCH FROM g.created)::int AS created,
g.player1,
COALESCE(g.player2, 0) AS player2,
COALESCE(EXTRACT(EPOCH FROM g.played1)::int, 0) AS played1,
COALESCE(EXTRACT(EPOCH FROM g.played2)::int, 0) AS played2,
ARRAY_TO_STRING(g.hand1, '') AS hand1,
ARRAY_TO_STRING(g.hand2, '') AS hand2,
-- g.letters,
-- g.values,
m.action,
m.tiles
FROM words_games g INNER JOIN words_moves m
ON g.gid = m.gid
AND ( g.player1 = m.uid OR g.player2 = m.uid )
AND ( g.player1 = 1 OR g.player2 = 1 )
ORDER BY g.gid;


gid | created | player1 | player2 | played1 | played2 | hand1 | hand2 | action | tiles
-----+------------+---------+---------+------------+------------+---------+---------+--------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1 | 1471794994 | 1 | 2 | 1471868012 | 1471810486 | ПЕАЯСАС | ЖИОБАЯС | play | [{"col": 7, "row": 10, "value": 1, "letter": "Н"}, {"col": 7, "row": 8, "value": 2, "letter": "К"}, {"col": 7, "row": 9, "value": 1, "letter": "И"}, {"col": 7, "row": 7, "value": 2, "letter": "С"}]
1 | 1471794994 | 1 | 2 | 1471868012 | 1471810486 | ПЕАЯСАС | ЖИОБАЯС | play | [{"col": 7, "row": 14, "value": 2, "letter": "К"}, {"col": 7, "row": 13, "value": 1, "letter": "Н"}, {"col": 7, "row": 11, "value": 3, "letter": "У"}, {"col": 7, "row": 12, "value": 2, "letter": "П"}]
1 | 1471794994 | 1 | 2 | 1471868012 | 1471810486 | ПЕАЯСАС | ЖИОБАЯС | play | [{"col": 6, "row": 2, "value": 2, "letter": "П"}, {"col": 6, "row": 3, "value": 1, "letter": "О"}, {"col": 6, "row": 4, "value": 1, "letter": "Е"}, {"col": 6, "row": 5, "value": 5, "letter": "Ж"}, {"col": 6, "row": 6, "value": 5, "letter": "Ы"}, {"col": 6, "row": 7, "value": 2, "letter": "П"}, {"col": 6, "row": 8, "value": 5, "letter": "Ы"}]
2 | 1471795037 | 1 | 2 | 1471806484 | 1471865696 | КЙВГКСМ | ЯРХЖИМН | swap | "А"
2 | 1471795037 | 1 | 2 | 1471806484 | 1471865696 | КЙВГКСМ | ЯРХЖИМН | play | [{"col": 7, "row": 10, "value": 5, "letter": "Ы"}, {"col": 7, "row": 9, "value": 2, "letter": "Д"}, {"col": 7, "row": 8, "value": 1, "letter": "А"}, {"col": 7, "row": 7, "value": 2, "letter": "Л"}]
(5 rows)


UPDATE:

Actually I would need a LEFT JOIN, because there can be games without any player moves yet...

Answer

Okay, let's build up the sql. First, we need to figure out the most recent move for all of the games. There are lots of ways to do this, but let's try this one:

SELECT *
FROM words_moves wm1
WHERE
  played = (SELECT max(played)
            FROM words_moves wm2
            WHERE wm1.gid = wm2.gid);

It's not the fastest way of doing it, but it's one of the easier to understand -- get every move from words_moves where the timestamp is the most recent.

Now that we have that, we can build a query with it to get games plus moves:

WITH last_moves AS (
  SELECT *
  FROM words_moves wm1
  WHERE
    played = (SELECT max(played)
              FROM words_moves wm2
              WHERE wm1.gid = wm2.gid))
SELECT *
FROM words_games wg
  LEFT JOIN last_moves lm
    ON (wg.gid = lm.gid)
WHERE
  player1 = 1 OR
  player2 = 1;

If you're not familiar, the WITH there indicates a common table expression which is a very handy sort of subquery. Among other things, it means if you end up using a different method for getting the most recent move per game (this question has a good set of alternatives to try), then it's easy to swap in without too much trouble.

Hope that helps!

Comments