上一篇探索了 Phoenix 的 Global Index,这一次来看一看 Local Index 的实现原理。

首先需要说明的是实验采用的 Phoenix 版本号是4.8,从4.8开始,Phoenix 的Local Index 不再采用新建数据表的方式,从官方的描述中可以看出这一点变化:

Unlike global indexes, all local indexes of a table are stored in a single, separate shared table prior to 4.8.0 version. From 4.8.0 onwards we are storing all local index data in the separate shadow column families in the same data table.

不过什么是 shadow column families? 这一点确实把我弄蒙了,百度 Google 都没有搜出相关的概念,先不管那么多了,先心无旁骛的直奔目标吧。

需要说明的另一点是由于上一次采用的表数据量有些大,在建立 Local Index总是出错,使用 !tables 命令查看索引表总是处于 BUILDING 状态,所以这次新建了一个表进行试验,原理明白就好。

1.新建数据表、索引和插入数据

1
2
3
4
5
6
7
CREATE TABLE IF NOT EXISTS zdq.test (id BIGINT NOT NULL PRIMARY KEY, X INTEGER, Y INTEGER, Z INTEGER, M DECIMAL, N SMALLINT);
ALTER TABLE zdq.test set IMMUTABLE_ROWS = true; # can not set local index if IMMUTABLE_ROWS = fasle or need set hbse-site.xml
CREATE LOCAL INDEX X_IDX ON zdq.test (x);
CREATE LOCAL INDEX Y_IDX ON zdq.test (y);
CREATE LOCAL INDEX Z_IDX ON zdq.test (z);
UPSERT INTO zdq.test (id, x, y, z, m, n) VALUES (12345, 1, 2, 3, 1, 0);
UPSERT INTO zdq.test (id, x, y, z, m, n) VALUES (123456, 4, 5, 6, 2, 1);

使用 !tables 查看一下数据表,发现多了四个表,一个主数据表,三个索引表。

1
2
3
4
5
6
7
8
9
10
11
12
13
+------------+--------------+-------------+---------------+----------+------------+----------------------------+-----------------+--------------+-----------------+---------------+---------------+--------+
| TABLE_CAT | TABLE_SCHEM | TABLE_NAME | TABLE_TYPE | REMARKS | TYPE_NAME | SELF_REFERENCING_COL_NAME | REF_GENERATION | INDEX_STATE | IMMUTABLE_ROWS | SALT_BUCKETS | MULTI_TENANT | VIEW_S |
+------------+--------------+-------------+---------------+----------+------------+----------------------------+-----------------+--------------+-----------------+---------------+---------------+--------+
| | ZDQ | X_IDX | INDEX | | | | | ACTIVE | true | null | false | |
| | ZDQ | Y_IDX | INDEX | | | | | ACTIVE | true | null | false | |
| | ZDQ | Z_IDX | INDEX | | | | | ACTIVE | true | null | false | |
| | SYSTEM | CATALOG | SYSTEM TABLE | | | | | | false | null | false | |
| | SYSTEM | FUNCTION | SYSTEM TABLE | | | | | | false | null | false | |
| | SYSTEM | SEQUENCE | SYSTEM TABLE | | | | | | false | null | false | |
| | SYSTEM | STATS | SYSTEM TABLE | | | | | | false | null | false | |
| | BIGJOY | IMOS | TABLE | | | | | | true | null | false | |
| | ZDQ | TEST | TABLE | | | | | | true | null | false | |
+------------+--------------+-------------+---------------+----------+------------+----------------------------+-----------------+--------------+-----------------+---------------+---------------+--------+

在 HBase Shell 中使用 list 命令查看一下,和 Global Index 不一样的是,只多出了一个表 ‘ZDQ.TEST’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
hbase(main):001:0> list
TABLE
BIGJOY.IMOS
BIGJOY.POIS
BIGJOY.POIS_MAC_IDX
SYSTEM.CATALOG
SYSTEM.FUNCTION
SYSTEM.SEQUENCE
SYSTEM.STATS
ZDQ.TEST
8 row(s) in 0.2320 seconds

=> ["BIGJOY.IMOS", "BIGJOY.POIS", "BIGJOY.POIS_MAC_IDX", "SYSTEM.CATALOG", "SYSTEM.FUNCTION", "SYSTEM.SEQUENCE", "SYSTEM.STATS", "ZDQ.TEST"]
hbase(main):002:0>

2.数据表、索引表结构探究

