前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

一、数据类型

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 列使用。目前支持在 DuplicateUnique 模型的表中使用。
MAP 不定长 K,V 类型元素组成的 map,不能作为 Key 列使用。目前支持在 DuplicateUnique 模型的表中使用。
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:用户地址信息结构化存储

  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;
  1. 数据导入(嵌套结构体赋值)
-- 导入用户1001的结构化信息
INSERT INTO user_profile VALUES (
    1001,
    STRUCT('张三', 25, '男'),
    STRUCT('广东省', '深圳市', '南山区', '518000'),
    STRUCT('13800138000', 'zhangsan@example.com')
);
  1. 嵌套查询与过滤
-- 查询广东省深圳市的用户姓名和电话
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_countbitmap_unionbitmap_hashbitmap_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

  • 本质:一种概率性数据结构,用于估算基数(不重复元素数量),而非精确统计。
  • 优势
    • 空间效率:相比传统 COUNT DISTINCT,存储 1000 万唯一值仅需约 12KB 空间。
    • 计算效率:聚合操作时间复杂度为 O (1),远优于 GROUP BY 去重。
  • 误差率:默认误差率约 1%(可通过参数调整,但空间占用会增加)。
  • 必用函数组合
    • 写入:HLL_HASH()HLL_RAW_AGG()(处理原始字符串)。
    • 聚合:HLL_UNION_AGG()(合并多个 HLL 值)。
    • 查询:HLL_CARDINALITY()(获取估算基数)。

a、场景1:网站日活用户(UV)统计

  1. 建表语句(聚合模型)
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;
  1. 数据导入(模拟用户访问)
-- 导入北京地区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'));  -- 重复用户自动去重
  1. 查询日活用户数
-- 估算北京地区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:广告点击去重统计

  1. 建表设计
CREATE TABLE ad_click (
    ad_id BIGINT,                    -- 广告ID(主键)
    date DATE,                       -- 日期
    click_users HLL HLL_UNION        -- 点击用户HLL列
) ENGINE=OLAP
AGGREGATE KEY(ad_id, date);
  1. 实时点击数据导入
-- 假设接收到广告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'));  -- 重复点击自动去重
  1. 多维度去重查询
-- 统计广告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:电商商品浏览量去重

  1. 表结构与数据导入
-- 建表
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'));  -- 同一用户浏览不同商品
  1. 跨商品的用户行为分析
-- 统计所有商品在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:用户标签交集分析(精确集合运算)

  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;
  1. 数据导入(标记用户所属标签)
-- 导入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'));
  1. 查询同时属于两个标签的用户数(交集运算)
-- 统计同时属于标签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:权限管理与用户组快速筛选

  1. 表结构设计
CREATE TABLE permission_groups (
    group_id BIGINT,                -- 权限组ID(主键)
    user_bitmap BITMAP BITMAP_UNION -- 组内用户BITMAP
) ENGINE=OLAP
AGGREGATE KEY(group_id);
  1. 数据导入与权限分配
-- 分配用户到权限组
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));
  1. 权限校验与用户组查询
-- 检查用户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:电商商品关联分析(精确去重)

  1. 表结构与数据导入
-- 建表:记录用户浏览的商品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));
  1. 商品关联分析(并集与差集)
-- 统计同时浏览商品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 优化

  1. 建表语句(聚合模型)
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) 需与聚合函数签名一致。

  1. 数据导入(存储中间状态)
-- 导入北京地区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'));  -- 重复用户自动去重
  1. 分布式聚合查询(合并中间状态)
-- 统计北京地区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:复杂聚合函数的中间结果存储
  1. 自定义聚合函数场景

假设需要计算 “最近 N 天活跃用户的平均消费金额”,传统方法需扫描全量数据,而 AGG_STATE 可存储中间状态:

  1. 建表设计
