系列专栏声明:比较流水,主要是写一些踩坑的点,和实践中与文档差距较大的地方的思考。这个专栏的典型特征可能是 次佳实践,争取能在大量的最佳实践中生存。

本篇基本都是伪代码,因为原始的 TableStore SDK 设计得太不可读了,实际使用请自行查阅文档。

一、Primary Key = Partition Key + Sort Key

考虑 BookStore 的场景:City、District、Road、No 都是业务上实际存在的字段,我们也确实在使用这些字段的组合去记做唯一的 Id 标识,因此支持多字段组合避免了由业务层做 PK 拼接和拆解。

那么自然就推导出 2 个重要的特征:1)这些 Key 都是必填的;2)取值的时候将组合整体视作条件,即 WHERE CityDistrictRoadNo = ShanghaiPudong001003,而不存在 WHERE District = Pudong

CityDistrictRoadNo
ShanghaiPudong001003
ShanghaiMinhang001004
ShenzhenNansha002005

关于如何设计 Partition Key 和 Sort Key,建议参考:

  1. Azure Cosmos DB
  2. AWS DynamoDB
  3. DynamoDB Keys - Everything You Need To Know

二、GetSingleRow,获取单行数据

TableStore 支持 1-4 个 Key 组成 PK;但下面为了演示的方便,我们只使用 2 个 Key。

Row row = client.getSingleRow("BookStore",
    new PrimaryKey("Shanghai", "Pudong"));

根据上面的介绍,我们应该已经习惯了 PK 的定义,如果在设计表的时候确认了要使用 2 个 Key 来组成 PK,那么这 2 个 Key 始终是必填的。

三、GetRangeRow,By PK 获取范围数据

考虑一个传统的 MySQL 设计,你需要先按照一定的排序规则将数据按顺序插入,然后搜索 WHERE 10 < Id AND Id < 20。但在 TableStore 里你只管插入,会自动去排序和索引,所以搜索条件就变成了 WHERE Shanghai.Pudong < City.District AND City.District < Shenzhen.Nanshan

再次体会一下这里把多个 Key 当作一个整体 PK 来使用的感觉。不要去想 WHERE Shanghai < City < Shenzhen, Pudong < District < Nanshan 或它的变体,在概念上就不成立。

List<Row> rows = client.getRangeRow("BookStore",
  new PrimaryKey("Shanghai", "Pudong"),   // start
  new PrimaryKey("Shenzhen", "Nanshan")); // end

搜索可以不设下限或上限,用系统提供的常量来表示:

List<Row> rows = client.getRangeRow("BookStore",
  new PrimaryKey("Shanghai", INF_MIN),  // start
  new PrimaryKey("Shenzhen", INF_MAX)); // end

也可以使用一些更灵活的组合,但可读性就差很多了:

List<Row> rows = client.getRangeRow("BookStore",
  new PrimaryKey("Shanghai", INF_MIN), // start
  new PrimaryKey(INF_MAX, "Nanshan")); // end

排序的规则为从左到右依次比较 4 个 Key,如果第 Key 0 已经比较出了大小,就不再比较第 Key 1 / 2 / 3 了。参考这个例子 更详细直观一些。还有一些在扫描海量数据时的限制、翻页、或者流式读取,请参考官方的参数说明。本篇的重点在于帮你强化 多个 Key 其实是一个整体的 PK 这个概念。

四、Global Secondary Index / GSI / 全局二级索引

排序是从左到右的,只要筛选条件包含了了 Key 0,那么 Key 1 / 2 / 3 就不会生效;不包含 Key 0,那么 Key 1 才能生效;但有些场景下你确实需要按 Key 1 和 Key 0 组合搜索,且优先考虑 Key 1。因此需要建立一个 Key 1 优于 Key 0 的 GSI,直观的看就是把 2 个 Key 的左右顺序对调一下。

DistrictCityRoadNo
PudongShanghai001003
MinhangShanghai001004
NanshanShanghai002005
// 原表:Shanghai.F 在这个搜索结果内
List<Row> rows = client.getRangeRow("BookStore",
  new PrimaryKey("Shanghai", "A"),  // start
  new PrimaryKey("Shenzhen", "D")); // end

