有一个很有趣的发现。
打开你的淘宝客户端或者PC端的淘宝,点开订单列表,打开几个订单,查看他们的订单号,你会发现什么?
比如这是我最近的3个订单,和10年前的3个订单。其订单号分别是:
淘宝中有一个非常重要的表,订单表,他里面存着订单的一些关键信息,例如订单号(order_id)、卖家id(seller_id)、买家id(buyer_id)、商品id等等。有两类查询是这个表上的高频查询:
select * from orders where buyer_id = ?
select * from orders where seller_id = ?
如果我们在单机数据库中做这两条SQL,都知道怎么做。嗯,在buyer_id和seller_id上分别建个建索引就可以了:
create index idx_buyer_id on orders (buyer_id);
create index idx_seller_id on orders (seller_id);
如果你是用一些分库分表中间件,例如MyCAT之类的产品,对这个表做了分库分表,就需要面临一个跟单机数据库完全不一样的一个问题,该如何选择分库分表键?
一般此类中间件都会告诉你,你哪个列查的最多,就选哪个列做拆分键。
但问题来了,这两类SQL都很高频,选了buyer_id做分库分表键,那按seller_id查就会全库全表扫描;如果按seller_id做分库分表键,那按buyer_id查就会全库全表扫描。
难道鱼和熊掌不可兼得?
一般解决这类的问题的方案是,使用两套订单表,其中一套使用buyer_id做分库分表建,另一套使用seller_id做分库分表键,中间使用binlog来做同步,类似下面的样子:
这个方案是OK的,能够落地的,只不过做的过程会有些小痛苦要解决,例如:
select * from orders where order_id =
这个SQL作用太简单了,根据订单id查订单详情嘛!
create database ms1 mode=auto;
use ms1;
create table orders(
order_id varchar(128) primary key,
buyer_id varchar(128),
seller_id varchar(128),
index idx_buyer_id(buyer_id),
index idx_seller_id(seller_id)
) partition by hash(order_id);
create clustered index gsi_buyer_id on orders (buyer_id) partition by hash(buyer_id);
create clustered index gsi_seller_id on orders (seller_id) partition by hash(seller_id);
然后?没有然后了啊,这就可以了。
真的可以了,你不用改SQL,不用研究Canal,不用维护同步链路,不用担心数据不一致,就O!K!了!
至于按订单id查?订单id本来就是orders的主键,默认就是orders表的分区键,所以没问题的。
这么简单就OK了?为什么?🤔️
这两条语句发生了什么?实际上,他们在orders表上,创建了两条全局索引。全局索引和单机索引的原理差不多,也是空间换时间的思想,只不过它的数据以索引的key分布在整个集群中。
有一个小问题,建索引的语句里面,有个Clustered关键字,这是什么意思?
我们先看,如果不加Clustered,会发生什么,例如:
create global index gsi_buyer_id on orders (buyer_id) partition by hash(buyer_id);
主表:
PolarDB分布式版会定义这样的一个索引结构:
这个索引中,会包含索引的key以及主键两个列,也即order_id与buyer_id。
PolarDB分布式版在执行 select * from orders where buyer_id = ? 的时候,会先根据buyer_id在索引idx_buyer_id上扫描出order_id,再使用order_id到主表上进行回表操作。
听起来好像没有什么问题。
但是,有一点需要考虑。请打开你的订单列表,看一下,你有多少订单:
呃,我有126页订单,数了下,每页15个,也就是大约1800个订单。
淘宝的订单表的分区数大约是数千这个量级,你会发现,这1800个订单的回表,要覆盖相当比例的分区,似乎跟全表扫描的代价没有什么太大的差异了。
怎么办?
我们为什么要回表?其是是因为,我们的查询是SELECT *,需要这个表所有的列,而我们的索引里只包含了索引key和主键,因此需要到主表中找到剩下的列。
所以为了不回表,我们想到的一个办法,是在索引表中冗余主表的所有列,用更多的空间来换取时间。
所以,一个合格的分布式数据库,不仅需要有全局索引,还需要有聚簇的(Clustered)全局索引。
Clustered index就是PolarDB分布式版中的概念,它相对于普通的全局索引的区别就是,它包含了表的所有列,可以避免回表的代价。
PolarDB分布式版提供了名为CO_HASH的分区算法,可以完成这个功能:
create database ms1 mode=auto;
use ms1;
create table orders(
order_id varchar(128) primary key,
buyer_id varchar(128),
seller_id varchar(128),
index idx_buyer_id(buyer_id),
index idx_seller_id(seller_id)
) partition by co_hash(right(order_id,4), right(buyer_id,4));
create clustered index gsi_seller_id on orders (seller_id) partition by hash(seller_id);
select * from orders where buyer_id=? //主表
select * from orders where order_id=? //主表
select * from orders where seller_id=? //gsi_seller_id
通过使用CO_HASH,可以省略掉buyer_id上的全局索引。
● 有些信息来源用户的输入,例如用户系统的手机号、邮箱,订单系统中的buyer_id、seller_id;
我们有时可以通过控制“业务系统”的生成逻辑,将其生成的内容与用户的输入关联起来,来达到降维的目的。
● user_id中可以携带手机号的某几位,或者携带邮箱的hash值的某几位;
CO_HASH是一种非常有用的小技巧,合理使用可以有效的减少GSI的数目。
OK,总结几条这个例子告诉我们的PolarDB分布式版的最佳实践:
1. 使用全局索引来解决类似买卖家问题的多维度查询的问题。
2. 当索引与主表是一对多的关系的时候,考虑使用Clustered index来消灭回表的代价。
3. 对订单ID等做巧妙的设计,结合CO_HASH,可以省略部分全局索引。
怎么样,PolarDB分布式版用起来是不是非常简单,欢迎点击文末「阅读原文」了解更多相关内容。