视图是否对PostgreSQL的性能有害?


45

以下是关于数据库设计的书的摘录(数据库设计ISBN:0-7645-7490-6):

使用视图的危险是针对视图过滤查询,期望读取很大表的很小部分。应该在视图内完成所有筛选,因为在视图中的查询完成执行之后,将对视图本身进行任何筛选。视图通常对于加快开发过程很有用,但是从长远来看,它可能会完全破坏数据库性能。

以下是PostgreSQL 9.5文档的摘录:

充分利用视图是良好的SQL数据库设计的关键方面。通过视图,您可以在一致的接口后面封装表结构的详细信息,该表结构的详细信息可能会随着应用程序的发展而变化。

这两个来源似乎相互矛盾(“不使用视图设计”与“不使用视图设计”)。

但是,在PG视图中是使用规则系统实现的。因此,可能(这是我的问题),针对视图的任何筛选都将重写为视图中的筛选器,从而导致针对基础表的单个查询执行。

我的解释正确吗,PG将WHERE子句组合进了视图?还是单独运行它们?有没有简短,自成体系,正确(可编译)的示例?


我认为这个问题是不对的,因为两个消息来源都没有谈论同一件事。第一个与从视图中查询有关,然后应用过滤器:SELECT * FROM my_view WHERE my_column = 'blablabla';。第二个与使用视图使数据模型对使用它的应用程序透明。第一个来源指出您将过滤器包括在WHERE my_column = 'blablabla'视图定义中,因为这样可以制定更好的执行计划。
EAmez

Answers:


49

这本书是错的。

从视图中进行选择与运行基础SQL语句完全一样快或慢-您可以轻松地使用进行检查explain analyze

Postgres优化器(以及许多其他现代DBMS的优化器)将视图上的谓词下推到实际的view语句中-只要这是一个简单的语句(同样可以使用来验证explain analyze)。

我认为,关于性能的“不良声誉”源于您过度使用视图并开始构建使用使用视图的视图的视图。通常,与没有视图的手工定制的语句相比,这导致语句执行的功能过多,例如因为不需要某些中间表。在几乎所有情况下,优化器都不够聪明,无法删除那些不需要的表/联接或在多个视图级别下推谓词(其他DBMS也是如此)。


3
给出了一些建议的反回答,您可能想对什么是简单的陈述进行一些阐述。
RDFozz

您可以解释如何使用该explain analyze语句吗?
达斯汀·米歇尔斯


19

给你举个例子什么@a_horse解释

Postgres实现了信息模式,该信息模式由(有时很复杂)视图组成,这些视图以标准化形式提供有关DB对象的信息。这既方便又可靠-并且比直接访问Postgres目录表要昂贵得多。

一个非常简单的示例,
从信息模式中获取表的所有可见列:

SELECT column_name
FROM   information_schema.columns
WHERE  table_name = 'big'
AND    table_schema = 'public';

...从系统目录中:

SELECT attname
FROM   pg_catalog.pg_attribute
WHERE  attrelid = 'public.big'::regclass
AND    attnum > 0
AND    NOT attisdropped;

将两者的查询计划和执行时间进行比较EXPLAIN ANALYZE

  • 第一个查询基于view information_schema.columns,该视图连接到我们根本不需要的多个表。

  • 第二个查询仅扫描一个pg_catalog.pg_attribute,因此速度更快。(但是在普通数据库中,第一个查询仍然只需要几毫秒。)

细节:


7

编辑:

对于道歉,我需要撤回断言,即接受的答案并不总是正确的-它指出视图始终与编写为子查询的同一事物相同。我认为这是无可争辩的,我想我现在知道我的情况了。

我现在还认为,对原始问题有更好的答案。

最初的问题是关于是否应该指导使用视图的实践(例如,与在例程中重复SQL可能需要维护两次或更多次相反)。

我的回答是“如果您的查询使用窗口函数或其他导致优化器在成为子查询时使查询区别对待的原因,则不会,因为创建子查询(无论是否表示为视图)的行为可能会降低性能。如果在运行时使用参数过滤。

我的窗口函数不需要太复杂。为此的解释计划:

SELECT DISTINCT ts.train_service_key,
            pc.assembly_key,
            count(*) OVER 
              (PARTITION BY ts.train_service_key) AS train_records
FROM staging.train_service ts
   JOIN staging.portion_consist pc 
     USING (ds_code, train_service_key)
WHERE assembly_key = '185132';

比这便宜得多:

