如何设计宽表主键

Lindorm宽表引擎是一款分布式数据引擎,宽表引擎中的数据均按照主键进行分布。在执行查询时,如果表中存在多列主键,系统会从最左边的主键开始匹配。如果主键设置不当,则可能导致主键无法被有效利用,进而产生热点问题,影响查询性能。因此,在数据分区和数据查询中,主键的设计至关重要。本文介绍设计主键前需要考虑的一些问题以及设计示例。

问题考虑

主键是唯一的吗?

相同的主键在Lindorm中被认为是同一条数据的多个版本,查询时默认返回最新版本的数据,所以通常主键都需要保证唯一,除非用到多版本特性。

最佳设计示例:主键可以是一列,也可以是多列的组合。每个主键表示一条记录。

  • [userid]:表示主键只有一列,每个用户只有一条记录。

  • [userid][orderid]:表示主键为两列的组合,每个用户有多条记录。

基于主键可以满足哪种查询场景?

主键的设计限制了数据的查询方式,一条SELECT查询语句,Lindorm服务器端会编译为两种查询方式。

  • 根据完整的主键查询(get方式),例如SELECT * FROM table WHERE userid='abc' AND orderid=123

    说明

    get方式需要知道所有的主键列,即组成主键所有字段的值都是确定的。

  • 根据主键的范围查询(scan方式),例如SELECT * FROM table WHERE userid='abc' AND 123<orderid<456

    说明

    scan方式需要指定第一列主键的范围,否则Lindorm默认会拒绝低效的全表扫描查询,具体说明,请参见低效查询语句及说明

最佳设计示例:在有限的查询方式下如何实现复杂查询?以下方法可以帮您实现。

  • 再新建一张表作为索引表。

  • 查询条件给定非主键列范围,服务端会使用Filter过滤不需要的数据。

  • 使用二级索引。

  • 使用ORDER BY方法实现倒序(将新数据排在前面),例如SELECT * FROM table WHERE userid='abc' AND 123<orderid<456 ORDER BY orderid DESC

    说明

    由于表字段原始顺序的倒序性能比正序性能差,如果大部分数据是倒序场景,可以体现在主键设计上,主键设计为[userid][orderid DESC]

设计主键应该考虑哪些因素?

需要考虑主键列值的长度和主键列的个数。

  • 主键列值的长度:主键列值的长度建议尽量短小,建议您采用固定长度的类型,例如长整型。对于非固定长度的类型,主键列值的长度控制在2 KB之内,有利于减少存储成本,提升写性能。

  • 主键列的个数:主键列越少,写入性能越高,同时可以降低存储成本。建议将主键列的数量控制在1~3个。

设计主键应该避免哪些情况?

云原生多模数据库 Lindorm是一个分布式数据库,数据按照主键分布。如果存在多列主键,则按照数据库的最左匹配原则分布。为避免产生写入热点问题,建议您遵循以下条件:

  • 主键的第一列尽量分散,不建议主键名使用相同的前缀。

  • 避免使用共同前缀或者自增的数据作为主键的第一列或者索引列(例如时间戳列)。

  • 避免使用有明显前缀的字段或者枚举(比如order_type)作为主键的第一列。

如果有类似的情况无法避免,可以利用Hash算法进行打散。例如:

假设原始主键pk是递增的字符串,可以设定新主键pk1 = hash(pk).substring(0,4)+pk,即选取原始主键pk经过Hash算法计算后的结果前4位作为前缀,拼接原始主键pk,最终形成新的主键pk1。

数据足够分散,会存在堆积的热点现象吗?

散列的目的是将数据分散到不同的分区,不至于产生热点使某一台服务器终止,其他服务器空闲,充分发挥分布式和并发的优势。

最佳设计示例:

  • 设计md5散列算法,主键设计为[md5(userid).subStr(0,4)][userId][orderid]

  • 设计反转,主键设计为[reverse(userid)][orderid]

  • 设计取模,主键设计为[bucket][timestamp][hostname][log-event]; long bucket = timestamp % numBuckets

  • 增加随机数,主键设计为[userId][orderid][random(100)]

主键可以再精简点吗?

精简的主键列可以减少数据量,提高数据查询和数据写入效率。

最佳设计示例:

  • 使用Long或Int代替String,例如'2015122410' => Long(2015122410)

  • 使用编码代替名称,例如'淘宝'=> 'tb'

常见设计示例

  • 日志类、时间序列数据。列举出三个场景设计主键。

    • 查询某台机器某个指标某段时间内的数据,主键设计为[hostname][log-event][timestamp]

    • 查询某台机器某个指标最新的几条数据,主键设计为[hostname][log-event][timestamp DESC]

    • 查询的数据存在只有时间一个维度或某一个维度数据量巨大的情况,主键设计为long bucket = timestamp % numBuckets; [bucket][timestamp][hostname][log-event]

  • 交易类数据。列举出四个场景设计主键。

    • 查询某个卖家某段时间内的交易记录,主键设计为[seller_id][timestamp][order_number]

    • 查询某个买家某段时间内的交易记录,主键设计为[buyer_id][timestamp][order_number]

    • 根据订单号查询,主键设计为[order_number]

    • 查询中同时满足三张表,一张买家维度表主键设计为[buyer_id][timestamp][order_number]。一张卖家维度表主键设计为[seller_id][timestamp][order_number]。一张订单索引表主键设计为[order_number]