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

£可爱£侵袭症+ 提交于 2021-01-27 21:04:18

问题


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% (e.g 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:

  • This is different from Postgresql : How do I select top n percent(%) entries from each group/category
    Because that one want top n% in each group, thus it could use partitions in the window functions.
    But this one want the top n% overall, thus group by is required.

回答1:


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.




回答2:


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



来源:https://stackoverflow.com/questions/54957288/in-postgresql-how-to-select-top-n-percent-of-rows-by-a-column

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!