Как присоединиться к первому ряду


773

Я буду использовать конкретный, но гипотетический пример.

Каждый заказ обычно имеет только одну позицию :

Заказы:

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

Позиции:

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

Но иногда будет заказ с двумя позициями:

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

Обычно при отображении заказов пользователю:

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

Я хочу показать один элемент в заказе. Но с этим время от времени заказа , содержащие два (или более) элементов, заказы будут появляться быть продублированы :

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

Я действительно хочу, чтобы SQL Server просто выбрал один , так как он будет достаточно хорош :

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

Если я предприму приключение, я мог бы показать пользователю многоточие, чтобы указать, что их больше одного:

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

Так что вопрос в том, как

  • устранить "дублирующиеся" строки
  • только присоединиться к одной из строк, чтобы избежать дублирования

Первая попытка

Моей первой наивной попыткой было присоединиться только к позициям " 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

Но это дает ошибку:

Столбец или префикс «Заказы» не
совпадает с именем таблицы или псевдонимом,
используемым в запросе.

Предположительно, потому что внутренний выбор не видит внешнюю таблицу.


3
Вы не можете использовать group by?
— Дариуш Джафари

2
Я думаю (и поправьте меня, если я ошибаюсь) group by, потребуется перечислить все остальные столбцы, за исключением того, где вы не хотите дубликатов. Источник
— Джошуа Нельсон

Ответы:


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
         )

В SQL Server 2005 и выше вы можете просто заменить INNER JOINна 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

Обратите внимание, что TOP 1без ORDER BYне является детерминированным: в этом запросе вы получите одну позицию на заказ, но не определено, какой она будет.

Многократные вызовы запроса могут дать вам разные позиции для одного и того же заказа, даже если базовый уровень не изменился.

Если вы хотите детерминированный порядок, вы должны добавить ORDER BYпредложение к самому внутреннему запросу.


3
Отлично, это работает; перемещение TOP 1 из предложения производной таблицы в предложение join.
— Ян Бойд

107
и эквивалент «ВНЕШНЕЕ СОЕДИНЕНИЕ» будет «ВНЕШНЕЕ ПРИМЕНЕНИЕ»
— Алекс

9
Как насчет LEFT OUTER JOIN?
— Алекс Ноласко

8
Как это сделать, если соединение осуществляется через составной ключ / имеет несколько столбцов?
— Бретт Райан

7
CROSS APPLYвместо INNER JOINи OUTER APPLYвместо LEFT JOIN(так же, как LEFT OUTER JOIN).
— hastrb

117

Я знаю, что на этот вопрос недавно был дан ответ, но при работе с большими наборами данных вложенные запросы могут быть дорогостоящими. Вот другое решение, где вложенный запрос будет выполняться только один раз, а не для каждой возвращаемой строки.

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

2
Это также намного быстрее, если ваш столбец LineItemId не проиндексирован должным образом. По сравнению с принятым ответом.
— GER

3
Но как бы вы это сделали, если Макс не используется, поскольку вам нужно упорядочить столбец, отличный от того, который вы хотите вернуть?
— NickG

2
вы можете заказать производную таблицу любым
— удобным

28

Вы могли бы сделать:

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
  )

Это требует наличия индекса (или первичного ключа) и включения LineItems.LineItemIDиндекса, LineItems.OrderIDили он будет медленным.


2
Это не работает, если у Orders нет LineItems. Затем подвыражение полностью вычисляет LineItems.LineItemID = nullи удаляет левые порядки объектов из результата.
— Лев

6
Это также эффект внутреннего соединения, так что ... да.
— Томалак

1
Решение, которое может быть адаптировано для LEFT OUTER JOIN: stackoverflow.com/a/20576200/510583
— Лев

3
@leo Да, но ОП сам использовал внутреннее соединение, поэтому я не понимаю вашего возражения.
— Томалак

27

Ответ @Quassnoi хорош, в некоторых случаях (особенно если внешняя таблица большая), более эффективный запрос может быть с использованием оконных функций, например:

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

Иногда вам просто нужно проверить, какой запрос дает лучшую производительность.


3
Это единственный ответ, который я нашел, который выполняет настоящее «левое» соединение, то есть оно не добавляет больше строк, чем в «левой» таблице. Вам просто нужно поместить в подзапрос и добавить «где RowNum не нуль»
— user890332

1
Договорились, что это лучшее решение. Это решение также не требует наличия уникального идентификатора в таблице, к которой вы присоединяетесь, и намного быстрее, чем ответ с наибольшим количеством голосов. Вы также можете добавить критерии, для которых вы предпочитаете возвращать строку, а не просто брать случайную строку, используя предложение ORDER BY в подзапросе.
— Джефф Грисвальд

Это хорошее решение. Пожалуйста, обратите внимание: при использовании для вашей собственной ситуации, будьте очень осторожны с тем, как вы используете PARTION BY (обычно вы, вероятно, хотите, чтобы там был какой-то столбец идентификатора) и ORDER BY (что может быть сделано почти всем, в зависимости от того, какую строку вы хотите сохранить, например, DateCreated desc будет одним из вариантов для некоторых таблиц, но это будет зависеть от многих вещей)
— JosephDoggie

14

Другой подход, использующий общее табличное выражение:

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

или, в конце концов, может быть, вы хотите показать все соединенные строки?

Разделенная запятыми версия здесь:

  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

13

Начиная с SQL Server 2012 и далее, я думаю, что это поможет:

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

2
Лучший ответ, если вы спросите меня.
— Томас

11

Коррелированные подзапросы - это подзапросы, которые зависят от внешнего запроса. Это как цикл for в SQL. Подзапрос будет выполняться один раз для каждой строки во внешнем запросе:

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
)

5

РЕДАКТИРОВАТЬ: не имеет значения, у Quassnoi есть лучший ответ.

Для SQL2K, что-то вроде этого:

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

4

Мой любимый способ выполнения этого запроса - с предложением «не существует». Я считаю, что это самый эффективный способ выполнить такой запрос:

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
    )

Но я не проверял этот метод против других методов, предложенных здесь.


2

Попробовал крестик, работает приятно, но занимает немного дольше. Скорректированы столбцы строк, чтобы иметь максимум и добавлена ​​группа, которая сохраняла скорость и отбрасывала дополнительную запись.

Вот скорректированный запрос:

SELECT Orders.OrderNumber, max(LineItems.Quantity), max(LineItems.Description)
FROM Orders
    INNER JOIN LineItems 
    ON Orders.OrderID = LineItems.OrderID
Group by Orders.OrderNumber

10
Но наличие max отдельно в двух столбцах означает, что количество может не относиться к описанию. Если бы порядок был 2 виджетов и 10 гаджетов, запрос вернул бы 10 виджетов.
— Брианорка

1

попробуй это

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

2
Пожалуйста, подумайте над тем, чтобы объяснить, что делает ваш запрос для решения проблемы ОП
— Симас Джонелиунас
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.