目录
一、Phoenix是什么
Phoenix 将 Hbase的数据模型映射到关系世界,让开发人员可以是使用SQL去操作HBase。
Phoenix与HBase之间的映射关系如下表
hbase | phoenix |
---|---|
namespace | database |
table | table |
rowkey | Primary key |
column family | 无 |
column qulifier | Column |
Phoenix特点如下:
- 将 SQL 编译为 HBase 扫描
- 确定扫描RowKey的最佳开始和结束位置
- 扫描并行执行
- 将where子句推送到服务器端的过滤器
- 通过协处理器进行聚合操作 coprocesssor select count(*) from stu;
- 完美支持HBase二级索引创建
- DML命令以及通过DDL命令创建和操作表
- 容易集成:如Spark,Hive,Pig,Flume和Map Reduce
二、Phoenix命令操作
2.1 基本命令
!table查看表信息
!describe tablename可以查看表字段信息
!history可以查看执行的历史SQL
!dbinfo
!index tb;查看tb的索引
!help查看其他操作
2.2 表操作
2.2.1 创建表
Hbase是区分大小写的,Phoenix 默认会把sql语句中的小写转换成大写,再建表,如果不希望转换,需要将表名,字段名等使用引号。HBase默认phoenix表的主键对应到ROW,column family 名为0,也可以在建表的时候指定column family。创建的表如果是联合主键,会把联合主键当作rowkey。
create table test (id varchar primary key,name varchar,age integer );
2.2.2 显示所有表
0: jdbc:phoenix:hadoop003:2181> !tables
2.2.3 删除表
drop table "cftest";
2.3 数据操作
2.3.1 全字段插入
upsert into "population" values('NY','NewYork',8143197); //数据存在则更新
2.3.2 部分字段插入
upsert into "population" (STATE,CITY) values('CA','Los Angeles');
2.3.3 删除数据
delete from "population" where state='CA' and city='Los Angeles';
2.4 HBase 表映射
如果hbase中已经有表存在,在phoenix中是看不到的,需要将hbase的表映射到phoenix中,如果只需要在phoenix中对该表进行查询操作,可以映射为视图,在phoenix中视图只能查询,不能插入数据;如果还需要在phoenix中对该表进行数据增删改查,可以映射为表。
2.4.1 视图映射
(1)先在hbase中准备好表,并添加数据
create 'emp','info'
put 'emp','7368','info:ename','TOM'
put 'emp','7368','info:job','CLERK'
put 'emp','7368','info:mgr','7902'
put 'emp','7368','info:sal','700.0'
put 'emp','7368','info:hiredate','1980-12-17'
put 'emp','7368','info:deptno','10'
put 'emp','7368','info:comm','200'
put 'emp','7369','info:ename','SMITH'
put 'emp','7369','info:job','SALES'
put 'emp','7369','info:mgr','7902'
put 'emp','7369','info:sal','800.0'
put 'emp','7369','info:hiredate','1986-12-17'
put 'emp','7369','info:deptno','20'
put 'emp','7369','info:comm','300'
(2)在phoenix中创建视图,并查看数据
// 创建表
create view "emp"(
"empno" varchar primary key,
"info"."ename" varchar,
"info"."job" varchar,
"info"."mgr" varchar,
"info"."hiredate" varchar,
"info"."sal" varchar,
"info"."comm" varchar,
"info"."deptno" varchar
) column_encoded_bytes=0;
// 查询数据
select * from "emp";
// 插入数据会报错
upsert into "emp" ("empno","ename","job") values ('8000','cat','SALES');
(3)删除视图
drop view "emp";
2.4.2 表映射
// 创建表
create table "emp"(
"empno" varchar primary key,
"info"."ename" varchar,
"info"."job" varchar,
"info"."mgr" varchar,
"info"."hiredate" varchar,
"info"."sal" varchar,
"info"."comm" varchar,
"info"."deptno" varchar
)column_encoded_bytes=0;
// 查询表
select * from "emp";
// 可以插入数据
upsert into "emp" ("empno","ename","job") values ('8000','cat','SALES');
三、Phoenix 索引
3.1 Phoenix 索引介绍
HBase 只能通过 rowkey 进行搜索, 一般把 rowkey 称作一级索引. 在很长的一段时间里 HBase 就只支持一级索引.
HBase 里面只有 rowkey 作为一级索引, 如果要对库里的非 rowkey 字段进行数据检索和查询, 往往要通过 MapReduce/Spark 等分布式计算框架进行,硬件资源消耗和时间延迟都会比较高。
为了 HBase 的数据查询更高效、适应更多的场景, 诸如使用非 rowkey 字段检索也能做到秒级响应,或者支持各个字段进行模糊查询和多字段组合查询等, 因此需要在 HBase 上面构建二级索引, 以满足现实中更复杂多样的业务需求。
从 0.94 版本开始, HBase 开始支持二级索引。
3.2 配置hbase支持Phoenix创建二级索引
(1)向hbase的配置文件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>
<description>Factory to create the Phoenix RPC Scheduler that uses separate queues for index and metadata updates</description>
</property>
<property>
<name>hbase.rpc.controllerfactory.class</name>
<value>org.apache.hadoop.hbase.ipc.controller.ServerRpcControllerFactory</value>
<description>Factory to create the Phoenix RPC Scheduler that uses separate queues for index and metadata updates</description>
</property>
<property>
<name>hbase.master.loadbalancer.class</name>
<value>org.apache.phoenix.hbase.index.balancer.IndexLoadBalancer</value>
</property>
<property>
<name>hbase.coprocessor.master.classes</name>
<value>org.apache.phoenix.hbase.index.master.IndexMasterObserver</value>
</property>
(2)同时把以前zookeeper的配置的端口号去掉
(3)将配置文件分发,重启Phoenix
3.2 覆盖索引
Phoenix特别强大,为我们提供了覆盖索引, 所谓覆盖索引就是从索引中直接获取查询结果。一旦从索引中找到数据,我们就不需要从主表再去查询数据了。可以将我们关心的数据捆绑在索引行中,从而节省了读取时间开销。
要使用覆盖索引需要注意:1、select查询列中包含在索引列中,2、where条件包含索引列或者复合索引的前导列。
//创建测试用例
create table indextest (
id char(4) not null primary key,
age integer,
name varchar,
company varchar,
school varchar)column_encoded_bytes=0;
upsert into indextest values('0001',18,'张三','张三公司','张三学校');
upsert into indextest values('0002',19,'李四','李四公司','李四学校');
upsert into indextest values('0003',20,'王五','王五公司','王五学校');
upsert into indextest values('0004',21,'赵六','赵六公司','赵六学校');
// 创建基于company和school的覆盖索引,并将name列绑定在索引上。
create index cover_indextest_index on indextest(company,school) include (name);
// 可以执行如下sql,观察是否走索引。
explain select name from indextest where school ='张三学校'; //不走
explain select school from indextest where company ='张三学校'; //走
// 删除索引
// drop index 索引名称 on 表名;
drop index cover_indextest_index on indextest;
// 接下来创建一个单字段覆盖索引看看,会走索引吗?
create index cover_indextest_index_company on indextest(company) include (name);
explain select name from indextest where company ='张三公司';
//当使用*号作为字段去检索时,走的FULL SCAN。
explain select * from indextest where company ='张三公司';
// 这个时候你需要在查询时强行指定索引。
explain select /*+index(indextest cover_indextest_index_company)*/ * from indextest where company='张三公司';
// 创建包含多个字段的联合索引
create index index_indextest_name_company on indextest(name,company);
// 如下查询走索引
explain select name from indextest where name ='张三';
// 如下查询不走索引
explain select name from indextest where company ='张三公司';
3.3 本地索引与全局索引
本地索引:即local index,索引数据会在原表添加新的列族存储在原表中,侵入性强, 适合写多读少的情况。索引数据和数据表的数据是存放在相同的服务器中的,避免了在写操作的时候往不同服务器的索引表中写索引带来的额外开销。查询的字段不是索引字段索引表也会被使用,这会带来查询速度的提升。
全局索引:即global index,索引数据会存储上单独的一张表中,是默认的索引格式,适合读多写少的情况。写数据的时候会消耗大量开销,因为索引表也要更新,而索引表是分布在不同的数据节点上的,跨节点的数据传输带来了较大的性能消耗。在读取索引列的数据的时候 Phoenix 会选择索引表来降低查询消耗的时间。如果想查询的字段不是索引字段的话索引表不会被使用,也就是说不会带来查询速度的提升。
// 建表
create table indextest (
id char(4) not null primary key,
age integer,
name varchar,
company varchar,
school varchar)column_encoded_bytes=0;
// 创建全局索引
0: jdbc:phoenix:> create index index_indextest_name on indextest(name);
// 在Hbase客户端查看,并没有多出新的列族,只有默认的0族,而查看phoenix中的表,多了个索引表。
hbase(main):024:0> describe 'INDEXTEST'
// 将刚刚创建的全局索引删除,再创建本地索引,在Hbase客户端查看,发现多了索引列族。
drop index index_indextest_name on indextest;
create local index index_indextest_name on indextest(name);
3.4 索引异步创建
默认情况下,创建索引时,会在CREATE INDEX调用期间同步填充索引。但是如果数据表太大的话,会耗时巨大。从4.5开始,通过在索引创建DDL语句中包含ASYNC关键字,可以异步完成索引的填充,可以理解为让它进入后台慢慢去创建索引,而不是卡在那里等着创建完索引才退出命令。
(1)在phoenix执行 并插入一些数据
drop table indextest;
create table indextest (
id char(10) not null primary key,
age integer,
name varchar,
company varchar,
school varchar)column_encoded_bytes=0;
(2)创建maven项目,引入hbase依赖
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.phoenix</groupId>
<artifactId>phoenix-core</artifactId>
<version>4.8.0-HBase-1.2</version>
</dependency>
</dependencies>
(3)采用工具类,插入10w条数据测试。Jdbc连接工具
package com.bigdata.myphoenix;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 功能介绍:使用jdbc对数据库操作:查询、更新(插入/修改/删除)、批量更新
*/
public class DButil {
private final Logger LOGGER = LoggerFactory.getLogger(getClass());
/**jdbc的链接*/
private Connection conn = null;
/**准备sql*/
private PreparedStatement ps = null;
{
initConnection();
}
/**
* @param sql
* @param params 参数
* 功能介绍:更新操作(修改,删除,插入)
*/
public int executeUpdate(String sql, Object[] params) {
if(null == conn){
initConnection();
}
try {
ps = conn.prepareStatement(sql);
if (params.length != 0) {
for (int i = 0; i < params.length; i++) {
ps.setObject(i + 1, params[i]);
}
}
int rows = ps.executeUpdate();
conn.commit();
return rows;
} catch (Exception e) {
e.printStackTrace();
} finally {
closeUpdate();
}
return 0;
}
/**
* @param sql
* @param list
* 功能介绍:批量更新
*/
public void batchUpdate(String sql, List<Object[]> list) {
if(null == conn){
initConnection();
}
try {
ps = conn.prepareStatement(sql);
//关闭自动提交事务
conn.setAutoCommit(false);
//防止内存溢出
final int batchSize = 1000;
//记录插入数量
int count = 0;
int size = list.size();
Object[] obj = null;
for (int i = 0; i < size; i++) {
obj = list.get(i);
for (int j = 0; j < obj.length; j++) {
ps.setObject(j + 1, obj[j]);
}
ps.addBatch();
if (++count % batchSize == 0) {
ps.executeBatch();
conn.commit();
}
}
ps.executeBatch();
conn.commit();
conn.setAutoCommit(true);
} catch (SQLException e) {
e.printStackTrace();
try {
conn.rollback();
conn.setAutoCommit(true);
} catch (SQLException e1) {
e1.printStackTrace();
}
} finally {
//关闭资源
closeUpdate();
}
}
/**
* @param sql
* @param params
* 功能介绍:查询操作
*/
public List<Map<String, Object>> executeQuery(String sql, Object[] params) {
if(null == conn){
initConnection();
}
ResultSet rs = null;
List<Map<String, Object>> list = null;
try {
ps = conn.prepareStatement(sql);
if (params != null) {
for (int i = 0; i < params.length; i++) {
ps.setObject(i + 1, params[i]);
}
}
long startTime = System.currentTimeMillis();
rs = ps.executeQuery();
LOGGER.info("UserBigTableService sql-executeQuery-time: " + (System.currentTimeMillis() - startTime) + "ms");
list = new ArrayList<>();
//移动光标,如果新的当前行有效,则返回 true;如果不存在下一行,则返回 false
while (rs.next()) {
ResultSetMetaData rsmd = rs.getMetaData();
Map<String, Object> map = new HashMap<>(16);
for (int i = 1; i <= rsmd.getColumnCount(); i++) {
map.put(rsmd.getColumnName(i), rs.getObject(i));
}
list.add(map);
}
return list;
} catch (Exception e) {
e.printStackTrace();
} finally {
closeQuery(rs);
}
return null;
}
/**
* @param sql
* @param params
* 功能介绍:查询操作一条记录
*/
public Map<String, Object> query (String sql, Object[] params) {
if(null == conn){
initConnection();
}
ResultSet rs = null;
Map<String, Object> map = null;
try {
ps = conn.prepareStatement(sql);
if (params != null) {
for (int i = 0; i < params.length; i++) {
ps.setObject(i + 1, params[i]);
}
}
rs = ps.executeQuery();
//移动光标,如果新的当前行有效,则返回 true;如果不存在下一行,则返回 false
while (rs.next()) {
ResultSetMetaData rsmd = rs.getMetaData();
map = new HashMap<>(16);
for (int i = 1; i <= rsmd.getColumnCount(); i++) {
map.put(rsmd.getColumnName(i), rs.getObject(i));
}
//若有多条记录,取第一条。
break;
}
return map;
} catch (Exception e) {
e.printStackTrace();
} finally {
closeQuery(rs);
}
return null;
}
/**
* 初始化连接
*/
private void initConnection() {
try {
//local
conn = DriverManager.getConnection("jdbc:phoenix:hadoop003");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 功能介绍:关闭更新资源
*/
private void closeUpdate() {
try {
if (ps != null) {
ps.close();
}
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* @param rs 功能介绍:关闭查询资源
*/
private void closeQuery(ResultSet rs) {
try {
if (rs != null) {
rs.close();
}
if (ps != null) {
ps.close();
}
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
(4)测试类
package com.bigdata.myphoenix;
import java.util.ArrayList;
import java.util.List;
public class TestPhoenix {
public static void main(String[] args) {
DButil dButil = new DButil();
int id = 1;
String sql = "upsert into indextest values(?,?,?,?,?)";
for(int i = 0;i < 10*10000;i++){
Object[] arr = new Object[5];
arr[0] = id+"";
arr[1] = 18;
arr[2] = "张三"+id;
arr[3] = "张三 公司"+id;
arr[4] = "张三 学校"+id;
dButil.executeUpdate(sql, arr);
id ++;
System.out.println(id);
}
}
}
(5)执行插入数据操作,等待10w条数据插入完毕。也可以在phoenix执行count语句查看
使用异步索引,需要两步。
第一步:创建异步索引
create index index_indextest_name on indextest(name) async;
这时,因为还没有真正生成索引,因此查看执行计划,不会走索引的。
explain select name from indextest where name='张三姓名';
第二步:通过HBase命令行单独启动填充索引表的map reduce作业,这时会提交yarn任务,为刚才的表创建索引。
[root@hadoop003 hbase-1.3.1]# bin/hbase org.apache.phoenix.mapreduce.index.IndexTool --data-table INDEXTEST --index-table INDEX_INDEXTEST_NAME --output-path /hbase/index/indextest/index_indextest_name
等待yarn任务完成,这时再查看执行计划,就会看到查询走索引了。
只有当map reduce作业完成时,才会激活索引并开始在查询中使用。
output-path选项用于指定用于写入HFile的HDFS目录