2013-05-23 9 views
10

Załóżmy, że mamy tabelę, która ma dwie kolumny, jedna kolumna zawiera nazwy niektórych osób, a druga kolumna zawiera wartości związane z każdą osobą. Jedna osoba może mieć więcej niż jedną wartość. Każda wartość ma typ numeryczny. Pytanie brzmi: chcemy wybrać 3 najlepsze wartości dla każdej osoby z tabeli. Jeśli jedna osoba ma mniej niż 3 wartości, wybieramy wszystkie wartości dla tej osoby.Jak wybrać 3 najwyższe wartości z każdej grupy w tabeli z SQL, które mają duplikaty

Problem można rozwiązać, jeśli w kwerendzie nie ma duplikatów w zapytaniu podanym w tym artykule Select top 3 values from each group in a table with SQL. Ale jeśli istnieją duplikaty, jakie jest rozwiązanie?

Na przykład, jeśli dla jednego imienia John ma on 5 wartości z nim związanych. Są 20,7,7,7,4. Muszę wrócić pary nazwa/wartość jak poniżej kolejności malejącej pod względem wartości dla każdej nazwy:

-----------+-------+ 
| name  | value | 
-----------+-------+ 
| John  | 20 | 
| John  |  7 | 
| John  |  7 | 
-----------+-------+ 

tylko 3 rzędy powinny być zwrócone do Jana, chociaż istnieją trzy 7s dla Jana.

+0

Co baza danych używasz? Oznaczono to za pomocą mysql, sql-server, Oracle i hadoop. Składnia może być różna w każdej bazie danych, pod warunkiem że bardziej szczegółowe informacje będą pomocne. – Taryn

+8

'Wybierz * z LackOfDetails Gdzie DataBaseSoftware ma wartość NULL' – Zane

+0

Z jakimi bazami danych naprawdę chcesz to zrobić? Oracle i SQL Server obsługują zapytania analityczne, które sprawiają, że jest to bardzo proste (kiedy przyzwyczaisz się do nowej składni). MySQL może to również zrobić, ale odpowiedź jest radykalnie inna. –

Odpowiedz

0

Miałem zamiar zgodzić się z tym pytaniem. Zdałem sobie jednak sprawę, że może to naprawdę wymagać rozwiązania opartego na wielu bazach danych.

Zakładając, że szukasz niezależnej od bazy danych metody, jedyny sposób, w jaki mogę myśleć, to skorelowane podkwerendy (lub nie-equijoins). Oto przykład:

select distinct t.personid, val, rank 
from (select t.*, 
      (select COUNT(distinct val) from t t2 where t2.personid = t.personid and t2.val >= t.val 
      ) as rank 
     from t 
    ) t 
where rank in (1, 2, 3) 

Jednak każda baza danych, które można wymienić (i pamiętać, Hadoop nie jest bazą danych) ma lepszy sposób to zrobić. Niestety, żaden z nich nie jest standardowym SQL.

Oto przykład z nim pracować w SQL Server:

with t as (
     select 1 as personid, 5 as val union all 
     select 1 as personid, 6 as val union all 
     select 1 as personid, 6 as val union all 
     select 1 as personid, 7 as val union all 
     select 1 as personid, 8 as val 
    ) 
select distinct t.personid, val, rank 
from (select t.*, 
      (select COUNT(distinct val) from t t2 where t2.personid = t.personid and t2.val >= t.val 
      ) as rank 
     from t 
    ) t 
where rank in (1, 2, 3); 
+0

Czy to działa, jeśli są zduplikowane wpisy? Spróbowałem, wygląda na to, że nie działa – PixelsTech

+0

Jeśli spróbowałeś, to na jakim DB próbowałeś? Nie wiem, dlaczego nie możemy wiedzieć, czym jest docelowy DB. –

+0

@wagregg Próbowałem go na MySQL – PixelsTech

24

W wielu nowoczesnych DBMS (np PostgreSQL, Oracle, SQL-Server, DB2 i wiele innych), co następuje będzie działać dobrze. Wykorzystuje współczynniki CTE i ranking funkcję ROW_NUMBER() który jest częścią najnowszego standardu SQL:

WITH cte AS 
    (SELECT name, value, 
      ROW_NUMBER() OVER (PARTITION BY name 
           ORDER BY value DESC 
          ) 
      AS rn 
    FROM t 
) 
SELECT name, value, rn 
FROM cte 
WHERE rn <= 3 
ORDER BY name, rn ; 

Bez CTE, tylko ROW_NUMBER():

