
# 代价估算
对于优化器来说，为了能够选出最优的执行计划，需要有可以衡量执行计划的标准。一个直观的比较不同查询计划优劣的标准即执行时间，一般而言，执行时间越短的查询计划越优，反之越差。然而同一个查询计划的执行时间会随着环境变量的不同而变化，如：机器配置（CPU 的频率、内存的大小和磁盘的介质等）会对实际执行性能产生影响，那么就需要一个无量纲的值来代替时间，在GaussDB中使用代价（Cost）这一概念来判断不同执行计划的优劣。代价是一个用于评判执行计划优劣的绝对指标，是一个估算值，而每个算子的代价都有其对应的计算公式，该公式被称为代价模型。通过将统计信息作为输入，代入到代价模型中，即可计算出相应算子的代价，可抽象为如下公式：
![](https://support.huaweicloud.com/distributed-devg-v10-gaussdb/figure/zh-cn_image_0000002620640657.png "点击放大")
如上述公式所示，每个算子的代价模型是对其执行逻辑的抽象，对于遇到的每一个可被作为基本计算单位量的操作（如访问页面、表达式运算等），都需要将其以代价为单位进行去量纲化的表示。以顺序扫描为例，其执行逻辑为：从文件中顺序读取页面，在没有谓词的情况下，顺序扫描的代价即扫描所有页面的代价，这里需要对顺序访问单个页面操作的代价进行定义，并作为计算顺序扫描多个页面的单位量。
为定义不同影响因素的评估基准，把一些影响执行效率的关键因素提取出来，抽象成标准值，所有路径的代价都基于标准值去计算。例如，将CPU频率参数、磁盘页面I/O方式、内存I/O方式进行标准化作为代价估算的基准，下面对磁盘I/O基准代价与CPU基准代价进行说明。
- 磁盘页面I/O的基准代价 磁盘页面的I/O方式通常包含：顺序读写和随机读写。以传统的机械硬盘为例，因为机械硬盘在读取数据的时候需要进行寻道，寻道要花费一定时间，因而随机读写就会有大量时间浪费到寻道上，顺序读写则能够节省这些寻道时间，带来I/O性能提升。另外磁盘中通常有缓存，顺序读写时磁盘可以预取数据，提升缓存命中率，相比随机读写也能带来性能的提升。
  基于上述读写方式的不同，需要对顺序读写和随机读写定义不同的代价基准，在GaussDB的早期版本中，定义顺序读写的默认值是1.0，随机读写的默认值是4.0。但是随着固态硬盘的普及，固态硬盘随机读写的性能已经大大提高，该基准值已经不再准确，因此在最新的版本中，已经将随机读写的默认值调整为1.1。在不同的生产环境中这两个基准值也并非一成不变，因此GaussDB支持GUC参数根据具体环境进行修改。调整顺序读写页面基准值的GUC参数是seq_page_cost，调整随机读写页面基准值的GUC参数是random_page_cost。
  此外，由于数据库本身有缓存系统，如果数据已经在缓存中就可以避免磁盘I/O的开销，这样在进行代价评估时不能简单对每个页面都计算磁盘I/O代价。所以，在进行代价评估时还需要对缓存页面数做一个预估，该值由GUC参数effective_cache_size控制。
  

- 元组与约束条件的CPU基准代价 数据从磁盘页面中被读取出来后，还需要以元组的形式呈现出来，因此需要依赖CPU的计算资源，这时会产生CPU的计算代价。由于元组的组织结构差异、表达式计算复杂度差异等原因，其消耗的计算资源也是不同的，因此，在GaussDB中主要区分了普通元组、索引元组以及对投影、表达式等不同数据组织形式的CPU计算代价，并且支持通过GUC参数调整这些基准代价：
  ```
  控制普通元组CPU基准的GUC参数：cpu_tuple_cost，默认值为0.01。
  控制索引元组CPU基准的GUC参数：cpu_index_tuple_cost，默认值为0.005。
  控制表达式CPU基准的GUC参数：cpu_operator_cost，默认值为0.0025。
  ```
  除了上述基准代价，GaussDB在进行代价比较时通常还要关注启动代价和总体代价。启动代价指的是SQL语句从开始执行到执行器查询获取到第一条元组的代价，而总代价则是指SQL语句从开始执行到结束的所有代价。因此，有如下公式：
  ```
  总代价（total_cost）= 启动代价(startup_cost) + 运行代价(run_cost)
  ```
  对于不同的扫描路径和连接路径，其启动代价通常不同，因此需要单独进行评估，以下面的场景为例，建表语句如下：
  ```
  gaussdb=# CREATE TABLE t1(a INT, b INT, c INT, d INT);
  NOTICE:  The 'DISTRIBUTE BY' clause is not specified. Using 'a' as the distribution column by default.
  HINT:  Please use 'DISTRIBUTE BY' clause to specify suitable data distribution column.
  CREATE TABLE
  gaussdb=# CREATE INDEX idx_t1_a ON t1(a);
  CREATE INDEX
  gaussdb=# INSERT INTO t1 (a, b, c) VALUES (generate_series(1, 5000), generate_series(1, 5000), floor(random()*10));
  INSERT 0 5000
  gaussdb=# ANALYZE t1;
  ANALYZE
  gaussdb=# SET explain_perf_mode=normal;
  SET
  ```
  执行相同SQL语句，生成不同的路径计划：
  ```
  gaussdb=# EXPLAIN SELECT * FROM t1 WHERE t1.a >= 100 ORDER BY a;
                                    QUERY PLAN
  ------------------------------------------------------------------------------
   Streaming (type: GATHER)  (cost=4.00..252.12 rows=4900 width=16)
     Merge Sort Key: a
     Node/s: All datanodes
     ->  Index Scan using idx_t1_a on t1  (cost=0.00..47.99 rows=4900 width=16)
           Index Cond: (a >= 100)
  (5 rows)
  gaussdb=# EXPLAIN SELECT /*+tablescan(t1)*/* FROM t1 WHERE t1.a >= 100 ORDER BY a;
                               QUERY PLAN
  --------------------------------------------------------------------
   Streaming (type: GATHER)  (cost=119.99..324.19 rows=4899 width=16)
     Merge Sort Key: a
     Node/s: All datanodes
     ->  Sort  (cost=115.99..120.07 rows=4899 width=16)
           Sort Key: a
           ->  Seq Scan on t1  (cost=0.57..28.84 rows=4900 width=16)
                 Filter: (a >= 100)
  (7 rows)
  gaussdb=# DROP TABLE t1;
  DROP TABLE
  gaussdb=# RESET explain_perf_mode;
  RESET
  ```
  上述示例中，可以看到同一条SQL语句在生成不同路径计划时启动代价的差异。第一次执行时选择了索引路径，因为这条语句需要排序，索引路径底层使用B+树能够快速定位到目标数据并且可以保证有序性，其启动代价为0。第二次执行选择的是顺序扫描路径，需要遍历磁盘找到第一条有效数据，启动代价为0.57，再叠加排序操作导致其整体代价高于索引扫描路径。对于本场景中的示例在默认情况下，数据库会根据计划选择策略淘汰掉顺序扫描+排序操作的计划，这里为了便于对比使用tablescan的HINT强制选择了顺序扫描计划。
  结合上述示例，顺序扫描路径的代价计算公式为：
  ```
  startup_cost（启动代价） = (cpu_tuple_cost+cpu_expr_cost) *M<tuple>+seq_page_cost*M<page>
  run_cost（运行代价）=cpu_run_cost + disk_run_cost=(cpu_tuple_cost+cpu_expr_cost) *N<tuple>+seq_page_cost*N<page>
  ```
  其中 seq_page_cost、 cpu_tuple_cost 的值来自前面基准代价的参数配置，本例中分别为：1.0和0.01，N\<tuple\> 和 N\<page\> 分别表示表中的元组总数与页面总数，可以使用以下SQL查询获取：
  ```
  gaussdb=# SELECT reltuples, relpages FROM pg_class WHERE relname='t1';
   reltuples | relpages
  -----------+----------
        5000 |       24
  (1 row)
  ```
  cpu_expr_cost的值来自WHERE条件中的所有表达式计算代价和，本例中的表达式只包含t1.a \>= 100 条件，t1表的a列是INT类型，表达式计算函数为GE（大于等于），因此可以通过如下SQL获取到其计算基准代价系数：
  ```
  gaussdb=# SELECT proname, procost FROM pg_proc WHERE proname = 'int4ge';
   proname | procost
  ---------+---------
   int4ge  |       1
  (1 row)
  ```
  表达式类型的基准代价由GUC参数cpu_operator_cost控制，本例中的默认值是0.0025。将这些信息带入公式计算得到如下结果：
  ```
  run_cost（运行代价）= (cpu_tuple_cost+cpu_expr_cost) *N<tuple>+seq_page_cost*N<page> 
                                    = (0.01+1x0.0025)*5000+1.0*24
                                    = 86.5
  ```
  但是上面的示例中，实际代价显示的却是28.84, 这是因为用例场景的数据库有三个DN节点，可以使用如下SQL查询CN和DN的节点信息：
  ```
  gaussdb=# SELECT node_name, node_type, hostis_primary FROM PGXC_NODE;
    node_name   | node_type | hostis_primary
  --------------+-----------+----------------
   coordinator1 | C         | t
   datanode1    | D         | t
   datanode2    | D         | t
   datanode3    | D         | t
  (4 rows)
  ```
  如上查询结果所示，t1表的数据均匀分布在三个数据节点上，因此对于每个DN节点的代价需要取平均值，即：86.5/3=28.84。
  启动代价的计算方式如下，因为这里选择的是顺序扫描的路径，即遍历整个数据表，根据启动代价的定义并且结合数据均匀分布假设，可以认为这5000条数据均匀的分布在24个页面上，获取到第一条有用数据的代价为：
  ```
  startup_cost（启动代价）= (cpu_tuple_cost+cpu_expr_cost) *M<tuple>+seq_page_cost*M<page> 
                                          = (0.01+1*0.0025) *100+1.0*(24/5000*100)
                                          = 1.25+0.48
                                          = 1.73
  ```
  同理，因为数据均匀分布在3个DN节点上，因此对于一个节点而言，其启动代价为1.73/3=0.57。
  因为顺序扫描本身就要扫描整个表，run_cost的部分计算实际已经包含了启动代价的值，所以可以看到总代价和执行代价是一致的。
  
 
