上饶建站公司,玉溪市住房和建设局公布网站,做卖挖掘机的网站,网站被k数据仓库表设计理论
数仓顾名思义是数据仓库#xff0c;其数据来源大多来自于业务数据(例如:关系型数据库)#xff0c;当设计数仓中表类型时(拉链表、增量表、全量表、流水表、切片表)时#xff0c;应先观察业务数据的特点再设计数仓表结构
首先业务数据是会不断增长的-即…数据仓库表设计理论
数仓顾名思义是数据仓库其数据来源大多来自于业务数据(例如:关系型数据库)当设计数仓中表类型时(拉链表、增量表、全量表、流水表、切片表)时应先观察业务数据的特点再设计数仓表结构
首先业务数据是会不断增长的-即增量而在不断增长的前提下业务数据又可以分为两类
增量更新数据源数据源允许新增、修改和删除操作的数据源增量非更新数据源数据源只允许新增数据不允许对历史数据进行修改的数据源 业务数据中的这两种数据源类型直接决定了数仓中的表设计的选择 一、增量更新数据源
增量更新数据源是指允许新增、修改和删除操作的数据源。这种数据源的主要特点是
数据可修改可以对历史数据进行修改、覆盖以反映数据的变更。实时性高由于数据可以随时更新具有很强的实时性和即时性。
常见的增量更新数据源包括关系型数据库、NoSQL数据库、文件系统等它们都支持对数据进行新增、修改和删除操作。
示例关系型数据库中的用户表其手机号字段经常会出现修改 在2017-01-02这一天表中的数据是 用户002和004资料进行了修改005是新增用户 针对这类数据源我们将分别阐述下数仓中的各种类型的表设计 1.1、全量表
全量表就是存储了全部数据的表没有分区之分可以理解为总共就一个分区。
全量表中存储了截至目前为止最新状态的全部记录这就表示会存在历史状态的更新。
1.1.1、场景
以业务数据-用户表为例2020-06-01有三个用户注册表如下 2020-06-02有一名用户注册即新增了一名用户标红此时数仓中全量表更新后会记录全量的数据此时数据表如下 1.1.2、实现方式
全量表的实现方式又分为两种
全量替换直接获取全表业务数据替换昨日全量单表join替换通过合并每日增改数据的临时表 或 流水表重写后得到全新全量表
1.1.2.1、全量替换
-- 创建一张新的全量单表
CREATE TABLE new_table (col1 STRING, col2 INT)
ROW FORMAT DELIMITED FIELDS TERMINATED BY \t
LOCATION /user/hive/warehouse/new_table;-- 将全部的业务数据导入至全量单表, 过程不赘述
insert into new_table ..........-- 将新表名替换为旧表名
ALTER TABLE old_table RENAME TO old_table_backup;
ALTER TABLE new_table RENAME TO old_table;-- 删除昨日全量单表
DROP TABLE old_table_backup;1.1.2.2、join替换
注意业务数据需要有唯一的ID 和 upadte_time否则无法使用join替换。
-- 创建一张新的临时增量表
CREATE TABLE incremental_table (id INT, column1 STRING, column2 STRING)
ROW FORMAT DELIMITED FIELDS TERMINATED BY \t
LOCATION /user/hive/warehouse/incremental_table;-- 将增量数据加载到一个临时表中一般是通过业务数据的update_time来区分哪些是增量数据
insert into incremental_table xxxx-- 使用LEFT JOIN语句将原表和增量数据合并
SELECT COALESCE(i.id, o.id) as id,COALESCE(i.column1, o.column1) as column1,COALESCE(i.column2, o.column2) as column2,...
FROM original_table o
LEFT JOIN incremental_table i
ON o.id i.id;-- 这将生成一个包含原表和增量数据的JOIN结果。在上面的例子中我们使用了COALESCE函数来选择非空值以便我们可以从原表和增量数据中选择最新的值。
-- 最后我们可以使用INSERT OVERWRITE语句将合并后的数据插入到原表中
INSERT OVERWRITE TABLE original_table
SELECT COALESCE(i.id, o.id) as id,COALESCE(i.column1, o.column1) as column1,COALESCE(i.column2, o.column2) as column2,...
FROM original_table o
LEFT JOIN incremental_table i
ON o.id i.id;总结全量表设计的优势是不会占用太多磁盘空间弊端也很明显-不支持历史记录溯源 1.2、增量表
增量表是指只负责追加新的数据记录而不负责历史数据更改记录新的数据记录保存在新分区中历史分区中的数据记录不发生变化。 增量表并不适用增量更新数据源只适用于增量非更新数据源。 1.3、快照表
快照表是用来存储某个时间点的所有数据-通常粒度是天相当于是对每天的业务数据做了一次快照存储当天的全量数据
例如快照表中某个分区内的数据是历史到此分区前一天的所有数据如12号分区中的数据是从历史到11号的所有数据13号分区中的数据是从历史到12号的所有数据其他的以此类推。
1.3.1、场景
也可以理解为每天将业务数据的全量数据存储至数仓-快照分区表的当天分区内这里假设分区的粒度是天
以业务数据-用户表为例2020-06-01有三个用户注册表如下 2020-06-02有一名用户注册即新增了一名用户标红此时数仓中快照分区表更新后2020-06-02分区内会记录全量的数据包括2020-06-01的用户数据标绿此时快照表如下 同理2020-06-03又有2名用户注册即新增了1名用户标蓝此时数仓中快照分区表更新后2020-06-03分区内会记录全量数据即包含2020-06-02的用户数据标黄此时快照表如下 1.3.2、实现
先将业务数据全部同步至hive临时表中随后将hive临时表的业务数据放置在快照分区表的今日分区内:
-- 创建一个新hive临时表存放当日业务的全量数据
CREATE TABLE temp_table (col1 STRING, col2 INT)
ROW FORMAT DELIMITED FIELDS TERMINATED BY \t
LOCATION /user/hive/warehouse/temp_table;-- 将全部的业务数据导入至hive临时表, 过程不赘述
insert into temp_table ..........-- 使用INSERT INTO语句将临时表中的数据插入到全量分区表的目标分区中
INSERT INTO original_table PARTITION (date2023-05-08)
SELECT * FROM temp_table;-- 删除hive临时表
DROP TABLE temp_table;总结此方式便是快照表该设计的弊端很明显会大量占用磁盘空间故并不推荐使用 1.4、流水表
流水表是用于记录数据变更的表理论上是对于表的每一个修改都会记录但在实际应用中通常按天为粒度划分例如流水表中的2017-01-02分区只记录这一天新增和修改的业务数据这样可以方便地追溯、计算和分析历史数据同时也提供了可靠的数据源供其他表和报告使用。
1.4.1、场景
还是用user用户数据举例2017-01-01这一天表中的数据是 此时流水表的数据为
注册日期用户编号手机号码dt-时间分区字段2017-01-010011111112017-01-012017-01-010022222222017-01-012017-01-010033333332017-01-012017-01-010044444442017-01-01
在2017-01-02这一天表中的数据是 用户002和004资料进行了修改005是新增用户 此时流水表的数据为
注册日期用户编号手机号码dt-时间分区字段2017-01-010011111112017-01-012017-01-010022222222017-01-012017-01-010033333332017-01-012017-01-010044444442017-01-012017-01-010022333332017-01-022017-01-010044324322017-01-022017-01-020055555552017-01-02
1.4.2、实现
-- 创建一个user_update_temp临时表用于存放当日业务的增量数据
CREATE EXTERNAL TABLE ods.user_update_temp (user_num STRING COMMENT 用户编号, mobile STRING COMMENT 手机号码, reg_date STRING COMMENT 注册日期
)
COMMENT 每日用户资料更新表
ROW FORMAT DELIMITED FIELDS TERMINATED BY \t LINES TERMINATED BY \n
STORED AS ORC
LOCATION /ods/user_update;-- 创建一张流水表用来存储每天变化数据
CREATE EXTERNAL TABLE dws.user_stream (user_num STRING COMMENT 用户编号, mobile STRING COMMENT 手机号码, reg_date STRING COMMENT 注册日期
)
COMMENT 用户流水表
PARTITIONED BY (dt STRING)
ROW FORMAT DELIMITED FIELDS TERMINATED BY \t LINES TERMINATED BY \n
STORED AS ORC
LOCATION /dws/orders;-- 通过业务字段update_time将2017-01-02变化的业务数据导入至user_update_temp临时表过程不赘述
INSERT OVERWRITE ods.user_update_temp .........-- 使用INSERT INTO语句将ods.user_update中的数据插入到流水表中2017-01-02分区中
INSERT INTO dws.user_stream PARTITION (dt2017-01-02)
SELECT * FROM ods.user_update_temp;注意流水表很容易和增量表的概念混淆这里再强调一下增量表只适用于增量非更新数据源只负责新增数据对于历史数据的修改并不记录而流水表通常用于记录数据变更包括新增、修改和删除等操作以便跟踪每个事实或维度的历史变化。为了方便管理和查询通常将流水表按时间分区。 1.5、拉链表
拉链表是一种维护历史状态以及最新状态数据的表。与快照表类似算是在快照表的基础上去除了重复状态的数据也就是一些不变的信息在快照表中每个分区都存储一份使用拉链表在更新频率和比例不是很大的情况下会十分节省存储。
1.5.1、场景
现在以用户的拉链表来说明2017-01-01这一天表中的数据是 在2017-01-02这一天表中的数据是 用户002和004资料进行了修改005是新增用户 在2017-01-03这一天表中的数据是 用户004和005资料进行了修改006是新增用户 如果在数据仓库中设计成历史拉链表保存该表则会有下面这样一张表这是最新一天即2017-01-03的数据 说明
t_start_date表示该条记录的生命周期开始时间t_end_date表示该条记录的生命周期结束时间t_end_date 9999-12-31’表示该条记录目前处于有效状态t_end_date 2017-01-02’表示该条记录在2017-01-02当日是有效的在当前日期是无效的如果查询当前所有有效的记录则select * from user where t_end_date ‘9999-12-31’如果查询2017-01-02的历史快照则select * from user where t_start_date ‘2017-01-02’ and t_end_date ‘2017-01-02’。此处要好好理解是拉链表比较重要的一块解释上一条sql需求是要查2017-01-02的历史快照t_start_date是代表这条记录的开始时间并非是原始数据的时间例如001用户数据在2017-01-02也有效故t_start_date ‘2017-01-02’而t_end_date 2017-01-02’表示该条记录在2017-01-02当日是有效的又因为t_end_date 9999-12-31’表示该条记录目前处于有效状态所以t_end_date ‘2017-01-02’
该sql查询结果如下 和下图2017-01-02的业务数据比较结果完全一致 1.5.2、实现
创建拉链表的前提是先根据全量数据表创建初始拉链表然后再根据每天的增改数据进行合并更新拉链表
还是以上面的用户表为例我们要实现用户的拉链表在实现它之前我们需要先确定一下我们有哪些数据源可以用。
我们需要一张ODS层的用户全量表。至少需要用它来初始化。流水表-记录每日增改数据。
建表语句
-- 先创建一张全量表用于初始化
CREATE EXTERNAL TABLE ods.user (user_num STRING COMMENT 用户编号,mobile STRING COMMENT 手机号码,reg_date STRING COMMENT 注册日期
) COMMENT 用户资料表
ROW FORMAT DELIMITED FIELDS TERMINATED BY \t LINES TERMINATED BY \n
STORED AS ORC
LOCATION /ods/user;-- 最后我们创建一张拉链表
CREATE EXTERNAL TABLE dws.user_his (user_num STRING COMMENT 用户编号,mobile STRING COMMENT 手机号码,reg_date STRING COMMENT 注册日期,t_start_date STRING COMMENT 资料开始日期,t_end_date STRING COMMENT 资料结束日期
) COMMENT 用户资料拉链表
ROW FORMAT DELIMITED FIELDS TERMINATED BY \t LINES TERMINATED BY \n
STORED AS ORC
LOCATION /dws/user_his;数据初始化我们以2017-01-01的数据作为初始化数据
-- 假设ods.user表已经存储了2017-01-01的全量数据此时拉链表的初始化sql
INSERT OVERWRITE TABLE dws.user_his
SELECT user_num, mobile, reg_date, 2017-01-01, 9999-12-31
FROM ods.user;初始化后的拉链表数据如下
注册日期用户编号手机号码t_start_datet_end_date2017-01-010011111112017-01-019999-12-312017-01-010022222222017-01-019999-12-312017-01-010033333332017-01-019999-12-312017-01-010044444442017-01-019999-12-31
数据更新我们以2017-01-02的数据更新拉链表
2017-01-02日的流水表数据如下
注册日期用户编号手机号码dt-时间分区字段2017-01-010011111112017-01-012017-01-010022222222017-01-012017-01-010033333332017-01-012017-01-010044444442017-01-012017-01-010022333332017-01-022017-01-010044324322017-01-022017-01-020055555552017-01-02
-- 2017-01-02拉链表更新sql:
INSERT OVERWRITE TABLE dws.user_his
SELECT *
FROM
(SELECT A.user_num,A.mobile,A.reg_date,A.t_start_date,CASEWHEN A.t_end_date 9999-12-31 AND B.user_num IS NOT NULL THEN 2017-01-01ELSE A.t_end_dateEND AS t_end_dateFROM dws.user_his AS ALEFT JOIN dws.user_stream AS B WHERE dt 2017-01-02ON A.user_num B.user_num
UNIONSELECT C.user_num,C.mobile,C.reg_date,2017-01-02 AS t_start_date,9999-12-31 AS t_end_dateFROM dws.user_stream AS C WHERE dt 2017-01-02
) AS T;更新后的拉链表数据如下
注册日期用户编号手机号码t_start_datet_end_date2017-01-010011111112017-01-019999-12-312017-01-010022222222017-01-012017-01-012017-01-010033333332017-01-019999-12-312017-01-010044444442017-01-012017-01-012017-01-010022333332017-01-029999-12-312017-01-010044324322017-01-029999-12-312017-01-02005555552017-01-029999-12-31
-- 查询有效数据:
select * from dws.user_his where t_end_date 9999-12-31查询有效数据结果
注册日期用户编号手机号码t_start_datet_end_date2017-01-010011111112017-01-019999-12-312017-01-010033333332017-01-019999-12-312017-01-010022333332017-01-029999-12-312017-01-010044324322017-01-029999-12-312017-01-02005555552017-01-029999-12-31
-- 查询2017-01-01历史数据:
select * from dws.user_his where t_start_date 2017-01-01 and t_end_date 2017-01-012017-01-01历史数据
注册日期用户编号手机号码t_start_datet_end_date2017-01-010011111112017-01-019999-12-312017-01-010022222222017-01-012017-01-012017-01-010033333332017-01-019999-12-312017-01-010044444442017-01-012017-01-01
1.6、切片表
切片表根据基础表往往只反映某一个维度的相应数据。其表结构与基础表结构相同但数据往往只有某一维度或者某一个事实条件的数据切片表以某个维度或者一些特定的条件对事实进行汇总计算并展示为一个交叉分析的表格。与事实表相比切片表的数据更加聚合只包含某些维度或者满足某些特定条件的数据。
1.6.1、场景
假设我们有一个基础表也称为事实表记录了一家公司的销售订单信息。该表包含以下字段订单ID、客户ID、产品ID、销售日期、销售数量和销售额等。
订单ID客户ID产品ID销售日期销售数量销售额1100120012022-01-0131502100220022022-01-022803100320012022-01-031504100120032022-01-0452505100220022022-01-054160
在这张表中客户端ID、产品ID、销售日期是维度而销售数量、销售额是事实。
现在我们希望按照客户维度创建一个切片表以便分析每个客户的销售情况。
具体来说我们需要选择客户维度并对销售数量和销售额这两个度量进行聚合计算通过多维分析工具或者SQL查询可以生成如下的切片表
客户ID销售数量总计销售额总计10018400100262401003150
在这个切片表中我们只选择了客户维度然后我们使用SUM函数对每个客户的销售数量和销售额进行聚合计算以便更好地分析不同客户之间的销售情况。
1.6.2、实现
在数据仓库中切片表的存储方式可以根据不同的需求和性能要求而定一般来说有以下两种常用的存储方式
全量替换每次运行ETL作业时都会重新生成整个切片表并将其覆盖原有的数据。这种方式适用于数据量较小、更新频率低的场景或者需要保证数据完整性和一致性的场景。按照天的粒度进行划分将切片表按照时间维度如日、周、月等进行分区每个分区存储一段时间内的数据。这种方式适用于数据量较大、更新频率高的场景或者需要快速查询历史数据的场景。
无论采用哪种存储方式都需要考虑切片表的设计问题。具体来说以下是一些设计上的注意点
维度表的设计维度表应该是离线化的静态数据它们应该是变化不大并且计算时被缓存在内存中以便提高查询性能。因此在设计维度表时应该尽可能地避免经常变化的属性。表结构的优化为了提高查询效率应该尽可能地减少JOIN操作尽量将不同维度的数据存储在同一个表中。如果必须进行JOIN操作则应该尽可能地使用分区表或者基于列存储的数据库以便提高查询性能。时间分区如果采用按照天的粒度进行划分的方式那么需要将切片表按照时间维度进行分区并且使用分区键作为查询条件。这样可以大大提高查询性能同时也方便进行数据备份和恢复。
总之在设计和实现切片表时需要考虑不同的因素包括数据量、更新频率、查询性能、数据一致性等等以便得出最优的解决方案。
这里以全量替换举例
-- 按照上面的例子假设有一个名为sales_order的基础表
CREATE TABLE sales_order (order_id int,customer_id int,product_id int,sale_date date,quantity int,amount double
)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY ,
STORED AS TEXTFILE;-- 我们可以将原始数据加载到sales_order表中
LOAD DATA INPATH path/to/sales.csv OVERWRITE INTO TABLE sales_order;-- 接下来我们可以使用INSERT INTO SELECT语句来创建切片表customer_sales并将数据按照客户维度进行聚合计算
CREATE TABLE customer_sales (customer_id int,total_quantity int,total_amount double
)
AS
SELECT customer_id, SUM(quantity) as total_quantity, SUM(amount) as total_amount
FROM sales_order
GROUP BY customer_id;-- 这里采用了全量替换的方式每次重新运行上面的SQL语句都会完全替换customer_sales表中的数据。
二、增量非更新数据源
当数据源只允许新增数据不允许对历史数据进行修改时我们称之为增量不更新数据源。这种数据源的主要特点是
数据只能增长无法对历史数据进行修改、删除或覆盖只能追加新的数据。数据变更历史保留由于数据源只追加新的数据因此可以完整地保留数据的变更历史便于后续的分析和回溯。数据量大由于数据只能增长因此数据量通常会随着时间的推移而不断增加。实时性高由于数据是实时生成的具有很强的实时性和即时性。
常见的增量不更新数据源包括
日志数据日志数据是指记录了系统或应用程序操作的信息的数据。由于日志数据一般是只追加不修改所以可以视为增量不更新数据源。传感器数据传感器数据是指由各种传感器采集的环境、设备或系统状态数据等。这些数据也是只追加不修改的因此可以视为增量不更新数据源。消息系统数据消息系统数据是指通过消息队列、主题或流处理框架等收集的异步消息数据。与日志数据和传感器数据类似这些数据也是只追加不修改的。用户活动数据用户活动数据是指记录了用户在应用程序中进行的各种操作和行为的数据。由于这些数据通常无法修改因此也可以视为增量不更新数据源。
在设计数据模型和存储方案时需要考虑到数据源的特点以便满足快速查询和分析的需求一般来说可以采用以下策略
使用列式存储由于增量不更新数据源的数据一般只用于查询和分析因此可以采用列式存储方式以提高查询性能。考虑分区根据数据源的特点可以将数据按照时间、地理位置等因素进行分区以便更快地查询历史数据。超大表设计由于增量不更新数据源的数据量很大因此需要考虑超大表的设计问题。例如可以使用分区表、分布式存储等技术来支持数据的高效存储和查询。
2.1、全量表
对于增量不更新数据源使用全量表进行处理可能会导致性能问题和资源浪费因为全量表需要重新加载所有数据并进行完全替换这将消耗大量的计算资源和时间。
相反使用增量表可以更有效地处理增量不更新数据源因为增量表只包含最近追加的数据并且每次只需要更新最新的增量数据即可。这样可以避免重复加载和处理历史数据从而提高处理效率和减少资源消耗。
2.2、增量表
增量表可以高效地处理增量不更新数据源因为增量表只包含最新的新增数据并且每次只需要更新最新的增量数据即可。这样可以避免重复加载和处理历史数据从而提高处理效率和减少资源消耗。 增量表通常按天为粒度 2.2.1、场景
假设我们有一个传感器监测系统每个传感器会在固定时间间隔内生成一些数据。这些数据在产生后不会被更改。我们需要设计一个增量表来处理这些传感器数据并按天分区管理数据。
原始数据格式展示
字段名称数据类型说明sensor_idINT传感器IDtimestampTIMESTAMP时间戳valueFLOAT数据值
14号原始数据
sensor_idtimestampvalue12023-05-14 10:00:0023.522023-05-14 10:00:0018.232023-05-14 10:00:0025.042023-05-14 11:00:0024.052023-05-14 11:00:0018.4
此时我们设计一个名为sensors_incremental的增量表用于存储每天新增的传感器数据并按照日期进行分区。该表包含了四个字段sensor_id、timestamp、value和dt(时间分区字段)。
增量表初始数据展示如下
sensor_idtimestampvaluedt(时间分区字段)12023-05-14 10:00:0023.52023-05-1422023-05-14 10:00:0018.22023-05-1432023-05-14 10:00:0025.02023-05-1442023-05-14 11:00:0024.02023-05-1452023-05-14 11:00:0018.42023-05-14
第二天原始数据如下
sensor_idtimestampvalue12023-05-14 10:00:0023.522023-05-14 10:00:0018.232023-05-14 10:00:0025.032023-05-14 11:00:0024.052023-05-14 11:00:0018.462023-05-15 11:00:0025.272023-05-15 12:00:0023.882023-05-15 12:00:0018.692023-05-15 12:00:0025.5
第二天增量表sensors_incremental数据
sensor_idtimestampvaluedt(时间分区字段)12023-05-14 10:00:0023.52023-05-1422023-05-14 10:00:0018.22023-05-1432023-05-14 10:00:0025.02023-05-1442023-05-14 11:00:0024.02023-05-1452023-05-14 11:00:0018.42023-05-1462023-05-15 1100:0025.22023-05-1572023-05-15 12:00:0023.82023-05-1582023-05-15 12:00:0018.62023-05-1592023-05-15 12:00:0025.52023-05-15
2.2.2、实现
-- 创建增量表
CREATE TABLE sensors_incremental (sensor_id INT,timestamp TIMESTAMP,value FLOAT
)
PARTITIONED BY (day DATE)
STORED AS PARQUET;-- 将原始数据加载到sensors_temp临时表中
LOAD DATA INPATH path/to/sensors.csv OVERWRITE INTO TABLE sensors_temp;-- 将临时表数据初始化加载进增量表
INSERT INTO sensors_incremental PARTITION (date2023-05-14) SELECT * FROM sensors_temp;-- 第二天将15号临时表数据加载进增量表
INSERT INTO sensors_incremental PARTITION (date2023-05-15) SELECT * FROM sensors_temp; 通过使用以上增量表设计和相应的SQL语句我们可以有效地处理传感器数据并实现按天分区的管理和查询。 2.3、快照表
快照表同样适用于增量非更新数据源快照表是用来存储某个时间点的所有数据-通常粒度是天相当于是对每天的业务数据做了一次快照存储当天的全量数据
其设计和实现与增量更新数据源保持一致详细参考1.1.3、快照表
2.4、流水表
流水表同样适用于增量不更新数据源流水表是用于记录数据变更的表流水表如果是按天为粒度划分那和增量表几乎一模一样因为不涉及到数据的修改故增量非更新数据源下的流水表和增量表的数据几乎保持一致。
其设计和实现与增量更新数据源保持一致详细参考1.1.4、流水表
2.5、拉链表
拉链表同样适用于增量不更新数据源其设计和实现与增量更新数据源保持一致详细参考1.1.5、拉链表
2.6、切片表
切片表是根据事实数据生成的维度表故同样适用于增量不更新数据源其设计和实现与增量更新数据源保持一致详细参考1.1.6、切片表
三、总结
增量表、全量表、快照表、拉链表、切片表和流水表都是常用的数据表格设计方式每种表格设计方式都有自己的优劣势和适用场景。
增量表只记录新增或更改操作的数据表可以减少存储和处理开销适用于高频产生新增事件的情况。全量表记录所有数据的完整表格可以提供准确的数据但无法提供历史溯源记录快照表记录某个时间点的部分或全部数据可用于数据快照和查询。通常使用在查询性质的报表系统中。拉链表记录数据变化的时间范围。适用于需要准确跟踪数据变化的场景例如会计系统。切片表记录具有多维度属性的实体例如客户、产品等适用于类似于分析型场景。流水表记录所有事件包括新增、删除、修改等可以追溯完整的历史信息适用于审计和数据分析以及为其他类型的表提供数据源
总体而言以上的表格设计方式都有其自身的优缺点以及适用场景应根据具体的业务需求进行合理选择。