SELECT name, value, rn 
FROM 
    (SELECT name, value, 
      ROW_NUMBER() OVER (PARTITION BY name 
           ORDER BY value DESC 
          ) 
      AS rn 
    FROM t 
) tmp 
WHERE rn <= 3 
ORDER BY name, rn ; 

Testowane:


W MySQL i innych DBMS, które nie mają funkcji ranking, trzeba używać zarówno tabele pochodzące skorelowanych podzapytania lub self-dołącza z GROUP BY.

The (tid) przyjmowana jest kluczem podstawowym tabeli:

SELECT t.tid, t.name, t.value,    -- self join and GROUP BY 
     COUNT(*) AS rn 
FROM t 
    JOIN t AS t2 
    ON t2.name = t.name 
    AND (t2.value > t.value 
     OR t2.value = t.value 
     AND t2.tid <= t.tid 
     ) 
GROUP BY t.tid, t.name, t.value 
HAVING COUNT(*) <= 3 
ORDER BY name, rn ; 


SELECT t.tid, t.name, t.value, rn 
FROM 
    (SELECT t.tid, t.name, t.value, 
      (SELECT COUNT(*)    -- inline, correlated subquery 
      FROM t AS t2 
      WHERE t2.name = t.name 
       AND (t2.value > t.value 
       OR t2.value = t.value 
       AND t2.tid <= t.tid 
       ) 
      ) AS rn 
    FROM t 
) AS t 
WHERE rn <= 3 
ORDER BY name, rn ; 

Testowane MySQL

+1

Doskonała odpowiedź, działało dobrze w Postgresie. – jwg

+1

@jwg W najnowszych wersjach Postgreatora można go nawet przyspieszyć (jeśli liczba grup (tutaj "nazwa" jest mała) za pomocą łączenia "LATERAL". –

0

Korzystanie GROUP_CONCAT i FIND_IN_SET można zrobić that.Check SQLFIDDLE.

SELECT * 
FROM tbl t 
WHERE FIND_IN_SET(t.value,(SELECT 
          SUBSTRING_INDEX(GROUP_CONCAT(t1.value ORDER BY VALUE DESC),',',3) 
          FROM tbl t1 
          WHERE t1.name = t.name 
          GROUP BY t1.name)) > 0 
ORDER BY t.name,t.value desc 
+0

Przykro mi. Ta działa również niezgodnie z oczekiwaniami. Jeśli mamy trzy 4s dla A w twoim przykładzie na SQL FIDDLE, zestaw wyników będzie wynosił 20,4,4,4 dla A zamiast 20,4,4. – PixelsTech

0

Jeśli Twój wynik nie jest tak ciężki, można napisać procedury przechowywanej (lub anonimowy PL/SQL-block) dla tego problemu, który dokonuje iteracji zestaw wyników i odnajduje bigges trzy przez prosty algorytm porównujący .

0

Spróbuj tego -

CREATE TABLE #list ([name] [varchar](100) NOT NULL, [value] [int] NOT NULL) 
INSERT INTO #list VALUES ('John', 20), ('John', 7), ('John', 7), ('John', 7), ('John', 4); 

WITH cte 
AS (
SELECT NAME 
    ,value 
    ,ROW_NUMBER() OVER (
     PARTITION BY NAME ORDER BY (value) DESC 
     ) RN 
FROM #list 
) 
SELECT NAME 
,value 
FROM cte 
WHERE RN < 4 
ORDER BY value DESC 
0

Działa to dla MS SQL. Powinno być wykonalne w każdym innym dialekcie SQL, który ma zdolność do przypisywania numerów wierszy w grupie lub przez ponad klauzuli (lub equivelant)

if object_id('tempdb..#Data') is not null drop table #Data; 
GO 

create table #data (name varchar(25), value integer); 
GO 
set nocount on; 
insert into #data values ('John', 20); 
insert into #data values ('John', 7); 
insert into #data values ('John', 7); 
insert into #data values ('John', 7); 
insert into #data values ('John', 5); 
insert into #data values ('Jack', 5); 
insert into #data values ('Jane', 30); 
insert into #data values ('Jane', 21); 
insert into #data values ('John', 5); 
insert into #data values ('John', -1); 
insert into #data values ('John', -1); 
insert into #data values ('Jane', 18); 
set nocount off; 
GO 

with D as (
SELECT 
    name 
    ,Value 
    ,row_number() over (partition by name order by value desc) rn 
From 
    #Data 
) 
SELECT Name, Value 
FROM D 
WHERE RN <= 3 
order by Name, Value Desc 

Name Value 
Jack 5 
Jane 30 
Jane 21 
Jane 18 
John 20 
John 7 
John 7 
Powiązane problemy