字典
ClickHouse 中的字典提供了来自各种内部和外部来源 的数据的内存中键值 表示,针对超低延迟查找查询进行优化。
字典对于以下方面很有用
- 提高查询性能,尤其是在与
JOIN
一起使用时 - 在不减慢数据摄取速度的情况下动态丰富摄取的数据
使用字典加速连接
字典可用于加速特定类型的 JOIN
:LEFT ANY
类型,其中连接键需要与底层键值存储的键属性匹配。
如果是这种情况,ClickHouse 可以利用字典执行直接连接。这是 ClickHouse 最快的连接算法,适用于右侧表的基础表引擎 支持低延迟键值请求。ClickHouse 有三种表引擎提供此功能:Join(基本上是一个预先计算的哈希表)、EmbeddedRocksDB 和 Dictionary。我们将描述基于字典的方法,但所有三个引擎的机制都是相同的。
直接连接算法要求右侧表由字典支持,以便来自该表的待连接数据以低延迟键值数据结构的形式已存在于内存中。
示例
使用 StackOverflow 数据集,让我们回答这个问题:*关于 SQL 的最具争议性的 Hacker News 帖子是什么?*
我们将争议定义为当帖子具有类似数量的赞成票和反对票时。我们计算此绝对差值,其中更接近 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 (UpVotes > 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 │ Comparision 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
列。假设我们想根据位置对 posts 表进行分析查询。这包含一个 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
的用户是系统用户。
为了在 posts 表的插入时利用此字典,我们需要修改模式
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
列(如果未提供值,则可以插入或计算该值)。
要填充表,我们可以使用来自 S3 的常规 INSERT INTO SELECT
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 还支持 分层、多边形 和 正则表达式 字典。