// GSI:优先排序 Key 1,所以 Shanghai.F 不在这个搜索结果内
List<Row> rows = client.search("BookStore", "Index2",
  new PrimaryKey("A", "Shanghai"),  // start
  new PrimaryKey("D", "Shenzhen")); // end

GSI 必须包含原表的所有 PK。如果原表用了 4 个 PK,那么索引的 PK 只能是它们的排列组合。如果原表没有用满 4 个 PK,那么可以把预定义列也放到 PK 里来:

原表:     [ PK0, SK1, SK2 ] C1, C2, C3
GSI:     [ C1, PK0, SK1, SK2 ] C2, C3
或者 GSI: [ C1, SK1, PK0, SK2 ] C2, C3

GSI 本质上就是重新设计 Primary Key。

Local Secondary Index / LSI / 本地二级索引 则是保持原 Partition Key 的前提下重新设计 Sort Key。

这个例子讲得很详细:通过全局二级索引加速表格存储上的数据查询

以及以上两者并不是用来做搜索的,主要的使用场景还是常规顺序取值,搜索应该用多元索引,见下。

五、Filter

Filter 可以按照单个 Key 筛选,既可以是 PK,也可以是 Attr Col。但扫描实际是按 PK 条件全表扫描的,Filter 只是减少了被返回的数据的量,即减少了网络传输的大小。Filter 也不是搜索,仍然应该按 PK 设计与扫描的思路去考虑。

List<Row> rows = client.getRangeRow("BookStore",
  new PrimaryKey(INF_MIN, INF_MIN), // start
  new PrimaryKey(INF_MAX, INF_MAX), // end
  new Filter("District", "==", "Pudong")); // Filter by PK Col

List<Row> rows = client.getRangeRow("BookStore",
  new PrimaryKey(INF_MIN, INF_MIN), // start
  new PrimaryKey(INF_MAX, INF_MAX), // end
  new Filter("Size", ">=", "700")); // Filter by Attr Col

以上演示的是 SingleColumnValueFilter,还有多重条件组合筛选 CompositeColumnValueFilter,等想到好的例子再来补充代码。

六、使用 SQL

好像没啥明确的意义,等想到好的例子再来补充。

也许优势是可以使用 AVG 等函数,不确定

七、多元索引

按文档的宣传,是 无视 Partition Key 和 Sort Key 设计的高性能,可能是倒排?

按你正常写搜索的思路就好:

List<Row> rows = client.search("BookStore", "SearchIndex",
  new Query("Road", "LIKE", "Hutai"),
  AND,
  new Query("Building", "LIKE", "Tower")); // 伪代码 

值得注意的是 Nested 类型必须是数组:

// 错误的示例: Building: { Name: "Tower" }
List<Row> rows = client.search("BookStore", "SearchIndex",
  new Query("Building.Name", "LIKE", "Tower"));

// Buildings: [ { Name: "Tower" }, { Name: "Plaza" } ]
List<Row> rows = client.search("BookStore", "SearchIndex",
  new Query("Buildings.Name", "LIKE", "Tower"));
// Buildings 可能有很多栋楼,只要其中有一栋楼叫 Tower,就返回这个 Buildings 所在的整个行
// 显然该行也会包含 Plaza

这个例子很好,直接看吧:详解TableStore模糊查询——以订单场景为例

参考文献

  1. Partitioning In Azure Cosmos DB
  2. Everything you need to know about DynamoDB Partitions
  3. 前端也要懂一点 MongoDB Schema 设计
  4. How to model and partition data on Azure Cosmos DB using a real-world example
  5. 海量结构化数据存储技术揭秘:Tablestore 存储和索引引擎详解
  6. GetRange 范围查询详解
  7. 从 SQL 到 NoSQL — 如何使用表格存储

理斯特
18 声望9 粉丝

web/mobile/iot, front/back, js/java