Selecione os 10 melhores registros para cada categoria


208

Desejo retornar os 10 principais registros de cada seção em uma consulta. Alguém pode ajudar com como fazê-lo? Seção é uma das colunas da tabela.

O banco de dados é o SQL Server 2005. Desejo retornar os 10 primeiros por data digitada. As seções são comerciais, locais e de recursos. Para uma data em particular, quero apenas as 10 (dez) linhas de negócios mais recentes (entrada mais recente), as 10 (dez) linhas locais e os 10 principais recursos.


Alguma dessas respostas funcionou para você?
Kyle Delaney

3
Eu acho que nós nunca saberemos ...
Denny

Faz 12 anos e não sabemos se algum deles funcionou.
aroma

Respostas:


222

Se você estiver usando o SQL 2005, poderá fazer algo assim ...

SELECT rs.Field1,rs.Field2 
    FROM (
        SELECT Field1,Field2, Rank() 
          over (Partition BY Section
                ORDER BY RankCriteria DESC ) AS Rank
        FROM table
        ) rs WHERE Rank <= 10

Se o seu RankCriteria tiver vínculos, você poderá retornar mais de 10 linhas e a solução de Matt poderá ser melhor para você.


31
Se você realmente quer apenas o top 10, altere-o para RowNumber () em vez de Rank (). Sem laços então.
Mike L

3
Isso funciona, mas saiba que é provável que rank () seja transformado em uma tabela completa pelo planejador de consultas, se não houver um índice cuja primeira chave seja o RankCriteria. Nesse caso, você pode obter uma milhagem melhor selecionando as seções distintas e aplicando a cruz para escolher os 10 melhores pedidos por RankCriteria desc.
21813 Joe Kearney

Ótima resposta! Me pegou quase exatamente o que eu precisava. Acabei indo com o DENSE_RANKque não tem lacunas na numeração. 1
Michael Stramel

1
@Facbed É apenas um pseudônimo em cima da mesa.
Darrel Miller

15
Para quem usa o Sql Server, a função RowNumber () mencionada por Mike L é ROW_NUMBER ().
Randomraccoon

99

No T-SQL, eu faria:

WITH TOPTEN AS (
    SELECT *, ROW_NUMBER() 
    over (
        PARTITION BY [group_by_field] 
        order by [prioritise_field]
    ) AS RowNo 
    FROM [table_name]
)
SELECT * FROM TOPTEN WHERE RowNo <= 10

2
: Seja mais descritivo sobre sua solução. Consulte: Como responder
askmish

A consulta select no CTE pode conter a cláusula where?
toha

1
@toha Sim pode
KindaTechy 11/17/17

1
Embora você diga "No T-SQL", isso funciona para qualquer banco de dados que implemente a ROW_NUMBERfunção. Por exemplo, eu usei esta solução no SQLite.
Tony

Também funciona para o postgres sql. Eu apenas tive que usar "order by [priorityise_field] desc"
Phun

35

Isso funciona no SQL Server 2005 (editado para refletir seu esclarecimento):

select *
from Things t
where t.ThingID in (
    select top 10 ThingID
    from Things tt
    where tt.Section = t.Section and tt.ThingDate = @Date
    order by tt.DateEntered desc
    )
    and t.ThingDate = @Date
order by Section, DateEntered desc

2
Porém, isso não funciona para linhas em que Seção é nula. Você precisaria dizer "onde (tt.Section é nula e t.Section é null) ou tt.Section = t.Section"
Matt Hamilton

29
SELECT r.*
FROM
(
    SELECT
        r.*,
        ROW_NUMBER() OVER(PARTITION BY r.[SectionID] ORDER BY r.[DateEntered] DESC) rn
    FROM [Records] r
) r
WHERE r.rn <= 10
ORDER BY r.[DateEntered] DESC

O que é tabela com o alias 'm'?
Chalky

@ Calky é erro de digitação, deve ser r. fixo.
lorond 8/08/16

Funcionou como um encanto. Obrigado!
Ron Nuni 25/05

