从 Postgres 到 ClickHouse:数据建模指南

图片

本文字数:7149;估计阅读时间:18 分钟

作者:Sai Srirampur

本文在公众号【ClickHouseInc】首发

图片

上个月,我们收购了专注于 Postgres CDC 的 PeerDB。PeerDB 使得数据从 Postgres 复制到 ClickHouse 变得既快速又简单。PeerDB 的用户经常会问,在数据复制到 ClickHouse 后,如何进行数据建模以充分发挥 ClickHouse 的优势。

这一问题的产生源于 ClickHouse 和 Postgres 在数据建模上的差异。两者分别为各自特定的工作负载量身打造——Postgres 是事务型(OLTP)数据库,而 ClickHouse 则是专注于分析的列式(OLAP)数据库。本指南将帮助来自 Postgres 的用户掌握在 ClickHouse 中关键的数据建模概念。这是该系列博客的第一篇,更多内容将在未来发布。

ReplacingMergeTree 表引擎

PeerDB 使用 ReplacingMergeTree 引擎将 PostgreSQL 表映射到 ClickHouse。ClickHouse 在追加写入的场景下表现最佳,并不推荐频繁的 UPDATE 操作,而 ReplacingMergeTree 正是专为此类需求而设计的。

ReplacingMergeTree 支持既包含数据导入又包含数据修改的工作负载。每个表都是追加写入的,用户的更新会作为带有版本号的 INSERT 记录。ReplacingMergeTree 引擎在后台自动处理行数据的去重和合并。这是 ClickHouse 能够在实时数据导入方面表现出色的关键原因之一。

在 PeerDB 中,Postgres 的 INSERT 和 UPDATE 操作会以不同版本(使用 _peerdb_version)的新行形式写入 ClickHouse。ReplacingMergeTree 表引擎会定期通过排序键(ORDER BY 列)在后台处理去重,保留最新版本的行。而 PostgreSQL 的 DELETE 操作则会作为标记为已删除的新行(通过 _peerdb_is_deleted 列)进行处理。下方代码片段展示了 ClickHouse 中 public_goals 表的目标表定义。

clickhouse-cloud :) SHOW CREATE TABLE public_goals;
CREATE TABLE peerdb.public_goals
(
    `id` Int64,
    `owned_user_id` String,
    `goal_title` String,
    `goal_data` String,
    `enabled` Bool,
    `ts` DateTime64(6),
    `_peerdb_synced_at` DateTime64(9) DEFAULT now(),
    `_peerdb_is_deleted` Int8,
    `_peerdb_version` Int64
)
ENGINE = SharedReplacingMergeTree
('/clickhouse/tables/{uuid}/{shard}', '{replica}', _peerdb_version)
PRIMARY KEY id
ORDER BY id
SETTINGS index_granularity = 8192

你可能仍然会看到重复的行数据——该如何处理?

ReplacingMergeTree 在后台异步去重,但无法保证完全消除重复数据。因此,查询时你可能会遇到同一行或主键存在不同版本的重复情况。这是正常现象。要去除重复数据,你可以考虑以下几种方法:

在查询中使用 FINAL

ClickHouse 提供了一个独特的修饰符 FINAL,它可以在查询时进行去重(合并行数据)。这种去重操作发生在过滤(WHERE 子句)之后,聚合(GROUP BY)之前。

过去人们担心 FINAL 会影响查询性能。虽然 FINAL 确实对性能有一定影响,但 ClickHouse 的最新版本对其进行了显著优化。因此,你可以放心使用 FINAL 子句,并观察查询性能。以下是使用 FINAL 子句的示例:

SELECT owner_user_id, COUNT(*) FROM goals FINAL 
WHERE enabled = true GROUP BY owner_user_id;

使用 argMax 函数在查询时去重

ClickHouse 提供了 argMax 函数,用于在查询时动态去除重复行。尤其在需要根据版本号或时间戳保留最新记录时,它非常有用。

例如,对于 peerdb.public_goals 表,如果 id 是主键,_peerdb_version 记录版本号,你可以使用 argMax 选出每个 id 下版本号最高的行。这种方式可以在不改变底层数据的前提下去除重复项。接着,你可以对去重后的结果集执行子查询来进行聚合分析。以下是使用 argMax 的示例查询。

SELECT
    owned_user_id,
    COUNT(*) AS active_goals_count,
    MAX(ts) AS latest_goal_time
