Seleccione los 10 mejores registros para cada categoría

208

Quiero devolver los 10 mejores registros de cada sección en una consulta. ¿Alguien puede ayudar con cómo hacerlo? La sección es una de las columnas de la tabla.

La base de datos es SQL Server 2005. Quiero devolver los 10 mejores por fecha ingresada. Las secciones son comerciales, locales y de características. Para una fecha en particular, solo quiero las primeras (10) filas comerciales (entrada más reciente), las principales (10) filas locales y las principales (10) características.

jbcedge
fuente
¿Alguna de estas respuestas funcionó para usted?
Kyle Delaney
3
Supongo que nunca lo sabremos ...
Denny
Han pasado 12 años y no sabemos si alguno de ellos funcionó.
aroma

Respuestas:

222

Si está utilizando SQL 2005, puede hacer algo como esto ...

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 su RankCriteria tiene vínculos, puede devolver más de 10 filas y la solución de Matt puede ser mejor para usted.

Darrel Miller
fuente
31
Si realmente solo quieres los 10 principales, cámbialo a RowNumber () en lugar de Rank (). Sin ataduras entonces.
Mike L
3
Esto funciona, pero tenga en cuenta que es probable que rank () se convierta en una tabla completa ordenada por el planificador de consultas si no hay un índice cuya primera clave es RankCriteria. En este caso, puede obtener un mejor kilometraje seleccionando las distintas secciones y aplicando en forma cruzada para elegir los 10 mejores ordenados por RankCriteria desc.
Joe Kearney
¡Gran respuesta! Me consiguió casi exactamente lo que necesitaba. Terminé yendo con DENSE_RANKlo que no tiene lagunas en la numeración. +1
Michael Stramel
1
@Facbed Es solo un alias sobre la mesa.
Darrel Miller
15
Para cualquiera que use Sql Server, la función RowNumber () mencionada por Mike L es ROW_NUMBER ().
randomraccoon
99

En T-SQL, haría:

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
fuente
2
: Por favor sea más descriptivo sobre su solución. Consulte: Cómo responder
askmish
¿La consulta de selección en CTE puede contener la cláusula where?
toha
1
@toha Sí se puede
KindaTechy
1
Aunque diga "En T-SQL", esto funciona para cualquier base de datos que implemente la ROW_NUMBERfunción. Por ejemplo, he usado esta solución en SQLite.
Tony
Funciona también para postgres sql. Solo tenía que usar "ordenar por [prioridades_campo] desc"
Phun
35

Esto funciona en SQL Server 2005 (editado para reflejar su aclaración):

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
fuente
2
Sin embargo, esto no funciona para las filas donde la Sección es nula. Debería decir "where (tt.Section es nulo y t.Section es nulo) o 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
fuente
¿Qué es la tabla con alias 'm'?
Chalky
@Chalky es un error tipográfico, debería ser r. fijo.
lorond
Trabajado como un encanto. ¡Gracias!
Ron Nuni
18

Lo hago de esta manera:

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;

actualización: este ejemplo de GROUP BY funciona solo en MySQL y SQLite, porque esas bases de datos son más permisivas que SQL estándar con respecto a GROUP BY. La mayoría de las implementaciones de SQL requieren que todas las columnas de la lista de selección que no forman parte de una expresión agregada también estén en GROUP BY.

Bill Karwin
fuente
1
¿Eso funciona? Estoy bastante seguro de que "a.somecolumn es válida en la lista de selección, ya que no está contenida en una función de agregado o de la cláusula group by" para todas las columnas de objetos, salvo article_id ..
Blorgbeard está fuera
1
Debería poder incluir otras columnas que sean funcionalmente dependientes de las columnas nombradas en GROUP BY. Las columnas que no dependen funcionalmente son ambiguas. Pero tiene razón, dependiendo de la implementación de RDBMS. Funciona en MySQL pero IIRC falla en InterBase / Firebird.
Bill Karwin
1
¿Funcionaría en el caso de que los once registros principales de una sección tuvieran la misma fecha? Todos tendrían cuentas de 11 y el resultado sería un conjunto vacío.
Arth
No, debes tener alguna forma de romper los lazos si todos tienen la misma fecha. Consulte stackoverflow.com/questions/121387/… para ver un ejemplo.
Bill Karwin
1
@carlosgg, si los artículos tienen una relación de muchos a muchos con las secciones, entonces necesitarías tener una tabla de intersección para asignar artículos a sus secciones. Luego, su consulta tendría que unirse a una tabla de intersección para la relación m2m, y agrupar por article_id y sección. Eso debería ayudarlo a comenzar, pero no voy a escribir la solución completa en un comentario.
Bill Karwin
16

