以下实例我们都以clickhouse最常用的*MergeTree
(合并树)子类引擎来做介绍。
以分布式表为例,那么ck数据存放于该集群下多个shard分片中。如果shard不在一个节点上,也就是数据会分散到多台机下。每个分片中的数据会根据建表时指定的partition在进行划分,而单个partition中,如果数据容量超过一定阈值又会重新拆分。
# 表结构:
${ck_data}/metadata/path_to_table/*.sql
# 实际数据存放目录:
${ck_data}/data/path_to_table/${partition_*}/**
# 装卸数据目录
${ck_data}/data/path_to_table/detached
clickhouse是真正的列式数据库管理系统,除了数据本身外基本不存在其他额外的数据。
下面我们看下clickhouse特有的计算优势。
Clickhouse 中最强大的表引擎当属 MergeTree (合并树)引擎及该系列(*MergeTree)中的其他引擎。MergeTree 引擎系列的基本理念如下。当你有巨量数据要插入到表中,你要高效地一批批写入数据片段,并希望这些数据片段在后台按照一定规则合并。相比在插入时不断修改(重写)数据进存储,这种策略会高效很多。主要优势:
以官网用例来看。我们以 (CounterID, Date) 以主键。排序好的索引的图示会是下面这样:
全部数据 : [-------------------------------------------------------------------------]
CounterID: [aaaaaaaaaaaaaaaaaabbbbcdeeeeeeeeeeeeefgggggggghhhhhhhhhiiiiiiiiikllllllll]
Date: [1111111222222233331233211111222222333211111112122222223111112223311122333]
标记: | | | | | | | | | | |
a,1 a,2 a,3 b,3 e,2 e,3 g,1 h,2 i,1 i,3 l,3
标记号: 0 1 2 3 4 5 6 7 8 9 10
如果指定查询如下:
CounterID in ('a', 'h')
,服务器会读取标记号在 [0, 3) 和 [6, 8) 区间中的数据。
CounterID IN ('a', 'h') AND Date = 3
,服务器会读取标记号在 [1, 3) 和 [7, 8) 区间中的数据。
Date = 3
,服务器会读取标记号在 [1, 10] 区间中的数据。上面例子可以看出使用索引通常会比全表描述要高效。
稀疏索引会引起额外的数据读取。当读取主键单个区间范围的数据时,每个数据块中最多会多读 index_granularity * 2
行额外的数据。大部分情况下,当 index_granularity = 8192
时,ClickHouse的性能并不会降级。
稀疏索引让你能操作有巨量行的表。因为这些索引是常驻内存(RAM)的。ClickHouse 不要求主键惟一。所以,你可以插入多条具有相同主键的行。下面看在实际语法:
CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
...
)
ENGINE MergeTree()
PARTITION BY toYYYYMM(EventDate)
ORDER BY (CounterID, EventDate, intHash32(UserID))
SETTINGS index_granularity=8192
存储部分相关大部分逻辑放在/src/Storages下。
表的顶层抽象是IStorage,对此接口不同的实现成为不同的表引擎. 例如 StorageMergeTree, StorageMemory等,这些类的实例是表。
该接口中包含很多通用的方法,常用的增删改查read、write、alter、drop等
virtual Pipes read(
const Names & /*column_names*/,
const SelectQueryInfo & /*query_info*/,
const Context & /*context*/,
QueryProcessingStage::Enum /*processed_stage*/,
size_t /*max_block_size*/,
unsigned /*num_streams*/)
{
throw Exception("Method read is not supported by storage " + getName(), ErrorCodes::NOT_IMPLEMENTED);
}
/** The same as read, but returns BlockInputStreams.
*/
BlockInputStreams readStreams(
const Names & /*column_names*/,
const SelectQueryInfo & /*query_info*/,
const Context & /*context*/,
QueryProcessingStage::Enum /*processed_stage*/,
size_t /*max_block_size*/,
unsigned /*num_streams*/);
virtual BlockOutputStreamPtr write(
const ASTPtr & /*query*/,
const Context & /*context*/)
{
throw Exception("Method write is not supported by storage " + getName(), ErrorCodes::NOT_IMPLEMENTED);
}
高可用存储对应生产来说是必不可少的。这里看下ck的分布式存储,和hive的区别还是较大,首先是在查询上需要借助分布式表才能实现。值得注意的是,ck的分布式表并不直接存储数据,而是类似于视图的存在。读是自动并行的。读取时,远程服务器表的索引(如果有的话)会被使用。
假设4个节点example01-01-1、example01-01-2、example01-02-1、example01-02-2。集群名称为logs。
<remote_servers>
<logs>
<shard>
<weight>1weight>
<internal_replication>falseinternal_replication>
<replica>
<host>example01-01-1host>
<port>9000port>
replica>
<replica>
<host>example01-01-2host>
<port>9000port>
replica>
shard>
<shard>
<weight>2weight>
<internal_replication>falseinternal_replication>
<replica>
<host>example01-02-1host>
<port>9000port>
replica>
<replica>
<host>example01-02-2host>
<secure>1secure>
<port>9440port>
replica>
shard>
logs>
remote_servers>
我们在logs集群4个节点中都创建test1表,根据totalDate分区。
CREATE TABLE default.test1 on cluster logs
(`uid` Int32, `totalDate` String )
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/test1', '{replica}')
PARTITION BY totalDate ORDER BY totalDate SETTINGS index_granularity = 8192;
之后在集群中创建分布式表test1_all
-- 建分布式表指向test1
CREATE TABLE default.test1_all on cluster logs
as test1
ENGINE = Distributed(logs, default, test1, rand())
然后可以向分布式表写入些测试数据,之后到具体的节点查原表进行校验。因为系列六中有类似的实例就不在此赘述。
ck推荐采用复制表+内部同步实现。我们先看下上述配置中internal_replication
属性。当设置false时,插入到分布式表中的数据被插入到两个本地表中,因为不会检查副本的一致性,并且随着时间的推移,副本数据可能会有些不一样。
复制表 ,ck数据副本是提供的表级的,而非服务器级别的,所以,服务器里可以同时有复制表和非复制表。这又是ck和hive的一个很大不同。
下面我们来看下四种复制模式