目录
一、为什么要使用二级索引?
对于HBase而言,如果想精确地定位到某行记录,唯一的办法是通过rowkey来查询。如果不通过rowkey来查找数据,就必须逐行地比较每一列的值,即全表扫瞄。
对于较大的表,全表扫描的代价是不可接受的。但是,很多情况下,需要从多个角度查询数据。
-
例如,在定位某个人的时候,可以通过姓名、身份证号、学籍号等不同的角度来查询
-
要想把这么多角度的数据都放到rowkey中几乎不可能(业务的灵活性不允许,对rowkey长度的要求也不允许)。
-
所以需要secondary index(二级索引)来完成这件事。secondary index的原理很简单,但是如果自己维护的话则会麻烦一些。
-
现在,Phoenix已经提供了对HBase secondary index的支持。
二、全局索引和本地索引
2.1 Global Indexing
-
Global indexing,全局索引,适用于==读多写少==的业务场景。
-
使用Global indexing在写数据的时候开销很大,因为所有对数据表的更新操作(DELETE, UPSERT VALUES and UPSERT SELECT),都会引起索引表的更新,而索引表是分布在不同的数据节点上的,跨节点的数据传输带来了较大的性能消耗。
-
在读数据的时候Phoenix会选择索引表来降低查询消耗的时间。
-
在默认情况下如果想查询的字段不是索引字段的话索引表不会被使用,也就是说不会带来查询速度的提升。
2.2 Local Indexing
-
Local indexing,本地索引,适用于==写操作频繁==以及空间受限制的场景。
-
与Global indexing一样,Phoenix会自动判定在进行查询的时候是否使用索引。
-
使用Local indexing时,索引数据和数据表的数据存放在相同的服务器中,这样避免了在写操作的时候往不同服务器的索引表中写索引带来的额外开销。
-
使用Local indexing的时候即使查询的字段不是索引字段索引表也会被使用,这会带来查询速度的提升,这点跟Global indexing不同。对于Local Indexing,一个数据表的所有索引数据都存储在一个单一的独立的可共享的表中。
三、可变索引和不可变索引
3.1 immutable index
-
immutable index,不可变索引,适用于数据==只增加不更新并且按照时间先后顺序存储==(time-series data)的场景,如保存日志数据或者事件数据等。
-
不可变索引的存储方式是write one,append only。
-
当在Phoenix使用create table语句时指定IMMUTABLE_ROWS = true表示该表上创建的索引将被设置为不可变索引。
-
不可变索引分为Global immutable index和Local immutable index两种。
-
Phoenix默认情况下如果在create table时不指定IMMUTABLE_ROW = true时,表示该表为mutable。
3.2 mutable index
-
mutable index,可变索引,适用于数据有==增删改==的场景。
-
Phoenix默认情况创建的索引都是可变索引,除非在create table的时候显式地指定IMMUTABLE_ROWS = true。
-
可变索引同样分为Global mutable index和Local mutable index两种。
四、配置HBase支持Phoenix二级索引
4.1 修改配置文件
-
如果要启用phoenix的二级索引功能,需要修改配置文件hbase-site.xml
-
注意:
-
vim hbase-site.xml
<!-- 添加配置 -->
<property>
<name>hbase.regionserver.wal.codec</name>
<value>org.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec</value>
</property>
<property>
<name>hbase.region.server.rpc.scheduler.factory.class</name>
<value>org.apache.hadoop.hbase.ipc.PhoenixRpcSchedulerFactory</value>
</property>
<property>
<name>hbase.rpc.controllerfactory.class</name>
<value>org.apache.hadoop.hbase.ipc.controller.ServerRpcControllerFactory</value>
</property>
4.2 重启hbase
-
完成上述修改后重启hbase集群使配置生效。
五、实战
5.1 在phoenix中创建表
-
首先,在phoenix中创建一个user table
-
node02执行以下命令,进入phoenix客户端,并创建表
cd /xsluo/install/apache-phoenix-4.14.0-cdh5.14.2-bin/
bin/sqlline.py node01:2181
create table user (
"session_id" varchar(100) not null primary key,
"f"."cookie_id" varchar(100),
"f"."visit_time" varchar(100),
"f"."user_id" varchar(100),
"f"."age" varchar(100),
"f"."sex" varchar(100),
"f"."visit_url" varchar(100),
"f"."visit_os" varchar(100),
"f"."browser_name" varchar(100),
"f"."visit_ip" varchar(100),
"f"."province" varchar(100),
"f"."city" varchar(100),
"f"."page_id" varchar(100),
"f"."goods_id" varchar(100),
"f"."shop_id" varchar(100)) column_encoded_bytes=0;
5.2 导入测试数据
-
将事先准备好的 user50w.csv 这个文件上传到node02的/xsluo/install/phoenixsql 这个路径下,该CSV文件中有50万条记录
-
node02执行以下命令,导入50W的测试数据
cd /xsluo/install/apache-phoenix-4.14.0-cdh5.14.2-bin/
bin/psql.py -t USER node01:2181 /xsluo/install/phoenixsql/user50w.csv
5.3 Global Indexing的二级索引测试
5.3.1 正常查询一条数据所需的时间
-
在为表USER创建secondary index之前,先看看查询一条数据所需的时间 在node02服务器,进入phoenix的客户端,然后执行以下sql语句,查询数据,查看耗费时间
cd /xsluo/install/apache-phoenix-4.14.0-cdh5.14.2-bin
bin/sqlline.py node01:2181
0: jdbc:phoenix:node01:2181> select "cookie_id" from user where "cookie_id" = '99738fd1-2084-44e9';
+---------------------+
| cookie_id |
+---------------------+
| 99738fd1-2084-44e9 |
+---------------------+
1 row selected (6.678 seconds)
可以看到,对名为cookie_id的列进行按值查询需要6秒左右。
我们可以通过explain来查看执行计划 EXPLAIN(语句的执行逻辑及计划):
0: jdbc:phoenix:node01:2181> explain select "cookie_id" from user where "cookie_id" = '99738fd1-2084-44e9';
+--------------------------------------------------------------------------------------------+-----------------+----+
| PLAN | EST_BYTES_READ | ES |
+--------------------------------------------------------------------------------------------+-----------------+----+
| CLIENT 2-CHUNK 289067 ROWS 314572862 BYTES PARALLEL 1-WAY ROUND ROBIN FULL SCAN OVER USER | 314572862 | 28 |
| SERVER FILTER BY f."cookie_id" = '99738fd1-2084-44e9' | 314572862 | 28 |
+--------------------------------------------------------------------------------------------+-----------------+----+
2 rows selected (0.054 seconds)
由此看出先进行了全表扫描再通过过滤器来筛选出目标数据,显示这种查询方式效率是很低的。
5.3.2 给表USER创建基于Global Indexing的二级索引
-
进入到phoenix的客户端,然后执行以下命令创建索引
-
在cookie_id列上面创建二级索引:
0: jdbc:phoenix:node01:2181> create index USER_COOKIE_ID_INDEX on USER ("f"."cookie_id");
-- 查看当前所有表会发现多一张USER_COOKIE_ID_INDEX索引表,查询该表数据。
0: jdbc:phoenix:node01:2181> select * from USER_COOKIE_ID_INDEX limit 5;
再次执行查询"cookie_id"='99738fd1-2084-44e9'的数据记录 :
0: jdbc:phoenix:node01:2181> select "cookie_id" from user where "cookie_id" = '99738fd1-2084-44e9';
+---------------------+
| cookie_id |
+---------------------+
| 99738fd1-2084-44e9 |
+---------------------+
1 row selected (0.031 seconds)
此时:查询速度由6秒左右减少到了毫秒级别。
注意:select所带的字段必须包含在覆盖索引内,EXPLAIN(语句的执行逻辑及计划):
0: jdbc:phoenix:node01:2181> explain select "cookie_id" from user where "cookie_id" = '99738fd1-2084-44e9';
+--------------------------------------------------------------------------------------------------------+----------+
| PLAN | EST_BYTE |
+--------------------------------------------------------------------------------------------------------+----------+
| CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN RANGE SCAN OVER USER_COOKIE_ID_INDEX ['99738fd1-2084-44e9'] | null |
| SERVER FILTER BY FIRST KEY ONLY | null |
+--------------------------------------------------------------------------------------------------------+----------+
2 rows selected (0.051 seconds)
可以看到使用到了创建的索引USER_COOKIE_ID_INDEX。
5.3.3 以下查询不会用到索引表
-
虽然cookie_id是索引字段,但age不是索引字段,所以不会使用到索引
0: jdbc:phoenix:node01:2181> select "cookie_id","age" from user where "cookie_id"='99738fd1-2084-44e9';
+---------------------+------+
| cookie_id | age |
+---------------------+------+
| 99738fd1-2084-44e9 | 47 |
+---------------------+------+
1 row selected (6.137 seconds)
也可以通过EXPLAIN查询语句的执行逻辑及计划:
0: jdbc:phoenix:node01:2181> explain select "cookie_id","age" from user where "cookie_id"='99738fd1-2084-44e9';
+--------------------------------------------------------------------------------------------+-----------------+----+
| PLAN | EST_BYTES_READ | ES |
+--------------------------------------------------------------------------------------------+-----------------+----+
| CLIENT 2-CHUNK 289067 ROWS 314572862 BYTES PARALLEL 1-WAY ROUND ROBIN FULL SCAN OVER USER | 314572862 | 28 |
| SERVER FILTER BY f."cookie_id" = '99738fd1-2084-44e9' | 314572862 | 28 |
+--------------------------------------------------------------------------------------------+-----------------+----+
2 rows selected (0.101 seconds)
同理要查询的字段不是索引字段,也不会使用到索引表:
0: jdbc:phoenix:node01:2181> select "sex" from user where "cookie_id"='99738fd1-2084-44e9';
+---------+
| sex |
+---------+
| female |
+---------+
1 row selected (2.856 seconds)
5.4 Local Indexing的二级索引测试
5.4.1 正常查询一条数据所需的时间
-
在为表USER创建secondary index之前,先看看查询一条数据所需的时间
select * from user where "user_id"='371e963d-c-487065';
-
可以看到,对名为user_id的列进行按值查询需要3秒左右。
0: jdbc:phoenix:node01:2181> select * from user where "user_id"='371e963d-c-487065';
+---------------------------------------+---------------------+----------------------+--------------------+------+--+
| session_id | cookie_id | visit_time | user_id | age | |
+---------------------------------------+---------------------+----------------------+--------------------+------+--+
| 99738fd1-2084-44e9-90b7-3cd43d36d0f0 | 99738fd1-2084-44e9 | 2016-12-10 08:51:11 | 371e963d-c-487065 | 47 | |
+---------------------------------------+---------------------+----------------------+--------------------+------+--+
1 row selected (3.913 seconds)
EXPLAIN(语句的执行逻辑及计划):
0: jdbc:phoenix:node01:2181> explain select * from user where "user_id"='371e963d-c-487065';
+--------------------------------------------------------------------------------------------+-----------------+----+
| PLAN | EST_BYTES_READ | ES |
+--------------------------------------------------------------------------------------------+-----------------+----+
| CLIENT 2-CHUNK 289067 ROWS 314572862 BYTES PARALLEL 1-WAY ROUND ROBIN FULL SCAN OVER USER | 314572862 | 28 |
| SERVER FILTER BY f."user_id" = '371e963d-c-487065' | 314572862 | 28 |
+--------------------------------------------------------------------------------------------+-----------------+----+
2 rows selected (0.25 seconds)
由此知道先进行了全表扫描再通过过滤器来筛选出目标数据,显示这种查询方式效率是很低的。
5.4.2 给表USER创建基于Local Indexing的二级索引
-
在user_id列上面创建二级索引:
create local index USER_USER_ID_INDEX on USER ("f"."user_id");
-
查看当前所有表会发现多一张USER_USER_ID_INDEX索引表,查询该表数据。
0: jdbc:phoenix:node01:2181> !tables
+------------+--------------+-----------------------+---------------+----------+------------+-----------------------+
| TABLE_CAT | TABLE_SCHEM | TABLE_NAME | TABLE_TYPE | REMARKS | TYPE_NAME | SELF_REFERENCING_COL_ |
+------------+--------------+-----------------------+---------------+----------+------------+-----------------------+
| | | USER_COOKIE_ID_INDEX | INDEX | | | |
| | | USER_USER_ID_INDEX | INDEX | | | |
| | SYSTEM | CATALOG | SYSTEM TABLE | | | |
| | SYSTEM | FUNCTION | SYSTEM TABLE | | | |
| | SYSTEM | LOG | SYSTEM TABLE | | | |
| | SYSTEM | SEQUENCE | SYSTEM TABLE | | | |
| | SYSTEM | STATS | SYSTEM TABLE | | | |
| | | USER | TABLE | | | |
| | | USER_PHOENIX | TABLE | | | |
| | | employee | TABLE | | | |
+------------+--------------+-----------------------+---------------+----------+------------+-----------------------+
再次执行查询"user_id"='371e963d-c-487065'的数据记录:
0: jdbc:phoenix:node01:2181> select * from user where "user_id"='371e963d-c-487065';
+---------------------------------------+---------------------+----------------------+--------------------+------+--+
| session_id | cookie_id | visit_time | user_id | age | |
+---------------------------------------+---------------------+----------------------+--------------------+------+--+
| 99738fd1-2084-44e9-90b7-3cd43d36d0f0 | 99738fd1-2084-44e9 | 2016-12-10 08:51:11 | 371e963d-c-487065 | 47 | |
+---------------------------------------+---------------------+----------------------+--------------------+------+--+
1 row selected (0.375 seconds)
可以看到,对名为user_id的列进行按值查询需要0.3秒左右。
EXPLAIN(语句的执行逻辑及计划):
0: jdbc:phoenix:node01:2181> explain select * from user where "user_id"='371e963d-c-487065';
+-----------------------------------------------------------------------------------------+-----------------+-------+
| PLAN | EST_BYTES_READ | EST_R |
+-----------------------------------------------------------------------------------------+-----------------+-------+
| CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN RANGE SCAN OVER USER [1,'371e963d-c-487065'] | null | null |
| SERVER FILTER BY FIRST KEY ONLY | null | null |
+-----------------------------------------------------------------------------------------+-----------------+-------+
2 rows selected (0.233 seconds)
查看执行计划,没有执行全表扫描,效率更高了
此时:查询速度由3秒左右减少到了毫秒级别。
那如果查询的字段不包含在索引表中,有如何呢?
0: jdbc:phoenix:node01:2181> select "user_id","age","sex" from user where "user_id"='371e963d-c-487065';
+--------------------+------+---------+
| user_id | age | sex |
+--------------------+------+---------+
| 371e963d-c-487065 | 47 | female |
+--------------------+------+---------+
1 row selected (0.05 seconds)
0: jdbc:phoenix:node01:2181> explain select "user_id","age","sex" from user where "user_id"='371e963d-c-487065';
+-----------------------------------------------------------------------------------------+-----------------+-------+
| PLAN | EST_BYTES_READ | EST_R |
+-----------------------------------------------------------------------------------------+-----------------+-------+
| CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN RANGE SCAN OVER USER [1,'371e963d-c-487065'] | null | null |
| SERVER FILTER BY FIRST KEY ONLY | null | null |
+-----------------------------------------------------------------------------------------+-----------------+-------+
2 rows selected (0.06 seconds)
可以看到,在本地索引当中,查询的字段不包含在索引表中也使用到了创建的索引USER_USER_ID_INDEX.
5.5 如何确保query查询使用Index
-
要想让一个查询使用index,有三种方式实现。
5.5.1 创建 covered index
-
如果在某次查询中,查询项或者查询条件中包含除被索引列之外的列(主键MY_PK除外)。
-
默认情况下,该查询会触发full table scan(全表扫描),但是使用covered index则可以避免全表扫描。创建包含某个字段的覆盖索引,创建方式如下:
create index USER_COOKIE_ID_AGE_INDEX on USER ("f"."cookie_id") include("f"."age");
-
查看当前所有表会发现多一张USER_COOKIE_ID_AGE_INDEX索引表,查询该表数据。
select * from USER_COOKIE_ID_AGE_INDEX limit 5;
0: jdbc:phoenix:node01:2181> create index USER_COOKIE_ID_AGE_INDEX on USER ("f"."cookie_id") include("f"."age");
500,000 rows affected (42.897 seconds)
0: jdbc:phoenix:node01:2181> select * from USER_COOKIE_ID_AGE_INDEX limit 5;
+---------------------+---------------------------------------+--------+
| f:cookie_id | :session_id | f:age |
+---------------------+---------------------------------------+--------+
| 000036bd-9ede-4d2e | 000036bd-9ede-4d2e-aac4-110b1924c35d | 24 |
| 00003aa1-aacd-4f48 | 00003aa1-aacd-4f48-8057-148d8012abcf | 13 |
| 0000417b-4412-445e | 0000417b-4412-445e-8431-4d9d7459ff40 | 66 |
| 0000a391-4fd8-44bd | 0000a391-4fd8-44bd-9970-da721fbe860d | 36 |
| 0000ce5e-3121-4da9 | 0000ce5e-3121-4da9-8ffb-19c9a3621548 | 9 |
+---------------------+---------------------------------------+--------+
5 rows selected (0.038 seconds)
- 查询数据
select "age" from user where "cookie_id"='99738fd1-2084-44e9';
select "age","sex" from user where "cookie_id"='99738fd1-2084-44e9';
0: jdbc:phoenix:node01:2181> select "age" from user where "cookie_id"='99738fd1-2084-44e9';
+------+
| age |
+------+
| 47 |
+------+
1 row selected (0.051 seconds)
0: jdbc:phoenix:node01:2181> select "age","sex" from user where "cookie_id"='99738fd1-2084-44e9';
+------+---------+
| age | sex |
+------+---------+
| 47 | female |
+------+---------+
1 row selected (2.394 seconds)
由此可见,当查询字段中包含索引的列和查询条件也包含索引的列时,查询会用到索引表;当查询的字段包含了不在索引表中的列,则不会走索引表。
5.5.2 在查询中提示其使用index
在select和column_name之间加上/*+ Index(<表名> <index表名>)*/
,通过这种方式强制使用索引。
select /*+ index(user,USER_COOKIE_ID_AGE_INDEX) */ "age" from user where "cookie_id"='99738fd1-2084-44e9';
如果age是索引字段,那么就会直接从索引表中查询。如果age不是索引字段,那么将会进行全表扫描,所以当用户明确知道表中数据较少且符合检索条件时才适用,此时的性能才是最佳的。
5.5.3 使用本地索引 (创建Local Indexing 索引)
详细见上面。
5.6 索引重建
Phoenix的索引重建是把索引表清空后重新装配数据。
alter index USER_COOKIE_ID_INDEX on user rebuild;
5.7 删除索引
删除某个表的某张索引: 语法 drop index 索引名称 on 表名 例如:
drop index USER_COOKIE_ID_INDEX on user;
如果表中的一个索引列被删除,则索引也将被自动删除
如果删除的是覆盖索引上的列,则此列将从覆盖索引中被自动删除。
六、索引性能调优
一般来说,索引已经很快了,不需要特别的优化。
这里也提供了一些方法,让你在面对特定的环境和负载的时候可以进行一些调优。下面的这些需要在hbase-site.xml文件中设置,针对所有的服务器。
1. index.builder.threads.max
创建索引时,使用的最大线程数。
默认值: 10。
2. index.builder.threads.keepalivetime
创建索引的创建线程池中线程的存活时间,单位:秒。
默认值: 60
3. index.writer.threads.max
写索引表数据的写线程池的最大线程数。
更新索引表可以用的最大线程数,也就是同时可以更新多少张索引表,数量最好和索引表的数量一致。
默认值: 10
4. index.writer.threads.keepalivetime
索引写线程池中,线程的存活时间,单位:秒。
默认值:60
5. hbase.htable.threads.max
每一张索引表可用于写的线程数。
默认值: 2,147,483,647
6. hbase.htable.threads.keepalivetime
索引表线程池中线程的存活时间,单位:秒。
默认值: 60
7. index.tablefactory.cache.size
允许缓存的索引表的数量。
增加此值,可以在写索引表时不用每次都去重复的创建htable,这个值越大,内存消耗越多。
默认值: 10
8. org.apache.phoenix.regionserver.index.handler.count
处理全局索引写请求时,可以使用的线程数。
默认值: 30