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


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.