非主键表(Table w/o PK)
概述
如果一个表未定义主键,那么它就是一个追加表(append table)。与主键表相比,它不具备直接接收变更日志的能力。无法通过插入更新(upsert)直接用数据更新该表,只能接收追加的数据。
Flink示例:
CREATE TABLE my_table (
product_id BIGINT,
price DOUBLE,
sales BIGINT
) WITH (
-- 'target-file-size' = '256 MB',
-- 'file.format' = 'parquet',
-- 'file.compression' = 'zstd',
-- 'file.compression.zstd-level' = '3'
);
在典型应用场景中进行批量写入和批量读取,类似于常规的Hive分区表,但与Hive表相比,它具有以下优势:
-
对对象存储(如S3、OSS)友好
-
支持时间旅行(Time Travel)和回滚(Rollback)
-
低成本的DELETE / UPDATE操作
-
流写入时自动合并小文件
-
像队列一样进行流读写
-
利用排序和索引实现高性能查询
拓展:
追加表(Append Table):在数据库和数据处理系统中,追加表是一种简单的数据存储结构,它只允许新数据以追加的方式插入,不支持像主键表那样直接基于主键进行更新或删除操作。这种表结构适用于某些特定的业务场景,例如日志数据的记录,新的日志条目只需不断追加到表中。与其他复杂的数据表结构相比,追加表的设计使得数据写入操作相对简单高效,因为不需要处理复杂的更新逻辑和主键约束。
时间旅行(Time Travel)和回滚(Rollback):时间旅行功能允许用户查询表在过去某个时间点的状态,这对于数据审计、错误恢复以及分析历史数据非常有用。回滚则是将表恢复到之前的某个状态,类似于版本控制。在数据处理过程中,由于各种原因(如错误的写入操作、数据损坏等),可能需要将数据恢复到之前正确的状态,回滚功能就提供了这种能力。例如,在数据仓库中,如果发现最近一次数据更新导致了错误的分析结果,可以利用时间旅行和回滚功能,查询和恢复到更新之前的数据状态,重新进行处理。
低成本的DELETE / UPDATE操作:虽然追加表不能像主键表那样直接进行常规的DELETE和UPDATE操作,但通过特定的方式也能实现类似功能且成本较低。例如,可以通过标记删除或使用新的追加数据覆盖旧数据等方式模拟DELETE和UPDATE操作。与传统数据库中直接修改数据相比,这种方式可能在存储和处理上更为高效,尤其是在处理大规模数据时,避免了因频繁修改数据块而带来的高开销。
自动合并小文件(Automatic Small File Merging):在流写入过程中,数据可能会以小文件的形式不断生成,这会导致存储碎片化和查询性能下降。追加表具备自动合并小文件的功能,它会在适当的时候将这些小文件合并成更大的文件,优化存储布局,提高查询性能。例如,在处理大量的实时传感器数据时,每个传感器的短时间数据可能会生成一个小文件,自动合并功能可以定期将这些小文件合并,减少文件数量,提升系统整体性能。
流读写像队列(Streaming Read & Write like a Queue):将追加表的流读写类比为队列,意味着数据按照写入的顺序依次被读取,先进先出(FIFO)。这种特性在一些需要按顺序处理数据的场景中非常适用,例如实时事件处理系统,事件数据按照发生顺序写入追加表,然后按照相同顺序被读取和处理,确保事件处理的顺序性和准确性。
利用排序和索引实现高性能查询(High Performance Query with Order and Index):通过对追加表中的数据进行排序和建立索引,可以显著提高查询性能。排序可以使数据在存储上按照特定的顺序排列,索引则提供了快速定位数据的方式。例如,在查询某个范围内的数据时,排序后的表可以通过二分查找等算法快速定位数据位置,索引则能直接指向满足查询条件的数据块,减少数据扫描范围,从而实现高性能查询。
流处理(Streaming)
你可以通过Flink以非常灵活的方式对流写入追加表,或者通过Flink对流读取追加表,将其当作队列使用。唯一的区别是它的延迟在几分钟级别。其优点是成本非常低,并且能够下推过滤器和投影。
自动小文件合并(Automatic small file merging)
在流写入作业中,如果没有定义桶,写入器中不会进行压缩操作,而是会使用压缩协调器(Compact Coordinator)扫描小文件,并将压缩任务传递给压缩工作器(Compact Worker)。在流模式下,如果你在Flink中运行插入SQL,拓扑结构将如下所示:

不用担心反压问题,压缩操作永远不会产生反压。
如果你将write-only
设置为true
,拓扑结构中将移除压缩协调器和压缩工作器。
自动压缩仅在Flink引擎流模式下受支持。你还可以通过Paimon中的Flink操作在Flink中启动一个压缩作业,并通过设置write-only
禁用所有其他压缩操作。
流查询(Streaming Query)
你可以对流读取追加表,并将其当作消息队列使用。与主键表一样,流读取有两种选择:
-
默认情况下,流读取在首次启动时生成表上的最新快照,然后继续读取最新的增量记录。
-
你可以指定
scan.mode
或scan.snapshot-id
或scan.timestamp-millis
或scan.file-creation-time-millis
以仅对流读取增量数据。
与Flink-Kafka类似,默认情况下不保证顺序,如果你的数据有某种顺序要求,你还需要考虑定义一个桶键(bucket-key),详见"分桶追加表(Bucketed Append)"。
拓展:
流处理在追加表中的应用:流处理使得追加表能够实时接收和处理数据,在大数据处理场景中,这种实时性至关重要。例如,在实时监控系统中,传感器不断产生的数据可以通过流写入追加表,然后实时分析系统通过流读取追加表,及时获取最新数据进行分析。
自动小文件合并机制:在大数据存储中,小文件会导致存储效率低下和查询性能降低。追加表的自动小文件合并机制通过压缩协调器和压缩工作器协同工作,有效解决了这个问题。压缩协调器负责扫描小文件并分配压缩任务,压缩工作器执行具体的合并操作。这种机制确保了数据存储的高效性,减少了因小文件过多带来的I/O开销。
流查询的灵活性:追加表的流查询提供了多种方式来满足不同的业务需求。默认的读取最新快照并持续读取增量记录的方式,适用于需要获取完整且最新数据视图的场景;而通过指定参数仅读取增量数据,则适用于对实时性要求极高,只关注最新变化的场景。例如,在金融交易监控系统中,可能只需要实时获取最新的交易增量数据进行风险评估。
顺序保证与桶键:在处理需要顺序的数据时,定义桶键是一种有效的方式。桶键可以帮助数据按照特定规则分布到不同桶中,从而在一定程度上保证数据的顺序性。例如,在按时间顺序处理事件的系统中,可以将时间字段作为桶键,确保同一时间段内的事件数据在存储和读取时保持相对顺序。
查询(Query)
按顺序的数据跳过(Data Skipping By Order)
Paimon 默认会在清单文件(manifest file)中记录每个字段的最大值和最小值。
在查询时,依据查询的 WHERE 条件,利用清单中的统计信息进行文件过滤。如果过滤效果良好,原本可能需要数分钟的查询将加速到几毫秒内完成执行。
通常数据分布并不总能实现有效过滤,那么能否依据 WHERE 条件中的字段对数据进行排序呢?你可以查看 Flink 的 COMPACT Action 或 Flink 的 COMPACT Procedure 或 Spark 的 COMPACT Procedure。
按文件索引的数据跳过(Data Skipping By File Index)
你也可以使用文件索引,它在读取端通过索引对文件进行过滤。
CREATE TABLE ( , ...) WITH (
'file-index.bloom-filter.columns' = 'c1,c2',
'file-index.bloom-filter.c1.items' = '200'
);
定义 file-index.bloom-filter.columns
后,Paimon 会为每个文件创建其对应的索引文件。如果索引文件过小,它将直接存储在清单中,否则存储在数据文件的目录下。每个数据文件对应一个索引文件,该索引文件有单独的文件定义,并且可以包含针对多个列的不同类型的索引。
数据文件索引是与特定数据文件对应的外部索引文件。如果索引文件过小,它将直接存储在清单中,否则存储在数据文件的目录下。每个数据文件对应一个索引文件,该索引文件有单独的文件定义,并且可以包含针对多个列的不同类型的索引。
不同的文件索引在不同场景下可能效率不同。例如,布隆过滤器(Bloom filter)在点查找场景中可能加快查询速度。使用位图(Bitmap)可能会消耗更多空间,但能带来更高的准确性。
目前,文件索引仅在仅追加表(append-only table)中受支持。
布隆过滤器(Bloom Filter):
-
file-index.bloom-filter.columns
:指定需要布隆过滤器索引的列。 -
file-index.bloom-filter.<column_name>.fpp
用于配置误判率(false positive probability)。 -
file-index.bloom-filter.<column_name>.items
用于配置一个数据文件中预期的不同项的数量。
位图(Bitmap):
file-index.bitmap.columns
:指定需要位图索引的列。
未来将支持更多的过滤类型......
如果你想在不进行任何重写操作的情况下为现有表添加文件索引,可以使用 rewrite_file_index
过程。在使用该过程之前,你应该在目标表中配置适当的配置。你可以使用 ALTER 子句为表配置 file-index.<filter-type>.columns
。
如何调用:详见 Flink 过程。
拓展:
按顺序的数据跳过:在数据库查询优化中,通过在清单文件记录字段最值来实现按顺序的数据跳过是一种常见策略。这种方式利用统计信息快速筛选出可能包含满足查询条件数据的文件,避免对所有文件进行全量扫描,从而大大提高查询效率。例如,在一个存储大量销售记录的表中,清单文件记录了每个文件中销售金额字段的最值,当查询某一金额区间的销售记录时,就可以根据这些最值快速排除不相关文件,加速查询执行。
按文件索引的数据跳过:文件索引为查询优化提供了另一种有效手段。通过创建与数据文件对应的索引文件,在读取时能够基于索引快速过滤文件。不同类型的索引如布隆过滤器和位图,各有其适用场景和优缺点。布隆过滤器在空间效率和点查找场景下表现出色,虽然存在一定误判率,但能快速判断数据是否可能存在。位图则以更高的空间消耗为代价,提供更精确的过滤。例如,在一个用户信息表中,若经常进行特定用户ID的点查询,使用布隆过滤器索引可以快速定位可能包含该用户ID的文件;而对于需要精确统计某一范围内用户数量的场景,位图索引可能更合适。
Flink 和 Spark 的 COMPACT 相关操作:Flink 和 Spark 作为流行的大数据处理框架,其 COMPACT 相关操作在数据排序和优化存储方面发挥重要作用。在按顺序的数据跳过场景中,通过这些操作可以对数据按特定字段排序,进一步提升基于清单统计信息的过滤效果。例如,在 Flink 中使用 COMPACT Action 或 Procedure 对数据按时间字段排序后,查询特定时间范围的数据时,过滤效率会更高。
rewrite_file_index 过程 :该过程为现有表添加文件索引提供了一种便捷方式,无需对数据进行复杂的重写操作。通过合理配置表的相关参数,利用该过程可以在不影响现有数据的情况下,为表添加如布隆过滤器或位图等文件索引,从而提升查询性能。例如,对于一个已经存储大量历史数据的仅追加表,使用
rewrite_file_index
过程添加布隆过滤器索引后,后续针对特定列的查询可以利用索引加速,而无需重新构建整个表结构。
更新(Update)
目前,只有Spark SQL支持DELETE(删除)和UPDATE(更新)操作,你可以查看Spark写入相关内容。
示例:
DELETE FROM my_table WHERE currency = 'UNKNOWN';
更新追加表有两种模式:
-
写时复制(Copy on Write, COW):搜索匹配的文件,然后重写每个文件,从文件中删除需要删除的数据。此操作成本较高。
-
写时合并(Merge on Write, MOW) :通过指定
'deletion-vectors.enabled' = 'true'
,可以启用删除向量模式。仅标记相应文件的某些记录为删除状态,并写入删除文件,而无需重写整个文件。
拓展:
DELETE和UPDATE操作在追加表中的实现:在数据库操作中,DELETE和UPDATE操作对于数据管理至关重要。在追加表场景下,由于其本身设计特点,实现这两种操作相对复杂。目前依赖Spark SQL来执行这些操作,体现了不同大数据处理框架在功能支持上的差异。例如在数据仓库的追加表中,需要删除某些无效数据时,就可借助Spark SQL的DELETE语句。
写时复制(COW)模式:COW模式在更新追加表时,需要重写包含要删除数据的文件,这种方式虽然能确保数据一致性,但I/O开销较大。因为重写文件涉及读取原始文件、修改数据并重新写入,在处理大规模文件时,性能会受到显著影响。比如在一个存储海量日志数据的追加表中,若采用COW模式删除部分日志记录,可能需要耗费大量时间和资源来重写日志文件。
写时合并(MOW)模式:MOW模式通过引入删除向量(Deletion Vectors),避免了对整个文件的重写,而是标记需删除的记录并写入删除文件。这种方式在一定程度上降低了更新成本,提高了更新效率。例如在同样的海量日志数据追加表中,采用MOW模式可以快速标记要删除的日志记录,而无需对整个日志文件进行重写操作,减少了I/O操作次数,提升了系统性能。
分桶追加表(Bucketed Append)
你可以定义桶(bucket)和桶键(bucket-key)来创建一个分桶追加表。
创建分桶追加表的示例:
Flink:
CREATE TABLE my_table (
product_id BIGINT,
price DOUBLE,
sales BIGINT
) WITH (
'bucket' = '8',
'bucket-key' = 'product_id'
);
流处理(Streaming)
普通的追加表对流写入和读取没有严格的顺序保证,但在某些情况下,你需要定义一个类似于Kafka的键。
同一桶中的每条记录都有严格的顺序,流读取将严格按照写入顺序将记录传输到下游。要使用此模式,无需配置特殊设置,所有数据将作为队列进入一个桶中。

