【问题标题】:Improving Subquery performance in Postgres提高 Postgres 中的子查询性能
【发布时间】:2013-05-02 21:56:47
【问题描述】:

我的数据库中有这两个表

  Student Table                   Student Semester Table
| Column     : Type     |       | Column     : Type     |
|------------|----------|       |------------|----------|
| student_id : integer  |       | student_id : integer  |      
| satquan    : smallint |       | semester   : integer  |
| actcomp    : smallint |       | enrolled   : boolean  | 
| entryyear  : smallint |       | major      : text     |
|-----------------------|       | college    : text     |
                                |-----------------------|

其中 student_id 是学生表中的唯一键,以及学生学期表中的外键。学期整数只是第一学期的 1,第二学期的 2,依此类推。

我正在查询想要按入学年份(有时按他们的SAT和/或ACT成绩)获取学生的查询,然后从学生学期表中获取所有这些学生的相关数据。

目前,我的查询如下所示:

SELECT * FROM student_semester
WHERE student_id IN(
    SELECT student_id FROM student_semester
    WHERE student_id IN(
        SELECT student_id FROM student WHERE entryyear = 2006
    ) AND college = 'AS' AND ...
)
ORDER BY student_id, semester;

但是,当我选择约 1k 个学生时,这会导致运行相对较长的查询(400 毫秒)。根据执行计划,大部分时间都花在了哈希连接上。为了改善这一点,我在 student_semester 表中添加了 satquan、actpcomp 和 entryyear 列。这将运行查询的时间减少了约 90%,但会导致大量冗余数据。有没有更好的方法来做到这一点?

这些是我目前拥有的索引(以及 student_id 上的隐式索引):

CREATE INDEX act_sat_entryyear ON student USING btree (entryyear, actcomp, sattotal)
CREATE INDEX student_id_major_college ON student_semester USING btree (student_id, major, college)

查询计划

