web-dev-qa-db-fra.com

SQL: comment limiter une jointure sur la première ligne trouvée?

Comment faire une jointure entre deux tables mais en se limitant à la première ligne qui remplit la condition de jointure?

Dans cet exemple simple, je voudrais obtenir pour chaque ligne de table_A la première ligne de table_B qui satisfait la 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

Peut-être qu'Oracle fournit une telle fonction (les performances sont une préoccupation).

16
kkung

Le mot clé ici est PREMIER . Vous pouvez utiliser la fonction analytique FIRST_VALUE ou construction agrégée FIRST.
Pour FIRST ou LAST les performances ne sont jamais moins bonnes et souvent meilleures que l'équivalent FIRST_VALUE ou LAST_VALUE construct car nous n'avons pas de tri de fenêtre superflu et par conséquent un coût d'exécution moindre:

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 */
;

Depuis que 12c a introduit l'opérateur LATERAL, ainsi que CROSS/OUTER APPLY jointures, permet d'utiliser une sous-requête corrélée à droite de la clause JOIN:

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 */
;
12
0xdb

Si vous ne voulez qu'une seule valeur, une sous-requête scalaire peut être utilisée:

SELECT
    id, name, (SELECT city FROM table_B WHERE id2 = table_A.id AND ROWNUM = 1) city
FROM
    table_A
9
Husqvik
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 ..
4
Mihai

Sur Oracle12c, il y a enfin le nouveau opérateur d'application croisée/externe qui permettra ce que vous avez demandé sans aucune solution.

ce qui suit est un exemple qui examine les vues de dictionnaire pour un seul des (probablement) nombreux objets appartenant aux utilisateurs dont le nom commence par "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
    )

Sur Oracle 11g et les versions antérieures, vous ne devez utiliser que des solutions de contournement qui analysent généralement complètement la deuxième table en fonction des ID de la deuxième table pour obtenir les mêmes résultats, mais pour les tests, vous pouvez activer opérateur latéral (également disponible sur 12c sans avoir besoin d'activer de nouvelles choses) et utilisez celui-ci

-- 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
    );
2
Alessandro Rossi

Requête:

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  ...

Sorties:

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

Cette solution utilise la table entière, comme dans une jointure régulière, mais limite la première ligne. Je poste ceci parce que pour moi, les autres solutions n'étaient pas suffisantes car elles n'utilisent qu'un seul champ ou elles ont des problèmes de performances avec de grandes tables. Je ne suis pas un expert d'Oracle, donc si quelqu'un peut améliorer cela, faites-le, je serai heureux d'utiliser votre 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
0
Martien de Jong