FROM
(
    SELECT
        id,
        argMax(owned_user_id, _peerdb_version) AS owned_user_id,
        argMax(goal_title, _peerdb_version) AS goal_title,
        argMax(goal_data, _peerdb_version) AS goal_data,
        argMax(enabled, _peerdb_version) AS enabled,
        argMax(ts, _peerdb_version) AS ts,
        argMax(_peerdb_synced_at, _peerdb_version) AS _peerdb_synced_at,
        argMax(_peerdb_is_deleted, _peerdb_version) AS _peerdb_is_deleted,
        max(_peerdb_version) AS _peerdb_version
    FROM peerdb.public_goals
    WHERE enabled = true
    GROUP BY id
) AS deduplicated_goals
GROUP BY owned_user_id;

使用窗口函数

你也可以利用 ClickHouse 的窗口函数,通过在每个 id 分区中选出 _peerdb_version 最高的行来实现去重。下面是一个示例:

SELECT
    owned_user_id,
    COUNT(*) AS active_goals_count,
    MAX(ts) AS latest_goal_time
FROM
(
    SELECT
        *,
        ROW_NUMBER() OVER (PARTITION BY id ORDER BY _peerdb_version DESC) AS rn
    FROM peerdb.public_goals
    WHERE enabled = true
) AS ranked_goals
WHERE rn = 1
GROUP BY owned_user_id;

通过视图简化去重

将去重逻辑封装在视图中,使得 BI 工具能够更方便地查询最新数据。例如,在视图中使用窗口函数,只保留每行的最新版本:

CREATE VIEW goals AS
SELECT * FROM
(
    SELECT
        *,
        ROW_NUMBER() OVER (PARTITION BY id ORDER BY _peerdb_version DESC) AS rn
    FROM peerdb.public_goals
    WHERE enabled = true
) WHERE rn = 1;
SELECT
    owned_user_id,
    COUNT(*) AS active_goals_count,
    MAX(ts) AS latest_goal_time
FROM goals
GROUP BY owned_user_id;

可为空列

对于习惯使用 Postgres 的用户来说,ClickHouse 的一个特别之处在于,它不会存储列的 NULL 值,除非你明确将列类型定义为 Nullable。例如,对于日期列,ClickHouse 会将 1970-01-01 作为默认值,而不是存储 NULL,这可能让人意外。这是因为存储 NULL 值会影响 ClickHouse 作为列式数据库的查询性能。因此,ClickHouse 需要用户显式定义 Nullable 类型。

在 PeerDB 中,我们引入了一个名为 PEERDB_NULLABLE 的设置。当设置为 true 时,该功能会在复制过程中自动检测 Postgres 中的可为空列,并在 ClickHouse 中将其标记为 Nullable。这样你在复制过程中无需手动定义 Nullable 类型。有关此功能的更多信息,请查看以下 PR。

数据类型

ClickHouse 提供了丰富的数据类型,从数字、文本、时间戳、日期、数组,到新引入的 JSON 类型。在大多数情况下,Postgres 中的数据类型可以直接存储到 ClickHouse,而无需进行复杂的转换。

以下是我们在 PeerDB 中从 Postgres 复制数据到 ClickHouse 时使用的数据类型对照表,供参考。

排序键

什么是排序键?

在 ClickHouse 中,选择合适的排序键对查询性能至关重要。排序键由创建表时的 ORDER BY 子句定义,类似于 Postgres 中的索引,但其专为分析型工作负载进行优化。与 Postgres 中使用的 B-tree 树索引不同,ClickHouse 使用稀疏索引:

  1. 数据按排序键排序

    排序键确保磁盘上的数据按照指定的列进行排序。

    这种排序有助于提升数据压缩效果,因为相似的值会集中存储在一起。

  2. 排序键还生成稀疏索引

    排序键还会生成稀疏索引,仅存储列的范围,每个条目指向一组已排序的行。

    这保持了索引的小巧,使 ClickHouse 能够通过二进制搜索快速定位相关行组,从而高效执行查询。

    你可以在此处相关内容【https://clickhouse.com/docs/en/migrations/postgresql/designing-schemas#primary-ordering-keys-in-clickhouse】。

你可以将排序键与 Postgres 中的 BRIN 索引类比,但在 ClickHouse 中,数据会通过异步合并部分自动按排序键排序,无需在数据导入时手动排序。

如何选择合适的排序键

选择排序键时,优先考虑在查询的 WHERE 子句中最常使用的列,并根据列基数(即唯一值的数量)升序排列——从具有最少不同值的列开始。这样能够优化数据压缩与查询性能。有关该主题的更多深入探讨,请参考此处的详细指南【https://clickhouse.com/docs/en/data-modeling/schema-design#choosing-an-ordering-key】。

