赞
踩
官网
ClickHouse release 24.1, 2024-01-30
以毫秒为单位查询数十亿行
ClickHouse是用于实时应用和分析的最快、资源效率最高的开源数据库。
使用ClickHouse,你有三个选择:
ClickHouse®是一个高性能,面向列的SQL数据库管理系统(DBMS),用于在线分析处理(OLAP)。它既可以作为开源软件,也可以作为云服务。
OLAP场景需要在大型数据集之上的实时响应,用于具有以下特征的复杂分析查询:
在面向行的DBMS中,数据以行为单位存储,与一行相关的所有值在物理上彼此相邻存储。
在面向列的DBMS中,数据存储在列中,来自相同列的值存储在一起。
面向列的数据库更适合OLAP场景:它们在处理大多数查询时至少快100倍
ClickHouse使用所有可用的系统资源来充分发挥其潜力,以尽可能快地处理每个分析查询。这是由于分析功能和对实现最快的OLAP数据库所需的底层细节的关注的独特组合而成为可能的。
深入了解这个主题的有用文章包括:
在面向行的DBMS中,数据是按以下顺序存储的:
换句话说,与一行相关的所有值在物理上都是挨个存储的。
面向行的DBMS的例子有MySQL、Postgres和MS SQL Server。
在面向列的DBMS中,数据是这样存储的:
这些例子只显示了数据排列的顺序。来自不同列的值被分开存储,来自同一列的数据被一起存储。
面向列DBMS的例子:Vertica, Paraccel (Actian Matrix和Amazon Redshift), Sybase IQ, Exasol, Infobright, InfiniDB, MonetDB (VectorWise和Actian Vector), LucidDB, SAP HANA, Google Dremel, Google PowerDrill, Druid和kdb+。
不同的数据存储顺序更适合不同的场景。数据访问场景指的是进行什么查询、查询的频率和比例;每种查询类型要读取多少数据——行、列和字节;读取和更新数据的关系;数据的工作大小以及如何在本地使用它;是否使用事务,以及它们的隔离程度;数据复制和逻辑完整性要求;对每种查询类型的延迟和吞吐量的需求,等等。
系统负载越高,定制系统设置以匹配使用场景的需求就越重要,定制的粒度也就越细。没有一个系统能同样适合于不同的场景。如果系统能够适应广泛的场景,那么在高负载下,系统处理所有场景的能力都很差,或者只能很好地处理一个或几个可能的场景。
很容易看出,OLAP场景与其他流行的场景(如OLTP或Key-Value访问)非常不同。因此,如果您想获得良好的性能,尝试使用OLTP或Key-Value DB来处理分析查询是没有意义的。例如,如果您尝试使用MongoDB或Redis进行分析,那么与OLAP数据库相比,您将获得非常差的性能。
1)对于分析查询,只需要读取少量的表列。在面向列的数据库中,您可以只读取所需的数据。例如,如果您需要100列中的5列,那么I/O可以减少20倍。
2)由于数据是以块(packets)的形式读取的,因此更容易压缩。列中的数据也更容易压缩。这进一步减少了I/O量。
3)由于减少了I/O,更多的数据可以放入系统缓存。
例如,查询“计数每个广告平台的记录数”需要读取一个“广告平台ID”列,该列未压缩占用1字节。如果大多数流量不是来自广告平台,你可以预期这个专栏的压缩至少是原来的10倍。当使用快速压缩算法时,可以以每秒至少几gb的未压缩数据的速度进行数据解压缩。换句话说,该查询可以在单个服务器上以大约每秒几十亿行的速度处理。这个速度实际上是在实践中达到的。
由于执行查询需要处理大量行,因此可以为整个向量(vectors )而不是单独的行分派所有操作,或者实现查询引擎,以便几乎没有分派成本。如果不这样做,对于任何不太好的磁盘子系统,查询解释器不可避免地会使CPU停滞。在列中存储数据和在可能的情况下按列处理数据是有意义的。
有两种方法:
1)矢量引擎。所有的操作都是针对向量编写的,而不是针对单独的值。这意味着您不需要经常调用操作,并且调度成本可以忽略不计。操作代码包含一个优化的内部循环。
2)代码生成。为查询生成的代码包含所有间接调用。
在面向行的数据库中不会这样做,因为在运行简单查询时,这样做没有意义。然而,也有例外。例如,MemSQL使用代码生成来减少处理SQL查询时的延迟。(相比之下,分析型DBMS需要优化吞吐量,而不是延迟。)
注意,为了提高CPU效率,查询语言必须是声明式的(SQL或MDX),或者至少是向量(J, K)。查询应该只包含隐式循环,以便进行优化。
ClickHouse本机运行在Linux、FreeBSD和macOS上,并通过WSL运行在Windows上。在本地下载ClickHouse的最简单方法是运行以下curl
命令。它决定你的操作系统是否被支持,然后下载一个合适的ClickHouse二进制文件:
curl https://clickhouse.com/ | sh
# 安装
./clickhouse install
Start clickhouse-server with:
sudo clickhouse start
Start clickhouse-client with:
clickhouse-client --password
运行如下命令启动ClickHouse服务:
./clickhouse server
使用ClickHouse -client连接到ClickHouse服务。打开一个新的终端,更改clickhouse二进制文件的保存目录,并运行以下命令:
./clickhouse client
当它连接到本地主机上运行的服务时,你应该看到一个笑脸:
my-host :)
使用CREATE TABLE
定义一个新表。典型的SQL DDL命令与一个添加可以工作在ClickHouse-表在ClickHouse需要一个ENGINE
子句。使用MergeTree
来利用ClickHouse的性能优势:
CREATE TABLE my_first_table
(
user_id UInt32,
message String,
timestamp DateTime,
metric Float32
)
ENGINE = MergeTree
PRIMARY KEY (user_id, timestamp)
您可以在ClickHouse中使用熟悉的INSERT INTO TABLE
命令,但重要的是要了解,每次插入到MergeTree
表中都会导致在存储中创建一个part
(文件夹)。为了最小化parts,一次批量插入许多行(一次插入数万甚至数百万行)。
INSERT INTO my_first_table (user_id, message, timestamp, metric) VALUES
(101, 'Hello, ClickHouse!', now(), -1.0 ),
(102, 'Insert a lot of rows per batch', yesterday(), 1.41421 ),
(102, 'Sort your data based on your commonly-used queries', today(), 2.718 ),
(101, 'Granules are the smallest chunks of data read', now() + 5, 3.14159 )
你可以写一个SELECT
查询,就像你会用任何SQL数据库:
SELECT *
FROM my_first_table
ORDER BY timestamp
注意,响应以一个漂亮的表格格式返回:
下一步是将你当前的数据输入ClickHouse。我们有很多表函数和集成来获取数据。我们在下面的选项卡中有一些例子,或者查看我们的集成列表,了解与ClickHouse集成的一长串技术。
在本教程中,您将创建一个表并插入一个大型数据集(200万行纽约出租车数据)。然后,您将在数据集上运行查询,包括如何创建字典并使用它执行JOIN的示例。
纽约市的出租车数据包含数百万次出租车出行的详细信息,包括接送时间和地点、成本、小费金额、通行费、付款类型等列。让我们创建一个表来存储这些数据…
在default
数据库中创建如下trips
表:
CREATE TABLE trips ( `trip_id` UInt32, `vendor_id` Enum8('1' = 1, '2' = 2, '3' = 3, '4' = 4, 'CMT' = 5, 'VTS' = 6, 'DDS' = 7, 'B02512' = 10, 'B02598' = 11, 'B02617' = 12, 'B02682' = 13, 'B02764' = 14, '' = 15), `pickup_date` Date, `pickup_datetime` DateTime, `dropoff_date` Date, `dropoff_datetime` DateTime, `store_and_fwd_flag` UInt8, `rate_code_id` UInt8, `pickup_longitude` Float64, `pickup_latitude` Float64, `dropoff_longitude` Float64, `dropoff_latitude` Float64, `passenger_count` UInt8, `trip_distance` Float64, `fare_amount` Float32, `extra` Float32, `mta_tax` Float32, `tip_amount` Float32, `tolls_amount` Float32, `ehail_fee` Float32, `improvement_surcharge` Float32, `total_amount` Float32, `payment_type` Enum8('UNK' = 0, 'CSH' = 1, 'CRE' = 2, 'NOC' = 3, 'DIS' = 4), `trip_type` UInt8, `pickup` FixedString(25), `dropoff` FixedString(25), `cab_type` Enum8('yellow' = 1, 'green' = 2, 'uber' = 3), `pickup_nyct2010_gid` Int8, `pickup_ctlabel` Float32, `pickup_borocode` Int8, `pickup_ct2010` String, `pickup_boroct2010` String, `pickup_cdeligibil` String, `pickup_ntacode` FixedString(4), `pickup_ntaname` String, `pickup_puma` UInt16, `dropoff_nyct2010_gid` UInt8, `dropoff_ctlabel` Float32, `dropoff_borocode` UInt8, `dropoff_ct2010` String, `dropoff_boroct2010` String, `dropoff_cdeligibil` String, `dropoff_ntacode` FixedString(4), `dropoff_ntaname` String, `dropoff_puma` UInt16 ) ENGINE = MergeTree PARTITION BY toYYYYMM(pickup_date) ORDER BY pickup_datetime;
现在已经创建了一个表,让我们添加NYC出租车数据。它在S3中的CSV文件中,您可以从那里加载数据。
1)下面的命令将来自S3中两个不同文件的~2,000,000行插入到trips
表中:
trips_1.tsv.gz
and trips_2.tsv.gz
INSERT INTO trips SELECT * FROM s3( 'https://datasets-documentation.s3.eu-west-3.amazonaws.com/nyc-taxi/trips_{1..2}.gz', 'TabSeparatedWithNames', " `trip_id` UInt32, `vendor_id` Enum8('1' = 1, '2' = 2, '3' = 3, '4' = 4, 'CMT' = 5, 'VTS' = 6, 'DDS' = 7, 'B02512' = 10, 'B02598' = 11, 'B02617' = 12, 'B02682' = 13, 'B02764' = 14, '' = 15), `pickup_date` Date, `pickup_datetime` DateTime, `dropoff_date` Date, `dropoff_datetime` DateTime, `store_and_fwd_flag` UInt8, `rate_code_id` UInt8, `pickup_longitude` Float64, `pickup_latitude` Float64, `dropoff_longitude` Float64, `dropoff_latitude` Float64, `passenger_count` UInt8, `trip_distance` Float64, `fare_amount` Float32, `extra` Float32, `mta_tax` Float32, `tip_amount` Float32, `tolls_amount` Float32, `ehail_fee` Float32, `improvement_surcharge` Float32, `total_amount` Float32, `payment_type` Enum8('UNK' = 0, 'CSH' = 1, 'CRE' = 2, 'NOC' = 3, 'DIS' = 4), `trip_type` UInt8, `pickup` FixedString(25), `dropoff` FixedString(25), `cab_type` Enum8('yellow' = 1, 'green' = 2, 'uber' = 3), `pickup_nyct2010_gid` Int8, `pickup_ctlabel` Float32, `pickup_borocode` Int8, `pickup_ct2010` String, `pickup_boroct2010` String, `pickup_cdeligibil` String, `pickup_ntacode` FixedString(4), `pickup_ntaname` String, `pickup_puma` UInt16, `dropoff_nyct2010_gid` UInt8, `dropoff_ctlabel` Float32, `dropoff_borocode` UInt8, `dropoff_ct2010` String, `dropoff_boroct2010` String, `dropoff_cdeligibil` String, `dropoff_ntacode` FixedString(4), `dropoff_ntaname` String, `dropoff_puma` UInt16 ") SETTINGS input_format_try_infer_datetimes = 0
2)等待INSERT操作完成——下载150mb的数据可能需要一些时间。
s3函数很聪明地知道如何解压缩数据,TabSeparatedWithNames格式告诉ClickHouse数据是用制表符分隔的,并跳过每个文件的标题行。
3)当插入完成后,验证它是否工作:
SELECT count() FROM trips
您应该看到大约2M行(准确地说是1,999,657行)。
注意ClickHouse需要处理的行数和速度有多快?只需处理6行,就可以在0.001秒内得到计数。(6恰好是trips
表当前拥有的parts
数,部件知道它们有多少行。)
4)如果你运行一个需要访问每一行的查询,你会注意到需要处理更多的行,但运行时间仍然非常快:
SELECT DISTINCT(pickup_ntaname) FROM trips
该查询必须处理2M行并返回190个值,但请注意,它在大约1秒内完成了此操作。pickup_ntaname
列表示出租车在纽约市的开始地。
让我们运行一些查询来分析这2M行数据…
1)我们将从一些简单的计算开始,比如计算平均小费金额:
SELECT round(avg(tip_amount), 2) FROM trips
2)下面的查询基于乘客数量计算平均成本:
SELECT
passenger_count,
ceil(avg(total_amount),2) AS average_total_amount
FROM trips
GROUP BY passenger_count
3)下面是一个查询,计算每个社区每天的接送次数:
SELECT
pickup_date,
pickup_ntaname,
SUM(1) AS number_of_trips
FROM trips
GROUP BY pickup_date, pickup_ntaname
ORDER BY pickup_date ASC
4)该查询计算行程长度,并根据该值对结果进行分组:
SELECT
avg(tip_amount) AS avg_tip,
avg(fare_amount) AS avg_fare,
avg(passenger_count) AS avg_passenger,
count() AS count,
truncate(date_diff('second', pickup_datetime, dropoff_datetime)/60) as trip_minutes
FROM trips
WHERE trip_minutes > 0
GROUP BY trip_minutes
ORDER BY trip_minutes DESC
5)这个查询显示了每个社区的接送次数,按小时分列:
SELECT
pickup_ntaname,
toHour(pickup_datetime) as pickup_hour,
SUM(1) AS pickups
FROM trips
WHERE pickup_ntaname != ''
GROUP BY pickup_ntaname, pickup_hour
ORDER BY pickup_ntaname, pickup_hour
如果你是ClickHouse的新手,了解字典(dictionaries
)的工作原理是很重要的。考虑字典的一种简单方法是存储在内存中的键->值对的映射。详细信息和字典的所有选项链接在本教程的末尾。
1)让我们看看如何在ClickHouse服务中创建与表关联的字典。表和字典将基于包含265行的CSV文件,其中一行代表纽约市的每个社区。这些社区被映射到纽约市的行政区名称(纽约市有5个行政区:布朗克斯、布鲁克林、曼哈顿、皇后区和斯塔顿岛),这个文件也将纽瓦克机场(EWR)列为一个行政区。
这是CSV文件的一部分(为清晰起见显示为表格)。文件中的LocationID
列映射到trips
表中的pickup_nyct2010_gid
和dropff_nyct2010_gid
列:
2)该文件的URL为https://datasets-documentation.s3.eu-west-3.amazonaws.com/nyc-taxi/taxi_zone_lookup.csv。运行以下SQL语句,创建一个名为taxi_zone_dictionary
的字典,并从S3中的CSV文件填充字典:
CREATE DICTIONARY taxi_zone_dictionary
(
`LocationID` UInt16 DEFAULT 0,
`Borough` String,
`Zone` String,
`service_zone` String
)
PRIMARY KEY LocationID
SOURCE(HTTP(URL 'https://datasets-documentation.s3.eu-west-3.amazonaws.com/nyc-taxi/taxi_zone_lookup.csv' FORMAT 'CSVWithNames'))
LIFETIME(MIN 0 MAX 0)
LAYOUT(HASHED_ARRAY())
将
LIFETIME
设置为0意味着此字典将永远不会更新其源。这里使用它来避免向S3存储桶发送不必要的流量,但通常您可以指定任何您喜欢的生命周期值。
例如:
LIFETIME(MIN 1 MAX 10)
指定要在1到10秒之间的随机时间之后更新的字典。(当在大量服务器上更新时,为了在字典源上分配负载,随机时间是必要的。)
3)验证它的工作-你应该得到265行(每个邻居一行):
SELECT * FROM taxi_zone_dictionary
4)使用dictGet
函数(或其变体)从字典中检索值。传入字典的名称、所需的值和键(在我们的示例中是taxxi_zone_dictionary
的LocationID
列)。
例如,下面的查询返回LocationID
为132的Borough
(如上所示为JFK机场):
SELECT dictGet('taxi_zone_dictionary', 'Borough', 132)
5)使用dictHas
函数查看字典中是否存在键。例如,下面的查询返回1(在ClickHouse中为“true”):
SELECT dictHas('taxi_zone_dictionary', 132)
6)下面的查询返回0,因为4567不是字典中LocationID
的值:
SELECT dictHas('taxi_zone_dictionary', 4567)
7)使用dictGet
函数在查询中检索市镇的名称。例如:
SELECT
count(1) AS total,
dictGetOrDefault('taxi_zone_dictionary','Borough', toUInt64(pickup_nyct2010_gid), 'Unknown') AS borough_name
FROM trips
WHERE dropoff_nyct2010_gid = 132 OR dropoff_nyct2010_gid = 138
GROUP BY borough_name
ORDER BY total DESC
这个查询汇总了每个行政区在拉瓜迪亚机场或肯尼迪机场结束的出租车次数。结果如下所示,请注意,有相当多的行程的落点是未知的:
让我们编写一些查询,将tax_zone_dictionary
与trips
表连接起来。
1)我们可以从一个简单的JOIN开始,它的作用类似于上面的机场查询:
SELECT
count(1) AS total,
Borough
FROM trips
JOIN taxi_zone_dictionary ON toUInt64(trips.pickup_nyct2010_gid) = taxi_zone_dictionary.LocationID
WHERE dropoff_nyct2010_gid = 132 OR dropoff_nyct2010_gid = 138
GROUP BY Borough
ORDER BY total DESC
注意,上面
JOIN
查询的输出与之前使用dictGetOrDefault
的查询相同(只是不包括Unknown
值)。在幕后,ClickHouse实际上调用了taxxi_zone_dictionary
字典的dictGet
函数,但是SQL开发人员更熟悉JOIN
语法。
2)我们不经常在ClickHouse中使用SELECT *
-你应该只检索你实际需要的列!但是很难找到一个花费很长时间的查询,所以这个查询故意选择每一列并返回每一行(除了在默认情况下响应中有一个内置的10,000行最大值),并且还使用字典对每一行进行右连接:
SELECT *
FROM trips
JOIN taxi_zone_dictionary
ON trips.dropoff_nyct2010_gid = taxi_zone_dictionary.LocationID
WHERE tip_amount > 0
ORDER BY tip_amount DESC
LIMIT 1000
恭喜!
做得好——你完成了教程,希望你对如何使用ClickHouse有了更好的理解。下面是接下来该怎么做的一些选择:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。