Comment rejoindre la première ligne

773

Je vais utiliser un exemple concret, mais hypothétique.

Chaque commande ne comporte normalement qu'un seul élément de campagne :

Ordres:

OrderGUID   OrderNumber
=========   ============
{FFB2...}   STL-7442-1      
{3EC6...}   MPT-9931-8A

LineItems:

LineItemGUID   Order ID Quantity   Description
============   ======== ========   =================================
{098FBE3...}   1        7          prefabulated amulite
{1609B09...}   2        32         spurving bearing

Mais il y aura parfois une commande avec deux éléments de campagne:

LineItemID   Order ID    Quantity   Description
==========   ========    ========   =================================
{A58A1...}   6,784,329   5          pentametric fan
{0E9BC...}   6,784,329   5          differential girdlespring 

Normalement, lors de la présentation des commandes à l'utilisateur:

SELECT Orders.OrderNumber, LineItems.Quantity, LineItems.Description
FROM Orders
    INNER JOIN LineItems 
    ON Orders.OrderID = LineItems.OrderID

Je veux montrer l'article unique sur la commande. Mais avec cette commande occasionnelle contenant deux (ou plus) articles, les commandes semblent être dupliquées :

OrderNumber   Quantity   Description
===========   ========   ====================
STL-7442-1    7          prefabulated amulite
MPT-9931-8A   32         spurving bearing
KSG-0619-81   5          panametric fan
KSG-0619-81   5          differential girdlespring

Ce que je veux vraiment, c'est que SQL Server n'en choisisse qu'un , car ce sera suffisant :

OrderNumber   Quantity   Description
===========   ========   ====================
STL-7442-1    7          prefabulated amulite
MPT-9931-8A   32         differential girdlespring
KSG-0619-81   5          panametric fan

Si je deviens aventureux, je pourrais montrer à l'utilisateur des points de suspension pour indiquer qu'il y en a plus d'un:

OrderNumber   Quantity   Description
===========   ========   ====================
STL-7442-1    7          prefabulated amulite
MPT-9931-8A   32         differential girdlespring
KSG-0619-81   5          panametric fan, ...

La question est donc de savoir comment

  • éliminer les lignes "en double"
  • joindre uniquement à l'une des lignes, pour éviter la duplication

Premier essai

Ma première tentative naïve a été de ne rejoindre que les éléments de campagne " TOP 1 ":

SELECT Orders.OrderNumber, LineItems.Quantity, LineItems.Description
FROM Orders
    INNER JOIN (
       SELECT TOP 1 LineItems.Quantity, LineItems.Description
       FROM LineItems
       WHERE LineItems.OrderID = Orders.OrderID) LineItems2
    ON 1=1

Mais cela donne l'erreur:

La colonne ou le préfixe «Commandes» ne
correspond pas au nom de table ou au nom d'alias
utilisé dans la requête.

Vraisemblablement parce que la sélection interne ne voit pas la table externe.

Ian Boyd
la source
3
Tu ne peux pas utiliser group by?
Dariush Jafari
2
Je pense (et corrigez-moi si je me trompe) group bynécessiterait de répertorier toutes les autres colonnes, à l'exception de celle où vous ne voulez pas de doublons. Source
Joshua Nelson

Réponses:

1213
SELECT   Orders.OrderNumber, LineItems.Quantity, LineItems.Description
FROM     Orders
JOIN     LineItems
ON       LineItems.LineItemGUID =
         (
         SELECT  TOP 1 LineItemGUID 
         FROM    LineItems
         WHERE   OrderID = Orders.OrderID
         )

Dans SQL Server 2005 et supérieur, vous pouvez simplement remplacer INNER JOINpar CROSS APPLY:

SELECT  Orders.OrderNumber, LineItems2.Quantity, LineItems2.Description
FROM    Orders
CROSS APPLY
        (
        SELECT  TOP 1 LineItems.Quantity, LineItems.Description
        FROM    LineItems
        WHERE   LineItems.OrderID = Orders.OrderID
        ) LineItems2

Veuillez noter que TOP 1sans ORDER BYn'est pas déterministe: cette requête vous donnera un élément de campagne par commande, mais il ne sera pas défini lequel.

Plusieurs appels de la requête peuvent vous donner différents éléments de campagne pour le même ordre, même si le sous-jacent n'a pas changé.

Si vous voulez un ordre déterministe, vous devez ajouter une ORDER BYclause à la requête la plus interne.