主键 (PRIMARY KEY) 与排序键 (Ordering Key) 的区别

在 public_goals 表的定义中,你可能会注意到它设置了主键 (PRIMARY KEY)。那么,主键与排序键 (Ordering Key) 之间的区别是什么呢?让我们深入探讨它们的不同之处:

  1. 如果定义了主键 (PRIMARY KEY),它将指定稀疏索引中的列,而 ORDER BY 子句则决定数据在磁盘上的排序方式。此外,主键和排序键也用于 ReplacingMergeTree 引擎的去重操作。

  2. 如果没有明确指定主键 (PRIMARY KEY),则排序键 (Ordering Key) 会自动作为主键,定义稀疏索引中的列。

注意:主键 (PRIMARY KEY) 中的列应该在排序键 (Ordering Key) 中优先排列。这可以确保索引和物理数据的排序一致,从而减少不必要的数据扫描,提升查询性能。

主键与排序键不同的应用场景

在一些场景中,主键和 ORDER BY 子句的列可能不完全相同。例如,如果你的查询大多基于 customer_id 进行过滤而非 id,这时你可以在 customer_id 上设置主键 (PRIMARY KEY),而 ORDER BY 子句中包括 customer_id 和 id。这样可以确保稀疏索引在查询时更紧凑高效,并且通过 id 进行数据去重,确保数据不会丢失。

注意:与 PostgreSQL 中的主键不同,PostgreSQL 的主键通过 B-tree 索引确保数据唯一性,而在 ClickHouse 中,主键并不强制唯一性。它主要用于指定哪些列应该被纳入稀疏索引中。

修改排序键 (Ordering Key)

在 ClickHouse 中,选择合适的排序键对于查询性能至关重要,因为它相当于查询数据时的索引。默认情况下,PeerDB 会将 PostgreSQL 的主键 (PRIMARY KEY) 用作 ClickHouse 表的排序键 (Ordering Key),但你可以通过以下方法进行更改:

使用物化视图 (Materialized Views)

通过物化视图,你可以创建一个适用于特定工作负载的表,使用不同的排序键。在定义排序键时,确保主键 (PRIMARY KEY) 列位于排序键的末尾。这是因为 ReplacingMergeTree 引擎在去重时依赖 ORDER BY 子句的顺序,包含主键可以确保数据不会丢失。

CREATE MATERIALIZED VIEW goals_mv
ENGINE = ReplacingMergeTree(_peerdb_version)
ORDER BY (enabled, ts, id)  POPULATE AS
SELECT * FROM peerdb.public_goals;

注意:在创建物化视图后,务必按照前述处理重复数据的步骤进行操作,以确保在查询时正确去重。

预先定义目标表及所需排序键

如果需要更改排序键,可以预先定义一个包含目标排序键的新表,并将其替换现有表。具体步骤如下:

1. 创建虚拟镜像:首先在 PeerDB 中创建一个虚拟镜像,它将生成一个具有正确元数据列和数据类型的默认表。

2. 创建新表并设置排序键:基于 PeerDB 创建的表定义一个新表,包含你需要的排序键。在排序键中,确保将主键列放在末尾以确保正确的去重。

CREATE TABLE public_events_new AS public_events
ENGINE = ReplacingMergeTree(_peerdb_version)
ORDER BY (user_id,id);

3. 删除旧表:将旧表删除以腾出空间。

DROP TABLE public_events;

4. 重命名新表:将新表命名为原表的名称。

RENAME TABLE public_events_new TO public_events;

5. 配置镜像指向新表:通过调整配置,使 PeerDB 的镜像指向新表。PeerDB 在后台使用 CREATE TABLE IF NOT EXISTS 命令,确保数据继续写入新表。

处理 DELETE 操作

正如前面提到的,PostgreSQL 中的 DELETE 操作会以标记为已删除的行(通过 _peerdb_is_deleted 列)形式同步到 ClickHouse。如果你希望在查询中排除这些已删除的数据,可以基于 _peerdb_is_deleted 列在 ClickHouse 中创建行级别的策略 (row-level policy)。例如:

CREATE ROW POLICY policy_name ON table_name
FOR SELECT USING _peerdb_is_deleted = 0;

这个策略会确保在查询表数据时,只有 _peerdb_is_deleted 等于 0 的行是可见的。

总结