SELECT *
FROM (SELECT DISTINCT ts.train_service_key,
            pc.assembly_key,
            count(*) OVER
              (PARTITION BY ts.train_service_key) AS train_records
FROM staging.train_service ts
   JOIN staging.portion_consist pc
     USING (ds_code, train_service_key)) AS query
WHERE assembly_key = '185132';

希望这更加具体和有用。

根据我最近的经验(使我能够找到此问题),上面接受的答案在所有情况下都不正确。我有一个相对简单的查询,其中包括一个窗口函数:

SELECT DISTINCT ts.train_service_key,
                pc.assembly_key,
                dense_rank() OVER (PARTITION BY ts.train_service_key
                ORDER BY pc.through_idx DESC, pc.first_portion ASC,
               ((CASE WHEN (NOT ts.primary_direction)
                 THEN '-1' :: INTEGER
                 ELSE 1
                 END) * pc.first_seq)) AS coach_block_idx
FROM (staging.train_service ts
JOIN staging.portion_consist pc USING (ds_code, train_service_key))

如果我添加此过滤器:

where assembly_key = '185132'

我得到的解释计划如下:

QUERY PLAN
Unique  (cost=11562.66..11568.77 rows=814 width=43)
  ->  Sort  (cost=11562.66..11564.70 rows=814 width=43)
    Sort Key: ts.train_service_key, (dense_rank() OVER (?))
    ->  WindowAgg  (cost=11500.92..11523.31 rows=814 width=43)
          ->  Sort  (cost=11500.92..11502.96 rows=814 width=35)
                Sort Key: ts.train_service_key, pc.through_idx DESC, pc.first_portion, ((CASE WHEN (NOT ts.primary_direction) THEN '-1'::integer ELSE 1 END * pc.first_seq))
                ->  Nested Loop  (cost=20.39..11461.57 rows=814 width=35)
                      ->  Bitmap Heap Scan on portion_consist pc  (cost=19.97..3370.39 rows=973 width=38)
                            Recheck Cond: (assembly_key = '185132'::text)
                            ->  Bitmap Index Scan on portion_consist_assembly_key_index  (cost=0.00..19.72 rows=973 width=0)
                                  Index Cond: (assembly_key = '185132'::text)
                      ->  Index Scan using train_service_pk on train_service ts  (cost=0.43..8.30 rows=1 width=21)
                            Index Cond: ((ds_code = pc.ds_code) AND (train_service_key = pc.train_service_key))

这是使用火车服务表上的主键索引和part_consist表上的非唯一索引。它在90毫秒内执行。

我创建了一个视图(将其粘贴在此处非常清楚,但这实际上是视图中的查询):

CREATE OR REPLACE VIEW staging.v_unit_coach_block AS
SELECT DISTINCT ts.train_service_key,
            pc.assembly_key,
            dense_rank() OVER (PARTITION BY ts.train_service_key
              ORDER BY pc.through_idx DESC, pc.first_portion ASC, (
                (CASE
              WHEN (NOT ts.primary_direction)
                THEN '-1' :: INTEGER
              ELSE 1
              END) * pc.first_seq)) AS coach_block_idx
 FROM (staging.train_service ts
  JOIN staging.portion_consist pc USING (ds_code, train_service_key))

当我使用相同的过滤器查询此视图时:

select * from staging.v_unit_coach_block
where assembly_key = '185132';

这是解释计划:

QUERY PLAN
Subquery Scan on v_unit_coach_block  (cost=494217.13..508955.10     rows=3275 width=31)
Filter: (v_unit_coach_block.assembly_key = '185132'::text)
 ->  Unique  (cost=494217.13..500767.34 rows=655021 width=43)
    ->  Sort  (cost=494217.13..495854.68 rows=655021 width=43)
          Sort Key: ts.train_service_key, pc.assembly_key, (dense_rank() OVER (?))
          ->  WindowAgg  (cost=392772.16..410785.23 rows=655021 width=43)
                ->  Sort  (cost=392772.16..394409.71 rows=655021 width=35)
                      Sort Key: ts.train_service_key, pc.through_idx DESC, pc.first_portion, ((CASE WHEN (NOT ts.primary_direction) THEN '-1'::integer ELSE 1 END * pc.first_seq))
                      ->  Hash Join  (cost=89947.40..311580.26 rows=655021 width=35)
                            Hash Cond: ((pc.ds_code = ts.ds_code) AND (pc.train_service_key = ts.train_service_key))
                            ->  Seq Scan on portion_consist pc  (cost=0.00..39867.86 rows=782786 width=38)
                            ->  Hash  (cost=65935.36..65935.36 rows=1151136 width=21)
                                  ->  Seq Scan on train_service ts  (cost=0.00..65935.36 rows=1151136 width=21)