CREATE TABLE user_consume (
    user_id BIGINT,                 -- 用户ID(主键)
    consume_agg AGG_STATE(MY_AVG_FUNC)  -- 自定义平均消费聚合状态
) ENGINE=OLAP
AGGREGATE KEY(user_id);
  1. 中间状态计算与查询
-- 导入消费数据并存储中间状态
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:大规模数据的增量聚合计算

  1. 场景描述:电商平台需要实时统计各品类的累计销售金额,但每日数据量达 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),支持亿级数据下的毫秒级响应。
  • 数据处理逻辑
    • 当单 KeyValue 数量 ≤ 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:系统性能监控(响应时间分位数统计)

  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 聚合类型。

  1. 数据导入(存储响应时间分位数状态)
-- 导入北京地区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));  -- 大值数据
  1. 分位数查询(计算 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:用户消费金额分布分析

  1. 建表设计
CREATE TABLE user_consume (
    date DATE,                      -- 日期(主键)
    category_id BIGINT,             -- 商品类别(主键)
    amount_quantile QUANTILE_STATE  -- 消费金额分位数状态
) ENGINE=OLAP
AGGREGATE KEY(date, category_id);
  1. 大规模数据导入(触发 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
  1. 多维度分位数查询
-- 统计电子产品类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 利用率分位数)

  1. 表结构与数据导入
-- 建表:存储各服务器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,存储明细)
  1. 分位数查询与异常检测
-- 查询该服务器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 支持多种压缩算法,每种算法在压缩率和解压速度之间有不同的权衡,可根据需求选择合适的算法:

选择合适的压缩算法需根据工作负载特性:

  • 对于 高性能实时分析 场景,推荐使用 LZ4Snappy
  • 对于 存储效率优先 的场景,推荐使用 ZSTDZlib
  • 对于需要兼顾速度和压缩率的场景,可选择 LZ4F
  • 对于 归档或冷数据存储 场景,建议使用 ZlibLZ4HC
压缩类型 特点 适用场景
无压缩 - 数据不进行压缩。 适用于不需要压缩的场景,例如数据已经被压缩或者存储空间不是问题的情况。
LZ4 - 压缩和解压速度非常快。
- 压缩比适中。
适用于对解压速度要求高的场景,如实时查询或高并发负载。
LZ4F (LZ4 框架) - LZ4 的扩展版本,支持更灵活的压缩配置。
- 速度快,压缩比适中。
适用于需要快速压缩并对配置有细粒度控制的场景。
LZ4HC (LZ4 高压缩) - 相比 LZ4 有更高的压缩比,但压缩速度较慢。
- 解压速度与 LZ4 相当。
适用于需要更高压缩比的场景,同时仍然关注解压速度。
ZSTD (Zstandard) - 高压缩比,支持灵活的压缩级别调整。
- 即使在高压缩比下,解压速度仍然很快。
适用于对存储效率要求较高且需要平衡查询性能的场景。
Snappy - 设计重点是快速解压。
- 压缩比适中。
适用于对解压速度要求高且对 CPU 消耗低的场景。
Zlib - 提供良好的压缩比与速度平衡。
- 与其他算法相比,压缩和解压速度较慢,但压缩比更高。
适用于对存储效率要求较高且对解压速度不敏感的场景,如归档和冷数据存储。