希望这篇博客对你有所帮助。我尽量涵盖了从 PostgreSQL 迁移到 ClickHouse 过程中常见的数据建模挑战。在接下来的博客中,我将深入探讨更高级的话题,例如如何进行表连接 (joins) 和编写高效的 SQL 查询等。如果你有兴趣尝试 PeerDB 和 ClickHouse,并开始从 PostgreSQL 复制数据到 ClickHouse,请查看下面的链接,或者直接联系我们!

1. 免费试用 ClickHouse Cloud【https://clickhouse.com/docs/en/cloud-quick-start】

2. 免费试用 PeerDB Cloud【https://auth.peerdb.cloud/en/signup?glxid=81340839-0371-47e4-aea0-f0b994d2c85d&pagePath=%2Fblog%2Fpostgres-to-clickhouse-data-modeling-tips&origPath=%2Fblog%2Fhow-to-learn-clickhouse-and-become-a-certified-clickhouse-developer&experiments=mktg-website-nav-cta-btn%3A0%2Cmktg-website-rockset-eyebrow%3A0】

3. Postgres 到 ClickHouse 复制文档 【https://docs.peerdb.io/mirror/cdc-pg-clickhouse】

4. 直接与 PeerDB 团队联系【https://www.peerdb.io/sign-up】

征稿启示

面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:Tracy.Wang@clickhouse.com

​​联系我们

手机号:13910395701

邮箱:Tracy.Wang@clickhouse.com

满足您所有的在线分析列式数据库管理需求

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/875671.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Docker日志管理之Filebeat+ELK日志管理

所需安装包及镜像 安装步骤 把所需镜像导入到Docker容器 打开/etc/sysctl.conf配置文件,添加参数 打开资源限制配置文件,添加参数 创建一个网络 在根目录下创建一个项目目录 创建Elasticsearch子目录 在项目目录下创建Elasticsearch子目录 将安装Elast…

微生物分类检测系统源码分享

微生物分类检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer V…

突发!OpenAI发布最强模型o1:博士物理92.8分,IOI金牌水平

梦晨 衡宇 发自 凹非寺量子位 | 公众号 QbitAI 参考ChatGPT:点击使用 来了来了!刚刚,OpenAI新模型无预警上新: o1系列,可以进行通用复杂推理,每次回答要花费更长时间思考。 在解决博士水平的物理问题时&a…

Notepad++插件:TextFX 去除重复行

目录 一、下载插件 TextFX Characters 二、去重实操 2.1 选中需要去重的文本 2.2 操作插件 2.3 结果展示 2.3.1 点击 Sort lines case sensitive (at column) 2.3.2 点击 Sort lines case insensitive (at column) 一、下载插件 TextFX Characters 点【插件】-【插件管理…

【OpenAPI】Spring3 集成 OpenAPI 生成接口文档

Spring3 集成 OpenAPI 生成接口文档 1. 依赖 Spring 版本&#xff1a;3.0.5 Java 版本&#xff1a;jdk21 OpenAPI 依赖&#xff1a; <!-- https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-starter-webmvc-ui --> <dependency><groupI…

如何通过 PhantomJS 模拟用户行为抓取动态网页内容

引言 随着网页技术的不断进步&#xff0c;JavaScript 动态加载内容已成为网站设计的新常态&#xff0c;这对传统的静态网页抓取方法提出了挑战。为了应对这一挑战&#xff0c;PhantomJS 作为一个无头浏览器&#xff0c;能够模拟用户行为并执行 JavaScript&#xff0c;成为了获…

GeoPandas在地理空间数据分析中的应用

GeoPandas是一个开源的Python库&#xff0c;专门用于处理和分析地理空间数据。它建立在Pandas库的基础上&#xff0c;扩展了Pandas的数据类型&#xff0c;使得用户能够在Python中方便地进行GIS操作。GeoPandas的核心数据结构是GeoDataFrame&#xff0c;它是Pandas的DataFrame的…

uniapp小程序,使用腾讯地图获取定位

本篇文章分享一下在实际开发小程序时遇到的需要获取用户当前位置的问题&#xff0c;在小程序开发过程中经常使用到获取定位功能。uniapp官方也提供了相应的API供我们使用。 官网地址&#xff1a;uni.getLocation(OBJECT)) 官网获取位置的详细介绍这里就不再讲述了&#xff0c;大…

红光一字激光器在工业中的性能指标怎样

红光一字激光器作为现代工业中不可或缺的重要设备&#xff0c;以其独特的性能和广泛的应用场景&#xff0c;成为众多行业的首选工具。本文就跟大家详细探讨红光一字激光器在工业中的性能指标&#xff0c;以及这些指标如何影响其在实际应用中的表现。 光束质量 红光一字激光器以…

