Como selecionar a primeira linha de cada grupo?

57

Eu tenho uma tabela como esta:

 ID |  Val   |  Kind
----------------------
 1  |  1337  |   2
 2  |  1337  |   1
 3  |   3    |   4
 4  |   3    |   4

Quero fazer um SELECTque retorne apenas a primeira linha de cada uma Val, ordenando por Kind.

Saída de amostra:

 ID |  Val   |  Kind
----------------------
 2  |  1337  |   1
 3  |   3    |   4

Como posso criar esta consulta?

BrunoLM
fonte
por que 3 | 3 | 4 e não 4 | 3 | 4 - qual é o tie-break ou você não se importa?
Jack Douglas
@JackDouglas Na verdade, tenho um ORDER BY ID DESC, mas isso não é relevante para a pergunta. Neste exemplo, eu não ligo.
BrunoLM

Respostas:

38

Esta solução também usa keep, mas vale kindtambém pode ser simplesmente calculado para cada grupo sem uma subconsulta:

select min(id) keep(dense_rank first order by kind) id
     , val
     , min(kind) kind
  from mytable
 group by val;
ID VAL TIPO
-: | ---: | ---:
 3 3 4
 2 1337 1 1

dbfiddle aqui

KEEP… PRIMEIRO e KEEP… LAST são um recurso agregado específico do Oracle - você pode ler sobre isso aqui nos documentos do Oracle ou em ORACLE_BASE :

As funções PRIMEIRO e ÚLTIMO podem ser usadas para retornar o primeiro ou o último valor de uma sequência ordenada

mik
fonte
62

Use uma expressão de tabela comum (CTE) e uma função de janela / classificação / particionamento como ROW_NUMBER .

Essa consulta criará uma tabela na memória chamada ORDERED e adicionará uma coluna adicional de rn, que é uma sequência de números de 1 a N. A PARTITION BY indica que ele deve reiniciar em 1 sempre que o valor de Val for alterado e queremos solicitar linhas pelo menor valor de Kind.

WITH ORDERED AS
(
SELECT
    ID
,   Val
,   kind
,   ROW_NUMBER() OVER (PARTITION BY Val ORDER BY Kind ASC) AS rn
FROM
    mytable
)
SELECT
    ID
,   Val
,   Kind
FROM
    ORDERED
WHERE
    rn = 1;

A abordagem acima deve funcionar com qualquer RDBMS que tenha implementado a função ROW_NUMBER (). O Oracle possui algumas funcionalidades elegantes, conforme expressas na resposta da mik, que geralmente resultam em melhor desempenho do que esta resposta.

billinkc
fonte
25

A solução da bilinkc funciona bem, mas eu pensei em jogar fora a minha também. Tem o mesmo custo, mas pode ser mais rápido (ou mais lento, não testei). A diferença é que ele usa o First_Value em vez de Row_Number. Como estamos interessados ​​apenas no primeiro valor, na minha opinião, é mais direto.

SELECT ID, Val, Kind FROM
(
   SELECT First_Value(ID) OVER (PARTITION BY Val ORDER BY Kind) First, ID, Val, Kind 
   FROM mytable
)
WHERE ID = First;

Dados de teste.

--drop table mytable;
create table mytable (ID Number(5) Primary Key, Val Number(5), Kind Number(5));

insert into mytable values (1,1337,2);
insert into mytable values (2,1337,1);
insert into mytable values (3,3,4);
insert into mytable values (4,3,4);

Se você preferir, aqui está o equivalente CTE.

WITH FirstIDentified AS (
   SELECT First_Value(ID) OVER (PARTITION BY Val ORDER BY Kind) First, ID, Val, Kind 
   FROM mytable
   )
SELECT ID, Val, Kind FROM FirstIdentified
WHERE ID = First;
Leigh Riffel
fonte
11
+1, mas acho que vale a pena enfatizar que suas respostas e billinkc não são logicamente iguais, a menos que idsejam únicas.
Jack Douglas
@ Jack Douglas - É verdade, eu assumi isso.
Leigh Riffel
14

Você pode usar keeppara selecionar um idde cada grupo:

select *
from mytable
where id in ( select min(id) keep (dense_rank first order by kind, id)
              from mytable
              group by val );
ID VAL TIPO
-: | ---: | ---:
 2 1337 1 1
 3 3 4

dbfiddle aqui

Jack Douglas
fonte
2
SELECT MIN(MyTable01.Id) as Id,
       MyTable01.Val     as Val,
       MyTable01.Kind    as Kind 
  FROM MyTable MyTable01,                         
       (SELECT Val,MIN(Kind) as Kind
          FROM MyTable                   
      GROUP BY Val) MyTableGroup
WHERE MyTable01.Val  = MyTableGroup.Val
  AND MyTable01.Kind = MyTableGroup.Kind
GROUP BY MyTable01.Val,MyTable01.Kind
ORDER BY Id;
frutado
fonte
Isso será muito menos eficiente do que as outras respostas, devido ao fato de serem necessárias duas varreduras no MyTable.
a_horse_with_no_name
2
Isso só é verdade se o otimizador considerar a consulta escrita literalmente. Otimizadores mais avançados podem ver a intenção (linha por grupo) e produzir um plano com acesso a uma única tabela.
Paul White