create arbitrary 1-1 correspondence between items in indirectly related tables

Viewed 57

Consider the following tables:

Rooms
+------+-------+
|  ID  | Room  | 
+------+-------+
|   1  | A101  |
|   2  | A102  |
|   3  | A103  |
|   4  | A101o |
|   5  | A102o |
|   6  | A103o |
+------+-------+

Beds
+------+---------+
|  ID  | RoomId  |
+------+---------+
|   1  |    1    |
|   2  |    1    |
|   3  |    2    |
|   4  |    2    |
|   5  |    3    |
|   6  |    3    |
|   7  |    4    |
|   8  |    4    |
|   9  |    5    |
|  10  |    5    |
|  11  |    6    |
|  12  |    6    |
+------+---------+

Each room has some number of beds (usually 2). I'd like to do a self join on rooms with an arbitrary 1-1 pairing of beds (ie. below 1-7 and 2-8 are paired, but 1-8 and 2-7 would be just as good. But I don't want all possible pairings, ie I don't want 1-7, 1-8, 2-7, 2-8.... I just want each bed involved in exactly one pairing with another bed in the corresponding room.

+--------+--------+--------+---------+
| Room 1 | Bed 1  | Room 2 | Bed 2   |
+--------+--------+--------+---------+
|  A101  |    1   | A101o  |    7    |
|  A101  |    2   | A101o  |    8    |
|  A102  |    3   | A102o  |    9    |
|  A102  |    4   | A102o  |   10    |
|  A103  |    5   | A103o  |   11    |
|  A103  |    6   | A103o  |   12    |
+--------+--------+--------+---------+

Note the ordering of my actual data is not so neat--but I do know that the number of beds in corresponding rooms are equal. If A102 has three beds, A102o will also have 3 beds, etc..

This is close but gives me duplicates:

SELECT beds.bed_id, rooms.room, rooms2.room as room2, beds2.bed_id AS bed_id2 
  FROM beds
  LEFT JOIN rooms ON (beds.room_id = rooms.room_id)
  CROSS JOIN rooms rooms2 ON (CONCAT(rooms.room,'o') = rooms2.room)
  JOIN beds beds2 ON (beds2.room_id = rooms2.room_id);
2 Answers

If you just want two rows for each pair of related rooms:

select r1.room 'Room 1',if(which_row=1,min(b1.bed_id),max(b1.bed_id)) 'Bed 1',r2.room 'Room 2',if(which_row=1,min(b2.bed_id),max(b2.bed_id)) 'Bed 2'
from (select 1 which_row union all select 2) which_row
cross join rooms r1
join rooms r2 on r2.room=concat(r1.room,'o')
join beds b1 on b1.room_id=r1.room_id
join beds b2 on b2.room_id=r2.room_id
group by r1.room_id,r2.room_id,which_row

If you want as many rows as there are beds (up to four), it's basically the same but the expression to get the beds for each rows is a little more complicated, and you'll want a subquery to get the number of beds for each room pair:

select
    room1 'Room 1',
    substring_index(substring_index(beds1, ',', which_row), ',', -1) 'Bed 1',
    room2 'Room 2',
    substring_index(substring_index(beds2, ',', which_row), ',', -1) 'Bed 2'
from (
    select
        r1.room room1,
        group_concat(distinct b1.bed_id order by b1.bed_id) beds1,
        r2.room room2,
        group_concat(distinct b2.bed_id order by b2.bed_id) beds2,
        least(count(distinct b1.bed_id),count(distinct b2.bed_id)) beds
    from rooms r1
    join rooms r2 on r2.room=concat(r1.room,'o')
    join beds b1 on b1.room_id=r1.room_id
    join beds b2 on b2.room_id=r2.room_id
    group by r1.room, r2.room
) room_pairs
join (
    select 1 which_row union all select 2 union all select 3 union all select 4
) which_row on which_row <= room_pairs.beds

Building it up in pieces, you want each pair of rooms to have up to four rows in the result. So you use a subquery that you can join onto the rest of the query that causes all the other rows to be repeated:

select 1 which_row union all select 2 union all select 3 union all select 4

+-----------+
| which_row |
+-----------+
|         1 |
|         2 |
|         3 |
|         4 |
+-----------+

And a subquery that gets each room pair with all their beds:

select
    r1.room room1,
        group_concat(distinct b1.bed_id order by b1.bed_id) beds1,
        r2.room room2,
        group_concat(distinct b2.bed_id order by b2.bed_id) beds2,
        least(count(distinct b1.bed_id),count(distinct b2.bed_id)) beds
    from rooms r1
    join rooms r2 on r2.room=concat(r1.room,'o')
    join beds b1 on b1.room_id=r1.room_id
    join beds b2 on b2.room_id=r2.room_id
    group by r1.room, r2.room

