Search code examples
mysqlsqlinner-join

SQL Joining on first match only


I am trying to write an SQL statement that uses an INNER JOIN to match bids and offers to each other.

I have a bids table which looks like the following

+-------------------------+-------------------+--------------------------------+--------------------------------+
| bid_id                  | user_id           | bid_volume                     | bid_price                      |
+-------------------------+-------------------+--------------------------------+--------------------------------+
|                     101 |                 1 |                       100.0000 |                       300.0000 |
|                     102 |                 2 |                       200.0000 |                        20.0000 |
|                     103 |                 3 |                        50.0000 |                        40.0000 |
|                     104 |                 4 |                        60.0000 |                       100.0000 |
|                     105 |                 5 |                        75.0000 |                        90.0000 |
+-------------------------+-------------------+--------------------------------+--------------------------------+

I have an offers table which looks like the following

+---------------------------+-------------------+----------------------------------+----------------------------------+
| offer_id                  | user_id           | offer_volume                     | offer_price                      |
+---------------------------+-------------------+----------------------------------+----------------------------------+
|                        91 |                 6 |                          60.0000 |                         100.0000 |
|                        92 |                 7 |                          75.0000 |                          85.0000 |
|                        93 |                 8 |                         100.0000 |                         200.0000 |
|                        94 |                 9 |                          75.0000 |                        1000.0000 |
|                        95 |                10 |                         100.0000 |                         300.0000 |
+---------------------------+-------------------+----------------------------------+----------------------------------+

I am trying to match bids to offers, there should be a one to one relationship i.e only one bid can match with one offer. The highest bid should also be the winner in the case if two or more parties are bidding.

A bid should be a winning bid if the bid_volume matches the offer_volume, the bid_price >= offer_price AND it is the highest bid_price of all the bids.

I am ordering the bids by bid_price high to low so the highest bid is processed first. The problem occurs when one bid matches multiple offers.

Taking the data above bid 101 will satisfy offer 93 and 95. It should really only match with offer 93 as it is the lower price.

The start of my SQL is below but it is limiting the matches I am really stuck on so there is a one to one relationship of bids to offers.

SELECT  
    Bids.user_id AS bidder_user_id,
    Offers.user_id AS dealer_user_id,
    Bids.bid_volume,
    Offers.offer_volume,
    Bids.bid_price, 
    Offers.offer_price
FROM  
    Bids
INNER JOIN 
    Offers
ON
    /* Volume - Volume must be equal*/
    Bids.bid_volume =
    Offers.offer_volume   AND
    /* Price - Bid price greater than or equal to offer price*/ 
    Bids.bid_price  >=
    Offers.offer_price
ORDER BY 
    bid_price DESC;

The output of the above is shown below (Notice there are two matches for bidder_user_id = 1 trying to get only one)

+----------------+----------------+------------+--------------+-----------+-------------+
| bidder_user_id | dealer_user_id | bid_volume | offer_volume | bid_price | offer_price |
+----------------+----------------+------------+--------------+-----------+-------------+
|              1 |              8 |   100.0000 |     100.0000 |  300.0000 |    200.0000 |
|              1 |             10 |   100.0000 |     100.0000 |  300.0000 |    300.0000 |
|              4 |              6 |    60.0000 |      60.0000 |  100.0000 |    100.0000 |
|              5 |              7 |    75.0000 |      75.0000 |   90.0000 |     85.0000 |
+----------------+----------------+------------+--------------+-----------+-------------+

The desired output would be as shown below, so user 1 bid is not matched with two offers but I am not sure how to limit the inner join to only match on first occurrence only.

+----------------+----------------+------------+--------------+-----------+-------------+
| bidder_user_id | dealer_user_id | bid_volume | offer_volume | bid_price | offer_price |
+----------------+----------------+------------+--------------+-----------+-------------+
|              1 |              8 |   100.0000 |     100.0000 |  300.0000 |    200.0000 |
|              4 |              6 |    60.0000 |      60.0000 |  100.0000 |    100.0000 |
|              5 |              7 |    75.0000 |      75.0000 |   90.0000 |     85.0000 |
+----------------+----------------+------------+--------------+-----------+-------------+

Solution

  • If your version of MySql is 8.0+ you can use ROW_NUMBER() window function in the table Offers to get the row with the lowest offer_price for each offer_volume and then join to Bids:

    SELECT  
        b.user_id AS bidder_user_id,
        o.user_id AS dealer_user_id,
        b.bid_volume,
        o.offer_volume,
        b.bid_price, 
        o.offer_price
    FROM  Bids b 
    INNER JOIN (
      SELECT *, ROW_NUMBER() OVER (PARTITION BY offer_volume ORDER BY offer_price) rn 
      FROM Offers 
    ) o
    ON b.bid_volume = o.offer_volume AND b.bid_price >= o.offer_price
    WHERE o.rn = 1
    ORDER BY b.bid_price DESC;
    

    For previous versions, you could use NOT EXISTS:

    SELECT  
        b.user_id AS bidder_user_id,
        o.user_id AS dealer_user_id,
        b.bid_volume,
        o.offer_volume,
        b.bid_price, 
        o.offer_price
    FROM  Bids b 
    INNER JOIN (
      SELECT o1.* FROM Offers o1
      WHERE NOT EXISTS (
        SELECT 1 FROM Offers o2 
        WHERE o2.offer_volume = o1.offer_volume AND o2.offer_price < o1.offer_price
      )
    ) o
    ON b.bid_volume = o.offer_volume AND b.bid_price >= o.offer_price
    ORDER BY b.bid_price DESC;
    

    See the demo.