Como ingressar na primeira linha


773

Vou usar um exemplo concreto, mas hipotético.

Cada pedido normalmente tem apenas um item de linha :

Encomendas:

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

Ocasionalmente, porém, haverá um pedido com dois itens de linha:

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

Normalmente, ao mostrar os pedidos ao usuário:

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

Quero mostrar o único item no pedido. Mas com esta ordem ocasional, contendo dois (ou mais) itens, as ordens que parecem ser duplicados :

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

O que eu realmente quero é que o SQL Server escolha apenas um , pois será bom o suficiente :

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

Se eu me aventurar, posso mostrar ao usuário uma elipse para indicar que há mais de um:

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

Então a questão é como

  • eliminar linhas "duplicadas"
  • unir-se apenas a uma das linhas, para evitar duplicação

Primeira tentativa

Minha primeira tentativa ingênua foi ingressar apenas nos itens de linha " 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

Mas isso dá o erro:

A coluna ou o prefixo 'Pedidos' não
corresponde ao nome da tabela ou ao nome alternativo
usado na consulta.

Presumivelmente, porque a seleção interna não vê a tabela externa.


3
Você não pode usar group by?
Dariush Jafari

2
Eu acho que (e me corrija se eu estiver errado) group byexigiria listar todas as outras colunas, excluindo a onde você não deseja duplicatas. Fonte
Joshua Nelson

Respostas:


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
         )

No SQL Server 2005 e acima, você pode simplesmente substituir INNER JOINpor 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

Observe que TOP 1sem ORDER BYnão é determinístico: nesta consulta, você receberá um item de linha por pedido, mas não está definido qual será.

Várias invocações da consulta podem fornecer itens de linha diferentes para o mesmo pedido, mesmo que o subjacente não tenha sido alterado.

Se você deseja ordem determinística, adicione uma ORDER BYcláusula à consulta mais interna.


3
Excelente, isso funciona; movendo o TOP 1 da cláusula de tabela derivada para a cláusula join.
Ian Boyd

107
e o equivalente "OUTER JOIN" seria "OUTTER APPLY"
Alex

9
Que tal para LEFT OUTER JOIN?
Alex Nolasco

8
Como você faz isso se a junção é por meio de uma chave composta / possui várias colunas?
Brett Ryan

7
CROSS APPLYao invés INNER JOINe OUTER APPLYem vez disso LEFT JOIN(a mesma que LEFT OUTER JOIN).
hastrb

117

Sei que essa pergunta foi respondida há um tempo, mas ao lidar com grandes conjuntos de dados, as consultas aninhadas podem ser caras. Aqui está uma solução diferente em que a consulta aninhada será executada apenas uma vez, em vez de para cada linha retornada.

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
Isso também é muito mais rápido se a coluna 'LineItemId' não estiver indexada corretamente. Comparado com a resposta aceita.
GER

3
Mas como você faria isso se Max não for utilizável, pois você precisa solicitar uma coluna diferente daquela que deseja devolver?
NickG

2
você pode solicitar a tabela derivada da maneira que desejar e usar o TOP 1 no SQL Server ou o LIMIT 1 no MySQL
stifin

28

Você poderia fazer:

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
  )

Isso requer um índice (ou chave primária) ativado LineItems.LineItemIDe um índice ativado LineItems.OrderIDou será lento.


2
Isso não funciona se um pedido não tiver itens de linha. A subexpressão avalia LineItems.LineItemID = nulle remove completamente os pedidos da entidade esquerda do resultado.
leo

6
Esse também é o efeito da junção interna, então ... sim.
Tomalak

1
Solução que pode ser adaptada para LEFT OUTER JOIN: stackoverflow.com/a/20576200/510583
leo

3
@leo Sim, mas o OP usou uma junção interna, então não entendo sua objeção.
218159Preço: R

27

A resposta do @Quassnoi é boa, em alguns casos (especialmente se a tabela externa for grande), uma consulta mais eficiente pode ser com o uso de funções em janelas, como esta:

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

Às vezes, você só precisa testar qual consulta oferece melhor desempenho.


3
Esta é a única resposta que encontrei que faz uma junção "Esquerda" real, o que significa que ela não adiciona mais linhas do que a tabela "Esquerda". Você só precisa colocar em subconsulta e adicionar "onde rowNum não é nulo"
user890332

1
Concordou que esta é a melhor solução. Essa solução também não exige que você tenha um ID exclusivo na tabela na qual você está ingressando e é muito mais rápida que a resposta votada. Você também pode adicionar critérios para qual linha você prefere retornar, em vez de apenas pegar uma linha aleatória, usando uma cláusula ORDER BY na subconsulta.
Geoff Griswald

Esta é uma boa solução. Observe: ao usar para sua própria situação, tenha muito cuidado com o modo como PARTION BY (normalmente você provavelmente deseja alguma coluna de ID) e ORDER BY (que pode ser feito por quase tudo, dependendo da linha que você deseja manter, por exemplo, O DateCreated desc seria uma opção para algumas tabelas, mas dependeria de muitas coisas)
JosephDoggie 23/03

14

, Outra abordagem usando a expressão de tabela comum:

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

ou, no final, talvez você queira mostrar todas as linhas unidas?

versão separada por vírgula aqui:

  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

Do SQL Server 2012 em diante, acho que isso funcionará:

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
Melhor resposta se você me perguntar.
Thomas

11

Subconsultas correlacionadas são subconsultas que dependem da consulta externa. É como um loop for no SQL. A subconsulta será executada uma vez para cada linha na consulta externa:

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: deixa pra lá, Quassnoi tem uma resposta melhor.

Para o SQL2K, algo como isto:

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

Minha maneira favorita de executar esta consulta é com uma cláusula não existe. Eu acredito que esta é a maneira mais eficiente de executar esse tipo de consulta:

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
    )

Mas eu não testei esse método contra outros métodos sugeridos aqui.


2

Tentei a cruz, funciona bem, mas leva um pouco mais de tempo. Colunas de linhas ajustadas para ter o grupo máximo e adicionado, que manteve a velocidade e reduziu o registro extra.

Aqui está a consulta ajustada:

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

10
Mas ter max separadamente em duas colunas significa que a quantidade pode não estar relacionada à descrição. Se o pedido fosse 2 Widgets e 10 Gadgets, a consulta retornaria 10 Widgets.
Brianorca 04/08/2015

1

tente isso

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
Por favor, considere explicar o que sua consulta faz para resolver o problema do OP
Simas Joneliunas
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.