2.1 TiDB Server
SQL 层,对外暴露 MySQL 协议的连接 endpoint,负责接受客户端的连接,执行鉴权、 SQL 解析和优化,最终生成分布式执行计划。TiDB 层本身是无状态的,实践中可以启动多个 TiDB 实例,通过负载均衡组件(如 LVS、HAProxy 或 F5)对外提供统一的接入地址,客户端的连接可以均匀地分摊在多个 TiDB 实例上以达到负载均衡的效果。TiDB Server 本身并不存储数据,只是解析 SQL,将实际的数据读取请求转发给底层的存储节点 TiKV(或 TiFlash)。
TiKV Server
TiFlash
这个 Map 中的 Key-Value pair 按照 Key 的二进制顺序有序,也就是可以 Seek 到某一个 Key 的位置,然后不断地调用 Next 方法以递增的顺序获取比这个 Key 大的 Key-Value。
为每一张表的每一行设计一个行id,用RowId表示,整数且同一张表内唯一。这里还有个小优化,如果这张表有主键,则TiDB把主键作为RowId,否则自行分配一个。
Key: tablePrefix{TableID}_recordPrefixSep{RowID}
Value: [col1, col2, col3, col4]
对于主键索引和唯一索引,由于列的数据是唯一的,因此对应的key-value的结构如下:
Key:tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue Value:RowID
其中indexedColumnsValue表示查询列的值,最终是列值对应一个RowId
对于非唯一索引,由于列的数据不唯一,一个键值可能对应多行,需要根据键值范围查询对应的 RowID。因此,按照如下规则编码成 (Key, Value) 键值对:
Key:tablePrefix{TableID}_indexPrefixSep{IndexID}_indexedColumnsValue_{RowID} Value:null
这里需要注意的是,value是null,并不是RowID,而RowID是拼接在key的结尾的。这里为什么不把value设计为RowID呢?猜测是因为key是不能够重复的,要全局唯一,而且要均匀分布在TiKV的各个节点,只能把RowID拼接在key的结尾处,而且查询的时候是根据键值RowID前面的范围来查询。
tablePrefix
、recordPrefixSep
和 indexPrefixSep
都是字符串常量,用于在 Key 空间内区分其他数据,定义如下:tablePrefix = []byte{'t'}
recordPrefixSep = []byte{'r'}
indexPrefixSep = []byte{'i'}
RowID
顺序地排列在 TiKV 的 Key 空间中,某一个索引的数据也会按照索引数据的具体的值(编码方案中的 indexedColumnsValue
)顺序地排列在 Key 空间内。CREATE TABLE User (
ID int,
Name varchar(20),
Role varchar(20),
Age int,
PRIMARY KEY (ID),
KEY idxAge (Age)
);
假设该表中有4行数据:
1, "TiDB", "SQL Layer", 10
2, "TiKV", "KV Engine", 20
3, "PD", "Manager", 30,
4, "TiFLASH", "STORAGE", 30
int
类型的主键,所以 RowID
的值即为该主键的值。假设该表的 TableID
为 10,则其存储在 TiKV 上的表数据为:t10_r1 --> ["TiDB", "SQL Layer", 10]
t10_r2 --> ["TiKV", "KV Engine", 20]
t10_r3 --> ["PD", "Manager", 30]
t10_r4 --> ["TiFLASH", "STORAGE", 30]
idxAge
,假设这个索引的 IndexID
为 1,则其存储在 TiKV 上的索引数据为:t10_i1_10_1 --> null
t10_i1_20_2 --> null
t10_i1_30_3 --> null
t10_i1_30_4 --> null
如按照age=30查询,则是根据t10_i1_30去做前缀匹配,可以匹配到t10_i1_30_3与t10_i1_30_4,取最后3和4便是RowID
access object 显示被访问的表、分区和索引。显示的索引为部分索引。以上示例中 TiDB 使用了 a 列的索引。尤其是在有组合索引的情况下,该字段显示的信息很有参考意义。
operator info 显示访问表、分区和索引的其他信息。
TiDB 会汇聚 TiKV/TiFlash 上扫描的数据或者计算结果,这种“数据汇聚”算子目前有如下几类:
RowID
精确地读取 TiKV 上的数据。Build 端是 IndexFullScan
或 IndexRangeScan
类型的算子,Probe 端是 TableRowIDScan
类型的算子。IndexMerge:和 IndexLookupReader
类似,可以看做是它的扩展,可以同时读取多个索引的数据,有多个 Build 端,一个 Probe 端。执行过程也很类似,先汇总所有 Build 端 TiKV 扫描上来的 RowID,再去 Probe 端上根据这些 RowID 精确地读取 TiKV 上的数据。Build 端是 IndexFullScan
或 IndexRangeScan
类型的算子,Probe 端是 TableRowIDScan
类型的算子。
Build 总是先于 Probe 执行,并且 Build 总是出现在 Probe 前面。即如果一个算子有多个子节点,子节点 ID 后面有 Build 关键字的算子总是先于有 Probe 关键字的算子执行。TiDB 在展现执行计划的时候,Build 端总是第一个出现,接着才是 Probe 端。
WHERE
/HAVING
/ON
条件中,TiDB 优化器会分析主键或索引键的查询返回。如数字、日期类型的比较符,如大于、小于、等于以及大于等于、小于等于,字符类型的 LIKE
符号等。YEAR(date_column) < 1992
不能使用索引,但 date_column < '1992-01-01
就可以使用索引。cast
操作而导致不能利用索引。AND
(求交集)和 OR
(求并集)进行组合。对于多维组合索引,可以对多个列使用条件。例如对组合索引 (a, b, c)
:a
为等值查询时,可以继续求 b
的查询范围。b
也为等值查询时,可以继续求 c
的查询范围。a
为非等值查询,则只能求 a
的范围。SQL 优化的目标之一是将计算尽可能地下推到 TiKV 中执行。TiKV 中的 Coprocessor 能支持大部分 SQL 内建函数(包括聚合函数和标量函数)、SQL LIMIT
操作、索引扫描和表扫描。但是,所有的 Join 操作都只能作为 root task 在 TiDB 上执行。
EXPLAIN
返回结果中 operator info
列可显示诸如条件下推等信息。本文以上示例中,operator info
结果各字段解释如下:range: [1,1]
表示查询的 WHERE
字句 (a = 1
) 被下推到了 TiKV,对应的 task 为 cop[tikv]
。keep order:false
表示该查询的语义不需要 TiKV 按顺序返回结果。如果查询指定了排序(例如 SELECT * FROM t WHERE a = 1 ORDER BY id
),该字段的返回结果为 keep order:true
。stats:pseudo
表示 estRows
显示的预估数可能不准确。TiDB 定期在后台更新统计信息。也可以通过执行 ANALYZE TABLE t
来手动更新统计信息。EXPLAIN
执行后,不同算子返回不同的信息。你可以使用 Optimizer Hints 来控制优化器的行为,以此控制物理算子的选择。例如 /*+ HASH_JOIN(t1, t2) */
表示优化器将使用 Hash Join 算法。算子IndexLookUp_18在TiDB汇总结果├─Limit_17(Build)上的RowID,之后使用RowID从算子└─TableRowIDScan_16(Probe)精确查找数据
写在最后,以上是执行计划,是相对粗略的结果,如果要获取详细的结果(带有性能、执行时间等),需要查看性能分析结果,方法为SQL语句前加上EXPLAIN ANALYZE,以下为SQL1的性能分析示例:
TiDB 简介 | PingCAP Docs
https://docs.pingcap.com/zh/tidb/stable/overview
TiDB介绍 - 知乎
https://zhuanlan.zhihu.com/p/494715695
*文/方正