QUERY PLAN
Hash Join  (cost=17311.74..35895.38 rows=81896 width=65) (actual time=121.097..326.934 rows=25680 loops=1)
  Hash Cond: (public.student_semester.student_id = public.student_semester.student_id)
  ->  Seq Scan on student_semester  (cost=0.00..14307.20 rows=698820 width=65) (actual time=0.015..154.582 rows=698820 loops=1)
  ->  Hash  (cost=17284.89..17284.89 rows=2148 width=8) (actual time=121.062..121.062 rows=1284 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 51kB
        ->  HashAggregate  (cost=17263.41..17284.89 rows=2148 width=8) (actual time=120.708..120.871 rows=1284 loops=1)
              ->  Hash Semi Join  (cost=1026.68..17254.10 rows=3724 width=8) (actual time=4.828..119.619 rows=6184 loops=1)
                    Hash Cond: (public.student_semester.student_id = student.student_id)
                    ->  Seq Scan on student_semester  (cost=0.00..16054.25 rows=42908 width=4) (actual time=0.013..109.873 rows=42331 loops=1)
                          Filter: ((college)::text = 'AS'::text)
                    ->  Hash  (cost=988.73..988.73 rows=3036 width=4) (actual time=4.801..4.801 rows=3026 loops=1)
                          Buckets: 1024  Batches: 1  Memory Usage: 107kB
                          ->  Bitmap Heap Scan on student  (cost=71.78..988.73 rows=3036 width=4) (actual time=0.406..3.223 rows=3026 loops=1)
                                Recheck Cond: (entryyear = 2006)
                                ->  Bitmap Index Scan on student_act_sat_entryyear_index  (cost=0.00..71.03 rows=3036 width=0) (actual time=0.377..0.377 rows=3026 loops=1)
                                      Index Cond: (entryyear = 2006)
Total runtime: 327.708 ms

我误认为查询中没有 Seq Scan。我认为由于符合大学条件的行数,Seq Scan 正在完成;当我将其更改为具有较少学生的索引时,将使用索引。来源:https://stackoverflow.com/a/5203827/880928

查询 entryyear 列包括学生学期表

SELECT * FROM student_semester
WHERE student_id IN(
    SELECT student_id FROM student_semester
    WHERE entryyear = 2006 AND collgs = 'AS'
) ORDER BY student_id, semester;

查询计划

Sort  (cost=18597.13..18800.49 rows=81343 width=65) (actual time=72.946..74.003 rows=25680 loops=1)
  Sort Key: public.student_semester.student_id, public.student_semester.semester
  Sort Method: quicksort  Memory: 3546kB
  ->  Nested Loop  (cost=9843.87..11962.91 rows=81343 width=65) (actual time=24.617..40.751 rows=25680 loops=1)
        ->  HashAggregate  (cost=9843.87..9845.73 rows=186 width=4) (actual time=24.590..24.836 rows=1284 loops=1)
              ->  Bitmap Heap Scan on student_semester  (cost=1612.75..9834.63 rows=3696 width=4) (actual time=10.401..23.637 rows=6184 loops=1)
                    Recheck Cond: (entryyear = 2006)
                    Filter: ((collgs)::text = 'AS'::text)
                    ->  Bitmap Index Scan on entryyear_act_sat_semester_enrolled_cumdeg_index  (cost=0.00..1611.82 rows=60192 width=0) (actual time=10.259..10.259 rows=60520 loops=1)
                          Index Cond: (entryyear = 2006)
        ->  Index Scan using student_id_index on student_semester  (cost=0.00..11.13 rows=20 width=65) (actual time=0.003..0.010 rows=20 loops=1284)
              Index Cond: (student_id = public.student_semester.student_id)
Total runtime: 74.938 ms

【问题讨论】:

  • 请使用explain analyze 和表上定义的任何索引发布执行计划。有关在此处发布此类问题的更多信息:wiki.postgresql.org/wiki/Slow_Query_Questions
  • 当要求性能优化时,您还必须提供您的 Postgres 版本。应该不言而喻。阅读tag info for postgresql-performance
  • @ErwinBrandstetter 我没有发布 Postgres 的版本,因为我认为这更像是一个一般的数据库架构/查询策略问题,但我会添加版本以及查询计划。
  • 您想要在 2006 年进入 AS 的学生还是在 2006 年进入(在任何大学)在某个时间进入 AS 的学生?对于您的上一个版本,我建议您尝试将IN 替换为类似的EXISTS(请参阅下面的答案)student_id, entry_year 上添加索引。
  • 在添加一些索引之前,我建议在表中添加主键约束。对于显然是 {student_id} 和 student_semester probably {student_id, semester} 的学生,但这从问题中并不清楚。另外:entryyear 的特异性可能太低而无法进行索引扫描(除非您拥有大约 20 年以上的数据)

标签: sql postgresql postgresql-9.1


【解决方案1】:

您查询的干净版本是

select ss.*
from
    student s
    inner join
    student_semester ss using(student_id)
where
    s.entryyear = 2006
    and exists (
        select 1
        from student_semester
        where
            college = 'AS'
            and student_id = s.student_id
    )
order by ss.student_id, semester

【讨论】:

  • 如果有涵盖 student.entryyear 和 student_semester.college 以及 student_semester.semester 的索引,我希望这会表现良好。另一方面,如果 student_semester.semester 中只有 2 个值,那么 可能会很烦人。 EXPLAIN ANALYZE 将讲述整个故事。
  • 这不是同一个查询。这仅返回来自“AS”学院的行。原始查询返回曾在“AS”大学就读的学生的记录。
  • @Gordon 我不明白你评论中的谁在'AS'大学
  • @ClodoaldoNeto 该查询旨在查找至少一个学期就读于“AS”大学的学生。根据学期,学生可以在不同的大学。
  • 我跑了这个。它的性能与原始查询差不多。我在这里发布了解释分析:pastebin.com/u4fneiQT
【解决方案2】:

执行查询的另一种方法是使用窗口函数。

select t.*  -- Has the extra NumMatches column.  To eliminate it, list the columns you want
from (select ss.*,
             sum(case when ss.college = 'AS' and s.entry_year = 206 then 1 else 0 end) over
                  (partition by student_id) as NumMatches
      from student_semester ss join
           student s
           on ss.student_id = s.student_id
    ) t
where NumMatches > 0;

窗口函数通常比加入聚合更快,所以我怀疑这可能会表现良好。

【讨论】:

  • 这个查询实际上比原始查询慢得多(几乎整整 1 秒)。完成大约需要 1 秒钟。根据查询计划,它会分别扫描表中的每一行 3 次(即使它声称正在使用索引)。
  • @cmorse 。 . .有趣的。我很高兴你做了测试。我认为查询的不同之处在于,这是对所有数据而不是子集计算NumMatches。聚合的选择性克服了(我认为是)窗口函数稍好的性能。
  • 感谢您发布此查询。我从来没有对窗口函数做过很多。看到它完成很有趣。
【解决方案3】:

看来,你想要的是 2006 年入学并曾经上过 AS 大学的学生。

版本一。

SELECT sem.*
FROM student s JOIN student_semester sem USING (student_id)
WHERE s.entry_year=2006
     AND student_id IN (SELECT student_id 
                        FROM student_semester s2 WHERE s2.college='AS')
     AND /* other criteria */
ORDER BY sem.student_id, semester;

第二版

SELECT sem.*
FROM student s JOIN student_semester sem USING (student_id)
WHERE s.entry_year=2006
     AND EXISTS 
         (SELECT 1 FROM student_semester s2 
          WHERE s2.student_id = s.student_id AND s2.college='AS')
          -- CREATE INDEX foo on student_semester(student_id, college);
     AND /* other criteria */
ORDER BY sem.student_id, semester;

我希望两者都很快,但它们是否比另一个表现更好(或完全相同的计划)是 PG 的谜。

[编辑]这是一个没有半连接的版本。我不指望它会很好地工作,因为每次学生在 AS 时,它都会给出多次点击。

SELECT DISTINCT ON ( /* PK of sem */ )
FROM student s 
   JOIN student_semester sem USING (student_id) 
   JOIN student_semester s2  USING (student_id)
WHERE s.entry_year=2006
   AND s2.college='AS'
ORDER BY sem.student_id, semester;

【讨论】:

  • 这些实际上都没有比原始查询更好。以下是查询计划。第一版:pastebin.com/zXafx0ct,第二版:pastebin.com/vntd96dU
  • 这很令人失望。我在编辑中添加了另一种可能性。顺便说一句,student_semester 上的索引是什么?
猜你喜欢
  • 2016-02-18
  • 1970-01-01
  • 1970-01-01
  • 2021-11-14
  • 1970-01-01
  • 2017-06-18
  • 2021-12-18
  • 1970-01-01
  • 2013-08-03
相关资源
最近更新 更多