本文介绍了全局二级索引的原理、特性和类型。
概述
全局二级索引(Global Secondary Index,简称GSI)是PolarDB-X中的一项重要特性,相比于本地二级索引,全局二级索引中的数据按照指定的分区方式分布在各个存储节点上。通过全局二级索引,用户能够按需增加分区维度、提供全局唯一约束等。
原理和特性
在分布式数据库的分区表中,数据被按照建表时指定的分区键进行路由和存储,因此包含分区键的查询可以快速定位到一个具体分区,而其它查询则需要全分区扫描。对于分布式数据库而言,全分区扫描除了会增加慢查询数量降低系统吞吐,还可能导致系统丧失线性扩展能力,因此需要尽量避免全分区扫描。
举例来说,当查询语句包含分区键时,单个逻辑查询只会被路由到一个具体分区,假设有N个存储节点,平均来看1个逻辑查询给单个存储节点施加的查询负载仅为1/N;当查询语句不含分区键时会引起全分区扫描,即单个查询会被路由到所有分区,平均来看1个逻辑查询给单个存储节点施加的查询负载为1,单个存储节点的性能上限就是整个分布式数据库的性能上限,系统失去了线性扩展能力。
PolarDB-X提供了全局二级索引以解决上述问题。在PolarDB-X中,GSI可以视为一个特殊的分区表,它冗余了主表上的部分列的数据。与普通分区表类似,GSI按照用户指定的分区规则水平拆分为若干个分区,分布在各个存储节点上。当一个查询不含主表分区键但包含GSI的分区键时,PolarDB-X通过先检索GSI的单个分区,然后回表的方式避免全分片扫描。
PolarDB-X使用分布式事务维护主表和GSI之间数据的强一致。
此外,GSI还支持以下特性:
支持在线变更,创建、删除GSI无需锁表。
用户可自定义覆盖列,减少回表操作开销。
支持invisible index。
类型
全局二级索引(Global Secondary Index 简称GSI)
全局二级索引可以提供和主表不同的分区方式,当查询SQL的条件中未包含主表的分区键但包含了GSI的分区键时,仍可以避免全分区扫描。
比如对于下面的用户表user_tbl, 如果既希望按照user_id查询,又希望按照用户名name查询,就可以建立全局二级索引 g_i_name,在按照用户名name查询的时候避免全分区扫描。
CREATE TABLE user(
user_id bigint,
name varchar(10),
addr varchar(30),
GLOBAL INDEX `g_i_name` (name) PARTITION BY HASH(name),
PRIMARY KEY(user_id)
) PARTITION BY KEY(user_id);
全局唯一索引(Unique Global Secondary Index 简称UGSI)
全局唯一索引是特殊的GSI,它不仅有普通GSI的性质,还能实现全局唯一约束。
比如对于下面的用户表user2,如果要求用户手机号全局唯一,那么可以建立一个phone字段为索引键的UGSI。
CREATE TABLE user2(
user_id bigint,
phone varchar(20),
addr varchar(30),
UNIQUE GLOBAL INDEX `g_i_phone`(phone) PARTITION BY HASH(phone),
PRIMARY KEY(user_id)
) PARTITION BY KEY(user_id);
全局聚簇索引 (Clustered Global Secondary Index 简称Clustered GSI)
全局聚簇索引是特殊的GSI,它默认冗余了主表的全部列(该索引所占磁盘空间等于主表所占磁盘空间)。如果既希望避免全分区扫描,又希望避免回表开销,可以使用全局聚簇索引。
比如对于订单表order_tbl,希望支持按照user_id或order_id来查询,且希望避免用user_id查询订单时回表,就可以创建一个以user_id为索引键的全局聚簇索引cg_i_user。以user_id为条件查询订单信息时,PolarDB-X会将查询路由到cg_i_user上的一个特定分区,又因为cg_i_user上有主表的所有数据,因此无需回表。
CREATE TABLE order_tbl(
order_id bigint,
user_id bigint,
addr varchar(30),
info text,
create_time datetime,
CLUSTERED INDEX `cg_i_user`(user_id) PARTITION BY HASH(user_id),
PRIMARY KEY(order_id)
) PARTITION BY KEY(order_id);
性能
全局索引对读写性能的影响,与具体业务场景有比较大关系,本质上是牺牲一部分写入性能换取读性能的大幅提升,下面以Sysbench场景为例,展示该场景下GSI对读写吞吐的影响。
读取性能数据
Table | Threads | Sysbench SeIect_random_ranges 场景 | Sysbench SeIect_random_points 场景 | ||||
QPS | Avg Latency | 95% Latency | QPS | Avg Latency | 95% Latency | ||
分区表 | 128 | 2769.17 | 46.21 | 99.33 | 5226.99 | 24.48 | 42.61 |
256 | 3415.64 | 144.97 | 144.97 | 5476.76 | 46.73 | 82.96 | |
512 | 3272.46 | 156.31 | 257.95 | 5290.67 | 96.72 | 179.94 | |
1024 | 2453.16 | 416.12 | 539.71 | 5165.31 | 198.07 | 404.61 | |
分区表+GSI | 128 | 9662.11 | 13.24 | 25.28 | 22584.89 | 5.66 | 9.73 |
256 | 10431.73 | 24.52 | 51.02 | 25558.26 | 10.01 | 17.95 | |
512 | 15634.51 | 32.72 | 73.13 | 27116.56 | 18.86 | 39.65 | |
1024 | 229448.76 | 44.53 | 108.68 | 32509.87 | 31.43 | 73.13 |
增加一个全局索引:
Select_random_ranges场景QPS:3415.64 -> 22948.76, range查询QPS提升571%。
Select_random_points场景QPS:5476 -> 32509.87, 点查QPS提升493%。
结论:通过全局索引可以提升Sysbench在索引列k上的查询性能。
写入性能数据
Table | Threads | Sysbench SeIect_random_ranges 场景 | Sysbench SeIect_random_points 场景 | ||||
QPS | Avg Latency | 95% Latency | QPS | Avg Latency | 95% Latency | ||
分区表 | 128 | 86548.12 | 8.87 | 10.27 | 113655.28 | 22.52 | 26.2 |
256 | 115774.71 | 13.26 | 19.29 | 149677.52 | 34.19 | 44.17 | |
512 | 143928.94 | 20.51 | 34.95 | 14555.16 | 70.28 | 112.67 | |
1024 | 153501.7 | 39.53 | 70.55 | 132150.69 | 131.58 | 287.38 | |
分区表+GSI | 128 | 52069.22 | 14.25 | 18.28 | 90074.59 | 28.41 | 33.72 |
256 | 66250.79 | 23.17 | 32.53 | 114420.32 | 44.73 | 57.87 | |
512 | 75700.74 | 39.1 | 59.99 | 111093.61 | 92.09 | 142.39 | |
1024 | 76557.94 | 80.14 | 134.9 | 101828.32 | 182.51 | 350.33 |
增加一个全局索引:
WriteOnly场景QPS:153501.7 -> 76557.94,写入QPS下降50%。
ReadWrite场景QPS:149677.52 -> 114420.32,读写混合QPS下降23%。
结论:增加一个全局索引,Sysbench写入性能有下降。