视频在线制作网站wordpress 精美主题
视频在线制作网站,wordpress 精美主题,网站营销概念,如何降低网站相似度1. 开篇#xff1a;为什么你的Doris表总是跑得慢#xff1f;
最近好几个朋友跟我吐槽#xff0c;说他们用Doris做数据分析#xff0c;数据量一上来查询就慢得不行#xff0c;有时候导入数据也卡顿。我帮他们看了一下#xff0c;发现十有八九的问题都出在表设计上#xf…1. 开篇为什么你的Doris表总是跑得慢最近好几个朋友跟我吐槽说他们用Doris做数据分析数据量一上来查询就慢得不行有时候导入数据也卡顿。我帮他们看了一下发现十有八九的问题都出在表设计上尤其是数据模型和分区策略没选对。这就像盖房子地基和结构没打好后面装修得再漂亮也白搭住起来肯定不舒服。Doris作为一款高性能的实时分析型数据库它的能力很强但它的性能表现非常依赖于我们如何“告诉”它数据该怎么组织。数据模型决定了数据以什么形式存储是保留每一笔明细还是只存汇总结果而分区和分桶策略则决定了数据在集群里怎么分布。这两个选型一旦在建表时定下来后期想改就得费大劲了所以前期设计至关重要。我自己在用户画像、订单分析、日志处理这些场景里都踩过不少坑。比如曾经用明细模型存用户行为流水结果数据量爆炸查询慢到怀疑人生也试过分区策略没设好导致热数据全挤在一个盘上IO直接打满。今天我就结合这些实战经验跟你聊聊Doris里三种数据模型和几种分区策略到底该怎么选、怎么优化帮你搭建一套清晰、能直接落地的表设计决策框架。2. 数据模型选型主键、明细、聚合到底用哪个数据模型是Doris表设计的灵魂它定义了数据行的唯一性、更新方式和存储形式。选错了模型要么存储空间浪费严重要么查询性能上不去甚至可能无法支持你的业务逻辑。Doris提供了三种模型主键模型、明细模型和聚合模型。咱们一个一个拆开看。2.1 主键模型应对频繁更新的利器主键模型的核心就一句话保证每一行数据都有一个唯一的标识Key新数据会覆盖旧数据。你可以把它想象成一个带版本控制的Excel表格同一个ID比如用户ID只保留最新的一条记录。它的工作原理是这样的当你写入一条数据时Doris会检查它的Key你定义的主键列组合是否已经存在。如果存在就用新数据替换掉旧的那条如果不存在就作为新行插入。这个“替换”动作Doris提供了两种实现方式你得根据业务特点来选写时合并Merge-On-Write, MoW这是Doris 1.2版本后的默认选项也是我现在最推荐的。数据在写入的那一刻如果发现Key冲突就直接在内存或磁盘上进行合并保证存下去的就是最终结果。好处是查询速度最快因为读的时候不需要再合并多个版本。适合读多写少或者对查询延迟极其敏感的场景比如用户画像表的实时点查。读时合并Merge-On-Read, MoR这是老版本的默认方式。写入时不做合并所有版本的数据都追加存储。查询时再把同一个Key的所有版本找出来合并返回最新结果。好处是写入吞吐量高因为写入就是简单的追加。适合写密集、但查询不那么频繁的场景比如某些高频的流水数据接入先存进来后续再批量分析。什么时候该用主键模型我给你几个典型的场景维度表同步比如从业务数据库MySQL实时同步用户表、商品表到Doris。业务库里的用户信息会变更改个昵称、换个头像同步到Doris后你需要确保每个用户ID只对应最新的信息。用主键模型通过INSERT语句就能实现UPSERT更新或插入的效果非常方便。需要高效去重的场景比如广告点击流水同一个用户可能多次点击同一个广告但你做分析时可能只需要记录他最后一次点击的时间和位置。用主键模型以用户ID和广告ID作为Key可以自动去重保留最新记录。需要部分列更新的场景这是主键模型一个很强大的功能。比如用户画像有上百个标签但每次可能只更新其中几个比如最近购买金额、活跃等级。使用写时合并模式并开启部分列更新你只需要写入变化的那些列其他列的值会保留这能极大减少数据传输和写入开销。建表示例与坑点提醒-- 创建一个使用写时合并的主键模型表用于存储实时用户画像 CREATE TABLE IF NOT EXISTS user_profile ( user_id BIGINT NOT NULL COMMENT 用户ID, city VARCHAR(20) COMMENT 城市, last_login_time DATETIME REPLACE COMMENT 最后登录时间, total_order_amount BIGINT SUM COMMENT 累计订单金额, tags VARCHAR(200) REPLACE_IF_NOT_NULL COMMENT 标签JSON空值不覆盖 ) ENGINEOLAP UNIQUE KEY(user_id) -- 指定user_id为主键 DISTRIBUTED BY HASH(user_id) BUCKETS 10 PROPERTIES ( enable_unique_key_merge_on_write true, -- 启用写时合并 replication_num 3 );注意一个关键约束如果你用了分区比如按天分区那么分区列必须包含在主键列里。这是因为Doris要保证同一个Key的数据肯定落在同一个分区里才能实现全局唯一。如果分区列不在Key里可能出现同一条数据的不同版本分布在两个分区导致查询结果错乱。2.2 明细模型保留每一笔原始痕迹明细模型正好相反它不关心唯一性你写入的每一行数据都会被原封不动地保存下来即使Key完全一样。它就像数据库的流水账本或者服务器的原始日志文件追求的是数据的完备性。它的特点非常鲜明全量存储没有任何聚合或去重适合做数据审计、回溯分析。灵活性高因为存了最细粒度的数据你可以基于它做任意维度的后聚合。比如你存了用户每次点击的日志后期既可以统计每日总点击量也可以分析每个按钮的点击路径。明细模型最适合那些“只追加不更新”的数据日志类数据Nginx访问日志、应用错误日志、审计日志。这些数据产生后就不会再变我们需要完整保留以备排查问题或做安全分析。交易流水或事件流水用户下单、支付、浏览商品等行为事件。一旦事件发生就是历史事实通常不会修改除了极少数冲正场景。物联网传感器数据设备定时上报的温度、压力读数。每个读数都是一个独立的事件。这里有个实战技巧明细表虽然存储量大但我们可以利用Doris强大的压缩能力。由于数据按排序键有序存储相似的数据紧挨在一起压缩比会非常高。我曾经有一个日志表原始文本数据1TB导入Doris后压缩到不到100GB查询速度还飞快。建表示例-- 创建一个存储用户行为事件的明细表 CREATE TABLE IF NOT EXISTS user_behavior_log ( event_time DATETIME NOT NULL COMMENT 事件时间, user_id BIGINT NOT NULL COMMENT 用户ID, event_type VARCHAR(50) COMMENT 事件类型如click, view, page_url VARCHAR(500) COMMENT 页面URL, device_id VARCHAR(100) COMMENT 设备ID, ip VARCHAR(40) COMMENT IP地址 ) ENGINEOLAP DUPLICATE KEY(event_time, user_id) -- 排序键用于数据排序和压缩优化 DISTRIBUTED BY HASH(user_id) BUCKETS 32 PARTITION BY RANGE(event_time) () -- 先留空后面用动态分区自动生成 PROPERTIES ( replication_num 3 );明细模型建表时指定的DUPLICATE KEY只是用来决定数据在文件内的物理排序顺序以优化查询和压缩并不代表唯一约束。2.3 聚合模型用空间换时间的艺术聚合模型是一种“预计算”模型。它允许你定义好聚合键Aggregate Key和聚合方式如SUM、MAX、REPLACE数据在写入过程中就会按照聚合键进行合并。最终存储的是聚合后的结果而不是原始明细。理解它的原理很重要想象你有一个电商订单明细表同一个订单号可能有多个商品子订单。如果使用聚合模型以订单号为聚合键对金额进行SUM那么这多个子订单在入库时就会被汇总成一条记录一个订单号对应一个总金额。这极大地节省了存储空间并提升了汇总查询的性能。聚合方式除了常见的SUM求和、MIN最小值、MAX最大值还有两个特别有用的REPLACE保留最新值。比如商品最新价格或者用户最后登录时间。REPLACE_IF_NOT_NULL仅当新值非空时才替换旧值。这在部分列更新场景下比主键模型更灵活比如逐步完善用户资料时不会用空值覆盖已有的信息。聚合模型的适用场景很聚焦统计报表类需求需要快速查询大盘指标如每日GMV商品交易总额、DAU日活跃用户数。数据一旦聚合查询就是简单的扫描速度极快。从数据湖查询加速原始全量明细数据存在HDFS或对象存储如S3中使用Doris作为加速层。每天将明细数据聚合后的结果导入Doris面向BI报表的查询直接走Doris又快又省资源。实时数据看板对实时性要求高的监控大盘需要亚秒级响应聚合指标。但是聚合模型有它的局限性你无法查询到被聚合掉的原始明细。比如你按天、按城市聚合了销售额就无法再查询出某个城市里具体是哪些订单贡献的销售额。所以它通常需要和明细模型搭配使用。建表示例-- 创建一个聚合模型表用于快速查询每日每城市的销售汇总 CREATE TABLE IF NOT EXISTS daily_city_sales ( dt DATE NOT NULL COMMENT 日期, city VARCHAR(20) NOT NULL COMMENT 城市, total_sales_amount BIGINT SUM COMMENT 销售总额, order_count BIGINT SUM COMMENT 订单数, max_single_order_amount BIGINT MAX COMMENT 最大单笔订单金额, last_order_time DATETIME REPLACE COMMENT 最后订单时间 ) ENGINEOLAP AGGREGATE KEY(dt, city) -- 聚合键按日期和城市聚合 DISTRIBUTED BY HASH(dt, city) BUCKETS 16 PARTITION BY RANGE(dt) () PROPERTIES ( replication_num 3 );2.4 模型对比与选型决策流说了这么多到底该怎么选我总结了一个简单的决策流程图你可以对照自己的业务场景来首先问数据写入后是否需要更新是- 选择主键模型。再根据读写比例选择MoW读多或MoR写多。否- 进入下一步。第二步问业务是否需要查询最细粒度的原始明细是- 选择明细模型。否- 选择聚合模型。此外还有一个混合策略明细模型 物化视图。你可以用明细模型存全量数据然后针对高频的聚合查询创建相应的物化视图。Doris会自动维护物化视图的数据查询时会自动路由。这提供了很大的灵活性但会占用额外的存储和计算资源来维护物化视图。3. 分区策略设计让数据管理变得清晰高效选好了模型接下来就要考虑如何把海量数据划分成小块来管理这就是分区。合理的分区策略能带来三大好处查询加速分区裁剪、数据生命周期管理轻松删除旧分区、优化存储冷热数据分离。Doris主要提供了手动、动态、自动三种分区模式。3.1 手动分区完全掌控的精细化管理手动分区就像你自己给衣柜分格每个格子放什么衣服由你决定。你需要在建表或后期通过ALTER TABLE语句明确创建每一个分区。Doris支持两种手动分区类型RANGE分区最常用根据分区列的值范围划分。特别适合时间序列数据比如按天、按月分区。-- 按日期范围分区管理订单表 CREATE TABLE orders ( order_id BIGINT, order_date DATE, customer_id INT, amount DECIMAL(10,2) ) PARTITION BY RANGE(order_date) ( PARTITION p202401 VALUES LESS THAN (2024-02-01), PARTITION p202402 VALUES LESS THAN (2024-03-01), PARTITION p202403 VALUES LESS THAN (2024-04-01), PARTITION p_future VALUES LESS THAN (2025-01-01) );查询WHERE order_date BETWEEN 2024-02-15 AND 2024-02-20时Doris只会扫描p202402这个分区效率极高。LIST分区根据分区列的具体值列表划分。适合离散的、枚举类型的列比如按地区、按业务线分区。-- 按城市列表分区管理门店销售表 CREATE TABLE store_sales ( sale_id BIGINT, city VARCHAR(20), sale_date DATE, revenue DECIMAL(12,2) ) PARTITION BY LIST(city) ( PARTITION p_east VALUES IN (Shanghai, Nanjing, Hangzhou), PARTITION p_north VALUES IN (Beijing, Tianjin), PARTITION p_south VALUES IN (Shenzhen, Guangzhou) );查询WHERE city Beijing时只会扫描p_north分区。手动分区的优缺点很明显优点控制力最强可以针对每个分区设置不同的属性比如副本数、存储介质。缺点运维成本高。需要预先知道数据范围对于持续产生的新数据如按天需要写脚本或手动定期添加分区否则数据会因为找不到对应分区而写入失败。3.2 动态分区解放双手的自动化时间管理动态分区就是为了解决手动分区运维麻烦的问题而生的。你只需要定义好规则Doris就会像闹钟一样按时自动创建新分区、删除旧分区。它特别适合时序数据管理比如日志、监控指标、每日报表。你只需要关心保留多久的数据Doris帮你搞定分区的创建和清理。核心参数就这几个dynamic_partition.enable true开启动态分区。dynamic_partition.time_unit DAY分区单位可以是DAY、WEEK、MONTH等。dynamic_partition.start -7保留过去7天的分区负数表示过去。dynamic_partition.end 3预先创建未来3天的分区正数表示未来。dynamic_partition.prefix p分区名前缀。一个实战例子我们希望日志表只保留最近30天的数据并且提前创建好明天的分区。CREATE TABLE server_logs ( log_time DATETIME, level VARCHAR(10), message TEXT ) DUPLICATE KEY(log_time) PARTITION BY RANGE(log_time) () DISTRIBUTED BY HASH(log_time) BUCKETS 16 PROPERTIES ( dynamic_partition.enable true, dynamic_partition.time_unit DAY, dynamic_partition.start -30, -- 删除30天前的分区 dynamic_partition.end 1, -- 创建今天和明天的分区 dynamic_partition.prefix p, dynamic_partition.replication_num 2, dynamic_partition.buckets 16 -- 动态分区的分桶数 );设置好后你完全不用管分区的事只管往表里灌数据就行Doris会自动将数据放入对应日期的分区并定期清理过期数据。注意动态分区只支持RANGE分区且分区键必须是单列的DATE或DATETIME类型。3.3 自动分区应对不可预测的数据分布动态分区解决了按时间自动划分的问题但如果你的分区键不是时间或者值的分布非常离散、无法提前预知呢比如按接入的“客户ID”或“项目编码”分区新客户/项目随时可能增加。这时就需要自动分区功能。你只需要指定分区类型RANGE或LIST和分区列Doris会在数据写入时自动创建对应的分区。-- 按客户ID自动创建LIST分区 CREATE TABLE customer_events ( event_id BIGINT, customer_id INT, -- 分区列 event_data VARCHAR(500) ) DUPLICATE KEY(event_id) PARTITION BY LIST(customer_id) () -- 括号内为空表示自动分区 DISTRIBUTED BY HASH(event_id) BUCKETS 8 PROPERTIES ( partition_type LIST -- 明确分区类型 );当写入一条customer_id1001假设该分区不存在的数据时Doris会自动创建一个名为1001的分区来存放它。使用自动分区要格外小心如果分区列存在大量脏数据或异常值比如NULL、空字符串或者无数个不同的值可能会导致创建出成千上万个微小分区严重拖累系统性能。因此它适用于分区列值相对可控、且数量不会无限膨胀的场景。4. 分桶策略与优化决定并行性能的关键分区是把数据分成大块比如按月份而分桶则是把每个大块再细分成许多小文件Tablet。分桶是数据物理分布和并行计算的最终单元它的设计直接影响查询的并行度和数据本地性。4.1 Hash分桶 vs Random分桶Doris提供两种分桶方式选择哪一种取决于你的查询模式。Hash分桶根据分桶列的Hash值将数据均匀分布到各个桶中。这是最常用、也是默认推荐的方式。优点对于包含分桶列等值条件的查询点查可以快速定位到具体一个或几个桶大幅减少数据扫描量。例如以user_id分桶查询WHERE user_id 123几乎可以秒回。如何选分桶键高基数列选择唯一值多的列保证数据分布均匀。高频过滤列选择经常出现在WHERE或JOIN ... ON条件中的列。点查场景用单列大范围扫描用多列组合让数据更分散。Random分桶数据随机分散到各个桶中。优点绝对的数据均匀完全避免因分桶键数据倾斜导致的“热点”问题。缺点任何查询都无法进行分桶裁剪必须扫描分区内的所有桶。仅适用于明细模型。使用场景数据没有明显的过滤维度或者主要进行全表扫描的聚合分析。在小批量数据快速写入时可以设置load_to_single_tablettrue将一次导入的所有数据先写入单个桶提升写入速度后续再由系统均衡。4.2 分桶数量多少才算合适分桶数量决定了并行度的上限和单个数据文件Tablet的大小。这里有两个核心原则当它们冲突时优先考虑大小原则大小原则单个Tablet的大小建议在1GB到10GB之间。太小则元数据过多管理开销大太大则不利于数据迁移、备份且Compaction数据合并压力大。数量原则整个表的Tablet总数分区数 × 分桶数建议略大于集群的磁盘总数。例如你有10台BE每台1块盘那么总Tablet数在10-30个左右比较合适这样可以保证每块盘上都有数据充分利用所有磁盘的IO能力。手动设置示例DISTRIBUTED BY HASH(user_id) BUCKETS 10 -- 分为10个桶自动设置推荐Doris 2.0以后支持自动设置分桶数你只需要告诉它你预估每个分区有多大。DISTRIBUTED BY HASH(user_id) BUCKETS AUTO PROPERTIES ( estimate_partition_size 20G -- 预估每个分区20GB );系统会根据这个预估值和集群规模自动计算一个合理的分桶数并在后续根据实际数据增长情况进行自适应调整非常省心。4.3 高级优化Colocate与分区裁剪当你的表设计好分区和分桶后还有两个高级技巧能带来巨大的性能提升Colocate Join协同定位Join对于需要频繁进行JOIN的两张或多张大表比如订单表和用户表如果让它们按照相同的分桶方式分桶列、分桶数量、副本分布分布那么JOIN时相同Key的数据必然在同一台机器上可以完全避免昂贵的数据网络传输Shuffle实现本地Join性能提升数倍。-- 在订单表和用户表上设置相同的分桶方案并创建Colocate Group -- 建表时指定 PROPERTIES ( colocate_with order_user_group ) -- 或建表后修改 ALTER TABLE user_profile SET (colocate_with order_user_group);分区裁剪这是分区带来的最直接收益。确保你的查询条件尽量包含分区列。比如表按dt分区查询一定要带上WHERE dt 2024-01-01这样的条件。Doris的优化器会很聪明地只去扫描p20240101这个分区忽略其他所有分区数据。5. 实战案例用户画像与订单分析场景下的设计光讲理论有点干我们结合两个最常见的场景看看完整的表设计长什么样。场景一实时用户画像表主键模型 动态分区需求需要实时更新用户标签如等级、消费区间并能快速按用户ID查询。CREATE TABLE dws_user_profile_all ( user_id BIGINT NOT NULL COMMENT 用户ID, dt DATE NOT NULL COMMENT 数据日期, -- 分区列必须包含在Key中 city VARCHAR(20) REPLACE COMMENT 城市, age_range VARCHAR(10) REPLACE COMMENT 年龄段, total_order_count BIGINT SUM DEFAULT 0 COMMENT 累计订单数, total_order_amount DECIMAL(16,2) SUM DEFAULT 0 COMMENT 累计消费金额, last_login_time DATETIME REPLACE DEFAULT 1970-01-01 COMMENT 最后登录时间, tags VARCHAR(500) REPLACE_IF_NOT_NULL COMMENT 标签集合 ) ENGINEOLAP UNIQUE KEY(user_id, dt) -- 主键包含分区列dt PARTITION BY RANGE(dt) () DISTRIBUTED BY HASH(user_id) BUCKETS AUTO PROPERTIES ( enable_unique_key_merge_on_write true, -- 写时合并点查快 dynamic_partition.enable true, dynamic_partition.time_unit DAY, dynamic_partition.start -90, -- 保留90天画像 dynamic_partition.end 3, dynamic_partition.prefix p, dynamic_partition.buckets 8, replication_num 3, storage_medium SSD -- 热数据用SSD );设计思路主键模型支持用户维度数据的实时更新按dt分区方便生命周期管理只保留90天以user_id分桶保证点查性能dt包含在Unique Key中满足全局唯一约束。场景二订单明细分析表明细模型 多级分区 Hash分桶需求存储全量订单明细支持按日期、地区快速筛选并经常与用户表关联查询。CREATE TABLE dwd_order_detail ( order_id BIGINT NOT NULL COMMENT 订单ID, order_date DATE NOT NULL COMMENT 订单日期, -- 一级分区列 region VARCHAR(10) NOT NULL COMMENT 大区, -- 二级LIST分区列 user_id BIGINT NOT NULL COMMENT 用户ID, product_id INT COMMENT 商品ID, amount DECIMAL(10,2) COMMENT 金额, status TINYINT COMMENT 状态 ) ENGINEOLAP DUPLICATE KEY(order_date, region, order_id) -- 排序键 PARTITION BY RANGE(order_date) SUBPARTITION BY LIST (region) ( PARTITION p2024 VALUES LESS THAN (2025-01-01) ( SUBPARTITION p_east VALUES IN (East), SUBPARTITION p_west VALUES IN (West), SUBPARTITION p_south VALUES IN (South), SUBPARTITION p_north VALUES IN (North) ), PARTITION p2025 VALUES LESS THAN (2026-01-01) ( SUBPARTITION p_east VALUES IN (East), SUBPARTITION p_west VALUES IN (West), SUBPARTITION p_south VALUES IN (South), SUBPARTITION p_north VALUES IN (North) ) ) DISTRIBUTED BY HASH(order_id) BUCKETS 32 PROPERTIES ( replication_num 3 );设计思路明细模型保留所有订单记录采用RANGELIST的多级分区先按年分区再按大区子分区同时实现时间范围裁剪和地域裁剪以order_id分桶保证均匀性与用户表采用相同的分桶方式HASH(user_id) BUCKETS 32以便启用Colocate Join加速关联查询。踩过的坑让我明白没有最好的设计只有最适合的设计。一开始不要追求一步到位可以先基于业务的核心查询模式设计一个基础版本上线后通过Doris的查询计划分析、慢查询日志持续观察和调整。比如发现某个分区的Tablet过大可以考虑将分区粒度调细从按月改为按周发现某些查询总是扫描全部桶可以评估调整分桶键。表结构的设计本身就是一个随着业务演进而不断优化的过程。