【痛点解决】跨网跨区域的文件传输摆渡解决办法指南

跨网跨区域的文件传输摆渡&#xff0c;顾名思义就是需要跨越不同网络、不同地区&#xff0c;或者是不同安全域的文件传输&#xff0c;一般有这样传输需求的机构&#xff0c;在组织架构、网络结构&#xff0c;或者传输需求上&#xff0c;都会比较复杂。 跨网跨区域文件传输是什…

51单片机快速入门之定时器和计数器

51单片机快速入门之定时器 断开外部输入 晶振振荡 假设为 12MHz 12分频之后,为1MHz 当其从0-65536 时,需要65536μs 微秒 也就是65.536ms 毫秒 溢出(值>65536 时)>中断>执行中断操作 假设需要1ms后产生溢出,则需要设置初始值为64536 此时定时器会从 64536 开始计…

掌握 JavaScript ES6+:现代编程技巧与模块化实践

掌握 JavaScript ES6&#xff1a;现代编程技巧与模块化实践 一 . 变量声明 let二 . 声明常量 const三 . 模板字符串四 . 函数的参数默认值五 . 箭头函数六 . 对象初始化七 . 解构7.1 接收 JSON 对象7.2 接收数组 八 . 延展操作符九 . 导入和导出9.1 方式一9.2 方式二 这篇文章我…

Android视频编辑:利用FFmpeg实现高级功能

在移动设备上进行视频编辑的需求日益增长&#xff0c;用户期望能够在智能手机或平板电脑上轻松地编辑视频&#xff0c;以满足社交媒体分享或个人存档的需求。Android平台因其广泛的用户基础和开放的生态系统&#xff0c;成为视频编辑应用的理想选择。FFmpeg&#xff0c;作为一个…

Maven入门学习笔记

一、maven介绍 Maven是一款自动化构建工具&#xff0c;专注服务于JAVA平台的项目构建和依赖管理。在javaEE开发的历史上构建工具的发展也经历了一系列的演化和变迁。 管理jar包 当我们使用SSM之后我们就需要使用非常多的jar包 没有maven找jar包非常的麻烦。 使用maven下载…

简单了解 JVM

目录 ♫什么是JVM ♫JVM的运行流程 ♫JVM运行时数据区 ♪虚拟机栈 ♪本地方法栈 ♪堆 ♪程序计数器 ♪方法区/元数据区 ♫类加载的过程 ♫双亲委派模型 ♫垃圾回收机制 ♫什么是JVM JVM 是 Java Virtual Machine 的简称&#xff0c;意为 Java虚拟机。 虚拟机是指通过软件模…

WPF DataGrid 列表中,DataGrid.Columns 列根据不同的值显示不同内容

需求&#xff1a;在WPF DataGrid 控件中&#xff0c;有以下列&#xff0c;绑定了一个LogType&#xff0c;值分别是0,1,2&#xff0c;根据不同的值&#xff0c;显示不同的内容以及背景 <DataGrid ItemsSource"{Binding EventLog}"><DataGrid.Columns><…

代码管理工具——git及阿里云云效的使用(包含git的使用及云效自动化部署)

1、做项目开发时都会用到代码管理工具,像是我之前使用过gitHub,Visual Studio等一些代码管理工具&#xff0c;这里介绍的是阿里云云效的使用。 2、首先登录阿里云云效&#xff0c;登录进去之后会看到公司给你开放的一个仓库。 3、进入仓库&#xff0c;点击克隆/下载&#xff0…

Google大数据架构技术栈

数据存储层 Colossus Colossus作为Google下一代GFS&#xff08;Google File System&#xff09;。 GFS本身存在一些不足 单主瓶颈 GFS 依赖单个主节点进行元数据管理&#xff0c;随着数据量和访问请求的增长&#xff0c;出现了可扩展性瓶颈。想象一下&#xff0c;只有一位…

网红酒店|基于java的网红酒店预定系统(源码+数据库+文档)

酒店预定|网红酒店|网红酒店预定系统 目录 基于java的网红酒店预定系统 一、前言 二、系统设计 三、系统功能设计 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大厂码农|毕设布道师&am…

R语言统计分析——功效分析(比例、卡方检验)

参考资料&#xff1a;R语言实战【第2版】 1、比例检验 当比较两个比例时&#xff0c;可使用pwr.2p.test()函数进行功效分析。格式为&#xff1a; pwr.2p.test(h, n, sig.level, power, alternative) 其中&#xff0c;h是效应值&#xff0c;n是各相同的样本量。效应值h的定义如…