3、压缩原理

  • 按列压缩 由于采用列式存储,Doris 能够对表中每一列独立压缩。这种方式提升了压缩效率,因为同一列的数据往往具有相似的分布特性。

  • 压缩前的编码 在压缩数据之前,Doris 会对列数据进行编码(例如字典编码游程编码等),将数据转换为更适合压缩的形式,从而进一步提升压缩效率。

  • 按页压缩Doris 采用 页(Page 级别的压缩策略。每一列的数据会被分成多个页,每个页内的数据会独立进行压缩。通过按页压缩,Doris 能够高效地处理大规模数据集,同时保证高效的压缩率和解压性能。

  • 可配置的压缩策略 用户可以在创建表时指定需要使用的压缩算法。这种灵活性使用户可以根据具体工作负载,在压缩效率和性能之间做出最佳选择。

4、影响压缩效果的因素

虽然不同的压缩算法有不同的优缺点,但压缩的效果不仅仅依赖于选择的算法,还受以下因素的影响:

  • 数据的序列性(Order of Data):数据的顺序对于压缩效果有重要影响。对于具有高序列性的列(例如时间戳或连续数值列),压缩算法通常能够获得更好的效果。数据的顺序越有规律,压缩算法在压缩时可以识别出更多的重复模式,从而提升压缩比

  • 数据的重复度(Data Redundancy):数据列中重复值越多,压缩效果越明显**。例如,使用字典编码对重复值进行编码能够显著降低存储空间。而对于没有明显重复的数据列,压缩效果可能不如预期。

  • 数据的类型(Data Type):数据的类型也会影响压缩效果。通常,数值类型的数据(如整数和浮点数)比字符串类型的数据更容易压缩。对于浮动范围较大的数据类型,压缩算法的效果可能会受到影响。

  • 列的长度(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"
);

三、索引

数据库索引是用于查询加速的,为了加速不同的查询场景,Apache Doris 支持了多种丰富的索引。从加速的查询和原理来看,Apache Doris 的索引分为点查索引和跳数索引两大类。

  • 前缀索引和 ZoneMap 索引是 Apache Doris 自动维护的内建智能索引,无需用户管理,

  • 倒排索引、BloomFilter 索引、NGram BloomFilter 索引则需要用户自己根据场景选择,手动创建、删除。

1、索引分类和原理

1)索引分类

  • 点查索引:原理是通过索引定位到满足 WHERE 条件的有哪些行,直接读取那些行
    • 前缀索引:
    • 倒排索引:
  • 跳数索引:原理是通过索引确定不满足 WHERE 条件的数据块,跳过这些不满足条件的数据块,只读取可能满足条件的数据块并再进行一次逐行过滤,最终得到满足条件的行。跳数索引在满足条件的行比较多时效果较好
    • ZoneMap 索引:自动维护每一列的统计信息,为每一个数据文件(Segment)和数据块(Page)记录最大值、最小值、是否有 NULL。对于等值查询、范围查询、IS NULL,可以通过最大值、最小值、是否有 NULL 来判断数据文件和数据块是否可以包含满足条件的数据,如果没有则跳过不读对应的文件或数据块减少 I/O 加速查询。
    • BloomFilter 索引:位图索引(存储块内所有值的哈希):Doris BloomFilter 索引以数据块(page)为单位构建,每个数据块存储一个 BloomFilter。写入时,对于数据块中的每个值,经过 Hash 存入数据块对应的 BloomFilter。查询时,根据等值条件的值,判断每个数据块对应的 BloomFilter 是否包含这个值,不包含则跳过对应的数据块不读取,达到减少I/O 查询加速的目的。
    • NGram BloomFilter 索引:用于加速文本 LIKE 查询,基本原理与 BloomFilter 索引类似,只是存入 BloomFilter 的不是原始文本的值,而是对文本进行 NGram 分词,每个词作为值存入 BloomFilter。对于 LIKE 查询,将 LIKEpattern 也进行 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)索引设计

数据库表的索引设计和优化跟数据特点和查询很相关,需要根据实际场景测试和优化。虽然没有 “银弹”,Apache Doris 仍然不断努力降低用户使用索引的难度,用户可以根据下面的简单建议原则进行索引选择和测试。

a、设计建议

  • 前缀索引高频过滤条件优化:最频繁使用的过滤条件指定为 Key 自动建前缀索引,因为它的过滤效果最好,但是一个表只能有一个前缀索引,因此要用在最频繁的过滤条件上
  • 倒排索引:
    • 对非 Key 字段如有过滤加速需求,首选建倒排索引,因为它的适用面广,可以多条件组合,次选下面两种索引:
    • 对索引存储空间很敏感,将倒排索引换成 BloomFilter 索引
  • NGram BloomFilter 索引:有字符串 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_typedevice 组合过滤(如查询使用 Android 设备的所有登录行为):

