I have a table (players) containing a list of players, I would like to pair up these players in a unique way, so that every player's opponent is unique every round (ie. every time the SELECT query is called).

The player_opponent_log column is an integer[] which contains the player_ids of players who have played with that player in a previous round (and is used to help pick out unique players). This column is populated afterwards using the results of the SELECT query - and is outside the scope of this question.

The table for instance would have the following data;

 player_id | player_opponent_log 
         1 | {2,3}
         2 | {1}
         3 | {1}
         4 | {}
         5 | {}
         6 | {}
         7 | {8}
         8 | {7}

What I am trying to achieve would be something along the following lines:

 player_id1 | player_id2 
          1 |          4
          2 |          3
          5 |          7
          6 |          8

I have tried countless different approaches, including GROUP BY, DISTINCT ON (), self JOINS, but haven't been able to come to a working solution.

The following result is what I am currently getting and am trying to avoid:

 player_id1 | player_id2 
          1 |          4
          2 |          3
          3 |          4
          4 |          1
          5 |          1
          6 |          1
          7 |          1
          8 |          1

Where I'm getting stuck is how to eliminate a single player being allocated for a round more than once per SELECT query.

Any ideas on how to solve this would be highly appreciated.

  • Where does 1,4 or 6,8 come from? It's not the sample data (the first "table") – a_horse_with_no_name Mar 18 '14 at 18:05
  • @a_horse_with_no_name They want a "draw", with pairing for a new round. The first table shows the pairs of previous rounds. – ypercubeᵀᴹ Mar 18 '14 at 18:10
  • Firstly, you should not be storing arrays as column values. Secondly, please describe the logic behind the desired results in human terms, as you would for your grandma, for example. – mustaccio Mar 18 '14 at 19:52
  • have you tried to use temp table ? – user4387158 Oct 29 '16 at 15:19

Every row of the result depends on the previous row. A recursive CTE comes to mind, I tried that. But one would need to refer to the worktable in an OUTER JOIN or a subquery expression which is not allowed. This does not work (building on the table layout in my fiddle):

   t0 AS (SELECT *, COALESCE(array_length(opp_log,1), 0) AS len FROM tbl)
,  t1 AS (
   SELECT t1.player_id AS pl, t2.player_id AS p2
         ,t1.len AS len1, t2.len AS len2
   FROM   t0 t1, t0 t2 
   WHERE  t2.player_id <> t1.player_id
   AND    t2.player_id <> ALL (t1.opp_log)
, cte AS (
   SELECT pl, p2
   FROM   t1
   ORDER  BY len1 DESC, len2 DESC
   LIMIT  1

   SELECT pl, p2
   FROM   t1
   LEFT   JOIN cte c ON t1.p1 IN (c.p1, c.p2)
                        OR t1.p2 IN (c.p1, c.p2)
   WHERE  c.p1 IS NULL
   ORDER  BY len1 DESC, len2 DESC
   LIMIT  1
FROM   cte;

> ERROR:  recursive reference to query "cte" must not appear within an outer join

I don't think there is a halfway decent way to solve this with pure SQL. I suggest:

Procedural solution with PL/pgSQL

  RETURNS TABLE (player_id1 int, player_id2 int) AS
   rows int := (SELECT count(*)/2 FROM tbl);  -- expected number of resulting rows
   ct   int := 0;                             -- running count

CREATE TEMP TABLE t ON COMMIT DROP AS         -- possible combinations
SELECT t1.player_id AS p1, t2.player_id AS p2
     , COALESCE(array_length(t1.opp_log,1), 0) AS len1
     , COALESCE(array_length(t2.opp_log,1), 0) AS len2
FROM   tbl t1, tbl t2 
WHERE  t2.player_id <> t1.player_id
AND    t2.player_id <> ALL (t1.opp_log)
AND    t1.player_id <> ALL (t2.opp_log)
ORDER  BY len1 DESC, len2 DESC;               -- opportune sort order

   SELECT INTO player_id1, player_id2  p1, p2 FROM t LIMIT 1;

   ct := ct + 1;                              -- running count

   DELETE FROM t                              -- remove obsolete pairs
   WHERE  p1 IN (player_id1, player_id2) OR 
          p2 IN (player_id1, player_id2);

IF ct < rows THEN
   RAISE EXCEPTION 'Could not find a solution';
ELSIF ct > rows THEN
   RAISE EXCEPTION 'Impossible result!';

$func$  LANGUAGE plpgsql VOLATILE;


Build a temporary table with remaining possible pairs. This kind of cross join produces a lot of rows with big tables, but since we seem to be talking about tournaments, numbers should be reasonably low.

Players with the longest list of opponents are sorted first. This way, players that would be hard to match come first, increasing the chance for a solution.

Pick the first row and delete related pairings now obsolete. Do need to sort again. Logically any row is good, practically we get the player with the longest list of opponents first due to initial sort (which is not reliable without ORDER BY, but good enough for the case).

Repeat until no match is left.
Keep count and raise an exception if the count is not as expected. PL/pgSQL conveniently allows to raise an exception after the fact, which cancels any previous return values. Details in the manual.


SELECT * FROM f_next_round();


player_id1 | player_id2
1          | 7
2          | 3
4          | 8
5          | 6

SQL Fiddle.


This does not guarantee to calculate the perfect solution. I just returns a possible solution and uses some limited smarts to improve the chances to find one. The problem is a bit like solving a Sudoku, really and is not trivially solved perfectly.

| improve this answer | |
  • Thank you very much for your time and effort in answering the question. While this is a solution to the problem - I would still (mostly out of curiosity) be very interested if anyone has an ideas on how to solve it in a pure SQL way. – AbuAbdoh Mar 20 '14 at 6:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.