ClickHouse 中的字典提供了一种基于内存的 键值 数据表示形式,数据来源于各种 内部和外部来源,优化超低延迟查找查询。
字典的用途包括
- 提高查询性能,尤其是在与
JOIN 配合使用时
- 在不降低摄取速度的情况下,动态丰富摄取的数据
使用字典加速连接
字典可用于加速特定类型的 JOIN:LEFT ANY 类型,其中连接键需要与底层键值存储的键属性匹配。
在这种情况下,ClickHouse 可以利用字典执行 直接连接。这是 ClickHouse 最快的连接算法,适用于右侧表的底层 表引擎 支持低延迟键值请求时。ClickHouse 有三个表引擎提供此功能:Join(基本上是一个预先计算的哈希表)、EmbeddedRocksDB 和 Dictionary。我们将描述基于字典的方法,但对于所有三个引擎,其机制是相同的。
直接连接算法要求右侧表由字典支持,以便从该表中要连接的数据已经以低延迟键值数据结构的格式存在于内存中。
使用 Stack Overflow 数据集,让我们回答这个问题:在 Hacker News 上关于 SQL 的最具争议的帖子是什么?
我们将把争议定义为帖子获得的赞成票和反对票数量相似。我们计算这个绝对差值,值越接近 0 表示争议越大。我们将假设帖子必须至少有 10 个赞成票和反对票 - 很少有人投票的帖子不太有争议。
在数据标准化后,此查询当前需要使用 posts 和 votes 表进行 JOIN
WITH PostIds AS
(
SELECT Id
FROM posts
WHERE Title ILIKE '%SQL%'
)
SELECT
Id,
Title,
UpVotes,
DownVotes,
abs(UpVotes - DownVotes) AS Controversial_ratio
FROM posts
INNER JOIN
(
SELECT
PostId,
countIf(VoteTypeId = 2) AS UpVotes,
countIf(VoteTypeId = 3) AS DownVotes
FROM votes
WHERE PostId IN (PostIds)
GROUP BY PostId
HAVING (UpVotes > 10) AND (DownVotes > 10)
) AS votes ON posts.Id = votes.PostId
WHERE Id IN (PostIds)
ORDER BY Controversial_ratio ASC
LIMIT 1
Row 1:
──────
Id: 25372161
Title: How to add exception handling to SqlDataSource.UpdateCommand
UpVotes: 13
DownVotes: 13
Controversial_ratio: 0
1 rows in set. Elapsed: 1.283 sec. Processed 418.44 million rows, 7.23 GB (326.07 million rows/s., 5.63 GB/s.)
Peak memory usage: 3.18 GiB.
在 JOIN 的右侧使用较小的数据集:此查询可能比需要的更冗长,对 PostId 的过滤发生在外部查询和子查询中。这是一个性能优化,可确保查询响应时间很快。为了获得最佳性能,请始终确保 JOIN 的右侧是较小的数据集,并且尽可能小。有关优化 JOIN 性能和了解可用算法的提示,我们建议您阅读 这系列博客文章。
虽然此查询很快,但它依赖于我们仔细编写 JOIN 以实现良好的性能。理想情况下,我们应该在查看 UpVote 和 DownVote 计数以计算我们的指标之前,简单地将帖子过滤为包含“SQL”的帖子。
应用字典
为了演示这些概念,我们为投票数据使用字典。由于字典通常保存在内存中(ssd_cache 除外),因此您应该了解数据的大小。确认我们的 votes 表的大小
SELECT table,
formatReadableSize(sum(data_compressed_bytes)) AS compressed_size,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_size,
round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratio
FROM system.columns
WHERE table IN ('votes')
GROUP BY table
┌─table───────────┬─compressed_size─┬─uncompressed_size─┬─ratio─┐
│ votes │ 1.25 GiB │ 3.79 GiB │ 3.04 │
└─────────────────┴─────────────────┴───────────────────┴───────┘
数据将以未压缩的形式存储在我们的字典中,因此如果我们要在字典中存储所有列(我们不会),则至少需要 4GB 的内存。字典将在我们的集群中复制,因此需要为每个节点保留这么多内存。
在下面的示例中,我们字典的数据来源于 ClickHouse 表。虽然这代表了字典最常见的来源,但支持许多来源,包括文件、http 和数据库,包括 Postgres。如我们所示,字典可以自动刷新,从而为频繁更改的小数据集提供直接连接的理想方式。
我们的字典需要一个主键,用于执行查找。这在概念上与事务数据库的主键相同,并且应该是唯一的。我们上面的查询需要对连接键 - PostId 进行查找。反过来,字典应该填充来自我们 votes 表的每个 PostId 的赞成票和反对票总数。这是获取此字典数据的查询
SELECT PostId,
countIf(VoteTypeId = 2) AS UpVotes,
countIf(VoteTypeId = 3) AS DownVotes
FROM votes
GROUP BY PostId
要创建我们的字典需要以下 DDL - 请注意我们上面查询的使用
CREATE DICTIONARY votes_dict
(
`PostId` UInt64,
`UpVotes` UInt32,
`DownVotes` UInt32
)
PRIMARY KEY PostId
SOURCE(CLICKHOUSE(QUERY 'SELECT PostId, countIf(VoteTypeId = 2) AS UpVotes, countIf(VoteTypeId = 3) AS DownVotes FROM votes GROUP BY PostId'))
LIFETIME(MIN 600 MAX 900)
LAYOUT(HASHED())
0 rows in set. Elapsed: 36.063 sec.
在自助管理的 OSS 中,上述命令需要在所有节点上执行。在 ClickHouse Cloud 中,字典将自动复制到所有节点。上述操作在一个具有 64GB RAM 的 ClickHouse Cloud 节点上执行,加载时间为 36 秒。
要确认我们的字典消耗的内存
SELECT formatReadableSize(bytes_allocated) AS size
FROM system.dictionaries
WHERE name = 'votes_dict'
┌─size─────┐
│ 4.00 GiB │
└──────────┘
现在可以使用简单的 dictGet 函数检索特定 PostId 的赞成票和反对票。下面我们检索帖子 11227902 的值
SELECT dictGet('votes_dict', ('UpVotes', 'DownVotes'), '11227902') AS votes
┌─votes──────┐
│ (34999,32) │
└────────────┘
Exploiting this in our earlier query, we can remove the JOIN:
WITH PostIds AS
(
SELECT Id
FROM posts
WHERE Title ILIKE '%SQL%'
)
SELECT Id, Title,
dictGet('votes_dict', 'UpVotes', Id) AS UpVotes,
dictGet('votes_dict', 'DownVotes', Id) AS DownVotes,
abs(UpVotes - DownVotes) AS Controversial_ratio
FROM posts
WHERE (Id IN (PostIds)) AND (UpVotes > 10) AND (DownVotes > 10)
ORDER BY Controversial_ratio ASC
LIMIT 3
3 rows in set. Elapsed: 0.551 sec. Processed 119.64 million rows, 3.29 GB (216.96 million rows/s., 5.97 GB/s.)
Peak memory usage: 552.26 MiB.
这不仅查询更简单,而且速度提高了两倍以上!可以通过仅将具有 10 个以上赞成票和反对票的帖子加载到字典中,并仅存储预先计算的争议值来进一步优化。
查询时间丰富
字典可用于在查询时查找值。这些值可以返回到结果中或用于聚合。假设我们创建一个字典,将用户 ID 映射到其位置
CREATE DICTIONARY users_dict
(
`Id` Int32,
`Location` String
)
PRIMARY KEY Id
SOURCE(CLICKHOUSE(QUERY 'SELECT Id, Location FROM stackoverflow.users'))
LIFETIME(MIN 600 MAX 900)
LAYOUT(HASHED())
我们可以使用此字典来丰富帖子结果
SELECT
Id,
Title,
dictGet('users_dict', 'Location', CAST(OwnerUserId, 'UInt64')) AS location
FROM posts
WHERE Title ILIKE '%clickhouse%'
LIMIT 5
FORMAT PrettyCompactMonoBlock
┌───────Id─┬─Title─────────────────────────────────────────────────────────┬─Location──────────────┐
│ 52296928 │ Comparison between two Strings in ClickHouse │ Spain │
│ 52345137 │ How to use a file to migrate data from mysql to a clickhouse? │ 中国江苏省Nanjing Shi │
│ 61452077 │ How to change PARTITION in clickhouse │ Guangzhou, 广东省中国 │
│ 55608325 │ Clickhouse select last record without max() on all table │ Moscow, Russia │
│ 55758594 │ ClickHouse create temporary table │ Perm', Russia │
└──────────┴───────────────────────────────────────────────────────────────┴───────────────────────┘
5 rows in set. Elapsed: 0.033 sec. Processed 4.25 million rows, 82.84 MB (130.62 million rows/s., 2.55 GB/s.)
Peak memory usage: 249.32 MiB.
与我们上面的连接示例类似,我们可以使用相同的字典有效地确定大多数帖子来自哪里
SELECT
dictGet('users_dict', 'Location', CAST(OwnerUserId, 'UInt64')) AS location,
count() AS c
FROM posts
WHERE location != ''
GROUP BY location
ORDER BY c DESC
LIMIT 5
┌─location───────────────┬──────c─┐
│ India │ 787814 │
│ Germany │ 685347 │
│ United States │ 595818 │
│ London, United Kingdom │ 538738 │
│ United Kingdom │ 537699 │
└────────────────────────┴────────┘
5 rows in set. Elapsed: 0.763 sec. Processed 59.82 million rows, 239.28 MB (78.40 million rows/s., 313.60 MB/s.)
Peak memory usage: 248.84 MiB.
索引时间丰富
在上面的示例中,我们在查询时使用字典来删除连接。字典也可以用于在插入时丰富行。如果丰富值不更改并且存在可以用于填充字典的外部来源,则通常是这样。在这种情况下,在插入时丰富行可以避免查询时查找字典。
假设 Stack Overflow 中用户的 Location 永远不会更改(实际上它们会更改) - 具体来说是 users 表的 Location 列。假设我们想通过位置对帖子表进行分析查询。这包含一个 UserId。
字典提供了一个从用户 ID 到位置的映射,该映射由 users 表支持
CREATE DICTIONARY users_dict
(
`Id` UInt64,
`Location` String
)
PRIMARY KEY Id
SOURCE(CLICKHOUSE(QUERY 'SELECT Id, Location FROM users WHERE Id >= 0'))
LIFETIME(MIN 600 MAX 900)
LAYOUT(HASHED())
我们省略 Id < 0 的用户,从而可以使用 Hashed 字典类型。具有 Id < 0 的用户是系统用户。
要利用在帖子表的插入时间使用此字典,我们需要修改模式
CREATE TABLE posts_with_location
(
`Id` UInt32,
`PostTypeId` Enum8('Question' = 1, 'Answer' = 2, 'Wiki' = 3, 'TagWikiExcerpt' = 4, 'TagWiki' = 5, 'ModeratorNomination' = 6, 'WikiPlaceholder' = 7, 'PrivilegeWiki' = 8),
...
`Location` MATERIALIZED dictGet(users_dict, 'Location', OwnerUserId::'UInt64')
)
ENGINE = MergeTree
ORDER BY (PostTypeId, toDate(CreationDate), CommentCount)
在上面的示例中,Location 被声明为 MATERIALIZED 列。这意味着该值可以作为 INSERT 查询的一部分提供,并且始终会计算出来。
ClickHouse 还支持 DEFAULT 列(其中可以插入该值或在未提供时计算出来)。
我们可以使用通常的 INSERT INTO SELECT 从 S3 填充表
INSERT INTO posts_with_location SELECT Id, PostTypeId::UInt8, AcceptedAnswerId, CreationDate, Score, ViewCount, Body, OwnerUserId, OwnerDisplayName, LastEditorUserId, LastEditorDisplayName, LastEditDate, LastActivityDate, Title, Tags, AnswerCount, CommentCount, FavoriteCount, ContentLicense, ParentId, CommunityOwnedDate, ClosedDate FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/posts/*.parquet')
0 rows in set. Elapsed: 36.830 sec. Processed 238.98 million rows, 2.64 GB (6.49 million rows/s., 71.79 MB/s.)
现在我们可以获得大多数帖子来自的位置名称
SELECT Location, count() AS c
FROM posts_with_location
WHERE Location != ''
GROUP BY Location
ORDER BY c DESC
LIMIT 4
┌─Location───────────────┬──────c─┐
│ India │ 787814 │
│ Germany │ 685347 │
│ United States │ 595818 │
│ London, United Kingdom │ 538738 │
└────────────────────────┴────────┘
4 rows in set. Elapsed: 0.142 sec. Processed 59.82 million rows, 1.08 GB (420.73 million rows/s., 7.60 GB/s.)
Peak memory usage: 666.82 MiB.
高级字典主题
选择字典 LAYOUT
LAYOUT 子句控制字典的内部数据结构。存在许多选项,并记录在 此处。有关选择正确布局的提示,请参见 此处。
刷新字典
我们为字典指定了 LIFETIME 为 MIN 600 MAX 900。LIFETIME 是字典的更新间隔,这里的数值会导致在 600 到 900 秒之间的随机间隔定期重新加载。当在大量服务器上更新时,这种随机间隔是必要的,以便在字典源上分配负载。在更新期间,字典的旧版本仍然可以查询,只有初始加载会阻止查询。请注意,设置 (LIFETIME(0)) 会阻止字典更新。可以使用 SYSTEM RELOAD DICTIONARY 命令强制重新加载字典。
对于 ClickHouse 和 Postgres 等数据库源,您可以设置一个查询,该查询仅在它们真正更改时(查询的响应确定这一点)更新字典,而不是以定期间隔进行更新。有关详细信息,请参见 此处。
其他字典类型
ClickHouse 还支持 分层、多边形 和 正则表达式 字典。
更多阅读