SELECT * FROM user_logs 
WHERE action_type = 'login' AND device LIKE '%Android%';

优化:为 action_typedevice 创建倒排索引:

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-Gram BloomFilter 索引。
  • 否 → 考虑其他索引类型(如 Bitmap 索引)。

2、前缀索引&排序键

索引结构: Doris 的数据存储在类似 SSTableSorted String Table)的数据结构中。该结构是一种有序的数据结构,可以按照指定的一个或多个列进行排序存储。有序性使得同排序键的数据相邻存储,为索引优化提供基础。

排序键:AggregateUniqueDuplicate 三种数据模型中。底层的数据存储,是按照各自建表语句中,Aggregate KeyUnique KeyDuplicate Key 中指定的列进行排序存储的。这些 Key,称为排序键(Sort Key)。借助排序键,在查询时,通过给排序列指定条件,Doris 不需要扫描全表即可快速找到需要处理的数据,降低搜索的复杂度,从而加速查询。

1)索引原理

前缀索引:在排序键的基础上,又引入了前缀索引(Prefix Index)。前缀索引是一种稀疏索引。

  • 有序存储 定义数据排列规则,为索引优化奠定基础;

  • 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 条件加速,还支持 OR NOT 条件加速
  • 支持多个条件的任意 AND OR NOT 逻辑组合

d、灵活高效的索引管理

  • 支持在创建表上定义倒排索引
  • 支持删除已有表上的倒排索引,无需重写表中的已有数据
  • 支持在已有的表上增加倒排索引,而且支持增量构建倒排索引,无需重写表中的已有数据

3)使用限制

  • 精度问题的浮点数类型不支持: 存在精度问题的浮点数类型 FLOATDOUBLE 不支持倒排索引,原因是浮点数精度不准确。解决方案是使用精度准确的定点数类型 DECIMALDECIMAL 支持倒排索引。

  • 复杂数据类型还不支持倒排索引:部分复杂数据类型还不支持倒排索引,包括:MAP、STRUCT、JSON、HLL、BITMAP、QUANTILE_STATE、AGG_STATE。其中 MAP、STRUCT 会逐步支持,JSON 类型可以换成 VARIANT 类型获得支持。其他几个类型因为其特殊用途暂不需要支持倒排索引。

  • 部分模型不支持

    • Aggregate KEY 表模型:只能为 Key 列建立倒排索引。
    • Unique KEY 表模型:
      • 开启 merge on write ,可以为任意列建立倒排索引。
      • 未开启 merge-on-Write ,仅支持 Key 列建倒排索引
    • Duplicate KEY 表模型:可以为任意列建立倒排索引。
    • AGGREGATE 和 未开启 Merge-on-WriteUNIQUE 模型仅支持 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 INDEXALTER 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)索引原理

位图索引(存储块内所有值的哈希)Doris BloomFilter 索引以数据块(page)为单位构建,每个数据块存储一个 BloomFilter。写入时,对于数据块中的每个值,经过 Hash 存入数据块对应的 BloomFilter。查询时,根据等值条件的值,判断每个数据块对应的 BloomFilter 是否包含这个值,不包含则跳过对应的数据块不读取,达到减少I/O 查询加速的目的。

2)使用场景

BloomFilter 索引能够对等值查询(包括 =IN)加速,对高基数字段效果较好,比如 userid 等唯一 ID 字段。

3)使用限制

  • IN=之外的查询没有效果,比如 !=, NOT INT, >, <
  • 不支持对 TinyintFloatDouble 类型的列建 BloomFilter 索引。
  • 对低基数字段的加速效果很有限,比如“性别”字段仅有两种值,几乎每个数据块都会包含所有取值,导致 BloomFilter 索引失去意义。