Quassnoi
la source
3
Excellent, ça marche; déplacer TOP 1 de la clause de table dérivée vers la clause join.
Ian Boyd
107
et l'équivalent "OUTER JOIN" serait "OUTER APPLY"
Alex
9
Et pour LEFT OUTER JOIN?
Alex Nolasco
8
Comment faire si la jointure se fait via une clé composée / a plusieurs colonnes?
Brett Ryan
7
CROSS APPLYà la place INNER JOINet à la OUTER APPLYplace LEFT JOIN(le même que LEFT OUTER JOIN).
hastrb
117

Je sais que cette question a reçu une réponse il y a un certain temps, mais lorsqu'il s'agit de grands ensembles de données, les requêtes imbriquées peuvent être coûteuses. Voici une solution différente où la requête imbriquée ne sera exécutée qu'une seule fois, au lieu de pour chaque ligne renvoyée.

SELECT 
  Orders.OrderNumber,
  LineItems.Quantity, 
  LineItems.Description
FROM 
  Orders
  INNER JOIN (
    SELECT
      Orders.OrderNumber,
      Max(LineItem.LineItemID) AS LineItemID
    FROM
      Orders INNER JOIN LineItems
      ON Orders.OrderNumber = LineItems.OrderNumber
    GROUP BY Orders.OrderNumber
  ) AS Items ON Orders.OrderNumber = Items.OrderNumber
  INNER JOIN LineItems 
  ON Items.LineItemID = LineItems.LineItemID
Justin Fisher
la source
2
Ceci est également beaucoup plus rapide si votre colonne 'LineItemId' n'est pas indexée correctement. Par rapport à la réponse acceptée.
GER
3
Mais comment feriez-vous si Max n'est pas utilisable car vous devez commander par une colonne différente de celle que vous souhaitez retourner?
NickG
2
vous pouvez commander la table dérivée comme vous le souhaitez et utiliser TOP 1 dans SQL Server ou LIMIT 1 dans MySQL
stifin
28

Vous pourriez faire:

SELECT 
  Orders.OrderNumber, 
  LineItems.Quantity, 
  LineItems.Description
FROM 
  Orders INNER JOIN LineItems 
  ON Orders.OrderID = LineItems.OrderID
WHERE
  LineItems.LineItemID = (
    SELECT MIN(LineItemID) 
    FROM   LineItems
    WHERE  OrderID = Orders.OrderID
  )

Cela nécessite un index (ou une clé primaire) sur LineItems.LineItemIDet un index sur LineItems.OrderIDou ce sera lent.

Tomalak
la source
2
Cela ne fonctionne pas si une commande n'a pas de LineItems. La sous-expression évalue ensuite LineItems.LineItemID = nullet supprime complètement les ordres d'entité de gauche du résultat.
leo
6
C'est aussi l'effet de la jointure intérieure, alors ... oui.
Tomalak
1
Solution adaptable pour LEFT OUTER JOIN: stackoverflow.com/a/20576200/510583
leo
3
@leo Oui, mais l'OP a utilisé une jointure interne, donc je ne comprends pas votre objection.
Tomalak
27

La réponse @Quassnoi est bonne, dans certains cas (surtout si la table externe est grande), une requête plus efficace pourrait être d'utiliser des fonctions fenêtrées, comme ceci:

SELECT  Orders.OrderNumber, LineItems2.Quantity, LineItems2.Description
FROM    Orders
LEFT JOIN 
        (
        SELECT  LineItems.Quantity, LineItems.Description, OrderId, ROW_NUMBER()
                OVER (PARTITION BY OrderId ORDER BY (SELECT NULL)) AS RowNum
        FROM    LineItems

        ) LineItems2 ON LineItems2.OrderId = Orders.OrderID And RowNum = 1

Parfois, il vous suffit de tester quelle requête donne de meilleures performances.

BornToCode
la source
3
C'est la seule réponse que j'ai trouvée qui fait une véritable jointure "gauche", ce qui signifie qu'elle n'ajoute plus de lignes, puis se trouve dans le tableau "gauche". Il vous suffit de mettre en sous-requête et d'ajouter "où RowNum n'est pas nul"
user890332
1
D'accord, c'est la meilleure solution. Cette solution ne vous oblige pas non plus à avoir un ID unique dans la table à laquelle vous vous joignez, et est beaucoup plus rapide que la réponse la plus votée. Vous pouvez également ajouter des critères pour la ligne que vous préférez renvoyer, plutôt que de simplement prendre une ligne aléatoire, en utilisant une clause ORDER BY dans la sous-requête.
Geoff Griswald
C'est une bonne solution. Veuillez noter: lorsque vous utilisez pour votre propre situation, faites très attention à la façon dont vous PARTIONNEZ (généralement, vous voulez probablement une colonne d'identification là-bas) et COMMANDEZ PAR (ce qui pourrait être fait par presque tout, selon la ligne que vous souhaitez conserver, par exemple La descente DateCreated serait un choix pour certaines tables, mais cela dépendrait de beaucoup de choses)
JosephDoggie
14