查看三个索引表的主键!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
0: jdbc:phoenix:localhost> !primarykeys zdq.z_idx
+------------+--------------+-------------+--------------+----------+----------+--------------+------------+------------+--------------+----------+----------------+
| TABLE_CAT | TABLE_SCHEM | TABLE_NAME | COLUMN_NAME | KEY_SEQ | PK_NAME | ASC_OR_DESC | DATA_TYPE | TYPE_NAME | COLUMN_SIZE | TYPE_ID | VIEW_CONSTANT |
+------------+--------------+-------------+--------------+----------+----------+--------------+------------+------------+--------------+----------+----------------+
| | ZDQ | Z_IDX | 0:Z | 2 | | A | 3 | DECIMAL | null | 3 | |
| | ZDQ | Z_IDX | :ID | 3 | | A | -5 | BIGINT | null | -5 | |
| | ZDQ | Z_IDX | _INDEX_ID | 1 | | A | 5 | SMALLINT | null | 5 | |
+------------+--------------+-------------+--------------+----------+----------+--------------+------------+------------+--------------+----------+----------------+
0: jdbc:phoenix:localhost> !primarykeys zdq.x_idx
+------------+--------------+-------------+--------------+----------+----------+--------------+------------+------------+--------------+----------+----------------+
| TABLE_CAT | TABLE_SCHEM | TABLE_NAME | COLUMN_NAME | KEY_SEQ | PK_NAME | ASC_OR_DESC | DATA_TYPE | TYPE_NAME | COLUMN_SIZE | TYPE_ID | VIEW_CONSTANT |
+------------+--------------+-------------+--------------+----------+----------+--------------+------------+------------+--------------+----------+----------------+
| | ZDQ | X_IDX | 0:X | 2 | | A | 3 | DECIMAL | null | 3 | |
| | ZDQ | X_IDX | :ID | 3 | | A | -5 | BIGINT | null | -5 | |
| | ZDQ | X_IDX | _INDEX_ID | 1 | | A | 5 | SMALLINT | null | 5 | |
+------------+--------------+-------------+--------------+----------+----------+--------------+------------+------------+--------------+----------+----------------+
0: jdbc:phoenix:localhost> !primarykeys zdq.y_idx
+------------+--------------+-------------+--------------+----------+----------+--------------+------------+------------+--------------+----------+----------------+
| TABLE_CAT | TABLE_SCHEM | TABLE_NAME | COLUMN_NAME | KEY_SEQ | PK_NAME | ASC_OR_DESC | DATA_TYPE | TYPE_NAME | COLUMN_SIZE | TYPE_ID | VIEW_CONSTANT |
+------------+--------------+-------------+--------------+----------+----------+--------------+------------+------------+--------------+----------+----------------+
| | ZDQ | Y_IDX | 0:Y | 2 | | A | 3 | DECIMAL | null | 3 | |
| | ZDQ | Y_IDX | :ID | 3 | | A | -5 | BIGINT | null | -5 | |
| | ZDQ | Y_IDX | _INDEX_ID | 1 | | A | 5 | SMALLINT | null | 5 | |
+------------+--------------+-------------+--------------+----------+----------+--------------+------------+------------+--------------+----------+----------------+

发现三个索引表中除了被索引的字段字段不同外,其他组成并且在主键中的顺序都是一样的,下面看一下执行计划!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
0: jdbc:phoenix:localhost> explain select * from zdq.test where x = 1;
+---------------------------------------------------------------------------+
| PLAN |
+---------------------------------------------------------------------------+
| CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN RANGE SCAN OVER ZDQ.TEST [1,1] |
| SERVER FILTER BY FIRST KEY ONLY |
+---------------------------------------------------------------------------+
2 rows selected (0.064 seconds)
0: jdbc:phoenix:localhost> explain select * from zdq.test where y > 3;
+-----------------------------------------------------------------------------------+
| PLAN |
+-----------------------------------------------------------------------------------+
| CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN RANGE SCAN OVER ZDQ.TEST [2,3] - [2,*] |
| SERVER FILTER BY FIRST KEY ONLY |
+-----------------------------------------------------------------------------------+
2 rows selected (0.067 seconds)
0: jdbc:phoenix:localhost> explain select * from zdq.test where z < 2;
+-----------------------------------------------------------------------------------+
| PLAN |
+-----------------------------------------------------------------------------------+
| CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN RANGE SCAN OVER ZDQ.TEST [3,*] - [3,2] |
| SERVER FILTER BY FIRST KEY ONLY |
+-----------------------------------------------------------------------------------+