Si usamos SQL Server> = 2005, entonces podemos resolver la tarea con una sola selección :

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
fuente
1
+1 Me gusta esta solución por su simplicidad, pero ¿podría explicar cómo top 1funciona el uso con la casedeclaración en la order bycláusula que devuelve 0 o 1?
Ceres
3
TOP 1 funciona con CON TIES aquí. WITH TIES significa que cuando ORDER BY = 0, SELECT toma este registro (por TOP 1) y todos los demás que tienen ORDER BY = 0 (por WITH TIES)
Vadim Loboda
9

Si sabe cuáles son las secciones, puede hacer:

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á fuera
fuente
3
Esta sería la forma más fácil de hacerlo.
Héctor Sosa Jr
3
Pero esto sería ineficiente si tiene 150 o si las categorías varían según el día, la semana, etc.
Rafa Barragan
1
Claro, pero para citar OP: "Las secciones son comerciales, locales y de características". Si tiene tres categorías estáticas, esta es la mejor manera de hacerlo.
Blorgbeard sale el
9

Sé que este hilo es un poco viejo, pero acabo de encontrarme con un problema similar (seleccione el artículo más nuevo de cada categoría) y esta es la solución que se me ocurrió:

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

Esto es muy similar a la solución de Darrel, pero supera el problema de RANGO que podría devolver más filas de lo previsto.

Diadistis
fuente
¿Por qué usar CTE Sir? ¿Es reducir el consumo de memoria?
toha
@toha porque los CTE son más simples y fáciles de entender
Ingeniero revertido el
¡¡Gran respuesta!! Se podría optimizar utilizando interno en JOINlugar de LEFT JOIN, ya que nunca habrá un registro TopCategoryArticlessin un Articleregistro correspondiente .
Ingeniero invertido el
6

Intenté lo siguiente y funcionó con lazos también.

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
fuente
5

Si desea generar una salida agrupada por sección, muestre solo los n registros superiores de cada sección de forma similar a esto:

SECTION     SUBSECTION

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

... entonces lo siguiente debería funcionar de manera bastante genérica con todas las bases de datos SQL. Si desea los 10 primeros, simplemente cambie el 2 a 10 hacia el final de la 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';
Craig
fuente
Esto no funciona cuando quiero solo el primer registro para cada sección. Elimina todos los grupos de secciones que tienen más de 1 registro. Lo intenté reemplazando <= 2 con <= 1
nils
@nils Solo hay tres valores de sección: ciervo, perro y caballo. Si cambia la consulta a <= 1, obtendrá una subsección para cada sección: American Elk / Wapiti para ciervos, Cocker Spaniel para perros y Appaloosa para caballos. Estos también son los primeros valores en cada sección alfabéticamente. La consulta está destinada a eliminar todos los demás valores.
Craig
Pero cuando intento ejecutar su consulta, elimina todo porque el recuento es> = 1 para todo. No conserva la primera subsección para cada sección. ¿Puede intentar ejecutar su consulta para <= 1 y avisarme si obtiene la primera subsección para cada sección?
nils
@nils Hola, recreé esta pequeña base de datos de prueba a partir de los scripts y ejecuté la consulta usando <= 1, y devolvió el primer valor de subsección de cada sección. ¿Qué servidor de base de datos estás usando? Siempre existe la posibilidad de que esté relacionado con su base de datos de elección. Acabo de ejecutar esto en MySQL porque era útil y se comportó como se esperaba. Estoy bastante seguro de que cuando lo hice por primera vez (quería asegurarme de que lo que publiqué realmente funcionó sin depurar), estoy bastante seguro de que lo hice usando Sybase SQL Anywhere o MS SQL Server.
Craig
funcionó perfectamente para mí en mysql. Cambié un poco la consulta, no estoy seguro de por qué usó <= para el campo varchar en la subsección. Lo cambié a y x2.subsection = x1.subsection
Mahen Nakar
4

¿Podría el operador UNION trabajar para usted? Tenga un SELECCIONAR para cada sección, luego UNIRlos juntos. Sin embargo, supongo que solo funcionaría para un número fijo de secciones.

sblundy
fuente
4

Q) Encontrar 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 filas seleccionadas.


bharathreddy
fuente
La pregunta era sobre SQL Server, no sobre Oracle.
Craig
2

Si bien la pregunta era sobre SQL Server 2005, la mayoría de las personas ha seguido adelante y, si encuentran esta pregunta, cuál podría ser la respuesta preferida en otras situaciones es la que se utiliza CROSS APPLYcomo se ilustra en esta publicación 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

Esta consulta involucra 2 tablas. La consulta del OP solo involucra 1 tabla, en caso de que una solución basada en la función de ventana sea más eficiente.

Lukas Eder
fuente
1

Puedes probar este enfoque. Esta consulta devuelve las 10 ciudades más pobladas 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;
Ali
fuente
Esta solución no pasa un caso de prueba cuando tenemos una tabla con un registro de un país con 9 mismas poblaciones, por ejemplo, devuelve nulo en lugar de devolver los 9 registros disponibles en orden. ¿Alguna sugerencia para solucionar este problema?
Mojgan Mazouchi