4)管理索引

a、建表时创建 BloomFilter 索引

由于历史原因,BloomFilter 索引定义的语法与倒排索引等通用 INDEX 语法不一样。BloomFilter 索引通过表的 PROPERTIESbloom_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 分词是将一句话或一段文字拆分成多个相邻的词组的分词方法。NGram BloomFilter 索引和 BloomFilter 索引类似,也是基于 BloomFilter 的跳数索引。

BloomFilter 索引不同的是,NGram BloomFilter 索引用于加速文本 LIKE 查询,它存入 BloomFilter 的不是原始文本的值,而是对文本进行 NGram 分词,每个词作为值存入 BloomFilter。对于 LIKE 查询,将 LIKE%pattern%’ 的 pattern 也进行 NGram 分词,判断每个词是否在 BloomFilter 中,如果某个词不在则对应的数据块就不满足 LIKE 条件,可以跳过这部分数据减少 IO 加速查询。

2)使用场景

NGram BloomFilter 索引只能加速字符串 LIKE 查询,而且 LIKE pattern 中的连续字符个数要大于等于索引定义的NGram 中的 N

3)使用限制

  • NGram BloomFilter 只支持字符串列,只能加速 LIKE 查询。
  • NGram BloomFilter 索引和 BloomFilter 索引为互斥关系,即同一个列只能设置两者中的一个。
  • NGram BloomFilter 索引的效果分析,跟 BloomFilter 索引类似。

  • 亿级别以上数据,如果有模糊匹配,使用倒排索引或者是 NGram Bloomfilter

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)使用示例

使用 NGram BloomFilter 索引需设置如下参数(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 支持两种类型的 Schema Change 操作:轻量级 Schema Change 和重量级 Schema Change。它们的区别主要体现在执行过程的复杂性、执行速度和资源消耗上。

特性 轻量级 Schema Change 重量级 Schema Change
执行速度 秒级(几乎实时) 分钟级、小时级、天级(依赖表的数据量,数据量越大,执行越慢)
是否需要数据重写 不需要 需要,涉及数据文件的重写
系统性能影响 影响较小 可能影响系统性能,尤其是在数据转换过程中
资源消耗 较低 较高,会占用计算资源重新组织数据,过程中涉及到的表的数据占用的存储空间翻倍。
操作类型 增加、删除 Value 列,修改列名,修改 VARCHAR 长度 修改列的数据类型、更改主键、修改列的顺序等

2、轻量级 Schema Change

轻量级 Schema Change 是指不涉及数据重写的简单模式更改操作。这些操作通常在元数据级别进行,仅需要修改表的元数据,而不涉及数据文件的物理修改。轻量级 Schema Change 操作通常能够在秒级别完成,不会对系统性能造成显著影响。轻量级 Schema Change 包括:

  • 增加或删除value
  • 更改列名
  • 修改 VARCHAR 列的长度(UNIQUEDUPKey 列除外)。

3、重量级 Schema Change

  • 数据变更:重量级 Schema Change 涉及到数据文件的重写或转换,这些操作相对复杂,通常需要借助 DorisBackendBE)进行数据的实际修改或重新组织。 重量级 Schema Change 操作通常涉及对表数据结构的深度变更,可能会影响到存储的物理布局。所有不支持轻量级 Schema Change 的操作,均属于重量级 Schema Change,比如:

    • 更改列的数据类型

    • 修改列的排序顺序

  • 数据转换:重量级操作会在后台启动一个任务进行数据转换。后台任务会对表的每个 tablet 进行转换,按 tablet 为单位,将原始数据重写到新的数据文件中。数据转换过程中,可能会出现数据”双写”现象,即在转换期间,新数据同时写入新 tablettablet 中。完成数据转换后,旧 tablet 会被删除,新 tablet 将取而代之。

4、作业管理

a、查看作业