从给出的执行计划中可以看出,三次查询都是进行的RANGE SCAN,但是注意到 [1,1],[2,3] - [2,*],[3,*] - [3,2]的不同,虽然对其中含义不完全确定,可以知道的是中括号的第二个数和查询约束相关,第一个数吗,是索引建立的顺序?这样也可以与索引表的 primary key 组成的_INDEX_ID的字面意义对应,猜测而已,接下来看一下在 HBase Shell 中的结果吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
hbase(main):004:0> scan 'ZDQ.TEST'
ROW COLUMN+CELL
\x00\x00\xC1\x02\x00\x80\x00\x00\x00\x00\x0009 column=L#0:_0, timestamp=1472824360985, value=_0
\x00\x00\xC1\x05\x00\x80\x00\x00\x00\x00\x01\xE2@ column=L#0:_0, timestamp=1472824362230, value=_0
\x00\x01\xC1\x03\x00\x80\x00\x00\x00\x00\x0009 column=L#0:_0, timestamp=1472824360985, value=_0
\x00\x01\xC1\x06\x00\x80\x00\x00\x00\x00\x01\xE2@ column=L#0:_0, timestamp=1472824362230, value=_0
\x00\x02\xC1\x04\x00\x80\x00\x00\x00\x00\x0009 column=L#0:_0, timestamp=1472824360985, value=_0
\x00\x02\xC1\x07\x00\x80\x00\x00\x00\x00\x01\xE2@ column=L#0:_0, timestamp=1472824362230, value=_0
\x80\x00\x00\x00\x00\x0009 column=0:M, timestamp=1472824360985, value=\xC1\x02
\x80\x00\x00\x00\x00\x0009 column=0:N, timestamp=1472824360985, value=\x80\x00
\x80\x00\x00\x00\x00\x0009 column=0:X, timestamp=1472824360985, value=\x80\x00\x00\x01
\x80\x00\x00\x00\x00\x0009 column=0:Y, timestamp=1472824360985, value=\x80\x00\x00\x02
\x80\x00\x00\x00\x00\x0009 column=0:Z, timestamp=1472824360985, value=\x80\x00\x00\x03
\x80\x00\x00\x00\x00\x0009 column=0:_0, timestamp=1472824360985, value=x
\x80\x00\x00\x00\x00\x01\xE2@ column=0:M, timestamp=1472824362230, value=\xC1\x03
\x80\x00\x00\x00\x00\x01\xE2@ column=0:N, timestamp=1472824362230, value=\x80\x01
\x80\x00\x00\x00\x00\x01\xE2@ column=0:X, timestamp=1472824362230, value=\x80\x00\x00\x04
\x80\x00\x00\x00\x00\x01\xE2@ column=0:Y, timestamp=1472824362230, value=\x80\x00\x00\x05
\x80\x00\x00\x00\x00\x01\xE2@ column=0:Z, timestamp=1472824362230, value=\x80\x00\x00\x06
\x80\x00\x00\x00\x00\x01\xE2@ column=0:_0, timestamp=1472824362230, value=x
8 row(s) in 0.0570 seconds

注意到,我们只插入了两行,应该只有两个主键才对,HBase Shell 中我们看到了8个主键,含有0:M,0:N,0:X,0:Y,0:Z的两行无疑是我们执行 UPSERT 命令插入的,剩下的六行是什么鬼?

\x00\x00\xC1\x02\x00\x80\x00\x00\x00\x00\x0009\x00\x00\xC1\x05\x00\x80\x00\x00\x00\x00\x01\xE2@ 为例,很快我们就可以发现这两行的后半截 \x80\x00\x00\x00\x00\x0009 就是原数据表的主键了,这与 !primarykeys 中看到的 :IDKEY_SEQ = 3 是一致的。

剩下前边的 \x00\x00\xC1\x02\x00\x00\x00\xC1\x05\x00 ,不过代表什么意思就得猜了,索引表主键查看的结果中,被索引字段(姑且还这么叫吧)是被放在了第二位的 KEY_SEQ = 2 ,并且是 Decimal类型的,这就是为什么在创建表的时候多了一个 Decimal 类型的字段 M,在 HBase Shell 中的查询结果有这么一行\x80\x00\x00\x00\x00\x00\x04\xD2 column=0:M, timestamp=1472822139596, value=\xC1\x02,看 M 的值对比\x00\x00\xC1\x02\x00,发现出后边多了一个 \x00之外,其余是吻合的,即 X = M,前缀 \x00\x00 \x00\x01 \x00\x02 应该就是三个索引的编号了,如果在创建第四个索引,就还会多出 N 行,N 为原始数据的行数,经过实践确实如此,只是中间的一个 \x00 是做什么用的还不清楚!

从这里大概可以猜出 shadow column families的意思了,应该就是指在同一个表中添加主键构成不同的行作为索引。

3.总结

经过以上实验,总结 Phoenix 的 Local Index :

  • 从 Phoenix 4.8 版本开始,Local Index 和原始数据是存在于一个表中的!
  • Local Index 先范围扫描约束,根据得到的 :ID 再查询原始数据,所以 Local Index 是可以用在任意字段的查询中都有作用!
  • shadow column families 应该就是一个小 trick 而已,弄得概念很高大上,如果我理解错了还望指正。
  • Local Index 查询至少经过一次 RANGE SCAN 和 GET ,所以效率上肯定不如 Global Index 高。
  • 疑问1:Local Index 是保证了索引表和原始表在同一个 RegionServer 上的,如果在一个表中,行健组成又不同,Phoenix 是如何实现这一点的?
  • 官方文档说:如果一个 Local Index 被使用,所用的 Region 都要被检查是否有满足约束的数据,所以读取效率很低呐!

最后,发现 Phoenix 把各种 Data Type 的都变成 Hexacdemical 字符串了,很不利于人来查看,虽然在 Phoenix 的交互命令行中是类如 RDMBS 的可读的,但在这次实验中也发现了这些 Hexacdemical 太不友好,如果有必要并且找到比较好的解决方案的话,大家给提供些帮助如何使 hexacdemical readable?