Since the question is not tagged a specific DBMS, here's a "standards-compliant" solution - it stays away from FOR-XML, GROUP_CONCAT and LISTAGG tricks. It works for only up to 3 members in a "pair" (set). The code works as-is in PostgreSQL.
SELECT m1.Pair,
p1.name || ' - ' || p2.name ||
COALESCE(' - ' || p3.name, '') AS Names
FROM Members m1
JOIN Members m2 on m1.pair = m2.pair and m1.player_id < m2.player_id
LEFT JOIN Members m3 on m2.pair = m3.pair and m2.player_id < m3.player_id
JOIN Players p1 ON m1.player_id = p1.id
JOIN Players p2 ON m2.player_id = p2.id
LEFT JOIN Players p3 ON m3.player_id = p3.id
WHERE p3.id is not null or NOT EXISTS (
SELECT *
FROM Members m4
WHERE m4.pair=m2.pair
AND m4.player_id NOT IN (m1.player_id,m2.player_id))
ORDER BY m1.Pair;
The problem with standards is that, well, there are many. See here for conformance the string concatenation operator (||
). For this solution to be applied to MySQL, the top bit has to be written using CONCAT()
SELECT m1.Pair,
CONCAT(p1.name, ' - ', p2.name,
COALESCE(Concat(' - ', p3.name), '')) AS Names
SQL Server uses the + operator.
Note: Even though SQL Server 2012 supports the CONCAT() function, it implements the same Oracle bug, i.e. CONCAT(' - ', NULL) => ' - '
instead of NULL.
SELECT m1.Pair,
p1.name + ' - ' + p2.name +
COALESCE(' - ' + p3.name, '') AS Names
Oracle will require a CASE statement to get around the concatenation-with-NULL bug mentioned above.
SELECT m1.Pair,
p1.name || ' - ' || p2.name ||
CASE when p3.name is null then '' else ' - ' || p3.name END AS Names
Performance
Despite the NOT EXISTS anti-semijoin, when put against Kevin and Matt's SQL Server solutions, it produces an estimated query cost of
Matt:Kevin:Richard = 24%: 65% : 11%
PostgreSQL 9.1+, SQL Server 2012+, Oracle, MySQL, DB2
The following query (SQLFiddle) proposed by Leigh in the comments works on all the listed DBMS:
SELECT m1.Pair
, CONCAT(CONCAT(CONCAT(p1.name, ' - '), p2.name),
COALESCE((SELECT CONCAT(' - ', p3.name) FROM Players p4 WHERE p4.id = p3.id),'')) AS Names
FROM Members m1
JOIN Members m2 on m1.pair = m2.pair and m1.player_id < m2.player_id
LEFT JOIN Members m3 on m2.pair = m3.pair and m2.player_id < m3.player_id
JOIN Players p1 ON m1.player_id = p1.id
JOIN Players p2 ON m2.player_id = p2.id
LEFT JOIN Players p3 ON m3.player_id = p3.id
WHERE p3.id is not null or NOT EXISTS (
SELECT *
FROM Members m4
WHERE m4.pair=m2.pair
AND m4.player_id NOT IN (m1.player_id,m2.player_id))
ORDER BY m1.Pair
The real trick is in getting the string concatenation correct, SQL Server is the last on the list to add a CONCAT() function. However, a function by the same name across 5 DBMS is by no means a standard, since it behaves differently. Already mentioned above is that Oracle treats NULLs in CONCAT as empty strings (''
), and PostgreSQL requires that operands are strings (will not auto-cast).