18

Eu faço assim:

SELECT a.* FROM articles AS a
  LEFT JOIN articles AS a2 
    ON a.section = a2.section AND a.article_date <= a2.article_date
GROUP BY a.article_id
HAVING COUNT(*) <= 10;

update: Este exemplo de GROUP BY funciona apenas no MySQL e SQLite, porque esses bancos de dados são mais permissivos que o SQL padrão em relação ao GROUP BY. A maioria das implementações SQL exige que todas as colunas na lista de seleção que não fazem parte de uma expressão agregada também estejam no GROUP BY.


1
Isso funciona? Eu tenho certeza que você iria "a.somecolumn é inválida na lista de seleção porque não está contida em uma função de agregação ou a cláusula GROUP BY" para cada coluna em artigos exceto article_id ..
Blorgbeard está fora

1
Você deve poder incluir outras colunas funcionalmente dependentes das colunas nomeadas no GROUP BY. As colunas que não são funcionalmente dependentes são ambíguas. Mas você está certo, dependendo da implementação do RDBMS. Funciona no MySQL, mas o IIRC falha no InterBase / Firebird.
Bill Karwin

1
Isso funcionaria no caso de os onze primeiros registros de uma seção terem a mesma data? Todos eles teriam contagens de 11 e o resultado seria um conjunto vazio.
Arth

Não, você precisa ter alguma maneira de romper os laços, se todos tiverem a mesma data. Veja stackoverflow.com/questions/121387/… para um exemplo.
precisa

1
@carlosgg, se os artigos tiverem um relacionamento muitos-para-muitos com as seções, será necessário ter uma tabela de interseção para mapear os artigos para suas seções. Em seguida, sua consulta teria que ingressar em uma tabela de interseção para o relacionamento m2m e agrupar por article_id e seção. Isso deve ajudar você a começar, mas não vou escrever a solução inteira em um comentário.
Bill Karwin

16

Se usarmos o SQL Server> = 2005, poderemos resolver a tarefa com apenas uma seleção :

declare @t table (
    Id      int ,
    Section int,
    Moment  date
);

insert into @t values
(   1   ,   1   , '2014-01-01'),
(   2   ,   1   , '2014-01-02'),
(   3   ,   1   , '2014-01-03'),
(   4   ,   1   , '2014-01-04'),
(   5   ,   1   , '2014-01-05'),

(   6   ,   2   , '2014-02-06'),
(   7   ,   2   , '2014-02-07'),
(   8   ,   2   , '2014-02-08'),
(   9   ,   2   , '2014-02-09'),
(   10  ,   2   , '2014-02-10'),

(   11  ,   3   , '2014-03-11'),
(   12  ,   3   , '2014-03-12'),
(   13  ,   3   , '2014-03-13'),
(   14  ,   3   , '2014-03-14'),
(   15  ,   3   , '2014-03-15');


-- TWO earliest records in each Section

select top 1 with ties
    Id, Section, Moment 
from
    @t
order by 
    case 
        when row_number() over(partition by Section order by Moment) <= 2 
        then 0 
        else 1 
    end;


-- THREE earliest records in each Section

select top 1 with ties
    Id, Section, Moment 
from
    @t
order by 
    case 
        when row_number() over(partition by Section order by Moment) <= 3 
        then 0 
        else 1 
    end;


-- three LATEST records in each Section

select top 1 with ties
    Id, Section, Moment 
from
    @t
order by 
    case 
        when row_number() over(partition by Section order by Moment desc) <= 3 
        then 0 
        else 1 
    end;

1
+1 Gostei dessa solução por sua simplicidade, mas você poderia explicar como o uso top 1funciona com a caseinstrução na order bycláusula retornando 0 ou 1?
Ceres

3
O TOP 1 funciona com WITH TIES aqui. Com meios laços que quando ORDER BY = 0, em seguida, selecione leva este registro (por causa do TOP 1) e todos os outros que têm ORDER BY = 0 (por causa de WITH TIES)
Vadim Loboda