用户可以通过 [SHOW ALTER TABLE COLUMN]命令查看 Schema Change 作业进度。可以查看当前正在执行或已经完成的Schema Change 作业。当一次 Schema Change 作业涉及到物化视图时,该命令会显示多行,每行对应一个物化视图。举例如下:

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、取消作业

在作业状态不为 FINISHEDCANCELLED 的情况下,可以通过以下命令取消 Schema Change 作业:

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
  • 非聚合模型(如 DUPLICATE KEY)如果增加 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)删除列

  • 不能删除分区列
  • 不能删除 UNIQUEKEY 列。
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)中,与物理路径一一对应。修改分区列会打破这种对应关系,可能导致:
      • 查询时无法找到数据(路径不匹配);
      • 元数据与实际数据不一致,引发数据丢失或查询错误。
  • 一张表在同一时间只能有一个 Schema Change 作业在运行。
  • 当修改列类型时,除 Type 以外的字段都需要按原列上的信息补全。
  • 注意,除新的列类型外,如聚合方式,Nullable 属性,以及默认值都要按照原信息补全。
  • 不支持修改聚合类型、Nullable 属性和默认值。
  • 聚合表
    • 如果聚合表中有 REPLACE 方式聚合的 Value 列,则不允许删除 Key 列。原因:基于Key 列确定唯一记录,当新数据插入时,若 Key 存在则用新 Value 替换旧值
    • 在新增聚合类型为 SUM 或者 REPLACEValue 列时,该列的默认值对历史数据没有含义。因为历史数据已经失去明细信息,所以默认值的取值并不能实际反映聚合后的取值。
  • Unique 表:不允许删除 Key 列。

五、自增列

Doris 中,自增列(Auto Increment Column)是一种自动生成唯一数字值的功能,常用于为每一行数据生成唯一的标识符,如主键。每当插入新记录时,自增列会自动分配一个递增的值,避免了手动指定数字的繁琐操作。使用 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、使用限制

  1. Duplicate 模型表和 Unique 模型表可以包含自增列。
  2. 一张表最多只能包含一个自增列。
  3. 自增列的类型必须是 BIGINT 类型,且必须为 NOT NULL
  4. 自增列手动指定的起始值必须大于等于 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
  • 更新规则
    1. 若表中存在对应 id 的行,则仅更新指定列(如name),其他列保持不变。
    2. 若表中无对应 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 列时:

  1. 用户指定的非 null 值(如 1000、500)直接更新。
  2. 未指定的列(如namescore)保持原值。

验证:表结构

  • 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) 版本号、流水号等非唯一标识
  1. 自增列作为 key
    • 插入新行时,自增列值需保证唯一性(Doris 不自动生成,需用户自行维护)。
    • 部分列更新时必须包含 key 列,否则会报错。
  2. 自增列作为非 key
    • 若希望自增列自动生成值,需在插入时不指定该列(或指定 null)。
    • 手动指定大值可能导致后续自动生成的数值不连续(如插入 aid=1000 后,下一个自动值为 1001)。
  3. 参数配置
    • 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、SSDHDD 层级存储

Doris 支持在不同磁盘类型(SSDHDD)之间进行分层存储,结合动态分区功能,根据冷热数据的特性将数据从 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 指定了不同的 Storage PolicyPartition 设置的 Storage policy 会被无视,整张表的所有 Partition 都会使用 tablePolicy.

  • 如果您需要让某个 PartitionPolicy 和别的不同,则可以使用上文中对一个已存在的 Partition,关联 Storage policy 的方式修改。

1)对一个已存在的表,设置远程存储,将创建好的 STORAGE POLICY 与表关联:

ALTER TABLE create_table_not_have_policy set ("storage_policy" = "test_policy");

2)对一个已存在的 PARTITION,设置远程存储,将创建好的 STORAGE POLICYPARTITON 关联:

ALTER TABLE create_table_partition MODIFY PARTITION (*) SET("storage_policy"="test_policy");

