物化视图,简单来说就是预先存储查询结果的一种数据库对象。众所周知 Apache Doris 是一款MPP分析型数据仓库,仅需压秒级就可以快速响应用户的查询需求。物化视图的出现,主要是为了满足用户的需求,对那些频繁使用的重复性的查询,可以提前将查询结果存储在一张特殊的表中,当用户再次使用相同查询需求时,可以自动命中预先处理好的数据,因而能大幅度提升查询效率。
Apache Doris有3种数据模型:唯一主键模型,聚合模型和明细模型。在物化视图出现之前,Doris是采用Rollup来做预聚合提升查询效率的。但Rollup有一个明显的缺点就是它无法对明细数据模型做预聚合操作。物化视图弥补了Rollup的不足,它可以针对Doris的任意模型做预聚合操作,使用范围更加广泛,可以说物化视图是Rollup的升级版。同时Doris自动维护物化视图的数据,无论是新的导入,还是删除操作都能保证base 表和物化视图表的数据一致性。无需任何额外的人工维护成本。
物化视图既能明细数据的任意维度分析,也能快速的对固定维度进行分析查询。当查询需求只包含很少列或行时,或者查询base表需要做聚合等操作时都可以使用物化视图来提升你的查询效率。
用户对base表创建完物化视图之后,SQL请求依然是对base表来进行操作的。但Doris内部会自动判断该SQL请求在是否能够命中物化视图,如果无法命中base表的所有物化视图,则SQL还是会去查询base表;相反,当SQL命中物化视图时,查询结果就会直接从物化视图的预聚合表种获取。那么SQL请求到底是如何命中物化视图的呢?
首先,是对物化视图候选集合的选择。只要SQL查询结果能从物化视图的表中获取到(部分行,部分列或者部分行列的聚合等)的数据,那么它就是一个候选集。
其次就是从候选集合中选择一个最优的物化视图(消耗时间最短的)来返回请求结果。
候选集的选择主要分为4步:
第一步:判断SQL请求where中的谓词数据是否能够从物化视图中获取到;
第二步:判断SQL请求中的分组列是否为物化视图列的子集;
第三步:判断SQL请求中的聚合列是否为物化视图列的子集;
第四步:判断SQL请求中的查询列是否为物化视图列的子集。
当物化视图中的所有列都满足SQL查询请求的标准时,我们就可以判断它为一个候选集。
当候选集合出现后,就需要根据前缀索引是否能匹配到,以及聚合程度的高低来选出一个最优的物化视图。
目前物化视图创建语句支持的聚合函数有:
BITMAP_UNION(TO_BITMAP(COLUMN))
column 列的类型只能是整数(largeint也不支持), 或者 BITMAP_UNION(COLUMN)
且 base 表为 AGG 模型。HLL_UNION(HLL_HASH(COLUMN))
column 列的类型不能是 DECIMAL , 或者 HLL_UNION(COLUMN)
且 base 表为 AGG 模型。在doris_sql_audit
上创建一个名为t_select_counts_per_min
的物化视图,该物化视图主要是来分析每秒的查询次数。
CREATE materialized view t_select_counts_per_min as SELECT time , sum(is_query) select_counts FROM doris_sql_audit GROUP BY time
使用desc doris_sql_audit all
来查看doris_sql_audit
的物化视图
mysql> desc `doris_sql_audit` all;
+----------------------------------+---------------+------------------------------------------------+-------------+------+-------+---------+-------+---------+
| IndexName | IndexKeysType | Field | Type | Null | Key | Default | Extra | Visible |
+----------------------------------+---------------+------------------------------------------------+-------------+------+-------+---------+-------+---------+
| doris_sql_audit | DUP_KEYS | query_id | VARCHAR(48) | Yes | true | NULL | | true |
| | | time | DATETIME | No | false | NULL | NONE | true |
| | | client_ip | VARCHAR(32) | Yes | false | NULL | NONE | true |
| | | user | VARCHAR(64) | Yes | false | NULL | NONE | true |
| | | db | VARCHAR(96) | Yes | false | NULL | NONE | true |
| | | state | VARCHAR(8) | Yes | false | NULL | NONE | true |
| | | query_time | BIGINT | Yes | false | NULL | NONE | true |
| | | scan_bytes | BIGINT | Yes | false | NULL | NONE | true |
| | | scan_rows | BIGINT | Yes | false | NULL | NONE | true |
| | | return_rows | BIGINT | Yes | false | NULL | NONE | true |
| | | stmt_id | INT | Yes | false | NULL | NONE | true |
| | | is_query | TINYINT | Yes | false | NULL | NONE | true |
| | | frontend_ip | VARCHAR(32) | Yes | false | NULL | NONE | true |
| | | stmt | TEXT | Yes | false | NULL | NONE | true |
| | | cpu_time_ms | BIGINT | Yes | false | NULL | NONE | true |
| | | sql_hash | VARCHAR(48) | Yes | false | NULL | NONE | true |
| | | peak_memory_bytes | BIGINT | Yes | false | NULL | NONE | true |
| | | | | | | | | |
| t_select_counts_per_user_seconds | AGG_KEYS | time | DATETIME | No | true | NULL | | true |
| | | user | VARCHAR(64) | Yes | true | NULL | | true |
| | | is_query | TINYINT | Yes | false | NULL | SUM | true |
| | | | | | | | | |
| t_total_counts_per_seconds | AGG_KEYS | time | DATETIME | No | true | NULL | | true |
| | | CASE WHEN `is_query` IS NULL THEN 0 ELSE 1 END | BIGINT | No | false | | SUM | true |
| | | | | | | | | |
| t_select_counts_per_seconds | AGG_KEYS | time | DATETIME | No | true | NULL | | true |
| | | is_query | TINYINT | Yes | false | NULL | SUM | true |
+----------------------------------+---------------+------------------------------------------------+-------------+------+-------+---------+-------+---------+
可以看到,我们的SQL请求还是对doris_sql_audit
表的查询,但根据profile可以看到(在OlapScanNode节点的 rollup: t_select_counts_per_seconds 中),自动命中了t_select_counts_per_seconds
物化视图。
mysql> EXPLAIN SELECT DATE_FORMAT(time,'%Y-%m-%d %H:%i:00') time ,sum(is_query) select_counts FROM doris_sql_audit WHERE time > day_floor(CURDATE()) AND time <= day_cei
l(CURDATE()) GROUP BY time ORDER BY time ;+---------------------------------------------------------------------------------------------------------------+
| Explain String |
+---------------------------------------------------------------------------------------------------------------+
| PLAN FRAGMENT 0 |
| OUTPUT EXPRS:<slot 4> <slot 2> date_format(`time`, '%Y-%m-%d %H:%i:00') | <slot 5> <slot 3> sum(`is_query`) |
| PARTITION: UNPARTITIONED |
| |
| RESULT SINK |
| |
| 5:MERGING-EXCHANGE |
| limit: 65535 |
| |
| PLAN FRAGMENT 1 |
| OUTPUT EXPRS: |
| PARTITION: HASH_PARTITIONED: <slot 2> date_format(`time`, '%Y-%m-%d %H:%i:00') |
| |
| STREAM DATA SINK |
| EXCHANGE ID: 05 |
| UNPARTITIONED |
| |
| 2:TOP-N |
| | order by: <slot 4> <slot 2> date_format(`time`, '%Y-%m-%d %H:%i:00') ASC |
| | offset: 0 |
| | limit: 65535 |
| | |
| 4:AGGREGATE (merge finalize) |
| | output: sum(<slot 3> sum(`is_query`)) |
| | group by: <slot 2> date_format(`time`, '%Y-%m-%d %H:%i:00') |
| | cardinality=-1 |
| | |
| 3:EXCHANGE |
| |
| PLAN FRAGMENT 2 |
| OUTPUT EXPRS: |
| PARTITION: RANDOM |
| |
| STREAM DATA SINK |
| EXCHANGE ID: 03 |
| HASH_PARTITIONED: <slot 2> date_format(`time`, '%Y-%m-%d %H:%i:00') |
| |
| 1:AGGREGATE (update serialize) |
| | STREAMING |
| | output: sum(`is_query`) |
| | group by: date_format(`time`, '%Y-%m-%d %H:%i:00') |
| | cardinality=-1 |
| | |
| 0:OlapScanNode |
| TABLE: doris_sql_audit |
| PREAGGREGATION: ON |
| PREDICATES: `time` > day_floor('2022-06-09'), `time` <= day_ceil('2022-06-09') |
| partitions=89/94 |
| rollup: t_select_counts_per_seconds |
| tabletRatio=89/89 |
| tabletList=17862694,17863998,17862510,17862806,17862214,17863826,17862954,17863590,17863990,17863770 ... |
| cardinality=5200524 |
| avgRowSize=1.7375114 |
| numNodes=10 |
+---------------------------------------------------------------------------------------------------------------+
54 rows in set (0.01 sec)
DROP MATERIALIZED VIEW t_select_counts_per_min ON doris_sql_audit;