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

В каждом заказе обычно есть только одна позиция :

Заказы:

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

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

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

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

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

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

899
Ian Boyd 11 Янв 2010 в 19:44
4
Разве вы не можете использовать group by?
 – 
Dariush Jafari
11 Апр 2017 в 10:54
3
Я думаю (поправьте меня, если я ошибаюсь) group by потребует перечисления всех других столбцов, за исключением того, где вы не хотите дублировать. Источник
 – 
Joshua Nelson
1 Июн 2018 в 15:25

12 ответов

Лучший ответ
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 к самому внутреннему запросу.

Пример sqlfiddle

1421
Alexander Schmidt 20 Ноя 2020 в 20:11
3
Отлично, это работает; перемещение TOP 1 из предложения производной таблицы в предложение соединения.
 – 
Ian Boyd
11 Янв 2010 в 19:54
126
И эквивалент "OUTER JOIN" будет "OUTER APPLY"
 – 
Alex from Jitbit
22 Дек 2011 в 14:41
9
Как насчет LEFT OUTER JOIN?
 – 
Alex Nolasco
1 Фев 2012 в 03:11
8
Как это сделать, если соединение выполняется с помощью составного ключа / имеет несколько столбцов?
 – 
Brett Ryan
12 Сен 2012 в 11:20
11
CROSS APPLY вместо INNER JOIN и OUTER APPLY вместо LEFT JOIN (то же, что и LEFT OUTER JOIN).
 – 
hastrb
15 Мар 2018 в 15:04

@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

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

49
Community 13 Апр 2017 в 15:42
4
Это единственный найденный мной ответ, который выполняет настоящее «левое» соединение, то есть не добавляет больше строк, чем находится в «левой» таблице. Вам просто нужно ввести подзапрос и добавить «где RowNum не равно нулю»
 – 
user890332
10 Май 2019 в 20:55
2
Согласен, это лучшее решение. Это решение также не требует наличия уникального идентификатора в таблице, к которой вы присоединяетесь, и работает намного быстрее, чем ответ, получивший наибольшее количество голосов. Вы также можете добавить критерии, для какой строки вы предпочитаете возвращать, а не просто брать случайную строку, используя предложение ORDER BY в подзапросе.
 – 
Geoff Griswald
3 Окт 2019 в 17:06
Это хорошее решение. Обратите внимание: при использовании для вашей собственной ситуации будьте очень осторожны при использовании PARTION BY (обычно вам, вероятно, нужен какой-то столбец идентификатора) и ORDER BY (что может быть сделано практически любым, в зависимости от того, какую строку вы хотите сохранить, например DateCreated desc будет одним из вариантов для некоторых таблиц, но это будет зависеть от многих вещей)
 – 
JosephDoggie
23 Мар 2020 в 23:30
Что означает (SELECT NULL) в предложении OVER?
 – 
Simone
8 Сен 2021 в 12:30

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

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, иначе это будет медленным.

30
Tomalak 11 Янв 2010 в 19:50
2
Это не работает, если в Order нет LineItems. Затем подвыражение оценивает LineItems.LineItemID = null и полностью удаляет левые порядки сущностей из результата.
 – 
leo
2 Июл 2015 в 12:53
8
Это также эффект внутреннего соединения, так что ... да.
 – 
Tomalak
2 Июл 2015 в 12:58
1
Решение, которое можно адаптировать для ЛЕВОГО ВНЕШНЕГО СОЕДИНЕНИЯ: stackoverflow.com/a/20576200/510583
 – 
leo
2 Июл 2015 в 13:38
4
Да, но ОП сам использовал внутреннее соединение, поэтому я не понимаю вашего возражения.
 – 
Tomalak
2 Июл 2015 в 13:56

Начиная с 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
24
P. Olesen 28 Ноя 2018 в 14:55
4
Лучший ответ, если вы спросите меня.
 – 
thomas
29 Ноя 2019 в 02:45
Я думаю это лучший ответ
 – 
Hoang Tran
23 Фев 2021 в 10:51
Разве «INNER JOIN LineItems» не приводит к возврату нескольких строк, когда в заказе более одной позиции?
 – 
David Pierson
27 Июл 2022 в 10:15
1
DISTINCT должен позаботиться об этом
 – 
P. Olesen
16 Сен 2022 в 16:32

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

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
16
avb 10 Май 2017 в 13:18

Коррелированные подзапросы - это подзапросы, которые зависят от внешнего запроса. Это похоже на цикл 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
)
11
Racil Hilan 17 Сен 2016 в 13:38

Мой любимый способ выполнить этот запрос - использовать предложение not exists. Я считаю, что это наиболее эффективный способ выполнить такой запрос:

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
    )

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

5
Anand 9 Май 2017 в 21:12

РЕДАКТИРОВАТЬ: неважно, у 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
Peter Radocchia 11 Янв 2010 в 20:23

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

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

SELECT Orders.OrderNumber, max(LineItems.Quantity), max(LineItems.Description)
FROM Orders
    INNER JOIN LineItems 
    ON Orders.OrderID = LineItems.OrderID
Group by Orders.OrderNumber
3
Krease 15 Фев 2013 в 02:07
13
Но указание максимума отдельно для двух столбцов означает, что количество не может быть связано с описанием. Если заказ был 2 виджета и 10 гаджетов, запрос вернет 10 виджетов.
 – 
Brianorca
5 Авг 2015 в 02:52

CROSS APPLY на помощь:

SELECT Orders.OrderNumber, topline.Quantity, topline.Description
FROM Orders
cross apply
(
    select top 1 Description,Quantity
    from LineItems 
    where Orders.OrderID = LineItems.OrderID
)topline

Вы также можете добавить order by по своему выбору.

1
George Menoutis 12 Янв 2021 в 11:46
Я думаю, что этот ответ является дубликатом принятого ответа.
 – 
Justin Fisher
23 Янв 2021 в 00:41
join и apply не одно и то же
 – 
George Menoutis
23 Янв 2021 в 14:19

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

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
139
Justin Fisher 7 Апр 2012 в 02:34
3
Это также намного быстрее, если ваш столбец LineItemId не проиндексирован должным образом. По сравнению с принятым ответом.
 – 
GER
20 Янв 2015 в 19:43
5
Но как бы вы это сделали, если Max нельзя использовать, поскольку вам нужно упорядочить по столбцу, отличному от того, который вы хотите вернуть?
 – 
NickG
24 Апр 2015 в 19:04
2
Вы можете заказать производную таблицу любым способом и использовать TOP 1 в SQL Server или LIMIT 1 в MySQL
 – 
stifin
9 Июн 2015 в 13:39
1
Обнаружено, что это намного быстрее на больших наборах данных
 – 
DotNetDublin
11 Янв 2021 в 19:27
1
Не могли бы вы уточнить? Что касается только синтаксиса, ваш ответ такой же вложенный, как и ответ Quassnoi: ровно один подзапрос. Вы не можете просто подразумевать, что один будет запускаться «для каждой возвращенной строки», а другой - не только потому, что синтаксис кажется таким. Вы должны включить план.
 – 
George Menoutis
21 Янв 2021 в 21:15

Попробуй это

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
1
Ian Boyd 20 Фев 2020 в 05:02
4
Пожалуйста, подумайте о том, чтобы объяснить, что делает ваш запрос для решения проблемы OP
 – 
Simas Joneliunas
19 Фев 2020 в 09:01
Это похоже на ответ Джастина Фишера.
 – 
Trisha
3 Дек 2021 в 04:58