+-------+----------+-------+----------+------+
| room1 | beds1    | room2 | beds2    | beds |
+-------+----------+-------+----------+------+
| A101  | 1,2      | A101o | 7,8      |    2 |
| A102  | 3,4      | A102o | 9,10     |    2 |
| A103  | 5,6      | A103o | 11,12    |    2 |
| A205  | 13,14,15 | A205o | 16,17,18 |    3 |
+-------+----------+-------+----------+------+

Join the two together, limiting the which_row to the number of beds for each room pair:

select which_row, room1, beds1, room2, beds2
from (
    select
        r1.room room1,
        group_concat(distinct b1.bed_id order by b1.bed_id) beds1,
        r2.room room2,
        group_concat(distinct b2.bed_id order by b2.bed_id) beds2,
        least(count(distinct b1.bed_id),count(distinct b2.bed_id)) beds
    from rooms r1
    join rooms r2 on r2.room=concat(r1.room,'o')
    join beds b1 on b1.room_id=r1.room_id
    join beds b2 on b2.room_id=r2.room_id
    group by r1.room, r2.room
) room_pairs
join (
    select 1 which_row union all select 2 union all select 3 union all select 4
) which_row on which_row <= room_pairs.beds

+-----------+-------+----------+-------+----------+
| which_row | room1 | beds1    | room2 | beds2    |
+-----------+-------+----------+-------+----------+
|         1 | A101  | 1,2      | A101o | 7,8      |
|         2 | A101  | 1,2      | A101o | 7,8      |
|         1 | A102  | 3,4      | A102o | 9,10     |
|         2 | A102  | 3,4      | A102o | 9,10     |
|         1 | A103  | 5,6      | A103o | 11,12    |
|         2 | A103  | 5,6      | A103o | 11,12    |
|         1 | A205  | 13,14,15 | A205o | 16,17,18 |
|         2 | A205  | 13,14,15 | A205o | 16,17,18 |
|         3 | A205  | 13,14,15 | A205o | 16,17,18 |
+-----------+-------+----------+-------+----------+

Then just change the selected fields to get the correct bed for each row from the comma-separated list:

select
    room1,
    substring_index(substring_index(beds1, ',', which_row), ',', -1) bed1,
    room2,
    substring_index(substring_index(beds2, ',', which_row), ',', -1) bed2

+-------+------+-------+------+
| room1 | bed1 | room2 | bed2 |
+-------+------+-------+------+
| A101  | 1    | A101o | 7    |
| A101  | 2    | A101o | 8    |
| A102  | 3    | A102o | 9    |
| A102  | 4    | A102o | 10   |
| A103  | 5    | A103o | 11   |
| A103  | 6    | A103o | 12   |
| A205  | 13   | A205o | 16   |
| A205  | 14   | A205o | 17   |
| A205  | 15   | A205o | 18   |
+-------+------+-------+------+

Basically, you want to pair the rooms together and then assign beds to them. Here is one method:

with br as (
      select b.*, r.room, r.pair,
             row_number() over (partition by r.room order by b.id) as seqnum,
             dense_rank() over (partition by pair order by r.room) as pair_seqnum
      from beds b join
           (select r.*,
                   floor( (row_number() over (order by rand()) - 1) % (count(*) over () / 2)) as pair
            from rooms r
           ) r
           on b.roomid = r.id
     )
select min(case when pair_seqnum = 1 then room end) as room_1,
       min(case when pair_seqnum = 1 then id end) as bed_1,
       min(case when pair_seqnum = 2 then room end) as room_2,
       min(case when pair_seqnum = 2 then id end) as bed_2
from br
group by pair, seqnum;

The innermost subquery assigns a pair number to rooms. This is just using arithmetic to generate a number from 0 - 2 for each room.

The CTE then enumerates both the rooms and the beds.

The outer query aggregates.

Originally, I thought you wanted a random pairing, so I'll include that logic as well:

select min(case when b.bedno = 1 then b.id end) as bed_1,
       min(case when b.bedno = 1 then r.room end) as room_1,
       min(case when b.bedno = 2 then b.id end) as bed_2,
       min(case when b.bedno = 2 then r.room end) as room_2
from (select b.*,
             row_number() over (partition by bedno order by rand()) as pair
      from (select b.*,
                   row_number() over (partition by roomid order by rand()) as bedno
            from beds b
           ) b
     ) b join
     rooms r
     on b.roomid = r.id
group by pair;

Here is a db<>fiddle.

Related