桶内压缩(Compaction in Bucket)
默认情况下,接收器节点会自动执行压缩以控制文件数量。以下选项控制压缩策略:
键 | 默认值 | 类型 | 描述 |
---|---|---|---|
write-only | false | Boolean | 如果设置为true,将跳过压缩和快照过期操作。此选项与专用压缩作业一起使用。 |
compaction.min.file-num | 5 | Integer | 对于文件集[f_0,...,f_N],满足sum(size(f_i)) >= targetFileSize时触发追加表压缩的最小文件数。此值可避免几乎已满的文件进行压缩,因为这种压缩成本效益不高。 |
compaction.max.file-num | 5 | Integer | 对于文件集[f_0,...,f_N],即使sum(size(f_i)) < targetFileSize,触发追加表压缩的最大文件数。此值可避免积压过多小文件,以免降低性能。 |
full-compaction.delta-commits | (无) | Integer | 增量提交后将不断触发完全压缩。 |
流读取顺序(Streaming Read Order)
对于流读取,记录按以下顺序生成:
-
对于来自两个不同分区的任意两条记录:
-
如果
scan.plan-sort-partition
设置为true,分区值较小的记录将首先生成。 -
否则,分区创建时间较早的记录将首先生成。
-
-
对于来自同一分区和同一桶的任意两条记录,先写入的记录将首先生成。
-
对于来自同一分区但不同桶的任意两条记录,不同的桶由不同的任务处理,它们之间没有顺序保证。
水印定义(Watermark Definition)
你可以为读取Paimon表定义水印:
CREATE TABLE t (
`user` BIGINT,
product STRING,
order_time TIMESTAMP(3),
WATERMARK FOR order_time AS order_time-INTERVAL '5' SECOND
) WITH (...);
-- 启动一个有界流作业来读取paimon_table
SELECT window_start, window_end, COUNT(`user`) FROM TABLE(
TUMBLE(TABLE t, DESCRIPTOR(order_time), INTERVAL '10' MINUTES)) GROUP BY window_start, window_end;
你还可以启用Flink水印对齐,这将确保没有数据源/分片/分区的水印比其他部分提前太多:
键 | 默认值 | 类型 | 描述 |
---|---|---|---|
scan.watermark.alignment.group | (none) | String | 用于对齐水印的一组数据源。 |
scan.watermark.alignment.max-drift | (none) | Duration | 在暂停从数据源/任务/分区消费之前,对齐水印的最大漂移量。 |
有界流(Bounded Stream)
流数据源也可以是有界的,你可以指定'scan.bounded.watermark'
来定义有界流模式的结束条件,直到遇到更大的水印快照,流读取才会结束。
快照中的水印由写入器生成,例如,你可以指定一个Kafka数据源并声明水印的定义。当使用此Kafka数据源写入Paimon表时,Paimon表的快照将生成相应的水印,以便在对流读取此Paimon表时可以使用有界水印功能。
CREATE TABLE kafka_table (
`user` BIGINT,
product STRING,
order_time TIMESTAMP(3),
WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND
) WITH ('connector' = 'kafka'...);
-- 启动一个流插入作业
INSERT INTO paimon_table SELECT * FROM kakfa_table;
-- 启动一个有界流作业来读取paimon_table
SELECT * FROM paimon_table /*+ OPTIONS('scan.bounded.watermark'='...') */;
批处理(Batch)
分桶表在批处理查询中必要时可用于避免数据混洗(shuffle),例如,你可以使用以下Spark SQL读取Paimon表:
SET spark.sql.sources.v2.bucketing.enabled = true;
CREATE TABLE FACT_TABLE (order_id INT, f1 STRING) TBLPROPERTIES ('bucket'='10', 'bucket-key' = 'order_id');
CREATE TABLE DIM_TABLE (order_id INT, f2 STRING) TBLPROPERTIES ('bucket'='10', 'primary-key' = 'order_id');
SELECT * FROM FACT_TABLE JOIN DIM_TABLE on t1.order_id = t4.order_id;
spark.sql.sources.v2.bucketing.enabled
配置用于为V2数据源启用分桶功能。启用后,Spark将通过SupportsReportPartitioning识别V2数据源报告的特定数据分布,并在必要时尝试避免数据混洗。
如果两个表具有相同的分桶策略和相同的桶数,则可以避免代价高昂的连接数据混洗。
拓展:
分桶追加表:在大数据存储和处理中,分桶追加表通过定义桶和桶键,将数据按照一定规则分布到不同桶中。这种方式不仅可以在流处理中保证同一桶内数据的顺序,便于按序处理数据,还能在批处理查询时优化性能,如避免数据混洗,提高查询效率。例如,在电商数据分析中,按商品ID分桶存储订单数据,在分析特定商品的销售趋势时,同一桶内的数据按时间顺序排列,方便快速获取相关信息。
桶内压缩策略 :桶内压缩策略通过控制文件数量来优化存储和查询性能。
write-only
选项用于跳过压缩和快照过期,适用于与专用压缩作业配合使用的场景。compaction.min.file-num
和compaction.max.file-num
分别从最小和最大文件数的角度触发压缩,平衡了压缩成本和性能。full-compaction.delta-commits
则控制完全压缩的触发频率。在实际应用中,合理调整这些参数能有效管理存储资源,提高系统整体性能。流读取顺序:明确的流读取顺序保证了数据在不同分区和桶之间的处理顺序,有助于确保数据处理结果的一致性。例如,在实时数据分析任务中,按顺序处理数据对于正确计算指标和分析趋势至关重要。不同的顺序规则(如按分区值、创建时间或写入顺序)适用于不同的业务场景,用户可以根据实际需求进行配置。
水印定义与应用:水印在流处理中用于处理乱序数据和定义窗口操作。通过为Paimon表定义水印,结合Flink水印对齐功能,可以更好地控制数据处理的进度和准确性。有界流模式下,水印还可以作为流读取的结束条件,确保在处理完特定范围的数据后停止读取。例如,在实时监控系统中,通过水印机制可以准确计算一段时间内的事件统计信息,同时避免因乱序数据导致的计算错误。
批处理中的分桶优化:在Spark SQL中,为V2数据源启用分桶功能后,当两个表具有相同的分桶策略和桶数时,在连接操作中可以避免数据混洗。数据混洗通常是一个代价高昂的操作,涉及数据的重新分区和传输,而分桶优化可以显著减少这种开销,提高批处理查询的性能。例如,在大规模数据的关联分析中,通过合理的分桶策略,可以将需要连接的数据预先分布在相同的桶中,直接进行连接操作,节省大量时间和资源。