, Autre approche utilisant une expression de table commune:

with firstOnly as (
    select Orders.OrderNumber, LineItems.Quantity, LineItems.Description, ROW_NUMBER() over (partiton by Orders.OrderID order by Orders.OrderID) lp
    FROM Orders
        join LineItems on Orders.OrderID = LineItems.OrderID
) select *
  from firstOnly
  where lp = 1

ou, à la fin, vous aimeriez peut-être afficher toutes les lignes jointes?

version séparée par des virgules ici:

  select *
  from Orders o
    cross apply (
        select CAST((select l.Description + ','
        from LineItems l
        where l.OrderID = s.OrderID
        for xml path('')) as nvarchar(max)) l
    ) lines
avb
la source
13

À partir de SQL Server 2012 et versions ultérieures, je pense que cela fera l'affaire:

SELECT DISTINCT
    o.OrderNumber ,
    FIRST_VALUE(li.Quantity) OVER ( PARTITION BY o.OrderNumber ORDER BY li.Description ) AS Quantity ,
    FIRST_VALUE(li.Description) OVER ( PARTITION BY o.OrderNumber ORDER BY li.Description ) AS Description
FROM    Orders AS o
    INNER JOIN LineItems AS li ON o.OrderID = li.OrderID
P. Olesen
la source
2
Meilleure réponse si vous me demandez.
thomas
11

Les sous-requêtes corrélées sont des sous-requêtes qui dépendent de la requête externe. C'est comme une boucle for en SQL. La sous-requête s'exécutera une fois pour chaque ligne de la requête externe:

select * from users join widgets on widgets.id = (
    select id from widgets
    where widgets.user_id = users.id
    order by created_at desc
    limit 1
)
Abdullah Yousuf
la source
5

EDIT: peu importe, Quassnoi a une meilleure réponse.

Pour SQL2K, quelque chose comme ceci:

SELECT 
  Orders.OrderNumber
, LineItems.Quantity
, LineItems.Description
FROM (  
  SELECT 
    Orders.OrderID
  , Orders.OrderNumber
  , FirstLineItemID = (
      SELECT TOP 1 LineItemID
      FROM LineItems
      WHERE LineItems.OrderID = Orders.OrderID
      ORDER BY LineItemID -- or whatever else
      )
  FROM Orders
  ) Orders
JOIN LineItems 
  ON LineItems.OrderID = Orders.OrderID 
 AND LineItems.LineItemID = Orders.FirstLineItemID
Peter Radocchia
la source
4

Ma façon préférée d'exécuter cette requête est avec une clause n'existe pas. Je pense que c'est le moyen le plus efficace d'exécuter ce type de requête:

select o.OrderNumber,
       li.Quantity,
       li.Description
from Orders as o
inner join LineItems as li
on li.OrderID = o.OrderID
where not exists (
    select 1
    from LineItems as li_later
    where li_later.OrderID = o.OrderID
    and li_later.LineItemGUID > li.LineItemGUID
    )

Mais je n'ai pas testé cette méthode par rapport aux autres méthodes suggérées ici.

Anand
la source
2

J'ai essayé la croix, ça marche bien, mais ça prend un peu plus de temps. Colonnes de ligne ajustées pour avoir un groupe max et ajouté qui a gardé la vitesse et supprimé l'enregistrement supplémentaire.

Voici la requête ajustée:

SELECT Orders.OrderNumber, max(LineItems.Quantity), max(LineItems.Description)
FROM Orders
    INNER JOIN LineItems 
    ON Orders.OrderID = LineItems.OrderID
Group by Orders.OrderNumber
ernst
la source
10
Mais avoir max séparément sur deux colonnes signifie que la quantité peut ne pas être liée à la description. Si la commande était de 2 widgets et 10 gadgets, la requête retournerait 10 widgets.
Brianorca
1

essaye ça

SELECT
   Orders.OrderNumber,
   LineItems.Quantity, 
   LineItems.Description
FROM Orders
   INNER JOIN (
      SELECT
         Orders.OrderNumber,
         Max(LineItem.LineItemID) AS LineItemID
       FROM Orders 
          INNER JOIN LineItems
          ON Orders.OrderNumber = LineItems.OrderNumber
       GROUP BY Orders.OrderNumber
   ) AS Items ON Orders.OrderNumber = Items.OrderNumber
   INNER JOIN LineItems 
   ON Items.LineItemID = LineItems.LineItemID
Bane Neba
la source
2
Veuillez expliquer ce que fait votre requête pour résoudre le problème du PO
Simas Joneliunas