Wybierz 10 najlepszych rekordów dla każdej kategorii


207

Chcę zwrócić 10 najlepszych rekordów z każdej sekcji w jednym zapytaniu. Czy ktoś może pomóc w tym, jak to zrobić? Sekcja jest jedną z kolumn w tabeli.

Bazą danych jest SQL Server 2005. Chcę zwrócić 10 najlepszych według wprowadzonej daty. Sekcje są biznesowe, lokalne i funkcjonalne. Dla jednej konkretnej daty chcę tylko górne (10) wierszy biznesowych (najnowszy wpis), górne (10) wierszy lokalnych i górne (10) funkcji.


Czy któraś z tych odpowiedzi działała dla Ciebie?
Kyle Delaney,

3
Chyba nigdy się nie dowiemy ...
Denny,

Minęło 12 lat i nie wiemy, czy któryś z nich zadziałał.
aromat

Odpowiedzi:


221

Jeśli używasz SQL 2005, możesz zrobić coś takiego ...

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

Jeśli twoje RankCriteria ma powiązania, możesz zwrócić więcej niż 10 rzędów, a rozwiązanie Matta może być dla Ciebie lepsze.


31
Jeśli naprawdę chcesz tylko 10 najlepszych, zmień ją na RowNumber () zamiast Rank (). Brak więzi.
Mike L,

3
Działa to, ale należy pamiętać, że ranking () prawdopodobnie przekształci się w sortowanie według pełnej tabeli przez narzędzie do planowania zapytań, jeśli nie ma indeksu, którego pierwszym kluczem jest RankCriteria. W takim przypadku możesz uzyskać lepszy przebieg, wybierając poszczególne sekcje i krzyżując się, aby wybrać 10 najlepszych uporządkowanych według RankCriteria desc.
Joe Kearney

Świetna odpowiedź! Dostałem prawie dokładnie to, czego potrzebowałem. Skończyło się na DENSE_RANKtym, że nie ma żadnych luk w numeracji. +1
Michael Stramel

1
@Facbed To tylko alias na stole.
Darrel Miller

15
Dla każdego, kto używa Sql Server, funkcja RowNumber () wspomniana przez Mike'a L to ROW_NUMBER ().
randomraccoon

99

W T-SQL zrobiłbym:

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
: Prosimy o bardziej opisowe rozwiązanie. Patrz: Jak odpowiedzieć
askmish,

Czy zapytanie select w CTE może zawierać klauzulę where?
toha

1
@toha Tak, może
KindaTechy

1
Chociaż mówisz „W języku T-SQL”, działa to na każdą bazę danych implementującą tę ROW_NUMBERfunkcję. Na przykład użyłem tego rozwiązania w SQLite.
Tony,

