Sélectionnez les 10 meilleurs enregistrements pour chaque catégorie

208

Je veux retourner les 10 meilleurs enregistrements de chaque section dans une seule requête. Quelqu'un peut-il m'aider à le faire? La section est l'une des colonnes du tableau.

La base de données est SQL Server 2005. Je souhaite renvoyer le top 10 par date entrée. Les sections sont commerciales, locales et fonctionnelles. Pour une date particulière, je veux uniquement les 10 premières lignes professionnelles (entrée la plus récente), les 10 premières lignes locales et les 10 meilleures fonctionnalités.

jbcedge
la source
L'une de ces réponses vous a-t-elle fonctionné?
Kyle Delaney
3
Je suppose que nous ne saurons jamais ...
Denny
Cela fait 12 ans et nous ne savons pas si l'un d'entre eux a fonctionné.
arôme

Réponses:

222

Si vous utilisez SQL 2005, vous pouvez faire quelque chose comme ça ...

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

Si vos RankCriteria ont des liens, vous pouvez renvoyer plus de 10 lignes et la solution de Matt peut être meilleure pour vous.

Darrel Miller
la source
31
Si vous voulez vraiment juste le top 10, changez-le en RowNumber () au lieu de Rank (). Aucun lien alors.
Mike L
3
Cela fonctionne, mais sachez que rank () est susceptible d'être transformé en un tableau complet par le planificateur de requêtes s'il n'y a pas d'index dont la première clé est le RankCriteria. Dans ce cas, vous pouvez obtenir un meilleur kilométrage en sélectionnant les sections distinctes et en effectuant une demande croisée pour sélectionner le top 10 ordonné par RankCriteria desc.
Joe Kearney
Très bonne réponse! J'ai presque exactement ce dont j'avais besoin. J'ai fini par aller avec DENSE_RANKqui n'a pas de lacunes dans la numérotation. +1
Michael Stramel
1
@Facbed C'est juste un alias sur la table.
Darrel Miller
15
Pour toute personne utilisant Sql Server, la fonction RowNumber () mentionnée par Mike L est ROW_NUMBER ().
randomraccoon
99

En T-SQL, je ferais:

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
Phil Rabbitt
la source
2
: Veuillez être plus descriptif sur votre solution. Référez: Comment répondre
Askmish
La requête de sélection sur CTE peut-elle contenir une clause where?
toha
1
@toha Oui c'est possible
KindaTechy
1
Bien que vous disiez "In T-SQL", cela fonctionne pour toute base de données implémentant la ROW_NUMBERfonction. Par exemple, j'ai utilisé cette solution dans SQLite.
Tony
Cela fonctionne également pour sql postgres. Je devais juste utiliser "ordre par [priorité_field] desc"
Phun
35

Cela fonctionne sur SQL Server 2005 (modifié pour refléter votre clarification):

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
Matt Hamilton
la source
2
Cela ne fonctionne pas pour les lignes où Section est null, cependant. Vous auriez besoin de dire "où (tt.Section est nul et t.Section est nul) 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
lorond
la source
Qu'est-ce que la table avec l'alias «m»?
Chalky
@Chalky c'est une faute de frappe, devrait l'être r. fixé.
lorond
A fonctionné comme un charme. Je vous remercie!
Ron Nuni
18

Je le fais de cette façon:

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;

mise à jour: Cet exemple de GROUP BY fonctionne uniquement dans MySQL et SQLite, car ces bases de données sont plus permissives que SQL standard concernant GROUP BY. La plupart des implémentations SQL nécessitent que toutes les colonnes de la liste de sélection qui ne font pas partie d'une expression d'agrégation se trouvent également dans GROUP BY.

Bill Karwin
la source
1
Est-ce que ça marche? Je suis à peu près sûr que "a.somecolumn n'est pas valide dans la liste de sélection car il n'est pas contenu dans une fonction d'agrégation ou dans la clause group by" pour chaque colonne des articles sauf article_id ..
Blorgbeard est sorti
1
Vous devriez pouvoir inclure d'autres colonnes qui dépendent fonctionnellement des colonnes nommées dans GROUP BY. Les colonnes qui ne sont pas fonctionnellement dépendantes sont ambiguës. Mais vous avez raison, selon la mise en œuvre du SGBDR. Il fonctionne dans MySQL mais IIRC échoue dans InterBase / Firebird.
Bill Karwin
1
Est-ce que cela fonctionnerait dans le cas où les onze premiers enregistrements d'une section avaient tous la même date? Ils auraient tous un nombre de 11 et le résultat serait un ensemble vide.
Arth
Non, vous devez avoir un moyen de rompre les liens s'ils ont tous la même date. Voir stackoverflow.com/questions/121387/… pour un exemple.
Bill Karwin
1
@carlosgg, si les articles ont une relation plusieurs-à-plusieurs avec les sections, vous devez disposer d'une table d'intersection pour mapper les articles sur leurs sections. Ensuite, votre requête devra être jointe à une table d'intersection pour la relation m2m et regroupée par article_id et section. Cela devrait vous aider à démarrer, mais je ne vais pas écrire toute la solution dans un commentaire.
Bill Karwin
16