2)使用限制

  • 使用了远程存储的表不支持备份。
  • 不支持修改远程存储的位置信息,比如 endpointbucketpath
  • Unique 模型表在开启 Merge-on-Write 特性时,不支持设置远程存储。
    • 性能瓶颈:网络延迟与合并效率冲突
    • 数据一致性与事务保障困难
  • Storage policy 支持创建、修改和删除,删除前需要先保证没有表引用此 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 的大小,也就是说读一行也需要产生一个 pageIO

  • 这个值越大压缩效果越好存储空间占用越低,但是点查时 IO 开销越大性能越低(因为一次 IO 至少读一个 page),反过来值越小存储空间极高,点查性能越好。

  • 默认值 16KB 是大多数情况下比较均衡的选择,如果更偏向查询性能可以配置较小的值比如 4KB 甚至更低,如果更偏向存储空间可以配置较大的值比如 64KB 甚至更高。

维度 16KB 的优势
压缩效率 比 4KB 提升约 30%,接近 64KB 压缩率的 80%
IO 性能 64KB 减少 75% 的无效数据读取
适用场景 兼顾点查(10ms 级响应)与存储成本(压缩率适中)
缓存利用率 多数服务器内存页大小为 16KB / 32KB,16KB 更易命中缓存

3、使用限制

  • 存储空间放大:开启行存后占用的存储空间会增加,存储空间的增加和数据特点有关,一般是原来表的 210 倍,具体空间占用需要使用实际数据测试。

  • 参数配置:行存的 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 数据量过小,则数据的聚合效果不佳,且元数据管理压力大。

  • 数据量过大,不利于副本的迁移、补齐,且会增加 Schema Change 或者 物化 操作失败重试的代价

4、分区配置:5 亿以上的数据必须设置 分区分桶策略

  • 分区配置:
    • 没有办法分区的,数据又较快增长的,没办法按照时间动态分区,可以适当放大一下你的 Bucket 数量按照你的数据保存周期数据总量,来估算你的 Bucket 数量应该是多少,建议还是单个 Bucket 大小在 1-10G。

5、分桶配置:对分桶字段进行加盐处理,业务上查询的时候也是要同样的加盐策略,这样能利用到分桶数据剪裁能力。

  • bucket 设置建议:
    • 大表的单个 Tablet 存储数据大小在 1G - 10G 区间,可防止过多的小文件产生。
    • 百兆左右的维表 Tablet 数量控制在 3 - 5 个,保证一定的并发数也不会产生过多的小文件。
  • 数据随机分桶:
  • 避免数据倾斜:如果 OLAP 表没有更新类型的字段,将表的数据分桶模式设置为 RANDOM,则可以避免严重的数据倾斜 (数据在导入表对应的分区的时候,单次导入作业每个 Batch 的数据将随机选择一个 Tablet 进行写入)
    • 适合聚合查询-而非高并发:当表的分桶模式被设置为 RANDOM 时,因为没有分桶列,无法根据分桶列的值仅对几个分桶查询,对表进行查询的时候将对命中分区的全部分桶同时扫描,该设置适合对表数据整体的聚合查询分析而不适合高并发的点查询
  • 如果 OLAP 表的是 Random Distribution 的数据分布,那么在数据导入的时候可以设置单分片导入模式(将 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 列和分区分桶列。

    • 适用于一些比较大的文本存储,一般如果没有这种需求的话,建议使用 VARCHARSTRING 列无法用在 Key 列和分桶列,局限性比较大。

  • 数值型字段:按照精度选择对应的数据类型即可,没有过于特殊的注意。

  • 时间字段:这里需要注意的是,如果有高精度(毫秒值时间戳)需求,需要指明使用 datetime(6),否则默认是不支持毫秒值时间戳的。

  • 建议使用 JSON 数据类型代替字符串类型存放 JSON 数据的使用方式。

ContactAuthor