9

Se você souber quais são as seções, poderá fazer:

select top 10 * from table where section=1
union
select top 10 * from table where section=2
union
select top 10 * from table where section=3

3
Essa seria a maneira mais fácil de fazer isso.
Hector Sosa Jr

3
Mas isso seria ineficiente se você tem 150 ou se as categorias são variáveis por dia, semana, etc.
Rafa Barragan

1
Claro, mas para citar OP: "As seções são comerciais, locais e de recursos". Se você tiver três categorias estáticas, esta é a melhor maneira de fazê-lo.
Blorgbeard sai em 08/09

9

Eu sei que este tópico é um pouco antigo, mas acabei de me deparar com um problema semelhante (selecione o artigo mais recente de cada categoria) e esta é a solução que me veio à cabeça:

WITH [TopCategoryArticles] AS (
    SELECT 
        [ArticleID],
        ROW_NUMBER() OVER (
            PARTITION BY [ArticleCategoryID]
            ORDER BY [ArticleDate] DESC
        ) AS [Order]
    FROM [dbo].[Articles]
)
SELECT [Articles].* 
FROM 
    [TopCategoryArticles] LEFT JOIN 
    [dbo].[Articles] ON
        [TopCategoryArticles].[ArticleID] = [Articles].[ArticleID]
WHERE [TopCategoryArticles].[Order] = 1

Isso é muito semelhante à solução de Darrel, mas supera o problema de RANK que pode retornar mais linhas do que o pretendido.


Por que usar o CTE Sir? É reduzir o consumo de memória?
toha

@toha porque CTEs são mais simples e mais fácil de entender
Engenheiro invertida

Ótima resposta!! Ele pode ser otimizado usando o interno em JOINvez de LEFT JOIN, pois nunca haverá um registro TopCategoryArticlessem um Articleregistro correspondente .
Reversed Engineer

6

Tentei o seguinte e funcionou com laços também.

SELECT rs.Field1,rs.Field2 
FROM (
    SELECT Field1,Field2, ROW_NUMBER() 
      OVER (Partition BY Section
            ORDER BY RankCriteria DESC ) AS Rank
    FROM table
    ) rs WHERE Rank <= 10

5

Se você deseja produzir uma saída agrupada por seção, exibindo apenas os n registros superiores de cada seção, algo como isto:

SECTION     SUBSECTION

deer        American Elk/Wapiti
deer        Chinese Water Deer
dog         Cocker Spaniel
dog         German Shephard
horse       Appaloosa
horse       Morgan

... o seguinte deve funcionar de maneira bastante genérica com todos os bancos de dados SQL. Se você deseja o top 10, basta alterar 2 para 10 no final da consulta.

select
    x1.section
    , x1.subsection
from example x1
where
    (
    select count(*)
    from example x2
    where x2.section = x1.section
    and x2.subsection <= x1.subsection
    ) <= 2
order by section, subsection;

Para configurar:

create table example ( id int, section varchar(25), subsection varchar(25) );

insert into example select 0, 'dog', 'Labrador Retriever';
insert into example select 1, 'deer', 'Whitetail';
insert into example select 2, 'horse', 'Morgan';
insert into example select 3, 'horse', 'Tarpan';
insert into example select 4, 'deer', 'Row';
insert into example select 5, 'horse', 'Appaloosa';
insert into example select 6, 'dog', 'German Shephard';
insert into example select 7, 'horse', 'Thoroughbred';
insert into example select 8, 'dog', 'Mutt';
insert into example select 9, 'horse', 'Welara Pony';
insert into example select 10, 'dog', 'Cocker Spaniel';
insert into example select 11, 'deer', 'American Elk/Wapiti';
insert into example select 12, 'horse', 'Shetland Pony';
insert into example select 13, 'deer', 'Chinese Water Deer';
insert into example select 14, 'deer', 'Fallow';

