大数据Doris之_3_数据表设计
前言
Github:https://github.com/HealerJean
一、数据类型
1、数值类型
| 类型名 | 存储空间(字节) | 描述 |
|---|---|---|
BOOLEAN |
1 | 布尔值,0 代表 false,1 代表 true。 |
TINYINT |
1 | 有符号整数,范围 [-128, 127]。 |
SMALLINT |
2 | 有符号整数,范围 [-32768, 32767]。 |
INT] |
4 | 有符号整数,范围 [-2147483648, 2147483647] |
BIGINT |
8 | 有符号整数,范围 [-9223372036854775808, 9223372036854775807]。 |
LARGEINT |
16 | 有符号整数,范围 [-2^127 + 1 ~ 2^127 - 1]。 |
FLOAT |
4 | 浮点数,范围 [-3.410^38 ~ 3.410^38]。 |
DOUBLE |
8 | 浮点数,范围 [-1.7910^308 ~ 1.7910^308]。 |
DECIMAL |
4/8/16/32 | 高精度定点数,格式:DECIMAL(P[,S] )。其中,P 代表一共有多少个有效数字(precision),S 代表小数位有多少数字(scale)。 |
1)DECIMAL
高精度定点数,格式:
DECIMAL(P[,S])。其中,P代表一共有多少个有效数字(precision),S代表小数位有多少数字(scale)。有效数字 P 的范围是 [1,MAX_P]
enable_decimal256 = false 时,MAX_P = 38,
enable_decimal256 = true 时,MAX_P = 76 。小数位数字数量 S 的范围是 [0, P]。
enable_decimal256 的默认值是 false,设置为 true 可以获得更加精确的结果,但是会带来一些性能损失。 存储空间:
-
0 < precision <= 9 时,占用 4 字节。
-
9 < precision <= 18 时,占用 8 字节。
-
16 < precision <= 38 时,占用 16 字节。
-
38 < precision <= 76 的场合,占用 32 字节。
2、日期类型
| 类型名 | 存储空间(字节) | 描述 |
|---|---|---|
DATE |
4 | 日期类型,目前的取值范围是 [‘0000-01-01’, ‘9999-12-31’],默认的打印形式是 ‘yyyy-MM-dd’。 |
DATETIME |
8 | 日期时间类型,格式:DATETIME([P])。可选参数 P 表示时间精度,取值范围是 [0, 6],即最多支持 6 位小数(微秒)。不设置时为 0。 取值范围是 [‘0000-01-01 00:00:00[.000000]’, ‘9999-12-31 23:59:59[.999999]’]。打印的形式是 ‘yyyy-MM-dd HH:mm:ss.SSSSSS’。 |
3、字符串类型
| 类型名 | 存储空间(字节) | 描述 |
|---|---|---|
CHAR |
M |
定长字符串,M 代表的是定长字符串的字节长度。M 的范围是 1-255。 |
VARCHAR |
不定长 | 变长字符串,M 代表的是变长字符串的字节长度。M 的范围是 1-65533。变长字符串是以 UTF-8 编码存储的,因此通常英文字符占 1 个字节,中文字符占 3 个字节。 |
STRING |
不定长 | 变长字符串,默认支持 1048576 字节(1MB),可调大到 2147483643 字节(2GB)。可通过 BE 配置 string_type_length_soft_limit_bytes 调整。String 类型只能用在 Value 列,不能用在 Key 列和分区分桶列。 |
4、半结构类型
| 类型名 | 存储空间(字节) | 描述 |
|---|---|---|
ARRAY |
不定长 | 由 T 类型元素组成的数组,不能作为 Key 列使用。目前支持在 Duplicate 和 Unique 模型的表中使用。 |
MAP |
不定长 | 由 K,V 类型元素组成的 map,不能作为 Key 列使用。目前支持在 Duplicate 和 Unique 模型的表中使用。 |
STRUCT |
不定长 | 由多个 Field 组成的结构体,也可被理解为多个列的集合。不能作为 Key 使用,目前 STRUCT 仅支持在 Duplicate 模型的表中使用。一个 Struct 中的 Field 的名字和数量固定,总是为 Nullable。 |
JSON |
不定长 | 二进制 JSON 类型,采用二进制 JSON 格式存储,通过 JSON 函数访问 JSON 内部字段。长度限制和配置方式与 String 相同 |
VARIANT |
不定长 | 动态可变数据类型,专为半结构化数据如 JSON 设计,可以存入任意 JSON,自动将 JSON 中的字段拆分成子列存储,提升存储效率和查询分析性能。长度限制和配置方式与 String 相同。Variant 类型只能用在 Value 列,不能用在 Key 列和分区分桶列。 |
1)STRUCT
- 本质:将多个相关字段组合成一个逻辑整体的复合数据类型,类似传统数据库中的 “行” 或编程语言中的 “结构体”。
- 核心优势
- 数据封装:将相关字段(如用户地址的省 / 市 / 区)封装为一个整体,简化表结构;
- 灵活查询:支持嵌套访问(如
struct_col.city),适配复杂业务场景; - 兼容性:兼容
JSON等半结构化数据的存储与解析。
- 限制条件
- 不能作为表的
Key列; - 字段名称不可重复,类型一旦定义不可修改。
- 不能作为表的
a、场景 1:用户地址信息结构化存储
- 建表语句(明细模型)
CREATE TABLE user_profile (
user_id BIGINT, -- 主键列
basic_info STRUCT< -- 基础信息结构体
name VARCHAR(50),
age INT,
gender VARCHAR(10)
>,
address STRUCT< -- 地址结构体
province VARCHAR(20),
city VARCHAR(20),
district VARCHAR(20),
zip_code CHAR(6)
>,
contact STRUCT< -- 联系方式结构体
phone VARCHAR(20),
email VARCHAR(50)
>
) ENGINE=OLAP
UNIQUE KEY(user_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 8;
- 数据导入(嵌套结构体赋值)
-- 导入用户1001的结构化信息
INSERT INTO user_profile VALUES (
1001,
STRUCT('张三', 25, '男'),
STRUCT('广东省', '深圳市', '南山区', '518000'),
STRUCT('13800138000', 'zhangsan@example.com')
);
- 嵌套查询与过滤
-- 查询广东省深圳市的用户姓名和电话
SELECT
basic_info.name,
contact.phone
FROM user_profile
WHERE address.province = '广东省' AND address.city = '深圳市';
-- 结果示例:
+--------+--------------+
| name | phone |
+--------+--------------+
| 张三 | 13800138000 |
+--------+--------------+
b、场景 2:电商订单商品明细存储
1. 表结构设计
CREATE TABLE order_detail (
order_id BIGINT, -- 订单ID(主键)
user_info STRUCT< -- 用户信息
user_id BIGINT,
username VARCHAR(50)
>,
product_list STRUCT< -- 商品列表结构体(含数组)
product_id BIGINT,
name VARCHAR(100),
price DECIMAL(10,2),
quantity INT
> ARRAY, -- 数组类型的结构体
order_time DATETIME
) ENGINE=OLAP
UNIQUE KEY(order_id);
2. 复杂数据导入(结构体数组)
-- 导入包含多个商品的订单
INSERT INTO order_detail VALUES (
20250626001,
STRUCT(1001, '张三'),
ARRAY(
STRUCT(101, '笔记本电脑', 5999.00, 1),
STRUCT(102, '鼠标', 99.00, 2)
),
'2025-06-26 10:00:00'
);
3. 数组结构体查询
-- 查询订单中价格超过5000元的商品名称
SELECT
order_id,
product_list.name
FROM order_detail
WHERE order_id = 20250626001
AND product_list.price > 5000;
-- 结果:
+------------+--------------+
| order_id | name |
+------------+--------------+
| 20250626001 | 笔记本电脑 |
+------------+--------------+
c、场景 3:日志数据的半结构化存储
1. 表结构与数据导入
-- 建表:存储用户行为日志(含结构化和半结构化数据)
CREATE TABLE user_behavior (
user_id BIGINT,
event_time DATETIME,
event_info STRUCT<
event_type VARCHAR(50),
page_path VARCHAR(200),
referrer STRUCT< -- 嵌套结构体
source VARCHAR(50),
medium VARCHAR(50)
>,
properties STRUCT< -- 扩展属性(可空)
session_id VARCHAR(50),
device_type VARCHAR(20)
>
>
) ENGINE=OLAP ...;
-- 导入日志数据
INSERT INTO user_behavior VALUES (
1001,
'2025-06-26 14:30:00',
STRUCT(
'page_view',
'/product/101',
STRUCT('google', 'organic'),
STRUCT('ses_123', 'mobile')
)
);
2. 嵌套结构体复杂查询
-- 统计通过Google organic渠道访问商品页的移动设备用户数
SELECT COUNT(DISTINCT user_id) AS user_count
FROM user_behavior
WHERE event_info.event_type = 'page_view'
AND event_info.referrer.source = 'google'
AND event_info.referrer.medium = 'organic'
AND event_info.properties.device_type = 'mobile';
5、聚合类型
| 类型名 | 存储空间(字节) | 描述 |
|---|---|---|
HLL |
不定长 | HLL 是模糊去重,在数据量大的情况性能优于 Count Distinct。HLL 的误差通常在 1% 左右,有时会达到 2%。HLL 不能作为 Key 列使用,建表时配合聚合类型为 HLL_UNION。用户不需要指定长度和默认值。长度根据数据的聚合程度系统内控制。HLL 列只能通过配套的 hll_union_agg、hll_raw_agg、hll_cardinality、hll_hash 进行查询或使用。 |
BITMAP |
不定长 | Bitmap 类型的列可以在 Aggregate 表、Unique 表或 Duplicate 表中使用。在 Unique 表或 Duplicate 表中使用时,其必须作为非 Key 列使用。在 Aggregate 表中使用时,其必须作为非 Key 列使用,且建表时配合的聚合类型为 BITMAP_UNION。用户不需要指定长度和默认值。长度根据数据的聚合程度系统内控制。BITMAP 列只能通过配套的 bitmap_union_count、bitmap_union、bitmap_hash、bitmap_hash64 等函数进行查询或使用。 |
QUANTILE_STATE |
不定长 | QUANTILE_STATE 是一种计算分位数近似值的类型,在导入时会对相同的 Key,不同 Value 进行预聚合,当 value 数量不超过 2048 时采用明细记录所有数据,当 Value 数量大于 2048 时采用 TDigest 算法,对数据进行聚合(聚类)保存聚类后的质心点。QUANTILE_STATE 不能作为 Key 列使用,建表时配合聚合类型为 QUANTILE_UNION。用户不需要指定长度和默认值。长度根据数据的聚合程度系统内控制。QUANTILE_STATE 列只能通过配套的 QUANTILE_PERCENT、QUANTILE_UNION、TO_QUANTILE_STATE 等函数进行查询或使用。 |
AGG_STATE |
不定长 | 聚合函数,只能配合 state/merge/union 函数组合器使用。AGG_STATE 不能作为 Key 列使用,建表时需要同时声明聚合函数的签名。用户不需要指定长度和默认值。实际存储的数据大小与函数实现有关。 |
1)HLL
- 本质:一种概率性数据结构,用于估算基数(不重复元素数量),而非精确统计。
- 优势
- 空间效率:相比传统
COUNTDISTINCT,存储1000万唯一值仅需约12KB空间。 - 计算效率:聚合操作时间复杂度为
O (1),远优于GROUPBY去重。
- 空间效率:相比传统
- 误差率:默认误差率约 1%(可通过参数调整,但空间占用会增加)。
- 必用函数组合:
- 写入:
HLL_HASH()或HLL_RAW_AGG()(处理原始字符串)。 - 聚合:
HLL_UNION_AGG()(合并多个HLL值)。 - 查询:
HLL_CARDINALITY()(获取估算基数)。
- 写入:
a、场景1:网站日活用户(UV)统计
- 建表语句(聚合模型)
CREATE TABLE website_uv (
date DATE, -- 日期(主键列)
region VARCHAR(20), -- 地区(主键列)
user_uv HLL HLL_UNION -- HLL列,聚合类型为HLL_UNION
) ENGINE=OLAP
AGGREGATE KEY(date, region)
DISTRIBUTED BY HASH(date) BUCKETS 8;
- 数据导入(模拟用户访问)
-- 导入北京地区3月1日的用户访问数据
INSERT INTO website_uv VALUES
('2025-03-01', '北京', HLL_HASH('user1')),
('2025-03-01', '北京', HLL_HASH('user2')),
('2025-03-01', '北京', HLL_HASH('user1')); -- 重复用户自动去重
- 查询日活用户数
-- 估算北京地区3月1日的UV
SELECT date, region, HLL_CARDINALITY(user_uv) AS uv_count
FROM website_uv
WHERE date = '2025-03-01' AND region = '北京';
-- 结果示例:
+------------+--------+------------+
| date | region | uv_count |
+------------+--------+------------+
| 2025-03-01 | 北京 | 2 | -- 正确估算2个唯一用户
+------------+--------+------------+
b、场景 2:广告点击去重统计
- 建表设计
CREATE TABLE ad_click (
ad_id BIGINT, -- 广告ID(主键)
date DATE, -- 日期
click_users HLL HLL_UNION -- 点击用户HLL列
) ENGINE=OLAP
AGGREGATE KEY(ad_id, date);
- 实时点击数据导入
-- 假设接收到广告1001的点击数据
INSERT INTO ad_click VALUES
(1001, '2025-06-26', HLL_RAW_AGG('userA')),
(1001, '2025-06-26', HLL_RAW_AGG('userB')),
(1001, '2025-06-26', HLL_RAW_AGG('userA')); -- 重复点击自动去重
- 多维度去重查询
-- 统计广告1001在6月的总点击UV(误差率约1%)
SELECT ad_id,
DATE_FORMAT(date, '%Y-%m') AS month,
HLL_CARDINALITY(click_users) AS unique_clicks
FROM ad_click
WHERE ad_id = 1001
GROUP BY ad_id, month;
-- 结果对比(假设实际唯一点击为100万):
-- HLL估算:99.2万(误差0.8%),传统COUNT DISTINCT:100万
c、场景 3:电商商品浏览量去重
- 表结构与数据导入
-- 建表
CREATE TABLE product_view (
product_id BIGINT,
day DATE,
viewer_hll HLL HLL_UNION
) ENGINE=OLAP ...;
-- 导入数据(HLL_HASH处理用户ID)
INSERT INTO product_view VALUES
(1001, '2025-06-26', HLL_HASH('u_123')),
(1001, '2025-06-26', HLL_HASH('u_456')),
(1002, '2025-06-26', HLL_HASH('u_123')); -- 同一用户浏览不同商品
- 跨商品的用户行为分析
-- 统计所有商品在6月26日的总浏览UV
SELECT SUM(HLL_CARDINALITY(viewer_hll)) AS total_uv
FROM product_view
WHERE day = '2025-06-26';
-- 传统方案(COUNT DISTINCT)耗时对比:
-- HLL方案:10ms(内存计算),传统方案:300ms(磁盘聚合)
2)BITMAP
- 本质:基于位运算的集合数据结构,用于精确存储和计算唯一值集合(如用户
ID、标签ID)。 - 核心优势
- 集合操作高效:支持交集、并集、差集等操作,时间复杂度为 O (n/32)(n 为位数)。
- 空间效率:存储
100万唯一值约需125KB(每位代表一个值是否存在)。
- 与
HLL的差异HLL是模糊估算基数,BITMAP是精确集合存储;BITMAP支持完整集合运算,HLL仅支持基数估算。
- 必用函数组合:
- 写入:
BITMAP_HASH()(32 位,可能冲突)或BITMAP_HASH64()(64 位,低冲突)。 - 聚合:
BITMAP_UNION()(合并多个BITMAP)。 - 查询:
BITMAP_UNION_COUNT()(计算并集元素数)、BITMAP_CONTAINS()(判断包含)。
- 写入:
a、场景 1:用户标签交集分析(精确集合运算)
- 建表语句(聚合模型)
CREATE TABLE user_tags (
date DATE, -- 日期(主键列)
tag_id BIGINT, -- 标签ID(主键列)
user_set BITMAP BITMAP_UNION -- 用户集合BITMAP列
) ENGINE=OLAP
AGGREGATE KEY(date, tag_id)
DISTRIBUTED BY HASH(tag_id) BUCKETS 8;
- 数据导入(标记用户所属标签)
-- 导入2025-06-26日标签1001的用户集合
INSERT INTO user_tags VALUES
('2025-06-26', 1001, BITMAP_HASH('u_123')),
('2025-06-26', 1001, BITMAP_HASH('u_456')),
('2025-06-26', 1002, BITMAP_HASH('u_123')), -- 用户u_123同时属于标签1001和1002
('2025-06-26', 1002, BITMAP_HASH('u_789'));
- 查询同时属于两个标签的用户数(交集运算)
-- 统计同时属于标签1001和1002的用户数
SELECT BITMAP_UNION_COUNT(
(SELECT user_set FROM user_tags WHERE tag_id = 1001 AND date = '2025-06-26'),
(SELECT user_set FROM user_tags WHERE tag_id = 1002 AND date = '2025-06-26')
) AS common_users;
-- 结果示例:
+--------------+
| common_users |
+--------------+
| 1 | -- 正确统计到u_123
+--------------+
b、场景 2:权限管理与用户组快速筛选
- 表结构设计
CREATE TABLE permission_groups (
group_id BIGINT, -- 权限组ID(主键)
user_bitmap BITMAP BITMAP_UNION -- 组内用户BITMAP
) ENGINE=OLAP
AGGREGATE KEY(group_id);
- 数据导入与权限分配
-- 分配用户到权限组
INSERT INTO permission_groups VALUES
(101, BITMAP_HASH64(1001)), -- 用户1001加入组101(使用64位哈希减少误差)
(101, BITMAP_HASH64(1002)),
(102, BITMAP_HASH64(1001)), -- 用户1001同时属于组101和102
(102, BITMAP_HASH64(1003));
- 权限校验与用户组查询
-- 检查用户1001是否在组101中
SELECT IF(BITMAP_CONTAINS(
(SELECT user_bitmap FROM permission_groups WHERE group_id = 101),
BITMAP_HASH64(1001)
), '有权限', '无权限') AS has_permission;
-- 统计同时属于组101和102的用户数
SELECT BITMAP_UNION_COUNT(
(SELECT user_bitmap FROM permission_groups WHERE group_id = 101),
(SELECT user_bitmap FROM permission_groups WHERE group_id = 102)
) AS shared_users;
c、场景 3:电商商品关联分析(精确去重)
- 表结构与数据导入
-- 建表:记录用户浏览的商品ID
CREATE TABLE product_browse (
user_id BIGINT,
date DATE,
product_set BITMAP BITMAP_UNION
) ENGINE=OLAP ...;
-- 导入数据(使用BITMAP_HASH64避免误差)
INSERT INTO product_browse VALUES
(1001, '2025-06-26', BITMAP_HASH64(101)),
(1001, '2025-06-26', BITMAP_HASH64(102)),
(1002, '2025-06-26', BITMAP_HASH64(101)),
(1002, '2025-06-26', BITMAP_HASH64(103));
- 商品关联分析(并集与差集)
-- 统计同时浏览商品101和102的用户数(交集)
SELECT BITMAP_UNION_COUNT(
(SELECT product_set FROM product_browse WHERE user_id = 1001),
(SELECT product_set FROM product_browse WHERE user_id = 1002)
) AS common_products;
-- 统计用户1001浏览但用户1002未浏览的商品数(差集)
SELECT BITMAP_UNION_COUNT(
(SELECT product_set FROM product_browse WHERE user_id = 1001),
(SELECT product_set FROM product_browse WHERE user_id = 1002),
'DIFF' -- 差集操作
) AS unique_products;
3)AGG_STATE
- 本质:用于存储聚合函数的中间计算状态,支持分布式环境下的聚合结果合并(如跨节点汇总)。
- 核心作用
- 优化分布式聚合性能:将复杂聚合拆分为 “本地计算状态→全局合并状态” 两步,减少数据传输量。
- 支持断点续算:中间状态可持久化,避免重复计算(如任务失败后恢复)。
-
必用函数组合:
-
写入:
函数名_STATE(参数)(如COUNT_DISTINCT_STATE('value'))。 -
聚合:
函数名_COMBINE(agg_state)(如COUNT_DISTINCT_COMBINE(agg_col))。
-
a、场景 1:分布式 COUNT DISTINCT 优化
- 建表语句(聚合模型)
CREATE TABLE sales_uv (
date DATE, -- 日期(主键列)
region VARCHAR(20), -- 地区(主键列)
user_agg AGG_STATE(COUNT_DISTINCT) -- COUNT DISTINCT的聚合状态列
) ENGINE=OLAP
AGGREGATE KEY(date, region)
DISTRIBUTED BY HASH(region) BUCKETS 8;
关键声明:AGG_STATE(COUNT_DISTINCT) 需与聚合函数签名一致。
- 数据导入(存储中间状态)
-- 导入北京地区3月1日的用户访问数据(自动计算COUNT DISTINCT状态)
INSERT INTO sales_uv VALUES
('2025-03-01', '北京', COUNT_DISTINCT_STATE('user1')),
('2025-03-01', '北京', COUNT_DISTINCT_STATE('user2')),
('2025-03-01', '北京', COUNT_DISTINCT_STATE('user1')); -- 重复用户自动去重
- 分布式聚合查询(合并中间状态)
-- 统计北京地区3月1日的UV(自动合并各节点的AGG_STATE)
SELECT date, region, COUNT_DISTINCT_COMBINE(user_agg) AS uv_count
FROM sales_uv
WHERE date = '2025-03-01' AND region = '北京'
GROUP BY date, region;
-- 结果示例:
+------------+--------+------------+
| date | region | uv_count |
+------------+--------+------------+
| 2025-03-01 | 北京 | 2 | -- 正确计算2个唯一用户
+------------+--------+------------+
场景 2:复杂聚合函数的中间结果存储
- 自定义聚合函数场景
假设需要计算 “最近 N 天活跃用户的平均消费金额”,传统方法需扫描全量数据,而 AGG_STATE 可存储中间状态:
- 建表设计
CREATE TABLE user_consume (
user_id BIGINT, -- 用户ID(主键)
consume_agg AGG_STATE(MY_AVG_FUNC) -- 自定义平均消费聚合状态
) ENGINE=OLAP
AGGREGATE KEY(user_id);
- 中间状态计算与查询
-- 导入消费数据并存储中间状态
INSERT INTO user_consume VALUES
(1001, MY_AVG_FUNC_STATE(100, 2)), -- 消费100元,计数2次
(1001, MY_AVG_FUNC_STATE(200, 1)); -- 新增消费200元,计数+1
-- 合并状态并计算平均消费
SELECT user_id, MY_AVG_FUNC_COMBINE(consume_agg) AS avg_consume
FROM user_consume
WHERE user_id = 1001;
-- 结果:(100*2 + 200*1)/(2+1) = 400/3 ≈ 133.33
b、场景 3:大规模数据的增量聚合计算
- 场景描述:电商平台需要实时统计各品类的累计销售金额,但每日数据量达 TB 级,全量计算耗时过长。
-- 建表:存储各品类销售金额的聚合状态
CREATE TABLE category_sales (
category_id BIGINT, -- 品类ID(主键)
sales_agg AGG_STATE(SUM) -- SUM聚合状态列
) ENGINE=OLAP
AGGREGATE KEY(category_id);
-- 每日增量导入(存储SUM中间状态)
INSERT INTO category_sales VALUES
(101, SUM_STATE(1000)), -- 第一天销售1000元
(101, SUM_STATE(2000)), -- 第二天销售2000元
-- 实时查询累计销售(合并状态)
SELECT category_id, SUM_COMBINE(sales_agg) AS total_sales
FROM category_sales
WHERE category_id = 101;
-- 结果:1000 + 2000 = 3000元(直接合并中间状态,无需全量扫描)
4)QUANTILE_STATE
- 本质:基于 TDigest 算法 的分位数近似计算数据结构,用于高效估算大规模数据的分位数(如 p95、p99 等)。
- 核心优势
- 空间效率:存储 100 万条数据的分位数状态仅需约 50KB 空间,远低于存储全量数据。
- 计算效率:分位数查询时间复杂度为
O(log n),支持亿级数据下的毫秒级响应。
- 数据处理逻辑
- 当单
Key下Value数量 ≤2048时,存储全量明细; - 当数量 >
2048时,使用TDigest算法聚类,仅存储质心点(误差率 ≤ 1%)。
- 当单
- 必用函数组合:
- 写入:
TO_QUANTILE_STATE(value)(将数值转换为分位数状态)。 - 查询:
QUANTILE_PERCENT(quantile_state, p)(计算 p 分位数,p∈[0,1])。 - 聚合:
QUANTILE_UNION(quantile_state1, quantile_state2)(合并多个分位数状态)。
- 写入:
a、场景 1:系统性能监控(响应时间分位数统计)
- 建表语句(聚合模型)
CREATE TABLE response_time (
date DATE, -- 日期(主键列)
region VARCHAR(20), -- 地区(主键列)
rt_quantile QUANTILE_STATE -- 响应时间分位数状态列
) ENGINE=OLAP
AGGREGATE KEY(date, region)
DISTRIBUTED BY HASH(region) BUCKETS 8;
关键声明:建表时无需指定参数,默认使用 QUANTILE_UNION 聚合类型。
- 数据导入(存储响应时间分位数状态)
-- 导入北京地区3月1日的响应时间数据(单位:毫秒)
INSERT INTO response_time VALUES
('2025-03-01', '北京', TO_QUANTILE_STATE(100)), -- 响应时间100ms
('2025-03-01', '北京', TO_QUANTILE_STATE(200)),
('2025-03-01', '北京', TO_QUANTILE_STATE(150)),
('2025-03-01', '北京', TO_QUANTILE_STATE(500)); -- 大值数据
- 分位数查询(计算
p95响应时间)
-- 统计北京地区3月1日的p95响应时间
SELECT date, region, QUANTILE_PERCENT(rt_quantile, 0.95) AS p95_rt
FROM response_time
WHERE date = '2025-03-01' AND region = '北京'
GROUP BY date, region;
-- 结果示例:
+------------+--------+--------+
| date | region | p95_rt |
+------------+--------+--------+
| 2025-03-01 | 北京 | 450 | -- 估算p95响应时间为450ms
+------------+--------+--------+
b、场景 2:用户消费金额分布分析
- 建表设计
CREATE TABLE user_consume (
date DATE, -- 日期(主键)
category_id BIGINT, -- 商品类别(主键)
amount_quantile QUANTILE_STATE -- 消费金额分位数状态
) ENGINE=OLAP
AGGREGATE KEY(date, category_id);
- 大规模数据导入(触发 TDigest 算法)
-- 导入3月1日电子产品类的消费金额(假设超2048条数据)
INSERT INTO user_consume VALUES
('2025-03-01', 101, TO_QUANTILE_STATE(199)),
('2025-03-01', 101, TO_QUANTILE_STATE(299)),
... -- 省略2000条数据
('2025-03-01', 101, TO_QUANTILE_STATE(9999)); -- 总数据量>2048,触发TDigest
- 多维度分位数查询
-- 统计电子产品类3月1日消费金额的p50(中位数)和p99
SELECT
date,
category_id,
QUANTILE_PERCENT(amount_quantile, 0.5) AS median_amount,
QUANTILE_PERCENT(amount_quantile, 0.99) AS p99_amount
FROM user_consume
WHERE date = '2025-03-01' AND category_id = 101;
-- 结果对比(假设实际分布):
-- 实际p99:9500元,QUANTILE估算:9420元(误差0.8%)
c、场景 3:服务器资源监控(CPU 利用率分位数)
- 表结构与数据导入
-- 建表:存储各服务器CPU利用率分位数
CREATE TABLE server_metrics (
server_id VARCHAR(50), -- 服务器ID(主键)
time_slot VARCHAR(20), -- 时间窗口(主键)
cpu_quantile QUANTILE_STATE -- CPU利用率分位数状态
) ENGINE=OLAP ...;
-- 导入某服务器10:00-10:10的CPU利用率(%)
INSERT INTO server_metrics VALUES
('server-001', '10:00-10:10', TO_QUANTILE_STATE(30)),
('server-001', '10:00-10:10', TO_QUANTILE_STATE(45)),
('server-001', '10:00-10:10', TO_QUANTILE_STATE(90)),
... -- 共1000条数据(<2048,存储明细)
- 分位数查询与异常检测
-- 查询该服务器10:00-10:10的CPU利用率p90
SELECT server_id, time_slot, QUANTILE_PERCENT(cpu_quantile, 0.9) AS p90_cpu
FROM server_metrics
WHERE server_id = 'server-001' AND time_slot = '10:00-10:10';
-- 结果:p90_cpu = 85%,若超过阈值(如80%)则触发告警
6、IP 类型
| 类型名 | 存储空间(字节) | 描述 |
|---|---|---|
IPv4 |
4 字节 | 以 4 字节二进制存储 IPv4 地址,配合 ipv4_* 系列函数使用。 |
IPv6 |
16 字节 | 以 16 字节二进制存储 IPv6 地址,配合 ipv6_* 系列函数使用。 |
二、数据压缩
Doris采用 列式存储 模型来组织和存储数据,这种存储模型特别适合分析型负载,能够显著提高查询效率。在列式存储中,表的每一列会独立存储,这为压缩技术的应用提供了便利,从而提高了存储效率。Doris提供多种压缩算法,用户可以根据工作负载的需求,选择合适的压缩方式来优化存储和查询性能。
1、为什么需要压缩
在 Doris 中,数据压缩主要有以下两个核心目标:
提升存储效率 压缩可以显著减少数据存储所需的磁盘空间,支持在同样的物理资源上存储更多数据。
优化性能: 压缩后的数据体积更小,查询时需要的 I/O 操作更少,从而加速查询响应时间。
- 现代压缩算法的解压速度通常非常快,能够在减少存储空间的同时提升读取效率。
2、如何选择合适的压缩算法
Doris支持多种压缩算法,每种算法在压缩率和解压速度之间有不同的权衡,可根据需求选择合适的算法:
选择合适的压缩算法需根据工作负载特性:
- 对于 高性能实时分析 场景,推荐使用
LZ4或Snappy。 - 对于 存储效率优先 的场景,推荐使用
ZSTD或Zlib。 - 对于需要兼顾速度和压缩率的场景,可选择
LZ4F。 - 对于 归档或冷数据存储 场景,建议使用
Zlib或LZ4HC。
| 压缩类型 | 特点 | 适用场景 |
|---|---|---|
| 无压缩 | - 数据不进行压缩。 | 适用于不需要压缩的场景,例如数据已经被压缩或者存储空间不是问题的情况。 |
LZ4 |
- 压缩和解压速度非常快。 - 压缩比适中。 |
适用于对解压速度要求高的场景,如实时查询或高并发负载。 |
LZ4F (LZ4 框架) |
- LZ4 的扩展版本,支持更灵活的压缩配置。 - 速度快,压缩比适中。 |
适用于需要快速压缩并对配置有细粒度控制的场景。 |
LZ4HC (LZ4 高压缩) |
- 相比 LZ4 有更高的压缩比,但压缩速度较慢。 - 解压速度与 LZ4 相当。 |
适用于需要更高压缩比的场景,同时仍然关注解压速度。 |
ZSTD (Zstandard) |
- 高压缩比,支持灵活的压缩级别调整。 - 即使在高压缩比下,解压速度仍然很快。 |
适用于对存储效率要求较高且需要平衡查询性能的场景。 |
Snappy |
- 设计重点是快速解压。 - 压缩比适中。 |
适用于对解压速度要求高且对 CPU 消耗低的场景。 |
Zlib |
- 提供良好的压缩比与速度平衡。 - 与其他算法相比,压缩和解压速度较慢,但压缩比更高。 |
适用于对存储效率要求较高且对解压速度不敏感的场景,如归档和冷数据存储。 |
3、压缩原理
-
按列压缩 由于采用列式存储,
Doris能够对表中每一列独立压缩。这种方式提升了压缩效率,因为同一列的数据往往具有相似的分布特性。 -
压缩前的编码 在压缩数据之前,
Doris会对列数据进行编码(例如字典编码、游程编码等),将数据转换为更适合压缩的形式,从而进一步提升压缩效率。 -
按页压缩:
Doris采用 页(Page) 级别的压缩策略。每一列的数据会被分成多个页,每个页内的数据会独立进行压缩。通过按页压缩,Doris能够高效地处理大规模数据集,同时保证高效的压缩率和解压性能。 -
可配置的压缩策略 用户可以在创建表时指定需要使用的压缩算法。这种灵活性使用户可以根据具体工作负载,在压缩效率和性能之间做出最佳选择。
4、影响压缩效果的因素
虽然不同的压缩算法有不同的优缺点,但压缩的效果不仅仅依赖于选择的算法,还受以下因素的影响:
-
数据的序列性(
OrderofData):数据的顺序对于压缩效果有重要影响。对于具有高序列性的列(例如时间戳或连续数值列),压缩算法通常能够获得更好的效果。数据的顺序越有规律,压缩算法在压缩时可以识别出更多的重复模式,从而提升压缩比。 -
数据的重复度(
DataRedundancy):数据列中重复值越多,压缩效果越明显**。例如,使用字典编码对重复值进行编码能够显著降低存储空间。而对于没有明显重复的数据列,压缩效果可能不如预期。 -
数据的类型(
DataType):数据的类型也会影响压缩效果。通常,数值类型的数据(如整数和浮点数)比字符串类型的数据更容易压缩。对于浮动范围较大的数据类型,压缩算法的效果可能会受到影响。 -
列的长度(
Column Length):列中数据的长度也会影响压缩效果。较短的列通常比长列更容易压缩,因为压缩算法在较短数据块上能够更高效地找到重复模式。 -
空值(
Nulls):列中空值的比例较高时,压缩算法可能会更有效,因为压缩算法会将这些空值作为一种特殊的模式进行编码,减少存储空间。
5、在 Doris 中设置压缩
CREATE TABLE example_table (
id INT,
name STRING,
age INT
)
DUPLICATE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 10
PROPERTIES (
"compression" = "zstd"
);
三、索引
数据库索引是用于查询加速的,为了加速不同的查询场景,
ApacheDoris支持了多种丰富的索引。从加速的查询和原理来看,ApacheDoris的索引分为点查索引和跳数索引两大类。
前缀索引和
ZoneMap索引是ApacheDoris自动维护的内建智能索引,无需用户管理,倒排索引、
BloomFilter索引、NGramBloomFilter索引则需要用户自己根据场景选择,手动创建、删除。
1、索引分类和原理
1)索引分类
- 点查索引:原理是通过索引定位到满足
WHERE条件的有哪些行,直接读取那些行- 前缀索引:
- 倒排索引:
- 跳数索引:原理是通过索引确定不满足
WHERE条件的数据块,跳过这些不满足条件的数据块,只读取可能满足条件的数据块并再进行一次逐行过滤,最终得到满足条件的行。跳数索引在满足条件的行比较多时效果较好ZoneMap索引:自动维护每一列的统计信息,为每一个数据文件(Segment)和数据块(Page)记录最大值、最小值、是否有NULL。对于等值查询、范围查询、ISNULL,可以通过最大值、最小值、是否有NULL来判断数据文件和数据块是否可以包含满足条件的数据,如果没有则跳过不读对应的文件或数据块减少I/O加速查询。BloomFilter索引:位图索引(存储块内所有值的哈希):DorisBloomFilter索引以数据块(page)为单位构建,每个数据块存储一个BloomFilter。写入时,对于数据块中的每个值,经过Hash存入数据块对应的BloomFilter。查询时,根据等值条件的值,判断每个数据块对应的BloomFilter是否包含这个值,不包含则跳过对应的数据块不读取,达到减少I/O查询加速的目的。NGramBloomFilter索引:用于加速文本LIKE查询,基本原理与BloomFilter索引类似,只是存入BloomFilter的不是原始文本的值,而是对文本进行NGram分词,每个词作为值存入BloomFilter。对于LIKE查询,将LIKE的pattern也进行NGram分词,判断每个词是否在BloomFilter中,如果某个词不在则对应的数据文件或者数据块就不满足LIKE条件,可以跳过这部分数据减少I/O加速查询。
2)索引对比
| 类型 | 索引 | 优点 | 局限 |
|---|---|---|---|
| 点查索引 | 前缀索引 | 内置索引,性能最好 | 一个表只有一组前缀索引 |
| 点查索引 | 倒排索引 | 支持分词和关键词匹配,任意列可建索引,多条件组合,持续增加函数加速 | 索引存储空间较大,与原始数据相当 |
| 跳数索引 | ZoneMap 索引 |
内置索引,索引存储空间小 | 支持的查询类型少,只支持等于、范围 |
| 跳数索引 | BloomFilter 索引 |
比 ZoneMap 更精细,索引空间中等 |
支持的查询类型少,只支持等于 |
| 跳数索引 | NGram BloomFilter 索引 |
支持 LIKE 加速,索引空间中等 |
支持的查询类型少,只支持 LIKE 加速 |
3)函数支持
| 运算符 / 函数 | 前缀索引 | 倒排索引 | ZoneMap 索引 |
BloomFilter 索引 |
NGram BloomFilter 索引 |
|---|---|---|---|---|---|
= |
YES | YES | YES | YES | NO |
!= |
YES | YES | NO | NO | NO |
IN |
YES | YES | YES | YES | NO |
NOT IN |
YES | YES | NO | NO | NO |
>, >=, <, <=, BETWEEN |
YES | YES | YES | NO | NO |
IS NULL |
YES | YES | YES | NO | NO |
IS NOT NULL |
YES | YES | NO | NO | NO |
LIKE |
NO | NO | NO | NO | YES |
MATCH, MATCH_* |
NO | YES | NO | NO | NO |
array_contains |
NO | YES | NO | NO | NO |
array_overlaps |
NO | YES | NO | NO | NO |
is_ip_address_in_range |
NO | YES | NO | NO | NO |
4)索引设计
数据库表的索引设计和优化跟数据特点和查询很相关,需要根据实际场景测试和优化。虽然没有 “银弹”,
ApacheDoris仍然不断努力降低用户使用索引的难度,用户可以根据下面的简单建议原则进行索引选择和测试。
a、设计建议
- 前缀索引:高频过滤条件优化:最频繁使用的过滤条件指定为
Key自动建前缀索引,因为它的过滤效果最好,但是一个表只能有一个前缀索引,因此要用在最频繁的过滤条件上 - 倒排索引:
- 对非
Key字段如有过滤加速需求,首选建倒排索引,因为它的适用面广,可以多条件组合,次选下面两种索引: - 对索引存储空间很敏感,将倒排索引换成
BloomFilter索引
- 对非
NGramBloomFilter索引:有字符串LIKE匹配需求(尤其是非前缀匹配)
CREATE TABLE user_logs (
log_id BIGINT, -- 日志ID(唯一键)
user_id BIGINT, -- 用户ID
log_time DATETIME, -- 日志时间
action_type VARCHAR(50), -- 行为类型(如"login", "purchase")
device VARCHAR(100), -- 设备信息(如"iPhone 14 Pro")
ip_address VARCHAR(50), -- IP地址
url VARCHAR(200), -- 访问URL
user_agent VARCHAR(500), -- 用户代理(浏览器/系统信息)
content TEXT -- 日志内容(长文本)
)
DUPLICATE KEY(log_id);
b、使用示例
a、前缀索引:高频过滤条件优化
场景:用户频繁按 user_id 过滤数据(如查询某个用户的所有日志):
SELECT * FROM user_logs WHERE user_id = 12345;
优化:将 user_id 指定为 KEY,自动创建前缀索引:
CREATE TABLE user_logs (
log_id BIGINT,
user_id BIGINT, -- 指定为 KEY 以创建前缀索引
log_time DATETIME,
...
)
DUPLICATE KEY(user_id, log_id); -- 将 user_id 作为第一 KEY
b、倒排索引-非 KEY 字段多条件过滤
场景:用户需按 action_type 和 device 组合过滤(如查询使用 Android 设备的所有登录行为):
SELECT * FROM user_logs
WHERE action_type = 'login' AND device LIKE '%Android%';
优化:为 action_type 和 device 创建倒排索引:
ALTER TABLE user_logs ADD INDEX idx_action_type USING INVERTED (action_type);
ALTER TABLE user_logs ADD INDEX idx_device USING INVERTED (device);
效果:查询时通过倒排索引快速定位符合条件的行 ID 集合,再合并结果。
c、N-Gram BloomFilter 索引:字符串模糊匹配
场景:用户需按 url 前缀或中间内容进行模糊匹配(如查询所有包含 /api/ 的 URL):
SELECT * FROM user_logs WHERE url LIKE '%/api/%';
优化:为 url 创建 N-Gram BloomFilter 索引:
ALTER TABLE user_logs ADD INDEX idx_url USING NGRAM_BF (url) PROPERTIES (
"gram_size" = "3", -- 按3个字符为一组进行分词
"bf_fpp" = "0.01" -- 布隆过滤器误判率设为1%
);
效果
- 将
url按 3 个字符分词(如/api/被拆分为[/ap, api, pi/]),存入布隆过滤器。 - 查询时先通过布隆过滤器快速判断是否可能存在匹配,再精确验证。
- 优势:相比全表扫描,显著减少 IO 开销;支持中间匹配(
LIKE '%pattern%')。
c、总结
1)是否为高频过滤条件?
- 是 → 指定为
KEY自动创建前缀索引。 - 否 → 转下一步。
2)是否需要多条件组合过滤?
- 是 → 为非
KEY字段创建倒排索引。 - 否 → 转下一步。
3)是否需要字符串模糊匹配(尤其是非前缀匹配)?
- 是 → 创建
N-GramBloomFilter索引。 - 否 → 考虑其他索引类型(如
Bitmap索引)。
2、前缀索引&排序键
索引结构:
Doris的数据存储在类似SSTable(SortedStringTable)的数据结构中。该结构是一种有序的数据结构,可以按照指定的一个或多个列进行排序存储。有序性使得同排序键的数据相邻存储,为索引优化提供基础。排序键:在
Aggregate、Unique和Duplicate三种数据模型中。底层的数据存储,是按照各自建表语句中,AggregateKey、UniqueKey和DuplicateKey中指定的列进行排序存储的。这些Key,称为排序键(SortKey)。借助排序键,在查询时,通过给排序列指定条件,Doris不需要扫描全表即可快速找到需要处理的数据,降低搜索的复杂度,从而加速查询。
1)索引原理
前缀索引:在排序键的基础上,又引入了前缀索引(
PrefixIndex)。前缀索引是一种稀疏索引。
有序存储 定义数据排列规则,为索引优化奠定基础;
1024行分块平衡索引粒度与扫描效率;内存前缀索引通过块首行前缀快速定位,避免全表扫描。
逻辑数据块(Data Block)划分:每 1024 行 数据划分为一个逻辑数据块,作为索引定位的基本单位,如果查询涉及已排序列,系统将找到相关 1024 行组的第一行并从那里开始扫描
索引构建规则:每个逻辑数据块对应一个前缀索引项,索引项内容为该块 第一行数据的排序列前缀(长度≤36 字节)示例:排序键为(user_id, date),某数据块第一行是(1001, '2023-06-23'),索引项存储为1001|2023-06(截断前缀以满足长度限制)。
索引的稀疏性与内存缓存:
-
前缀索引是 稀疏索引,仅存储每个数据块的首行前缀,而非每行数据,因此体积小。
-
索引全量加载到内存缓存,查询时无需磁盘
IO即可快速定位数据块。
2)使用场景
- 加速等值查询和范围查询,查询时直接通过前缀索引定位数据,无需扫描全量数据。
-
前缀索引仅支持最左匹配,若
KEY为(user_id, log_time),则无法单独用log_time触发前缀索引。 - 前缀索引没有专门的语法去定义,建表时自动取表的
Key的前 36 字节作为前缀索引。
3)使用示例
数据块一行数据的前
36个字节作为这行数据的前缀索引。当遇到VARCHAR类型时,前缀索引会直接截断。如果第一列即为VARCHAR,那么即使没有达到36字节,也会直接截断,后面的列不再加入前缀索引。
案例1:假如表的排序列为如下 5 列,那么前缀索引为:user_id(8 Bytes) + age(4 Bytes) + message(prefix 20 Bytes)。
| ColumnName | Type |
|---|---|
| user_id | BIGINT |
| age | INT |
| message | VARCHAR(100) |
| max_dwell_time | DATETIME |
| min_dwell_time | DATETIME |
案例2:假如表的排序列为如下 5 列,则前缀索引为 user_name(20 Bytes)。即使没有达到 36 个字节,因为遇到 VARCHAR,所以直接截断,不再往后继续。
| ColumnName | Type |
|---|---|
| user_name | VARCHAR(20) |
| age | INT |
| message | VARCHAR(100) |
| max_dwell_time | DATETIME |
| min_dwell_time | DATETIME |
3、倒排索引
1)索引原理
倒排索引,是信息检索领域常用的索引技术,将文本分成一个个词,构建 词 -> 文档编号 的索引,可以快速查找一个词在哪些文档出现。
从 2.0.0 版本开始,Doris 支持倒排索引,可以用来进行文本类型的全文检索、普通数值日期类型的等值范围查询,快速从海量数据中过滤出满足条件的行。
在 Doris 的倒排索引实现中,Table 的一行对应一个文档、一列对应文档中的一个字段,因此利用倒排索引可以根据关键词快速定位包含它的行,达到 WHERE 子句加速的目的。
与 Doris 中其他索引不同的是,在存储层倒排索引使用独立的文件,跟数据文件一一对应、但物理存储上文件相互独立。这样的好处是可以做到创建、删除索引不用重写数据文件,大幅降低处理开销。
2)使用场景
倒排索引的使用范围很广泛,可以加速等值、范围、全文检索(关键词匹配、短语系列匹配等)。一个表可以有多个倒排索引,查询时多个倒排索引的条件可以任意组合。
a、加速字符串类型的全文检索
- 支持关键词检索,包括同时匹配多个关键字
MATCH_ALL、匹配任意一个关键字MATCH_ANY - 支持短语查询
MATCH_PHRASE- 支持指定词距
slop - 支持短语 + 前缀
MATCH_PHRASE_PREFIX
- 支持指定词距
- 支持分词正则查询
MATCH_REGEXP - 支持英文、中文以及
Unicode多种分词
b、加速普通等值、范围查询
- 支持字符串、数值、日期时间类型的 =, !=, >, >=, <, <= 快速过滤
- 支持字符串、数字、日期时间数组类型的 =, !=, >, >=, <, <=
- 支持多条件组合查询,且适用于非
KEY字段。
c、支持完善的逻辑组合
- 不仅支持
AND条件加速,还支持ORNOT条件加速 - 支持多个条件的任意
ANDORNOT逻辑组合
d、灵活高效的索引管理
- 支持在创建表上定义倒排索引
- 支持删除已有表上的倒排索引,无需重写表中的已有数据
- 支持在已有的表上增加倒排索引,而且支持增量构建倒排索引,无需重写表中的已有数据
3)使用限制
-
精度问题的浮点数类型不支持: 存在精度问题的浮点数类型
FLOAT和DOUBLE不支持倒排索引,原因是浮点数精度不准确。解决方案是使用精度准确的定点数类型DECIMAL,DECIMAL支持倒排索引。 -
复杂数据类型还不支持倒排索引:部分复杂数据类型还不支持倒排索引,包括:MAP、STRUCT、JSON、HLL、BITMAP、QUANTILE_STATE、AGG_STATE。其中 MAP、STRUCT 会逐步支持,JSON 类型可以换成 VARIANT 类型获得支持。其他几个类型因为其特殊用途暂不需要支持倒排索引。
-
部分模型不支持:
AggregateKEY表模型:只能为Key列建立倒排索引。UniqueKEY表模型:- 开启
merge on write,可以为任意列建立倒排索引。 - 未开启
merge-on-Write,仅支持Key列建倒排索引
- 开启
DuplicateKEY表模型:可以为任意列建立倒排索引。AGGREGATE和 未开启Merge-on-Write的UNIQUE模型仅支持Key列建倒排索引,非Key列不能建倒排索引,这是因为这两个模型需要读取所有数据后做合并,因此不能利用索引做提前过滤。
4)管理索引
a、建表时定义倒排索引
CREATE TABLE table_name
(
column_name1 TYPE1,
column_name2 TYPE2,
column_name3 TYPE3,
INDEX idx_name1(column_name1) USING INVERTED [PROPERTIES(...)] [COMMENT 'your comment'],
INDEX idx_name2(column_name2) USING INVERTED [PROPERTIES(...)] [COMMENT 'your comment']
)
table_properties;
| 语法 | 说明 | |
|---|---|---|
idx_column_name(column_name) |
column_name 是建索引的列名,必须是前面列定义中出现过的,idx_column_name 是索引名字,必须表级别唯一,建议命名规范:列名前面加前缀 idx_ |
|
USING INVERTED |
用于指定索引类型是倒排索引 | |
PROPERTIES |
可选的,用于指定倒排索引的额外属性,目前支持的属性如下: |
b、已有表增加倒排索引
1、ADD INDEX:支持CREATE INDEX 和 ALTER TABLE ADD INDEX 两种语法(新写入数据会生成倒排索引)
-- 语法 1
CREATE INDEX idx_name ON table_name(column_name) USING INVERTED [PROPERTIES(...)] [COMMENT 'your comment'];
-- 语法 2
ALTER TABLE table_name ADD INDEX idx_name(column_name) USING INVERTED [PROPERTIES(...)] [COMMENT 'your comment'];
c、BUILD INDEX:存量数据需要使用 BUILD INDEX 触发:
-- 语法 1,默认给全表的所有分区 BUILD INDEX
BUILD INDEX index_name ON table_name;
-- 语法 2,可指定 Partition,可指定一个或多个
BUILD INDEX index_name ON table_name PARTITIONS(partition_name1, partition_name2);
通过 SHOW BUILD INDEX 查看 BUILD INDEX 进度:
SHOW BUILD INDEX [FROM db_name];
-- 示例 1,查看所有的 BUILD INDEX 任务进展
SHOW BUILD INDEX;
-- 示例 2,查看指定 table 的 BUILD INDEX 任务进展
SHOW BUILD INDEX where TableName = "table1";
通过 CANCEL BUILD INDEX 取消 BUILD INDEX:
CANCEL BUILD INDEX ON table_name;
CANCEL BUILD INDEX ON table_name (job_id1,jobid_2,...);
c、已有表删除倒排索引
DROP INDEX会删除索引定义,新写入数据不会再写索引,同时会生成一个异步任务执行索引删除操作,在每个BE上有多个线程执行索引删除任务,通过 BE 参数alter_index_worker_count可以设置,默认值是 3。
-- 语法 1
DROP INDEX idx_name ON table_name;
-- 语法 2
ALTER TABLE table_name DROP INDEX idx_name;
d、查看倒排索引
-- 语法 1,表的 schema 中 INDEX 部分 USING INVERTED 是倒排索引
SHOW CREATE TABLE table_name;
-- 语法 2,IndexType 为 INVERTED 的是倒排索引
SHOW INDEX FROM idx_name;
4、BloomFilter 索引
1)索引原理
位图索引(存储块内所有值的哈希):
DorisBloomFilter索引以数据块(page)为单位构建,每个数据块存储一个BloomFilter。写入时,对于数据块中的每个值,经过Hash存入数据块对应的BloomFilter。查询时,根据等值条件的值,判断每个数据块对应的BloomFilter是否包含这个值,不包含则跳过对应的数据块不读取,达到减少I/O查询加速的目的。
2)使用场景
BloomFilter索引能够对等值查询(包括=和IN)加速,对高基数字段效果较好,比如userid等唯一 ID 字段。
3)使用限制
- 对
IN和=之外的查询没有效果,比如!=,NOTINT,>,<等 - 不支持对
Tinyint、Float、Double类型的列建BloomFilter索引。 - 对低基数字段的加速效果很有限,比如“性别”字段仅有两种值,几乎每个数据块都会包含所有取值,导致
BloomFilter索引失去意义。
4)管理索引
a、建表时创建 BloomFilter 索引
由于历史原因,
BloomFilter索引定义的语法与倒排索引等通用INDEX语法不一样。BloomFilter索引通过表的PROPERTIES“bloom_filter_columns” 指定哪些字段建BloomFilter索引,可以指定一个或者多个字段。
PROPERTIES (
"bloom_filter_columns" = "column_name1,column_name2"
);
b、查看 BloomFilter 索引
SHOW CREATE TABLE table_name;
c、已有表增加 BloomFilter 索引
ALTER TABLE table_name SET ("bloom_filter_columns" = "column_name1,column_name2,column_name3");
d、已有表删除 BloomFilter 索引
ALTER TABLE table_name SET ("bloom_filter_columns" = "column_name2,column_name3");
5)使用示例
CREATE TABLE IF NOT EXISTS sale_detail_bloom (
sale_date date NOT NULL COMMENT "销售时间",
customer_id int NOT NULL COMMENT "客户编号",
saler_id int NOT NULL COMMENT "销售员",
sku_id int NOT NULL COMMENT "商品编号",
category_id int NOT NULL COMMENT "商品分类",
sale_count int NOT NULL COMMENT "销售数量",
sale_price DECIMAL(12,2) NOT NULL COMMENT "单价",
sale_amt DECIMAL(20,2) COMMENT "销售总金额"
)
Duplicate KEY(sale_date, customer_id,saler_id,sku_id,category_id)
DISTRIBUTED BY HASH(saler_id) BUCKETS 10
PROPERTIES (
"replication_num" = "1",
"bloom_filter_columns"="saler_id,category_id"
);
5、NGram BloomFilter 索引
1)索引原理
n-gram分词是将一句话或一段文字拆分成多个相邻的词组的分词方法。NGramBloomFilter索引和BloomFilter索引类似,也是基于BloomFilter的跳数索引。与
BloomFilter 索引不同的是,NGramBloomFilter索引用于加速文本LIKE查询,它存入BloomFilter的不是原始文本的值,而是对文本进行NGram分词,每个词作为值存入BloomFilter。对于LIKE查询,将LIKE‘%pattern%’ 的pattern也进行NGram分词,判断每个词是否在BloomFilter中,如果某个词不在则对应的数据块就不满足LIKE条件,可以跳过这部分数据减少IO加速查询。
2)使用场景
NGramBloomFilter索引只能加速字符串LIKE查询,而且LIKEpattern中的连续字符个数要大于等于索引定义的NGram中的N。
3)使用限制
NGramBloomFilter只支持字符串列,只能加速LIKE查询。NGramBloomFilter索引和BloomFilter索引为互斥关系,即同一个列只能设置两者中的一个。-
NGramBloomFilter索引的效果分析,跟BloomFilter索引类似。 - 亿级别以上数据,如果有模糊匹配,使用倒排索引或者是
NGramBloomfilter
4)管理索引
a、创建 NGram BloomFilter 索引
在建表语句中
COLUMN的定义之后是索引定义:
INDEX `idx_column_name` (`column_name`) USING NGRAM_BF PROPERTIES("gram_size"="3", "bf_size"="1024") COMMENT 'username ngram_bf index'
b、查看 NGram BloomFilter 索引
-- 语法 1,表的 schema 中 INDEX 部分 USING NGRAM_BF 是倒排索引
SHOW CREATE TABLE table_name;
-- 语法 2,IndexType 为 NGRAM_BF 的是倒排索引
SHOW INDEX FROM idx_name;
c、删除 NGram BloomFilter 索引
ALTER TABLE table_ngrambf DROP INDEX idx_ngrambf;
d、修改 NGram BloomFilter 索引
CREATE INDEX idx_column_name2(column_name2) ON table_ngrambf USING NGRAM_BF PROPERTIES("gram_size"="3", "bf_size"="1024") COMMENT 'username ngram_bf index';
ALTER TABLE table_ngrambf ADD INDEX idx_column_name2(column_name2) USING NGRAM_BF PROPERTIES("gram_size"="3", "bf_size"="1024") COMMENT 'username ngram_bf index';
5)使用示例
使用
NGramBloomFilter索引需设置如下参数(enable_function_pushdown默认为false):
SET enable_function_pushdown = true;
SELECT count() FROM table1 WHERE message LIKE '%error%';
四、Schema 变更
用户可以通过[
Alter Table]操作来修改Doris表的Schema。Schema变更主要涉及列的修改和索引的变化。本文主要介绍列相关的Schema变更,关于索引相关的变更,请参考[表索引]了解不同索引的变更方法。
1、原理介绍
Doris支持两种类型的SchemaChange操作:轻量级SchemaChange和重量级SchemaChange。它们的区别主要体现在执行过程的复杂性、执行速度和资源消耗上。
| 特性 | 轻量级 Schema Change | 重量级 Schema Change |
|---|---|---|
| 执行速度 | 秒级(几乎实时) | 分钟级、小时级、天级(依赖表的数据量,数据量越大,执行越慢) |
| 是否需要数据重写 | 不需要 | 需要,涉及数据文件的重写 |
| 系统性能影响 | 影响较小 | 可能影响系统性能,尤其是在数据转换过程中 |
| 资源消耗 | 较低 | 较高,会占用计算资源重新组织数据,过程中涉及到的表的数据占用的存储空间翻倍。 |
| 操作类型 | 增加、删除 Value 列,修改列名,修改 VARCHAR 长度 |
修改列的数据类型、更改主键、修改列的顺序等 |
2、轻量级 Schema Change
轻量级
SchemaChange是指不涉及数据重写的简单模式更改操作。这些操作通常在元数据级别进行,仅需要修改表的元数据,而不涉及数据文件的物理修改。轻量级SchemaChange操作通常能够在秒级别完成,不会对系统性能造成显著影响。轻量级SchemaChange包括:
- 增加或删除
value列 - 更改列名
- 修改
VARCHAR列的长度(UNIQUE和DUP表Key列除外)。
3、重量级 Schema Change
-
数据变更:重量级
SchemaChange涉及到数据文件的重写或转换,这些操作相对复杂,通常需要借助Doris的Backend(BE)进行数据的实际修改或重新组织。 重量级SchemaChange操作通常涉及对表数据结构的深度变更,可能会影响到存储的物理布局。所有不支持轻量级SchemaChange的操作,均属于重量级SchemaChange,比如:-
更改列的数据类型
-
修改列的排序顺序
-
-
数据转换:重量级操作会在后台启动一个任务进行数据转换。后台任务会对表的每个
tablet进行转换,按tablet为单位,将原始数据重写到新的数据文件中。数据转换过程中,可能会出现数据”双写”现象,即在转换期间,新数据同时写入新tablet旧tablet中。完成数据转换后,旧tablet会被删除,新tablet将取而代之。
4、作业管理
a、查看作业
用户可以通过 [
SHOW ALTER TABLE COLUMN]命令查看SchemaChange作业进度。可以查看当前正在执行或已经完成的SchemaChange作业。当一次SchemaChange作业涉及到物化视图时,该命令会显示多行,每行对应一个物化视图。举例如下:
mysql > SHOW ALTER TABLE COLUMN\G;
*************************** 1. row ***************************
JobId: 20021
TableName: tbl1
CreateTime: 2019-08-05 23:03:13
FinishTime: 2019-08-05 23:03:42
IndexName: tbl1
IndexId: 20022
OriginIndexId: 20017
SchemaVersion: 2:792557838
TransactionId: 10023
State: FINISHED
Msg:
Progress: NULL
Timeout: 86400
1 row in set (0.00 sec)
b、取消作业
在作业状态不为
FINISHED或CANCELLED的情况下,可以通过以下命令取消SchemaChange作业:
CANCEL ALTER TABLE COLUMN FROM tbl_name;
4、使用示例
1)修改列名称
ALTER TABLE [database.]table RENAME COLUMN old_column_name new_column_name;
2)添加一列
- 聚合模型如果增加
Value列,需要指定agg_type。 - 非聚合模型(如
DUPLICATEKEY)如果增加Key列,需要指定KEY关键字。
a、往非聚合表添加列
CREATE TABLE IF NOT EXISTS example_db.my_table(
col1 int,
col2 int,
col3 int,
col4 int,
col5 int
) DUPLICATE KEY(col1, col2, col3)
DISTRIBUTED BY RANDOM BUCKETS 10;
-- 向 example_db.my_table 的 col1 后添加一个 Key 列 key_col
ALTER TABLE example_db.my_table ADD COLUMN key_col INT KEY DEFAULT "0" AFTER col1;
-- 向 example_db.my_table 的 col4 后添加一个 Value 列 value_col
ALTER TABLE example_db.my_table ADD COLUMN value_col INT DEFAULT "0" AFTER col4;
b、往聚合表添加列
CREATE TABLE IF NOT EXISTS example_db.my_table(
col1 int,
col2 int,
col3 int,
col4 int SUM,
col5 varchar(32) REPLACE DEFAULT "abc"
) AGGREGATE KEY(col1, col2, col3)
DISTRIBUTED BY HASH(col1) BUCKETS 10;
-- 向 example_db.my_table 的 col1 后添加一个 Key 列 key_col
ALTER TABLE example_db.my_table ADD COLUMN key_col INT DEFAULT "0" AFTER col1;
-- 向 example_db.my_table 的 col4 后添加一个 Value 列 value_col SUM 聚合类型
ALTER TABLE example_db.my_table ADD COLUMN value_col INT SUM DEFAULT "0" AFTER col4;
3)添加多列
- 聚合模型如果增加
Value列,需要指定agg_type - 聚合模型如果增加
Key列,需要指定KEY关键字
CREATE TABLE IF NOT EXISTS example_db.my_table(
col1 int,
col2 int,
col3 int,
col4 int SUM,
col5 varchar(32) REPLACE DEFAULT "abc"
) AGGREGATE KEY(col1, col2, col3)
DISTRIBUTED BY HASH(col1) BUCKETS 10;
ALTER TABLE example_db.my_table ADD COLUMN (c1 INT DEFAULT "1", c2 FLOAT SUM DEFAULT "0");
4)删除列
- 不能删除分区列
- 不能删除
UNIQUE的KEY列。
CREATE TABLE IF NOT EXISTS example_db.my_table(
col1 int,
col2 int,
col3 int,
col4 int SUM,
col5 varchar(32) REPLACE DEFAULT "abc"
) AGGREGATE KEY(col1, col2, col3)
DISTRIBUTED BY HASH(col1) BUCKETS 10;
-- 从 example_db.my_table 删除col3列
ALTER TABLE example_db.my_table DROP COLUMN col4;
5)修改列类型和列位置
- 聚合模型如果修改
Value列,需要指定agg_type - 非聚合类型如果修改
Key列,需要指定KEY关键字 - 只能修改列的类型,列的其他属性维持原样
- 分区列和分桶列不能做任何修改
CREATE TABLE IF NOT EXISTS example_db.my_table(
col0 int,
col1 int DEFAULT "1",
col2 int,
col3 varchar(32),
col4 int SUM,
col5 varchar(32) REPLACE DEFAULT "abc"
) AGGREGATE KEY(col0, col1, col2, col3)
DISTRIBUTED BY HASH(col0) BUCKETS 10;
-- 修改 Key 列 col1 的类型为 BIGINT,并移动到 col2 列后面
ALTER TABLE example_db.my_table MODIFY COLUMN col1 BIGINT KEY DEFAULT "1" AFTER col2;
-- 修改 Base Table 的 val1 列最大长度。原 val1 为 (val1 VARCHAR(32) REPLACE DEFAULT "abc")
ALTER TABLE example_db.my_table MODIFY COLUMN col5 VARCHAR(64) REPLACE DEFAULT "abc";
-- 修改 Key 列的某个字段的长度
ALTER TABLE example_db.my_table MODIFY COLUMN col3 varchar(50) KEY NULL comment 'to 50';
6)重新排序
- 所有列都要写出来
Value列在Key列之后
CREATE TABLE IF NOT EXISTS example_db.my_table(
k1 int DEFAULT "1",
k2 int,
k3 varchar(32),
k4 date,
v1 int SUM,
v2 int MAX,
) AGGREGATE KEY(k1, k2, k3, k4)
DISTRIBUTED BY HASH(k1) BUCKETS 10;
ALTER TABLE example_db.my_table ORDER BY (k3,k1,k2,k4,v2,v1);
5、使用限制
- 分区列和分桶列不能修改。
- 物理存储与路径强绑定,若修改分区列的值,意味着数据需要从原路径移动到新路径,这会导致:
- 大规模数据迁移,消耗大量计算和 IO 资源;
- 原路径的元数据(如分区信息)失效,需重新更新元数据,增加系统复杂度。
- 元数据一致性风险:分区信息被记录在元数据数据库(如 Hive 的 Metastore)中,与物理路径一一对应。修改分区列会打破这种对应关系,可能导致:
- 查询时无法找到数据(路径不匹配);
- 元数据与实际数据不一致,引发数据丢失或查询错误。
- 物理存储与路径强绑定,若修改分区列的值,意味着数据需要从原路径移动到新路径,这会导致:
- 一张表在同一时间只能有一个
SchemaChange作业在运行。 - 当修改列类型时,除
Type以外的字段都需要按原列上的信息补全。 - 注意,除新的列类型外,如聚合方式,
Nullable属性,以及默认值都要按照原信息补全。 - 不支持修改聚合类型、
Nullable属性和默认值。 - 聚合表
- 如果聚合表中有
REPLACE方式聚合的Value列,则不允许删除Key列。原因:基于Key列确定唯一记录,当新数据插入时,若Key存在则用新Value替换旧值 - 在新增聚合类型为
SUM或者REPLACE的Value列时,该列的默认值对历史数据没有含义。因为历史数据已经失去明细信息,所以默认值的取值并不能实际反映聚合后的取值。
- 如果聚合表中有
Unique表:不允许删除Key列。
五、自增列
在
Doris中,自增列(AutoIncrementColumn)是一种自动生成唯一数字值的功能,常用于为每一行数据生成唯一的标识符,如主键。每当插入新记录时,自增列会自动分配一个递增的值,避免了手动指定数字的繁琐操作。使用Doris自增列,可以确保数据的唯一性和一致性,简化数据插入过程,减少人为错误,并提高数据管理的效率。这使得自增列成为处理需要唯一标识的场景(如用户ID等)时的理想选择。
1、功能
对于具有自增列的表,Doris 处理数据写入的方式如下:
- 自动填充(列排除): 如果写入的数据不包括自增列,
Doris会生成并填充该列的唯一值。 - 部分指定(列包含):
- 空值:
Doris会用系统生成的唯一值替换写入数据中的空值。 - 非空值:用户提供的值保持不变。
- 空值:
1)唯一性
Doris保证自增列中生成的值具有 表级唯一性。但是:
- 保证唯一性:这仅适用于系统生成的值。
- 用户提供的值:
Doris不会验证或强制执行用户在自增列中指定的值的唯一性。这可能导致重复条目。
2)聚集性
Doris生成的自增值通常是密集的 (自增值不能用于推断写入的时间顺序。),但有一些考虑:
- 潜在的间隙:由于性能优化,可能会出现间隙。每个后端节点(
BE)会预分配一块唯一值以提高效率,这些块在节点之间不重叠。 - 非时间顺序值:
Doris不保证后续写入生成的值大于早期写入的值。
2、语法
要使用自增列,需要在建表 [
CREATE-TABLE] 时为对应的列添加AUTO_INCREMENT属性。若要手动指定自增列起始值,可以通过建表时AUTO_INCREMENT(start_value)语句指定,如果未指定,则默认起始值为 1。
1)创建一个 Dupliciate 模型表,其中一个 key 列是自增列
CREATE TABLE `demo`.`tbl` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`value` BIGINT NOT NULL
) ENGINE=OLAP
DUPLICATE KEY(`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);
2)创建一个 Dupliciate 模型表,其中一个 key 列是自增列,并设置起始值为 100
CREATE TABLE `demo`.`tbl` (
`id` BIGINT NOT NULL AUTO_INCREMENT(100),
`value` BIGINT NOT NULL
) ENGINE=OLAP
DUPLICATE KEY(`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);
3)创建一个 Dupliciate 模型表,其中一个 value 列是自增列
CREATE TABLE `demo`.`tbl` (
`uid` BIGINT NOT NULL,
`name` BIGINT NOT NULL,
`id` BIGINT NOT NULL AUTO_INCREMENT,
`value` BIGINT NOT NULL
) ENGINE=OLAP
DUPLICATE KEY(`uid`, `name`)
DISTRIBUTED BY HASH(`uid`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);
4)创建一个 Unique 模型表,其中一个 key 列是自增列
CREATE TABLE `demo`.`tbl` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`name` varchar(65533) NOT NULL,
`value` int(11) NOT NULL
) ENGINE=OLAP
UNIQUE KEY(`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);
5)创建一个 Unique 模型表,其中一个 value 列是自增列
CREATE TABLE `demo`.`tbl` (
`text` varchar(65533) NOT NULL,
`id` BIGINT NOT NULL AUTO_INCREMENT,
) ENGINE=OLAP
UNIQUE KEY(`text`)
DISTRIBUTED BY HASH(`text`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);
3、使用限制
- 仅
Duplicate模型表和Unique模型表可以包含自增列。 - 一张表最多只能包含一个自增列。
- 自增列的类型必须是
BIGINT类型,且必须为NOTNULL。 - 自增列手动指定的起始值必须大于等于
0。
4、使用方式
1)普通导入
- 导入不指定自增列
id时,id列会被自动填充生成的值。 - 导入时指定自增列
id,则该列数据中的null值会被生成的值替换。
CREATE TABLE `demo`.`tbl` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`name` varchar(65533) NOT NULL,
`value` int(11) NOT NULL
) ENGINE=OLAP
UNIQUE KEY(`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);
-- 1、使用 insert into 语句导入并且不指定自增列id时,id列会被自动填充生成的值。
insert into tbl(name, value) values("Bob", 10), ("Alice", 20), ("Jack", 30);
select * from tbl order by id;
+------+-------+-------+
| id | name | value |
+------+-------+-------+
| 1 | Bob | 10 |
| 2 | Alice | 20 |
| 3 | Jack | 30 |
+------+-------+-------+
-- 2、类似地,使用 stream load 导入文件 test.csv 且不指定自增列id,id列会被自动填充生成的值。
-- test.csv:
-- Tom,40
-- John,50
curl --location-trusted -u user:passwd -H "columns:name,value" -H "column_separator:," -T ./test.csv http://{host}:{port}/api/{db}/tbl/_stream_load
select * from tbl order by id;
+------+-------+-------+
| id | name | value |
+------+-------+-------+
| 1 | Bob | 10 |
| 2 | Alice | 20 |
| 3 | Jack | 30 |
| 4 | Tom | 40 |
| 5 | John | 50 |
-- 使用 insert into 导入时指定自增列id,则该列数据中的 null 值会被生成的值替换。
insert into tbl(id, name, value) values(null, "Doris", 60), (null, "Nereids", 70);
select * from tbl order by id;
+------+---------+-------+
| id | name | value |
+------+---------+-------+
| 1 | Bob | 10 |
| 2 | Alice | 20 |
| 3 | Jack | 30 |
| 4 | Tom | 40 |
| 5 | John | 50 |
| 6 | Doris | 60 |
| 7 | Nereids | 70 |
+------+---------+-------+
2)部分列更新
-- 部分列更新(仅更新name列)
set enable_unique_key_partial_update=true;
set enable_insert_strict=false;
a、自增列是 KEY 列:
- 必须条件:部分列更新时必须显式指定
key列(自增列id)。 - 更新规则
- 若表中存在对应
id的行,则仅更新指定列(如name),其他列保持不变。 - 若表中无对应
id的行,则插入新行,未指定列使用默认值。
- 若表中存在对应
id既是自增列又是唯一键,决定了数据行的唯一性。
CREATE TABLE `demo`.`tbl2` (
`id` BIGINT NOT NULL AUTO_INCREMENT, -- 自增列且为UNIQUE KEY
`name` varchar(65533) NOT NULL,
`value` int(11) NOT NULL DEFAULT "0"
) ENGINE=OLAP
UNIQUE KEY(`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 3",
"enable_unique_key_merge_on_write" = "true"
);
insert into tbl2(id, name, value) values(1, "Bob", 10), (2, "Alice", 20), (3, "Jack", 30);
select * from tbl2 order by id;
+------+-------+-------+
| id | name | value |
+------+-------+-------+
| 1 | Bob | 10 |
| 2 | Alice | 20 |
| 3 | Jack | 30 |
-- 部分列更新(仅更新name列)
set enable_unique_key_partial_update=true;
set enable_insert_strict=false;
insert into tbl2(id, name) values(1, "modified"), (4, "added");
-- 结果分析
+------+----------+-------+
| id | name | value |
+------+----------+-------+
| 1 | modified | 10 | -- id=1的行仅name列更新
| 2 | Alice | 20 | -- 未更新的行保持不变
| 3 | Jack | 30 | -- 未更新的行保持不变
| 4 | added | 0 | -- 插入新行,value使用默认值0
+------+----------+-------+
b、当自增列是非 key 列时:
- 用户指定的非
null值(如 1000、500)直接更新。 - 未指定的列(如
name、score)保持原值。
验证:表结构
aid是自增列但非唯一键的一部分,id是唯一键。
CREATE TABLE `demo`.`tbl3` (
`id` BIGINT NOT NULL, -- 唯一键(非自增)
`name` varchar(100) NOT NULL,
`score` BIGINT NOT NULL,
`aid` BIGINT NOT NULL AUTO_INCREMENT -- 自增列(非key)
) ENGINE=OLAP
UNIQUE KEY(`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 1
PROPERTIES (
"replication_allocation" = "tag.location.default: 3",
"enable_unique_key_merge_on_write" = "true"
);
insert into tbl2(id, name, value) values(1, "Bob", 10), (2, "Alice", 20), (3, "Jack", 30);
select * from tbl2 order by id;
+------+-------+-------+
| id | name | value |
+------+-------+-------+
| 1 | Bob | 10 |
| 2 | Alice | 20 |
| 3 | Jack | 30 |
+------+-------+-------+
1)未指定自增列时的处理逻辑
核心逻辑:未指定自增列时,其值保持表中原有值(不会自动生成新值)。
-- 部分列更新(未指定aid列)
set enable_unique_key_partial_update=true;
set enable_insert_strict=false;
insert into tbl3(id, score) values(1, 999), (2, 888);
-- 结果分析
+------+---------+-------+------+
| id | name | score | aid |
+------+---------+-------+------+
| 1 | Doris | 999 | 0 | -- aid保持原值0
| 2 | Nereids | 888 | 1 | -- aid保持原值1
| 3 | Bob | 300 | 2 | -- 未更新的行保持不变
+------+---------+-------+------+
2)指定自增列时的处理逻辑
-- 部分列更新(指定aid列,包含null和非null值)
insert into tbl3(id, aid) values(1, 1000), (3, 500);
-- 结果分析
+------+---------+-------+------+
| id | name | score | aid |
+------+---------+-------+------+
| 1 | Doris | 999 | 1000 | -- 用户指定aid=1000,直接更新
| 2 | Nereids | 888 | 1 | -- 未更新的行保持不变
| 3 | Bob | 300 | 500 | -- 用户指定aid=500,直接更新
+------+---------+-------+------+
3)总结
| 维度 | 自增列作为 key 列 | 自增列作为非 key 列 |
|---|---|---|
| 必须指定列 | 必须包含自增列(key 列) | 可选择是否指定自增列 |
| 自增列未指定时 | 不允许(key 列必须指定) | 保持表中原有值 |
| 自增列指定 null 时 | 不允许(key 列不能为 null) | 自动生成递增数值(基于现有最大值 + 1) |
| 更新语义 | 标准唯一键部分列更新 | 支持自增列的特殊处理(null 值自动生成) |
| 典型应用场景 | 主键自增(如用户 ID) | 版本号、流水号等非唯一标识 |
- 自增列作为
key列:- 插入新行时,自增列值需保证唯一性(
Doris不自动生成,需用户自行维护)。 - 部分列更新时必须包含
key列,否则会报错。
- 插入新行时,自增列值需保证唯一性(
- 自增列作为非
key列:- 若希望自增列自动生成值,需在插入时不指定该列(或指定
null)。 - 手动指定大值可能导致后续自动生成的数值不连续(如插入 aid=
1000后,下一个自动值为1001)。
- 若希望自增列自动生成值,需在插入时不指定该列(或指定
- 参数配置:
enable_unique_key_partial_update=true:启用唯一键部分列更新。enable_insert_strict = false:允许插入时忽略非必填列(非严格模式)。
5、使用场景
1)高效分页
select * from records_tbl2 where unique_value > 99 order by unique_value limit 100;
- 限制:
- 深分页性能衰减:
OFFSET过大时(如10万 +)仍需全量扫描,需结合业务限制分页深度。 - 导出文件大小限制:单批次导出建议不超过
50GB,超大表需按分区拆分导出。 - 实时更新成本高:频繁更新会触发数据重分布,影响分页性能。
- 深分页性能衰减:
- 常规分页:
Doris在处理亿级数据量的常规分页查询时,如果查询条件合理,能够利用索引和分区等优化手段,也可以有较好的性能表现,一般能在秒级内返回结果。- 例如:对于有主键或唯一键约束的表,按照主键或唯一键进行分页查询,性能较为稳定。
- 深分页:
Doris没有像ClickHouse那样明确不支持深分页,但在深分页场景下性能也会受到一定影响。随着偏移量的增大,查询性能可能会逐渐下降。不过,如果查询语句能够通过索引、分区等方式有效减少数据扫描范围,即使是较深的分页查询,也能在可接受的时间内返回结果。- 例如,在查询条件中指定了具体的分区范围或使用了索引列进行过滤,然后再进行分页,对于亿级数据量,可能在几十秒内完成深分页查询。
- 适用场景:企业级
OLAP分页(如多维度销售报表)、批量数据导出(如月度财务数据备份)、实时数仓场景下的交互式查询。
六、冷热数据分层
为了帮助用户节省存储成本,
Doris针对冷数据提供了灵活的选择。
| 冷数据选择 | 适用条件 | 特性 |
|---|---|---|
| 存算分离 | 用户具备部署存算分离的条件 | 存储与计算资源独立扩展,显著降低存储成本 |
| 数据以单副本完全存储在对象存储中 | ||
| 通过本地缓存加速热数据访问 | ||
| 本地分层 | 存算一体模式下,用户希望进一步优化本地存储资源 | 支持将冷数据从 SSD 冷却到 HDD - , |
| 充分利用本地存储层级特性,节省高性能存储成本 | ||
| 远程分层 | 存算一体模式下,使用廉价的对象存储或者 HDFS 进一步降低成本 |
冷数据以单副本形式保存到对象存储或者 HDFS 中 |
| 热数据继续使用本地存储 - 不能对一个表和本地分层混合使用 |
1、SSD 和 HDD 层级存储
Doris支持在不同磁盘类型(SSD和HDD)之间进行分层存储,结合动态分区功能,根据冷热数据的特性将数据从SSD动态迁移到HDD。这种方式既降低了存储成本,又在热数据的读写上保持了高性能。
1)动态分区与层级存储
通过配置动态分区参数,用户可以设置哪些分区存储在
SSD上,以及冷却后自动迁移到HDD上。
- 热分区:最近活跃的分区,优先存储在
SSD上,保证高性能。- 冷分区:较少访问的分区,会逐步迁移到
HDD,以降低存储开销。
2)参数说明
a、dynamic_partition.hot_partition_num
- 功能:指定最近的多少个分区为热分区,这些分区存储在
SSD上,其余分区存储在HDD上。 - 注意:
- 必须同时设置
dynamic_partition.storage_medium = HDD,否则此参数不会生效。 - 如果存储路径下没有
SSD设备,则该配置会导致分区创建失败。
- 必须同时设置
示例说明:假设当前日期为 2021-05-20,按天分区,动态分区配置如下:
dynamic_partition.hot_partition_num = 2
dynamic_partition.start = -3
dynamic_partition.end = 3
系统会自动创建以下分区,并配置其存储介质和冷却时间:
p20210517:["2021-05-17", "2021-05-18") storage_medium=HDD storage_cooldown_time=9999-12-31 23:59:59
p20210518:["2021-05-18", "2021-05-19") storage_medium=HDD storage_cooldown_time=9999-12-31 23:59:59
p20210519:["2021-05-19", "2021-05-20") storage_medium=SSD storage_cooldown_time=2021-05-21 00:00:00
p20210520:["2021-05-20", "2021-05-21") storage_medium=SSD storage_cooldown_time=2021-05-22 00:00:00
p20210521:["2021-05-21", "2021-05-22") storage_medium=SSD storage_cooldown_time=2021-05-23 00:00:00
p20210522:["2021-05-22", "2021-05-23") storage_medium=SSD storage_cooldown_time=2021-05-24 00:00:00
p20210523:["2021-05-23", "2021-05-24") storage_medium=SSD storage_cooldown_time=2021-05-25 00:00:00
b、dynamic_partition.storage_medium:
- 指定动态分区的最终存储介质。默认是
HDD,可选择SSD。 - 当设置为
SSD时,hot_partition_num属性将不再生效,所有分区将默认为SSD存储介质并且冷却时间为 9999-12-31 23:59:59。
3)使用示例
1、创建一个分层存储表
CREATE TABLE tiered_table (k DATE)
PARTITION BY RANGE(k)()
DISTRIBUTED BY HASH (k) BUCKETS 5
PROPERTIES
(
"dynamic_partition.storage_medium" = "hdd",
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.hot_partition_num" = "2",
"dynamic_partition.end" = "3",
"dynamic_partition.prefix" = "p",
"dynamic_partition.buckets" = "5",
"dynamic_partition.create_history_partition"= "true",
"dynamic_partition.start" = "-3"
);
2、检查分区存储介质
SHOW PARTITIONS FROM tiered_table;
-- 可以看见 7 个分区,5 个使用 SSD, 其它的 2 个使用 HDD。
p20210517:["2021-05-17", "2021-05-18") storage_medium=HDD storage_cooldown_time=9999-12-31 23:59:59
p20210518:["2021-05-18", "2021-05-19") storage_medium=HDD storage_cooldown_time=9999-12-31 23:59:59
p20210519:["2021-05-19", "2021-05-20") storage_medium=SSD storage_cooldown_time=2021-05-21 00:00:00
p20210520:["2021-05-20", "2021-05-21") storage_medium=SSD storage_cooldown_time=2021-05-22 00:00:00
p20210521:["2021-05-21", "2021-05-22") storage_medium=SSD storage_cooldown_time=2021-05-23 00:00:00
p20210522:["2021-05-22", "2021-05-23") storage_medium=SSD storage_cooldown_time=2021-05-24 00:00:00
p20210523:["2021-05-23", "2021-05-24") storage_medium=SSD storage_cooldown_time=2021-05-25 00:00:00
2、远程存储
远程存储支持将冷数据放到外部存储(例如对象存储,
HDFS)上。远程存储的数据只有一个副本,数据可靠性依赖远程存储的数据可靠性,您需要保证远程存储有
ec(擦除码)或者多副本技术确保数据可靠性。
1)使用方法
a、冷数据保存到 S3 兼容存储
UNIQUE表如果设置了"enable_unique_key_merge_on_write" = "true"的话,无法使用此功能。
第一步: 创建 S3 Resource。
CREATE RESOURCE "remote_s3"
PROPERTIES
(
"type" = "s3",
"s3.endpoint" = "bj.s3.com",
"s3.region" = "bj",
"s3.bucket" = "test-bucket",
"s3.root.path" = "path/to/root",
"s3.access_key" = "bbb",
"s3.secret_key" = "aaaa",
"s3.connection.maximum" = "50",
"s3.connection.request.timeout" = "3000",
"s3.connection.timeout" = "1000"
);
第二步: 创建 STORAGE POLICY。之后创建 STORAGE POLICY,关联上文创建的 RESOURCE:
CREATE STORAGE POLICY test_policy
PROPERTIES(
"storage_resource" = "remote_s3",
"cooldown_ttl" = "1d"
);
第三步: 建表时使用 STORAGE POLICY。
CREATE TABLE IF NOT EXISTS create_table_use_created_policy
(
k1 BIGINT,
k2 LARGEINT,
v1 VARCHAR(2048)
)
UNIQUE KEY(k1)
DISTRIBUTED BY HASH (k1) BUCKETS 3
PROPERTIES(
"enable_unique_key_merge_on_write" = "false",
"storage_policy" = "test_policy"
);
b、冷数据保存到 HDFS
UNIQUE表如果设置了"enable_unique_key_merge_on_write" = "true"的话,无法使用此功能。
第一步: 创建 HDFS RESOURCE:
CREATE RESOURCE "remote_hdfs" PROPERTIES (
"type"="hdfs",
"fs.defaultFS"="fs_host:default_fs_port",
"hadoop.username"="hive",
"hadoop.password"="hive",
"dfs.nameservices" = "my_ha",
"dfs.ha.namenodes.my_ha" = "my_namenode1, my_namenode2",
"dfs.namenode.rpc-address.my_ha.my_namenode1" = "nn1_host:rpc_port",
"dfs.namenode.rpc-address.my_ha.my_namenode2" = "nn2_host:rpc_port",
"dfs.client.failover.proxy.provider.my_ha" = "org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider"
);
第二步: 创建 STORAGE POLICY。
CREATE STORAGE POLICY test_policy PROPERTIES (
"storage_resource" = "remote_hdfs",
"cooldown_ttl" = "300"
)
第三步: 使用 STORAGE POLICY 创建表。
CREATE TABLE IF NOT EXISTS create_table_use_created_policy (
k1 BIGINT,
k2 LARGEINT,
v1 VARCHAR(2048)
)
UNIQUE KEY(k1)
DISTRIBUTED BY HASH (k1) BUCKETS 3
PROPERTIES(
"enable_unique_key_merge_on_write" = "false",
"storage_policy" = "test_policy"
);
c、存量表冷却到远程存储
除了新建表支持设置远程存储外,
Doris还支持对一个已存在的表或者PARTITION,设置远程存储。
如果用户在建表时给整张
Table和部分Partition指定了不同的StoragePolicy,Partition设置的Storagepolicy会被无视,整张表的所有Partition都会使用table的Policy.如果您需要让某个
Partition的Policy和别的不同,则可以使用上文中对一个已存在的Partition,关联Storagepolicy的方式修改。
1)对一个已存在的表,设置远程存储,将创建好的 STORAGE POLICY 与表关联:
ALTER TABLE create_table_not_have_policy set ("storage_policy" = "test_policy");
2)对一个已存在的 PARTITION,设置远程存储,将创建好的 STORAGE POLICY 与 PARTITON 关联:
ALTER TABLE create_table_partition MODIFY PARTITION (*) SET("storage_policy"="test_policy");
2)使用限制
- 使用了远程存储的表不支持备份。
- 不支持修改远程存储的位置信息,比如
endpoint、bucket、path。 Unique模型表在开启Merge-on-Write特性时,不支持设置远程存储。- 性能瓶颈:网络延迟与合并效率冲突
- 数据一致性与事务保障困难
Storagepolicy支持创建、修改和删除,删除前需要先保证没有表引用此Storage policy。
3)查询与性能优化
为了优化查询的性能和对象存储资源节省,引入了本地
Cache。在第一次查询远程存储的数据时,Doris会将远程存储的数据加载到BE的本地磁盘做缓存,Cache有以下特性:
Cache实际存储于BE本地磁盘,不占用内存空间。Cache是通过LRU+ 空间阈值 管理的,不支持TTL。Cache定位是 “加速热点数据查询”,而非长期存储。LRU确保缓存空间始终被高频数据占据,避免 “冷数据” 占用资源。
七、行列混存
1、行列混存介绍
-
列式存储的分析场景优势:
Doris默认采用列式存储,每个列连续存储,在分析场景(如聚合,过滤,排序等)有很好的性能,因为只需要读取所需要的列减少不必要的IO。但是在点查场景(比如SELECT *),需要读取所有列,每个列都需要一次IO导致IOPS成为瓶颈,特别对列多的宽表(比如上百列)尤为明显。 -
点查场景下的
IO瓶颈问题:为了解决点查场景IOPS的瓶颈问题,Doris 2.0.0版本开始支持行列混存,用户建表时指定开启行存后,点查(比如SELECT *)每一行只需要一次IO,在宽表列很多的情况下性能有数量级提升。 - 行列混存的解决方案:行存的原理是在存储时增加了一个额外的列,这个列将对应行的所有列拼接起来采用特殊的二进制格式存储
-
列存部分:保留分析场景下的高效查询能力。
- 行存部分:新增特殊行存列,将每行数据的所有列按顺序拼接为二进制格式存储,形成行级存储单元。
2、使用语法
建表时在表的
PROPERTIES中指定是否开启行存,哪些列开启行存,行存的存储压缩单元大小page_size。
1)参数配置
1、是否开启行存:默认为 false 不开启
"store_row_column" = "true"
2、哪些列开启行存:如果 "store_row_column" = "true"
- 默认所有列开启行存,若需要指定部分列开启行存,
- 设置
row_store_columns 参数(3.0 之后的版本),格式为逗号分割的列名
"row_store_columns" = "column1,column2,column3"
3、行存 page_size:默认为 16KB。
"row_store_page_size" = "16384"
2)原理解析
page是存储读写的最小单元,page_size是行存page的大小,也就是说读一行也需要产生一个page的IO。
-
这个值越大压缩效果越好存储空间占用越低,但是点查时
IO开销越大性能越低(因为一次IO至少读一个page),反过来值越小存储空间极高,点查性能越好。 -
默认值
16KB是大多数情况下比较均衡的选择,如果更偏向查询性能可以配置较小的值比如4KB甚至更低,如果更偏向存储空间可以配置较大的值比如64KB甚至更高。
| 维度 | 16KB 的优势 |
|---|---|
| 压缩效率 | 比 4KB 提升约 30%,接近 64KB 压缩率的 80% |
IO 性能 |
比 64KB 减少 75% 的无效数据读取 |
| 适用场景 | 兼顾点查(10ms 级响应)与存储成本(压缩率适中) |
| 缓存利用率 | 多数服务器内存页大小为 16KB / 32KB,16KB 更易命中缓存 |
3、使用限制
-
存储空间放大:开启行存后占用的存储空间会增加,存储空间的增加和数据特点有关,一般是原来表的
2到10倍,具体空间占用需要使用实际数据测试。 -
参数配置:行存的
page_size对存储空间的也有影响,可以根据前面的表属性参数row_store_page_size说明进行调整。
4、使用示例
下面的例子创建一个 8 列的表,其中 “key,v1,v3,v5,v7” 这 5 列开启行存,为了高并发点查性能配置 page_size 为 4KB。
CREATE TABLE `tbl_point_query` (
`key` int(11) NULL,
`v1` decimal(27, 9) NULL,
`v2` varchar(30) NULL,
`v3` varchar(30) NULL,
`v4` date NULL,
`v5` datetime NULL,
`v6` float NULL,
`v7` datev2 NULL
) ENGINE=OLAP
UNIQUE KEY(`key`)
COMMENT 'OLAP'
DISTRIBUTED BY HASH(`key`) BUCKETS 1
PROPERTIES (
"enable_unique_key_merge_on_write" = "true",
"light_schema_change" = "true",
"row_store_columns" = "key,v1,v3,v5,v7",
"row_store_page_size" = "4096"
);
八、最佳实战
1、数据表创建
1、数据库字符集指定 UTF-8,并且只支持 UTF-8。
2、表的副本数建议为 3(未指定副本数时,默认为 3)兼顾可靠性、性能与成本的最优解。
3、桶数量配置:单个 Tablet 的数据量理论上没有上下界,除小表(百兆维表)外需确保在 1G - 10G 的范围内:
-
Tablet数 = 分区数 * 桶数 * 副本数 -
单个
Tablet数据量过小,则数据的聚合效果不佳,且元数据管理压力大。 -
数据量过大,不利于副本的迁移、补齐,且会增加
SchemaChange或者 物化 操作失败重试的代价
4、分区配置:5 亿以上的数据必须设置 分区分桶策略:
- 分区配置:
- 没有办法分区的,数据又较快增长的,没办法按照时间动态分区,可以适当放大一下你的
Bucket数量按照你的数据保存周期数据总量,来估算你的Bucket数量应该是多少,建议还是单个Bucket大小在 1-10G。
- 没有办法分区的,数据又较快增长的,没办法按照时间动态分区,可以适当放大一下你的
5、分桶配置:对分桶字段进行加盐处理,业务上查询的时候也是要同样的加盐策略,这样能利用到分桶数据剪裁能力。
bucket设置建议:- 大表的单个
Tablet存储数据大小在1G-10G区间,可防止过多的小文件产生。 - 百兆左右的维表
Tablet数量控制在3-5个,保证一定的并发数也不会产生过多的小文件。
- 大表的单个
- 数据随机分桶:
- 避免数据倾斜:如果
OLAP表没有更新类型的字段,将表的数据分桶模式设置为RANDOM,则可以避免严重的数据倾斜 (数据在导入表对应的分区的时候,单次导入作业每个Batch的数据将随机选择一个Tablet进行写入)- 适合聚合查询-而非高并发:当表的分桶模式被设置为
RANDOM时,因为没有分桶列,无法根据分桶列的值仅对几个分桶查询,对表进行查询的时候将对命中分区的全部分桶同时扫描,该设置适合对表数据整体的聚合查询分析而不适合高并发的点查询。
- 适合聚合查询-而非高并发:当表的分桶模式被设置为
- 如果
OLAP表的是RandomDistribution的数据分布,那么在数据导入的时候可以设置单分片导入模式(将load_to_single_tablet设置为true),那么在大数据量的导入的时候,一个任务在将数据写入对应的分区时将只写入一个分片,这样将能提高数据导入的并发度和吞吐量,减少数据导入和Compaction导致的写放大问题,保障集群的稳定性。原因是:导入任务不再将数据分散到多个分桶,而是仅写入一个指定的分桶(Tablet),即单个导入任务对应单个分片
6、表类别:
- 维度表:缓慢增长的,可以使用单分区,在分桶策略上使用常用查询条件(这个字段数据分布相对均衡)分桶。
- 例如:用户维度表存储用户基本信息,新增用户量远小于总数据量,属于 “缓慢增长”。
8、单表物化视图不能超过 6 个
-
单表物化视图是实时构建
-
在
Unqiue模型上物化视图只能起到Key重新排序的作用,不能做数据的聚合,因为Unqiue模型的聚合模型是Replace
2、字段类型
-
VARCHAR-
变长字符串,长度为:
1-65533字节长度,以UTF-8编码存储的,因此通常英文字符占1个字节,中文字符占3个字节。 -
这里存在一个误区,即
varchar(255)和varchar(65533)的性能问题,这二者如果存的数据是一样的,性能也是一样的,建表时如果不确定这个字段最大有多长,建议直接使用65533即可,防止由于字符串过长导致的导入问题。
-
-
STRING-
变长字符串,默认支持
1048576字节(1MB),可调大到2147483643 字节(2G),以UTF-8编码存储的,因此通常英文字符占1个字节,中文字符占3个字节。 -
只能用在
Value列,不能用在Key列和分区分桶列。 -
适用于一些比较大的文本存储,一般如果没有这种需求的话,建议使用
VARCHAR,STRING列无法用在Key列和分桶列,局限性比较大。
-
-
数值型字段:按照精度选择对应的数据类型即可,没有过于特殊的注意。
-
时间字段:这里需要注意的是,如果有高精度(毫秒值时间戳)需求,需要指明使用
datetime(6),否则默认是不支持毫秒值时间戳的。 -
建议使用
JSON数据类型代替字符串类型存放JSON数据的使用方式。


