kkung
kkung

Reputation: 743

SQL: how to limit a join on the first found row?

How to make a join between two tables but limiting to the first row that meets the join condition ?

In this simple example, I would like to get for every row in table_A the first row from table_B that satisfies the condition :

select table_A.id, table_A.name, table_B.city 
from table_A join table_B 
on table_A.id = table_B.id2
where ..

table_A (id, name)
1, John
2, Marc

table_B (id2, city)
1, New York
1, Toronto
2, Boston

The output would be:
1, John, New York
2, Marc, Boston

May be Oracle provides such a function (performance is a concern).

Upvotes: 31

Views: 54558

Answers (7)

PaCrow Delgado
PaCrow Delgado

Reputation: 11

I use the partition to separate the id2 and then just take the r_num = 1.

SELECT A.ID, A.NAME, B.CITY
  FROM TABLE_A A,
      (SELECT ID2, CITY,
              ROW_NUMBER() OVER (PARTITION BY ID2 ORDER BY ID2) AS R_NUM
        FROM TABLE_B) B
  WHERE A.ID = B.ID2
  AND R_NUM = 1;

Upvotes: 1

Martien de Jong
Martien de Jong

Reputation: 781

This solution uses the whole table, like in a regular join, but limits to the first row. I am posting this because for me the other solutions were not sufficient because they use one field only, or they have performance issues with large tables. I am no expert at Oracle so if someone can improve this please do so, I will be happy to use your version.

select *
from tableA A
cross apply (
    select *
    from (
        select B.*, 
            ROW_NUMBER() OVER (
                -- replace this by your own partition/order statement
                partition by B.ITEM_ID order by B.DELIVERYDATE desc
            ) as ROW_NUM
        from tableB B
        where 
        A.ITEM_ID=B.ITEM_ID
    )
    where ROW_NUM=1
) B

Upvotes: 1

0xdb
0xdb

Reputation: 3697

The key word here is FIRST. You can use analytic function FIRST_VALUE or aggregate construct FIRST.
For FIRST or LAST the performance is never worse and frequently better than the equivalent FIRST_VALUE or LAST_VALUE construct because we don't have a superfluous window sort and as a consequence a lower execution cost:

select table_A.id, table_A.name, firstFromB.city 
from table_A 
join (
    select table_B.id2, max(table_B.city) keep (dense_rank first order by table_B.city) city
    from table_b
    group by table_B.id2
    ) firstFromB on firstFromB.id2 = table_A.id 
where 1=1 /* some conditions here */
;

Since 12c introduced operator LATERAL, as well as CROSS/OUTER APPLY joins, make it possible to use a correlated subquery on right side of JOIN clause:

select table_A.id, table_A.name, firstFromB.city 
from table_A 
cross apply (
    select max(table_B.city) keep (dense_rank first order by table_B.city) city
    from table_b
    where table_B.id2 = table_A.id 
    ) firstFromB
where 1=1 /* some conditions here */
;

Upvotes: 23

Alessandro Rossi
Alessandro Rossi

Reputation: 2450

On Oracle12c there finally is the new cross/outer apply operator that will allow what you asked for without any workaround.

the following is an example that looks on dictionary views for just one of the (probably)many objects owned by those users having their name starting with 'SYS':

select *
from (
        select USERNAME
        from ALL_USERS
        where USERNAME like 'SYS%'
    ) U
    cross apply (
        select OBJECT_NAME
        from ALL_OBJECTS O
        where O.OWNER = U.USERNAME
            and ROWNUM = 1
    )

On Oracle 11g and prior versions you should only use workarounds that generally full scan the second table based on IDs of the second table to get the same results, but for testing puposes you may enable the lateral operator (also available on 12c without need of enabling new stuff) and use this other one

-- Enables some new features
alter session set events '22829 trace name context forever';

select *
from (
        select USERNAME
        from ALL_USERS
        where USERNAME like 'SYS%'
    ) U,
    lateral (
        select OBJECT_NAME
        from ALL_OBJECTS O
        where O.OWNER = U.USERNAME
            and ROWNUM = 1
    );

Upvotes: 2

MT0
MT0

Reputation: 168026

Query:

SELECT a.id,
       a.name,
       b.city
FROM   table_A a
       INNER JOIN
       ( SELECT id2,
                city
         FROM   (
           SELECT id2,
                  city,
                  ROW_NUMBER() OVER ( PARTITION BY id2 ORDER BY NULL ) rn
           FROM   Table_B
         )
         WHERE rn = 1
       ) b
       ON ( a.id = b.id2 )
--WHERE  ...

Outputs:

        ID NAME CITY   
---------- ---- --------
         1 John New York 
         2 Marc Boston   

Upvotes: 4

Husqvik
Husqvik

Reputation: 5809

If you want just single value a scalar subquery can be used:

SELECT
    id, name, (SELECT city FROM table_B WHERE id2 = table_A.id AND ROWNUM = 1) city
FROM
    table_A

Upvotes: 15

Mihai
Mihai

Reputation: 26784

select table_A.id, table_A.name,
FIRST_VALUE(table_B.city) IGNORE NULLS 
         OVER (PARTITION BY table_B.id2 ORDER BY table_B.city) AS "city"
from table_A join table_B 
on table_A.id = table_B.id2
where ..

Upvotes: 3

Related Questions