Isso não funciona quando eu quero apenas o primeiro registro para cada seção. Elimina todos os grupos de seções que possuem mais de 1 registro. Eu tentei substituindo <= 2 por <= 1
nils

@nils Existem apenas três valores de seção: veado, cachorro e cavalo. Se você alterar a consulta para <= 1, obterá uma subseção para cada seção: American Elk / Wapiti para veados, Cocker Spaniel para cães e Appaloosa para cavalos. Esses também são os primeiros valores em cada seção em ordem alfabética. A consulta é significou para eliminar todos os outros valores.
Craig

Mas quando tento executar sua consulta, ela elimina tudo porque a contagem é> = 1 para tudo. Não preserva a 1ª subseção de cada seção. Você pode tentar executar sua consulta para <= 1 e me informar se você receber a primeira subseção de cada seção?
nils

Olá, recriei esse pequeno banco de dados de teste a partir dos scripts e executei a consulta usando <= 1, e ele retornou o primeiro valor da subseção de cada seção. Qual servidor de banco de dados você está usando? Sempre há uma chance de ele estar relacionado ao seu banco de dados de escolha. Eu apenas executei isso no MySQL porque era útil e se comportou conforme o esperado. Tenho certeza de que quando o fiz pela primeira vez (queria ter certeza de que o que eu postei realmente funcionava sem depuração), tenho certeza de que o fiz usando o Sybase SQL Anywhere ou o MS SQL Server.
Craig

funcionou perfeitamente para mim no mysql. Eu mudei uma consulta um pouco, não sei por que ele usou <= para o campo varchar na subseção .. eu mudei para e x2.subsection = x1.subsection
Mahen Nakar

4

O operador UNION pode trabalhar para você? Faça um SELECT para cada seção e depois UNIQUE-os juntos. Acho que funcionaria apenas para um número fixo de seções.


4

Q) Localizando registros TOP X de cada grupo (Oracle)

SQL> select * from emp e 
  2  where e.empno in (select d.empno from emp d 
  3  where d.deptno=e.deptno and rownum<3)
  4  order by deptno
  5  ;

 EMPNO ENAME      JOB              MGR HIREDATE         SAL       COMM     DEPTNO

  7782 CLARK      MANAGER         7839 09-JUN-81       2450                    10
  7839 KING       PRESIDENT            17-NOV-81       5000                    10
  7369 SMITH      CLERK           7902 17-DEC-80        800                    20
  7566 JONES      MANAGER         7839 02-APR-81       2975                    20
  7499 ALLEN      SALESMAN        7698 20-FEB-81       1600        300         30
  7521 WARD       SALESMAN        7698 22-FEB-81       1250        500         30

6 linhas selecionadas.



A pergunta era sobre o SQL Server, não o Oracle.
Craig

2

Enquanto a pergunta era sobre o SQL Server 2005, a maioria das pessoas seguiu em frente e, se a encontrar, a resposta preferida em outras situações é aquela que é usada CROSS APPLYconforme ilustrado nesta postagem do blog .

SELECT *
FROM t
CROSS APPLY (
  SELECT TOP 10 u.*
  FROM u
  WHERE u.t_id = t.t_id
  ORDER BY u.something DESC
) u

Esta consulta envolve 2 tabelas. A consulta do OP envolve apenas 1 tabela, no caso em que uma solução baseada em função de janela pode ser mais eficiente.


1

Você pode tentar esta abordagem. Esta consulta retorna 10 cidades mais populosas de cada país.

   SELECT city, country, population
   FROM
   (SELECT city, country, population, 
   @country_rank := IF(@current_country = country, @country_rank + 1, 1) AS country_rank,
   @current_country := country 
   FROM cities
   ORDER BY country, population DESC
   ) ranked
   WHERE country_rank <= 10;

Essa solução não passa em um caso de teste quando temos uma tabela com um registro de um país com 9 da mesma população, por exemplo, ela retorna nula em vez de retornar todos os 9 registros disponíveis em ordem. Alguma sugestão para corrigir esse problema?
Mojgan Mazouchi
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.