Si nous utilisons SQL Server> = 2005, nous pouvons résoudre la tâche avec un seul choix :

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;
Vadim Loboda
la source
1
+1 J'aime cette solution pour sa simplicité, mais pourriez-vous expliquer comment l'utilisation top 1fonctionne avec l' caseinstruction dans la order byclause renvoyant 0 ou 1?
Ceres
3
TOP 1 fonctionne avec WITH TIES ici. WITH TIES signifie que lorsque ORDER BY = 0, alors SELECT prend cet enregistrement (en raison du TOP 1) et tous les autres qui ont ORDER BY = 0 (en raison de WITH TIES)
Vadim Loboda
9

Si vous savez quelles sont les sections, vous pouvez faire:

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
Blorgbeard est sorti
la source
3
Ce serait la façon la plus simple de le faire.
Hector Sosa Jr
3
Mais cela serait inefficace si vous en avez 150 ou si les catégories sont variables par jour, semaine, etc.
Rafa Barragan
1
Bien sûr, mais pour citer OP: "Les sections sont commerciales, locales et fonctionnelles". Si vous avez trois catégories statiques, c'est la meilleure façon de le faire.
Blorgbeard est sorti le
9

Je sais que ce fil est un peu ancien mais je viens de tomber sur un problème similaire (sélectionnez le plus récent article de chaque catégorie) et voici la solution que j'ai trouvée:

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

Ceci est très similaire à la solution de Darrel mais surmonte le problème de RANK qui pourrait renvoyer plus de lignes que prévu.

Diadistis
la source
Pourquoi utiliser CTE Sir? Est-ce que cela réduit la consommation de mémoire?
toha
@toha parce que les CTE sont plus simples et plus faciles à comprendre
Reversed Engineer
Très bonne réponse!! Il pourrait être optimisé en utilisant interne JOINau lieu de LEFT JOIN, car il n'y aura jamais d'enregistrement TopCategoryArticlessans Articleenregistrement correspondant .
Ingénieur inversé
6

J'ai essayé ce qui suit et cela a fonctionné avec des liens aussi.

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
Raghu S
la source
5

Si vous souhaitez produire une sortie groupée par section, en affichant uniquement les n premiers enregistrements de chaque section, quelque chose comme ceci:

SECTION     SUBSECTION

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

... alors ce qui suit devrait fonctionner de façon assez générique avec toutes les bases de données SQL. Si vous voulez le top 10, changez simplement le 2 en 10 vers la fin de la requête.

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;

Installer:

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';
Craig
la source
Cela ne fonctionne pas quand je veux juste le premier enregistrement de chaque section. Il élimine tous les groupes de sections qui ont plus d'un enregistrement. J'ai essayé de remplacer <= 2 par <= 1
nils
@nils Il n'y a que trois valeurs de section: cerf, chien et cheval. Si vous modifiez la requête à <= 1, vous obtenez une sous-section pour chaque section: Elan américain / Wapiti pour le cerf, Cocker Spaniel pour chien et Appaloosa pour cheval. Ce sont également les premières valeurs de chaque section par ordre alphabétique. La requête vise à éliminer toutes les autres valeurs.
Craig
Mais lorsque j'essaie d'exécuter votre requête, cela élimine tout car le nombre est> = 1 pour tout. Il ne conserve pas la 1ère sous-section pour chaque section. Pouvez-vous essayer d'exécuter votre requête pour <= 1 et faites-moi savoir si vous obtenez la première sous-section pour chaque section?
nils
@nils Salut, j'ai recréé cette petite base de données de test à partir des scripts et j'ai exécuté la requête en utilisant <= 1, et elle a renvoyé la première valeur de sous-section de chaque section. Quel serveur de base de données utilisez-vous? Il y a toujours une chance que cela soit lié à la base de données de votre choix. Je viens de l'exécuter dans MySQL car c'était pratique et il s'est comporté comme prévu. Je suis à peu près sûr quand je l'ai fait la première fois (je voulais m'assurer que ce que j'ai publié fonctionnait réellement sans débogage), je suis presque sûr de l'avoir fait en utilisant Sybase SQL Anywhere ou MS SQL Server.
Craig
cela a parfaitement fonctionné pour moi dans mysql. J'ai changé une requête un peu, je ne sais pas pourquoi il a utilisé <= pour le champ varchar dans la sous-section .. je l'ai changé en et x2.subsection = x1.subsection
Mahen Nakar
4

L' opérateur UNION pourrait-il travailler pour vous? Avoir un SELECT pour chaque section, puis les UNION ensemble. Je suppose que cela ne fonctionnerait que pour un nombre fixe de sections.

sblundy
la source
4

Q) Trouver les enregistrements TOP X de chaque groupe (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 lignes sélectionnées.


bharathreddy
la source
La question concernait SQL Server, pas Oracle.
Craig
2

Alors que la question concernait SQL Server 2005, la plupart des gens sont passés à autre chose et s'ils trouvent cette question, quelle pourrait être la réponse préférée dans d'autres situations est celle utilisée CROSS APPLYcomme illustré dans ce billet de 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

Cette requête implique 2 tables. La requête de l'OP ne concerne qu'une seule table, au cas où une solution basée sur une fonction de fenêtre pourrait être plus efficace.

Lukas Eder
la source
1

Vous pouvez essayer cette approche. Cette requête renvoie les 10 villes les plus peuplées pour chaque pays.

   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;
Ali
la source
Cette solution ne passe pas un cas de test lorsque nous avons une table avec un enregistrement d'un pays avec 9 même population par exemple, elle retourne null au lieu de renvoyer les 9 enregistrements disponibles dans l'ordre. Une suggestion pour résoudre ce problème?
Mojgan Mazouchi