Як приєднатися до першого ряду


773

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

Кожне замовлення зазвичай має лише одну позицію :

Замовлення:

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, ...

Тож питання в тому, як це зробити

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

Перша спроба

Моя перша наївна спроба полягала лише в тому, щоб приєднатися до позицій рядка " ТОП 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зажадає перерахувати всі інші стовпці, виключаючи той, де ви не хочете копій. Джерело
Джошуа Нельсон

Відповіді:


1212
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
Відмінно, що працює; переміщення ТОП 1 з пункту похідної таблиці для приєднання.
Ян Бойд

107
а еквівалент "OUTER JOIN" був би "OUTER APPLY"
Alex

9
Як щодо ЛІВОГО ВНУТРІШНОГО ПРИЄДНАННЯ?
Олексій Ноласко

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» не індексується належним чином. Порівняно з прийнятою відповіддю.
ГЕР

3
Але як би ви це зробили, якщо Макс не є корисним, оскільки вам потрібно замовити стовпчик, відмінний від того, який ви хочете повернути?
NickG

2
Ви можете замовити отриману таблицю в будь-який спосіб і використовувати TOP 1 на SQL Server або LIMIT 1 в MySQL
stifin

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
Це не працює, якщо для замовлень немає LineItems. Потім суб-вираз оцінює LineItems.LineItemID = nullта видаляє замовлення лівої сутності повністю з результату.
лео

6
Це теж ефект внутрішнього приєднання, так що ... так.
Томалак

1
Рішення, яке можна адаптувати для ВІДНІШОГО ПРИЄДНАННЯ: 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.
Джефф Грісвальд

Це хороше рішення. Зверніть увагу: використовуючи для власної ситуації, будьте дуже обережні, як ви розділяєте розділ (зазвичай ви хочете, щоб там був стовпець ідентифікатора) та ЗАМОВИТИ (що може бути зроблено більшістю всього, залежно від того, який рядок ви хочете зберегти, наприклад DateCreate 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

EDIT: ніколи не має значення, у 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
Але наявність максимуму в двох стовпцях означає, що кількість може бути не пов'язана з описом. Якщо замовлення складало 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
Будь ласка, подумайте, як пояснити, що робить ваш запит для вирішення проблеми ОП
Simas Joneliunas,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.