这将对两个表进行全面扫描,并花费17s。

在我遇到这个问题之前,我一直在PostgreSQL中自由使用视图(已经理解了公认的答案中表达的广泛持有的观点)。如果需要预聚合过滤,我特别避免使用视图,为此我将使用集返回功能。

我也知道PostgreSQL中的CTE是通过设计严格进行单独评估的,因此我不会像在SQL Server中那样使用它们,例如,它们似乎已作为子查询进行了优化。

因此,我的回答是,在某些情况下,视图的性能可能不完全符合其所基于的查询,因此建议您格外小心。我正在使用基于PostgreSQL 9.6.6的Amazon Aurora。


2
请注意另一个答案注意事项-“ 只要这是一个简单的语句 ”。
RDFozz

附带说明一下,CASE WHEN (NOT ts.primary_direction) THEN '-1' :: INTEGER ELSE 1 END不必要使查询变慢,因为您最好在顺序中再写两个条件。
埃文·卡罗尔

@EvanCarroll我为此苦了一段时间。刚刚发现,将CASE提升到一个水平的速度CASE WHEN (NOT ts.primary_direction) THEN dense_rank() OVER (PARTITION BY ts.train_service_key ORDER BY pc.through_idx DESC, pc.first_portion ASC, pc.first_seq DESC) ELSE dense_rank() OVER (PARTITION BY ts.train_service_key ORDER BY pc.through_idx DESC, pc.first_portion ASC, pc.first_seq ASC) END AS coach_block_idx

这也不是一个好主意..您在这里遇到了一些问题。我的意思是,最大的观点是您的视图没有任何意义,并且由于您的使用,它会执行不同的操作,dense_rank()因此这实际上不是性能问题。
埃文·卡罗尔

1
@EvanCarroll您的评论促使我自己到达那里(因此,我编辑过的答案)。谢谢。
enjayaitch

-1

(我是视图的忠实拥护者,但是您在这里必须非常小心地使用PG,我希望鼓励大家也普遍使用PG中的视图,以提高查询/代码的易懂性和可维护性)

实际上,可悲的是(警告:)在Postgres中使用视图会导致我们真正的问题,并严重降低我们的性能,具体取决于我们在其中使用的功能:-((至少在v10.1中)。(其他情况并非如此) Oracle等现代数据库系统。)

因此,可能(这是我的问题)针对视图的任何筛选...导致针对基础表的单个查询执行。

(取决于您的确切意思-否-可能会实现中间临时表,而您可能不想成为中间临时表,或者未将谓词推到何处...)

我至少知道两个主要的“功能”,这使我们从Oracle迁移到Postgres的过程中感到沮丧,因此我们不得不在项目中放弃PG:

  • CTEwith-clause子查询/ 公共表表达式)(通常)对于结构更复杂的查询(即使在较小的应用程序中)也很有用,但在PG中,通过设计将其实现为“隐藏”优化器提示(例如生成非索引临时表),并且因此违反声明式SQL(对我和其他许多人而言很重要)的概念Oracle docu):例如

    • 简单查询:

      explain
      
        select * from pg_indexes where indexname='pg_am_name_index'
      
      /* result: 
      
      Nested Loop Left Join  (cost=12.38..26.67 rows=1 width=260)
        ...
        ->  Bitmap Index Scan on pg_class_relname_nsp_index  (cost=0.00..4.29 rows=2 width=0)
                                               Index Cond: (relname = 'pg_am_name_index'::name)
        ...
      */
    • 使用某些CTE重写:

      explain
      
        with 
      
        unfiltered as (
          select * from pg_indexes
        ) 
      
        select * from unfiltered where indexname='pg_am_name_index'
      
      /* result:
      
      CTE Scan on unfiltered  (cost=584.45..587.60 rows=1 width=288)
         Filter: (indexname = 'pg_am_name_index'::name)
         CTE unfiltered
           ->  Hash Left Join  (cost=230.08..584.45 rows=140 width=260)  
      ...
      */
    • 有关讨论等的更多资源:https : //blog.2ndquadrant.com/postgresql-ctes-are-optimization-fences/

  • 具有over-statement的窗口函数可能不可用 (通常在视图中使用,例如,基于更复杂的查询作为报告的来源)


我们针对- with条款的解决方法

我们将使用特殊的前缀将所有“内联视图”转换为真实视图,以使它们不会弄乱视图的列表/命名空间,并且可以轻松地与原始“外部视图”相关:-/


我们针对窗口功能的解决方案

我们使用Oracle数据库成功实现了它。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.