Działa również dla Postgres sql. Musiałem po prostu użyć polecenia „zamów przez [
Prioritise_field

35

Działa to na SQL Server 2005 (edytowany w celu odzwierciedlenia twoich wyjaśnień):

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
Nie działa to jednak w przypadku wierszy, w których sekcja jest pusta. Musisz powiedzieć „gdzie (tt.Sekcja jest zerowa, a t.Sekcja jest zerowa) lub tt.Sekcja = t.Sekcja”
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

Co to jest tabela z aliasem „m”?
Chalky

@Chalky to literówka, powinno być r. naprawiony.
lorond

Działa jak urok. Dziękuję Ci!
Ron Nuni

18

Robię to w ten sposób:

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;

aktualizacja: Ten przykład GROUP BY działa tylko w MySQL i SQLite, ponieważ te bazy danych są bardziej liberalne niż standardowy SQL dotyczący GROUP BY. Większość implementacji SQL wymaga, aby wszystkie kolumny na liście wyboru, które nie są częścią wyrażenia agregującego, również znajdowały się w grupie GROUP BY.


1
Czy to działa? Jestem prawie pewien, że „a.somecolumn jest niepoprawny na liście wyboru, ponieważ nie jest zawarty w funkcji agregującej ani w grupach według klauzul” dla każdej kolumny w artykułach z wyjątkiem id_artykuła.
Blorgbeard jest niedostępny

1
Powinieneś być w stanie dołączyć inne kolumny, które są funkcjonalnie zależne od kolumn wymienionych w GROUP BY. Kolumny, które nie są funkcjonalnie zależne, są niejednoznaczne. Ale masz rację, w zależności od implementacji RDBMS. Działa w MySQL, ale IIRC nie działa w InterBase / Firebird.
Bill Karwin,

1
Czy działałoby to w przypadku, gdy wszystkie jedenaście najlepszych rekordów dla sekcji miało tę samą datę? Wszystkie miałyby liczbę 11, a wynikiem byłby pusty zestaw.
Arth

Nie, musisz mieć jakiś sposób na zerwanie więzi, jeśli wszystkie mają tę samą datę. Zobacz stackoverflow.com/questions/121387/... dla przykładu.
Bill Karwin

1
@carlosgg, jeśli artykuły mają relację wiele do wielu z sekcjami, to musisz mieć tabelę przecięć, aby zamapować artykuły na ich sekcje. Następnie zapytanie musiałoby dołączyć do tabeli przecięć dla relacji m2m i pogrupować według id_artykułu i sekcji. To powinno wystartować, ale nie zamierzam pisać całego rozwiązania w komentarzu.
Bill Karwin,

16

Jeśli użyjemy SQL Server> = 2005, możemy rozwiązać zadanie za pomocą tylko jednego wyboru :

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 Podoba mi się to rozwiązanie ze względu na jego prostotę, ale czy mógłbyś wyjaśnić, w jaki sposób używanie top 1działa z caseinstrukcją w order byklauzuli zwracającej 0 lub 1?
Ceres

3
TOP 1 działa tutaj z TIES. Z TIES oznacza, że ​​gdy ORDER BY = 0, wówczas SELECT bierze ten rekord (z powodu TOP 1) i wszystkich innych, którzy mają ORDER BY = 0 (z powodu WITH TIES)
Vadim Loboda

9

Jeśli wiesz, jakie są sekcje, możesz:

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
To byłby najłatwiejszy sposób na zrobienie tego.
Hector Sosa Jr

3
Ale byłoby to nieefektywne, jeśli masz 150 lub jeśli kategorie są zmienne przez dzień, tydzień, itd.
Rafa Barragan

1
Jasne, ale cytując OP: „Sekcje mają charakter biznesowy, lokalny i funkcjonalny”. Jeśli masz trzy kategorie statyczne, jest to najlepszy sposób, aby to zrobić.
Blorgbeard jest poza

9

Wiem, że ten wątek jest trochę stary, ale właśnie wpadłem na podobny problem (wybierz najnowszy artykuł z każdej kategorii) i oto rozwiązanie, które wymyśliłem:

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

Jest to bardzo podobne do rozwiązania Darrela, ale rozwiązuje problem RANK, który może zwrócić więcej wierszy niż planowano.


Dlaczego warto korzystać z CTE Sir? Czy to zmniejsza zużycie pamięci?
toha

@toha, ponieważ CTE są prostsze i łatwiejsze do zrozumienia
Reversed Engineer,

Świetna odpowiedź!! Można go zoptymalizować za pomocą wewnętrznego JOINzamiast LEFT JOIN, ponieważ nigdy nie będzie rekordu TopCategoryArticlesbez odpowiedniego Articlerekordu.
Reversed Engineer

6

Próbowałem następujących rzeczy i to również działało z więzami.

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

Jeśli chcesz produkować wyniki pogrupowane według sekcji, wyświetlając tylko górne n rekordów z każdej sekcji mniej więcej tak:

SECTION     SUBSECTION

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

... to poniższe powinny działać dość ogólnie ze wszystkimi bazami danych SQL. Jeśli chcesz 10 najlepszych, po prostu zmień 2 na 10 pod koniec zapytania.

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;

Ustawić:

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';

To nie działa, gdy chcę tylko pierwszy rekord dla każdej sekcji. Eliminuje wszystkie grupy sekcji, które mają więcej niż 1 rekord. Próbowałem, zastępując <= 2 przez <= 1
zero,

@ nils Istnieją tylko trzy wartości sekcji: jeleń, pies i koń. Jeśli zmienisz zapytanie na <= 1, otrzymasz jedną podsekcję dla każdej sekcji: Elk amerykański / Wapiti dla jelenia, Cocker Spaniel dla psa i Appaloosa dla konia. Są to również pierwsze wartości w każdej sekcji alfabetycznie. Zapytanie ma na celu wyeliminowanie wszystkich pozostałych wartości.
Craig,

Ale kiedy próbuję uruchomić zapytanie, wszystko to eliminuje, ponieważ liczba dla wszystkich wynosi> = 1. Nie zachowuje 1. podsekcji dla każdej sekcji. Czy możesz spróbować uruchomić zapytanie dla <= 1 i dać mi znać, jeśli otrzymasz pierwszą podsekcję dla każdej sekcji?
zero

@nils Cześć, odtworzyłem tę małą testową bazę danych ze skryptów i uruchomiłem zapytanie używając <= 1, i zwróciło pierwszą wartość podsekcji z każdej sekcji. Z jakiego serwera bazy danych korzystasz? Zawsze istnieje szansa, że ​​jest to związane z wybraną bazą danych. Właśnie uruchomiłem to w MySQL, ponieważ było to przydatne i działało zgodnie z oczekiwaniami. Jestem pewien, że kiedy zrobiłem to po raz pierwszy (chciałem się upewnić, że to, co opublikowałem, działa bez debuggin), jestem pewien, że zrobiłem to za pomocą Sybase SQL Anywhere lub MS SQL Server.
Craig,

działało idealnie dla mnie w mysql. Zmieniłem trochę zapytanie, nie jestem pewien, dlaczego użył <= dla pola varchar w podsekcji. Zmieniłem go na x2.subsection = x1.subsection
Mahen Nakar

4

Czy operator UNION może dla ciebie pracować? Posiadaj jeden WYBÓR dla każdej sekcji, a następnie ZJEDNOCZENIE je razem. Chyba zadziałałoby to tylko dla określonej liczby sekcji.


4

Q) Wyszukiwanie rekordów TOP X z każdej grupy (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

Wybrano 6 wierszy.



Pytanie dotyczyło SQL Servera, a nie Oracle.
Craig

2

Chociaż pytanie dotyczyło programu SQL Server 2005, większość ludzi przeszło do przodu i jeśli znajdzie to pytanie, w innych sytuacjach preferowaną odpowiedzią jest skorzystanie z niego, CROSS APPLYjak pokazano w tym poście na blogu .

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

To zapytanie obejmuje 2 tabele. Zapytanie OP dotyczy tylko 1 tabeli, w przypadku której rozwiązanie oparte na funkcji okna może być bardziej wydajne.


1

Możesz wypróbować to podejście. To zapytanie zwraca 10 najbardziej zaludnionych miast dla każdego kraju.

   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;

To rozwiązanie nie zalicza przypadku testowego, gdy mamy tabelę z zapisem jednego kraju z 9 tą samą populacją, na przykład zwraca zero zamiast zwracać wszystkie 9 dostępnych rekordów w kolejności. Wszelkie sugestie, aby rozwiązać ten problem?
Mojgan Mazouchi
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.