简体   繁体   中英

In Postgresql, how to select top n percent of rows by a column?

In Postgresql (version 10) , following sql select all rows order by the avg_grade .

-- query - students list, order by average grade,
select s.student_id, s.student_name, avg(ce.grade) as avg_grade
from students as s
       left join course_enrollment as ce on s.student_id = ce.student_id
group by s.student_id
order by avg_grade desc NULLS LAST;

Relevant tables

students:

create table students (
  student_id   bigserial                           not null primary key,
  student_name varchar(200)                        not null,
  created      timestamp default CURRENT_TIMESTAMP not null
);

course_enrollment:

-- create table,
create table course_enrollment
(
  course_id  bigint                              not null,
  student_id bigint                              not null,
  grade      float                               not null,
  created    timestamp default CURRENT_TIMESTAMP not null,
  unique (course_id, student_id)
);

Questions:

  • How to retrieve only the top n% (eg 10%) of rows, whose avg_grade have the highest values?
    Wondering is there a window function to do this, or a sub query is required?

BTW:

I would use a subquery:

select student_id, student_name, avg_grade, rank() over (order by avg_grade desc)
from (select s.student_id,
             s.student_name,
             avg(ce.grade)                                        as avg_grade,
             rank() over (order by avg(ce.grade) desc nulls last) as seqnum,
             count(*) over ()                                     as cnt
      from students s
             left join
           course_enrollment ce
           on s.student_id = ce.student_id
      group by s.student_id
     ) as ce_avg
where seqnum <= cnt * 0.1;

There are other window functions you can use instead, such as NTILE() and PERCENTILE_DISC() . I prefer the direct calculation because it gives more control over how ties are handled.

After trying for a while, got a ugly yet working solution by myself.

select *, rank() over (order by avg_grade desc)
from (
       select s.student_id, s.student_name, avg(ce.grade) as avg_grade
       from students as s
              left join course_enrollment as ce on s.student_id = ce.student_id
       group by s.student_id
       order by avg_grade desc nulls last
     ) as ce_avg
where avg_grade >= (
  select ce_avg.avg_grade
  from (
         select s.student_id, s.student_name, avg(ce.grade) as avg_grade
         from students as s
                left join course_enrollment as ce on s.student_id = ce.student_id
         group by s.student_id
         order by avg_grade desc nulls last
       ) as ce_avg
  limit 1 offset (select (count(*) * 0.1)::int from students) - 1
);

Tips:

  • Can't simply use ( limit %n * total ) or ( top n percent ) anyway. Since the students with the avg_grade = minimal top avg_grade, might be only partly included, Which is not fair.
    The ugly sql above could handle that case, with performance cost.

    Here is an example that shows the differences of the running results with duplication handled or unhandled:

    • Duplication handled - more fair. 处理重复

    • Duplication unhandled - not as fair 未处理重复

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM