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

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

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

Заказы:

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

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

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

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


person Ian Boyd    schedule 11.01.2010    source источник
comment
Разве вы не можете использовать group by?   -  person Dariush Jafari    schedule 11.04.2017
comment
Я думаю (и поправьте меня, если я ошибаюсь) group by потребует перечислить все остальные столбцы, за исключением того, где вам не нужны дубликаты. Источник   -  person Joshua Nelson    schedule 01.06.2018


Ответы (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

person Quassnoi    schedule 11.01.2010
comment
Отлично, это работает; перемещение TOP 1 из предложения производной таблицы в предложение соединения. - person Ian Boyd; 11.01.2010
comment
и эквивалент OUTER JOIN будет OUTER APPLY - person Alex from Jitbit; 22.12.2011
comment
Как насчет LEFT OUTER JOIN? - person Alex Nolasco; 01.02.2012
comment
@AlexanderN: что именно ты хочешь знать о LEFT OUTER JOIN? - person Quassnoi; 01.02.2012
comment
Как это сделать, если соединение выполняется с помощью составного ключа / имеет несколько столбцов? - person Brett Ryan; 12.09.2012
comment
Благодаря этому я просто сократил время выполнения сложного запроса T-SQL на 1,3 секунды (с 3,7 до 2,4 секунды). :-D - person Sunday Ironfoot; 11.12.2012
comment
@Lex: INNER JOIN не генерирует NULL значение для строк, отсутствующих в объединенном запросе, LEFT JOIN делает. - person Quassnoi; 29.11.2013
comment
Есть ли способ сделать это без top 1 во внутреннем выборе? К сожалению, OpenEdge настолько современен, что вы получаете [DataDirect][OpenEdge JDBC Driver][OpenEdge] TOP clause used in unsupported context. (13694) - person Brett Ryan; 26.06.2014
comment
Есть ли преимущество в использовании Cross Apply? - person Jeff Davis; 17.09.2015
comment
@JeffDavis: это сэкономит вам один поиск на строку, если OrderId является ведущим столбцом в первичном ключе. - person Quassnoi; 17.09.2015
comment
@Quassnoi - Не могли бы вы объяснить свой последний комментарий о том, что перекрестное применение более эффективно, если идентификатор заказа является первичным ключом? Почему это так и почему этого не происходит с соединением? - person BornToCode; 06.12.2015
comment
@BornToCode: cross apply будет извлекать всю запись и сразу же возвращать ее. Join извлечет строку, возьмет из нее guid позиции, сам присоединится к таблице по guid (это дополнительный поиск) и вернет запись из присоединенной таблицы. - person Quassnoi; 06.12.2015
comment
К сожалению, не поддерживается в Sybase IQ - person Dirk Horsten; 07.04.2016
comment
Важно отметить, что второе решение - Cross Apply / Outer Apply будет работать с SQLCE, который не может использовать скалярные подзапросы в соединениях согласно верхнему запросу. - person statler; 16.01.2017
comment
CROSS APPLY вместо INNER JOIN и OUTER APPLY вместо LEFT JOIN (то же, что и LEFT OUTER JOIN). - person hastrb; 15.03.2018
comment
Имейте в виду, что TOP без ORDER BY недетерминирован. Вы не гарантируете получение той строки, на которую рассчитываете. - person Paul Wehland; 21.09.2018
comment
В качестве альтернативы используйте MIN () вместо TOP 1. Я обнаружил, что обычно меня интересует первый Id. - person 9Rune5; 06.12.2018
comment
CROSS APPLY (в большинстве случаев) займет много времени ... Вместо этого пользовательское OUTER APPLY. - person user274294; 24.07.2019
comment
???? КРЕСТНОЕ ПРИМЕНЕНИЕ. Спасибо!! - person Andrew; 07.07.2020
comment
Это не сработает, если PK также является FK, который ссылается на родительскую таблицу. т.е. если один и тот же столбец используется для "on" и с "where"! Например. : from Parent pt inner join Child cd on cd.ChildId = ( select top 1 ChildId from Child where ChildId = pt.ChildId ) - person Abdellah GRIB; 25.01.2021
comment
@AbdellahGrib: вопрос был о том, как избавиться от дубликатов. Первичный ключ не может иметь дубликатов. - person Quassnoi; 25.01.2021

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

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
person Justin Fisher    schedule 06.04.2012
comment
Это также намного быстрее, если ваш столбец LineItemId не проиндексирован должным образом. По сравнению с принятым ответом. - person GER; 20.01.2015
comment
Но как бы вы это сделали, если Max нельзя использовать, поскольку вам нужно упорядочить по столбцу, отличному от того, который вы хотите вернуть? - person NickG; 24.04.2015
comment
вы можете заказать производную таблицу любым способом и использовать TOP 1 в SQL Server или LIMIT 1 в MySQL - person stifin; 09.06.2015
comment
Обнаружено, что это намного быстрее на больших наборах данных - person DotNetDublin; 11.01.2021
comment
Не могли бы вы уточнить? Что касается только синтаксиса, ваш ответ такой же вложенный, как и ответ Quassnoi: ровно один подзапрос. Вы не можете просто подразумевать, что один будет запускаться для каждой возвращаемой строки, а другой - не только потому, что синтаксис кажется таким. Вы должны включить план. - person George Menoutis; 21.01.2021
comment
@GeorgeMenoutis Насколько я понимаю, такой запрос, как bar = (SELECT ...), похож на выполнение подзапроса для каждой строки (пример: stackoverflow.com/questions/2577174/join-vs-sub-query). В плане выполнения это заметно по количеству казней. Я предлагаю сравнить различные методы, чтобы определить, какой из них лучше всего работает в вашей ситуации. В моем случае метод, использованный в моем ответе, был значительно быстрее, чем методы подзапроса и перекрестного применения в принятом ответе при работе с большими наборами данных, и необходимо было упорядочить элементы LineItem (детерминированные). - person Justin Fisher; 23.01.2021

@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

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

person BornToCode    schedule 03.03.2016
comment
Это единственный найденный мной ответ, который выполняет настоящее левое соединение, что означает, что он не добавляет больше строк, чем находится в левой таблице. Вам просто нужно ввести подзапрос и добавить, где RowNum не равно нулю - person user890332; 10.05.2019
comment
Согласен, это лучшее решение. Это решение также не требует наличия уникального идентификатора в таблице, к которой вы присоединяетесь, и работает намного быстрее, чем ответ, получивший наибольшее количество голосов. Вы также можете добавить критерии, для какой строки вы предпочитаете возвращать, а не просто брать случайную строку, используя предложение ORDER BY в подзапросе. - person Geoff Griswald; 03.10.2019
comment
Это хорошее решение. Обратите внимание: при использовании для вашей собственной ситуации будьте очень осторожны при использовании PARTION BY (обычно вам, вероятно, нужен какой-то столбец идентификатора) и ORDER BY (что может быть сделано практически любым, в зависимости от того, какую строку вы хотите сохранить, например DateCreated desc будет одним из вариантов для некоторых таблиц, но это будет зависеть от многих вещей) - person JosephDoggie; 23.03.2020

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

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

person Tomalak    schedule 11.01.2010
comment
Это не работает, если в Order нет LineItems. Затем подвыражение оценивает LineItems.LineItemID = null и полностью удаляет левые порядки сущностей из результата. - person leo; 02.07.2015
comment
Это также эффект внутреннего соединения, так что ... да. - person Tomalak; 02.07.2015
comment
Решение, которое можно адаптировать для ЛЕВОГО ВНЕШНЕГО СОЕДИНЕНИЯ: stackoverflow.com/a/20576200/510583 - person leo; 02.07.2015
comment
@leo Да, но OP сам использовал внутреннее соединение, поэтому я не понимаю вашего возражения. - person Tomalak; 02.07.2015

Начиная с 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
person P. Olesen    schedule 28.11.2018
comment
Лучший ответ, если вы спросите меня. - person thomas; 29.11.2019
comment
Я думаю это лучший ответ - person Hoang Tran; 23.02.2021

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

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
person avb    schedule 10.05.2017

Коррелированные подзапросы - это подзапросы, которые зависят от внешнего запроса. Это похоже на цикл 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
)
person Abdullah Yousuf    schedule 17.09.2016

РЕДАКТИРОВАТЬ: неважно, у 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
person Peter Radocchia    schedule 11.01.2010

Мой любимый способ выполнить этот запрос - использовать предложение 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
    )

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

person Anand    schedule 09.05.2017

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

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

SELECT Orders.OrderNumber, max(LineItems.Quantity), max(LineItems.Description)
FROM Orders
    INNER JOIN LineItems 
    ON Orders.OrderID = LineItems.OrderID
Group by Orders.OrderNumber
person ernst    schedule 14.02.2013
comment
Но указание максимума отдельно для двух столбцов означает, что количество не может быть связано с описанием. Если заказ был 2 виджета и 10 гаджетов, запрос вернет 10 виджетов. - person Brianorca; 05.08.2015

попробуй это

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
person Bane Neba    schedule 19.02.2020
comment
Пожалуйста, подумайте о том, чтобы объяснить, что делает ваш запрос для решения проблемы OP - person Simas Joneliunas; 19.02.2020

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 по вашему выбору.

person George Menoutis    schedule 12.01.2021
comment
Я думаю, что этот ответ является дубликатом принятого ответа. - person Justin Fisher; 23.01.2021
comment
join и apply не одно и то же - person George Menoutis; 23.01.2021