在数据库中,我们通常所说的“统计信息”实际上指的是优化器统计信息(Optimizer Statistics),它是一个描述数据库中表和列信息的数据集合。优化器代价模型(Optimizer Cost Model)依赖于查询中涉及到的表、列、谓词等对象的统计信息来选取计划,它通常是代价模型选取最优执行计划的非常关键的部分。在 OceanBase 数据库中,统计信息存储以普通数据的形式存储在内部表中,并且会在本地维护一个的统计信息缓存,以提高优化器对统计信息的访问速度。
统计信息的类型
在 OceanBase 数据库中,统计信息有表统计信息(Table Level Statistics)和列统计信息(Column Level Statistics)两种。表的统计信息主要存储在内部表 __all_meta_table
中,它包含了以下信息:
表的行数
表所占用的微块数
表的平均行长
表所占的空间大小
优化器可以利用表统计信息来优化计划的选择。除了非实体表以及统计信息内部表自身以外,所有的表都可以收集表统计信息。以下是使用表统计信息进行计划选择的一个示例。
create table t1 (c1 int, c2 int);
create table t2 (d1 int key, d2 int);
-- t1插入1000行
-- t2插入1000行
EXPLAIN select * from t1 left join t2 on t1.c1 = t2.d1;
========================================
|ID|OPERATOR |NAME|EST. ROWS|COST|
----------------------------------------
|0 |HASH OUTER JOIN| |1980 |8224|
|1 | TABLE SCAN |t1 |1000 |499 |
|2 | TABLE SCAN |t2 |1000 |499 |
========================================
-- t1插入100行
-- t2插入100000行
EXPLAIN select /*+ use_hash(t1 t2) */ from t1 left join t2 on t1.c1 = t2.d1;
==============================================
|ID|OPERATOR |NAME|EST. ROWS|COST |
----------------------------------------------
|0 |HASH LEFT OUTER JOIN| |19800 |89496|
|1 | TABLE SCAN |t1 |100 |61 |
|2 | TABLE SCAN |t2 |100000 |40055|
==============================================
-- 根据t2的表统计信息,HASH JOIN的代价在右表行数较大时代价较大,优化器选择了更优的计划,使用
-- NESTED LOOP JOIN。
EXPLAIN select * from t1 left join t2 on t1.c1 = t2.d1;
=====================================================
|ID|OPERATOR |NAME|EST. ROWS|COST |
-----------------------------------------------------
|0 |NESTED-LOOP LEFT OUTER JOIN| |19800 |23649|
|1 | TABLE SCAN |t1 |100 |61 |
|2 | TABLE GET |t2 |198 |108 |
=====================================================
列的统计信息存储在内部表 __all_column_statistic
中,它包含了以下信息:
列中不同的值的数量(Number of Distinct Values,NDV)
列中 NULL 值的数量
列的最大值最小值
优化器可以利用列的统计信息来优化的选择。除了非实体表以及统计信息内部表自身以外,所有的列都可以收集列统计信息。列的统计信息主要用来计算谓词、连接条件的选择率,从而更好地计算算子向上层吐出的行数。
以下是使用列统计信息来优化查询计划选择的一个例子。
create table t1 (c1 int, c2 int);
create table t2 (d1 int key, d2 int);
-- t1插入200行
-- t2插入100000行,其中满足t2.d2 = 1的有50000行
-- 在没有列统计信息的情况下,优化器认为t2.d2 = 1是一个强过滤条件,满足该条件的约为198行,
-- 所以选择以行数更少的表t2作为build table(建立hash表),t1为probe table(扫描)
EXPLAIN select * from t1 join t2 on c1 = d1 and d2 = 1;
=========================================
|ID|OPERATOR |NAME|EST. ROWS|COST |
-----------------------------------------
|0 |HASH INNER JOIN| |79 |63920|
|1 | TABLE SCAN |t2 |198 |63417|
|2 | TABLE SCAN |t1 |200 |121 |
=========================================
-- ALTER SYSTEM MAJOR FREEZE;
-- 在有列统计信息的情况下,优化器可以察觉到右表吐出的行数信息,从而以小表t1为build table。
EXPLAIN select * from t1 join t2 on c1 = d1 and d2 = 1;
=========================================
|ID|OPERATOR |NAME|EST. ROWS|COST |
-----------------------------------------
|0 |HASH INNER JOIN| |101 |62671|
|1 | TABLE SCAN |t1 |200 |85 |
|2 | TABLE SCAN |t2 |50000 |44543|
=========================================
在大多数情况下,表和列的统计信息共同作用,优化器得以利用这些信息更精确地选择执行计划。
收集统计信息的时机
统计信息的收集是一个相对消耗时间的过程。在 OceanBase 数据库中,主要的统计信息在进行一次大版本合并(Major Freeze)之后进行一次统一的收集。在这一阶段,存储层会收集所有表、所有列的统计信息,更新到内部表中。
说明
列 NDV 的信息更新采用业内较为认可的近似算法 HyperLogLog 实现,它具有执行速度快、占用空间少的特点,并且理论的平均准确率期望可以达到 98% 以上。
此外,由于 OceanBase 数据库底层的存储由 MemTable 和 SSTable 构成,MemTable 中数据的统计信息也非常重要,除了统一的收集之外,目前还会对 MemTable 中的表的行数进行收集,这个收集的代价相对较小,它的收集分散到各个优化过程中,每一次优化器的优化都会收集这部分的信息。
需要说明的是,由于数据库并不是一直在收集统计信息,所以会存在部分表、部分行没有统计信息的情况。对于这类情况,OceanBase 数据库会使用一个缺省的统计信息。
统计信息缓存
数据库收集的最新的统计信息存储在内部表中,而频繁地访问内部表代价较大,而统计信息的更新并不是那么频繁,而且一张表、一个列的统计信息可以被多个查询使用。
基于上述原因引入了统计信息缓存(Optimizer Statistic Cache)机制,本地使用一个 KV 缓存结构将统计信息缓存在内存当中,当优化器需要访问统计信息时,首先查询本地的缓存,当缓存中没有相应的统计信息时,再向内部表查询。
统计信息查询
目前,sys 租户可以通过前述的内部表来查询表和列的统计信息,如下例所示,SQL 给出了一个使用内部表查询表、列的统计信息的示例。使用该 SQL 可以通过传入数据库名、表名和列名这三个信息的组合获取到包括表的行数、列的 NDV、列的 NULL 值数量以及列的最大最小值信息。
-- 内部表使用16进制格式存储最大值最小值信息,使用des_hex_str()函数可以将它转化为可读的格式。
select
r_c as row_count,
s.num_distinct as NDV,
s.num_null as num_null,
des_hex_str(s.min_value) as min,
des_hex_str(s.max_value) as max
from
__all_column_statistic s,
__all_database d,
__all_table t,
__all_column c,
(select max(row_count)as r_c, table_id from __all_meta_table group by table_id) m
where
s.table_id= t.table_id
and t.database_id= d.database_id
and d.database_name= 'tpch_1g' -- 数据库名
and t.table_name= 'orders' -- 表名
and c.table_id= t.table_id
and c.column_name= 'o_orderkey' -- 列名
and s.column_id= c.column_id
and s.column_id= c.column_id
and s.table_id= m.table_id;
-- 查询结果示例
+-----------+---------+----------+------+---------+
| row_count | NDV | num_null | min | max |
+-----------+---------+----------+------+---------+
| 1500000 | 1479709 | 0 | 1 | 6000000 |
+-----------+---------+----------+------+---------+
1 row in set (0.14 sec)