# 实时数据查询优化实战

0 阅读11分钟

大数据开发高阶技能:ClickHouse/Doris/StarRocks 选型、实时查询优化、索引设计、物化视图、分区分桶、生产环境案例,打造秒级查询体验


📌 前言

真实生产问题

问题场景:

某电商公司数据平台遇到的问题:

问题 1:实时报表太慢
- 运营打开实时大屏,加载要 30 秒
- 刷新一次,等半天
- 业务投诉:还不如看 Excel

问题 2:并发一高就挂
- 平时查询 3 秒,还能接受
- 大促期间,10 个人同时查,直接超时
- 错误:Too many connections / Timeout

问题 3:数据量一大就慢
- 1000 万数据:3 秒
- 1 亿数据:30 秒
- 10 亿数据:直接超时

问题 4:复杂查询跑不动
- 单表查询还行
- 多表 Join,直接卡死
- 聚合查询,内存溢出

实时查询优化解决:

- 选型合适:ClickHouse/Doris/StarRocks
- 表设计:分区/分桶/索引
- 查询优化:物化视图/预聚合
- 资源隔离:读写分离/队列控制

优化后效果:

- 查询速度:30 秒 → 0.3 秒(100 倍)
- 并发能力:10 → 200(20 倍)
- 数据规模:1 亿 → 100 亿(100 倍)
- 复杂查询:从超时到 2 秒

🔧 实时 OLAP 引擎选型

主流引擎对比

特性ClickHouseDorisStarRocks
开源时间201620182020
公司Yandex百度星环科技
查询速度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
并发能力⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Join 能力⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
运维复杂度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
社区活跃度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
国内支持⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

选型建议

选择 ClickHouse 的场景:

✓ 单表查询为主(宽表)
✓ 数据量超大(10 亿+)
✓ 对查询速度要求极高
✓ 并发不高(< 50 QPS)
✓ 团队技术能力强

典型案例:
- 用户行为分析
- 日志分析
- 监控指标查询

选择 Doris 的场景:

✓ 需要高并发(> 100 QPS)
✓ 多表 Join 较多
✓ 需要实时更新
✓ 运维能力一般
✓ 国内团队(中文支持好)

典型案例:
- 实时报表
- 用户画像
- 广告分析

选择 StarRocks 的场景:

✓ 复杂查询(多表 Join)
✓ 对查询速度要求高
✓ 需要高并发
✓ 需要物化视图
✓ 预算充足(企业版功能强)

典型案例:
- 实时数仓
- 自助分析
- 数据服务 API

🔧 表设计优化

分区设计(Partition)

原则:

1. 按时间分区(最常用)
   按天/周/月分区,便于 TTL 和历史数据管理

2. 分区粒度适中
   - 太细:分区太多,管理困难
   - 太粗:分区裁剪效果差

3. 分区数量控制
   - 单表分区数:100-1000 个
   - 单分区数据量:1GB-10GB

ClickHouse 示例:

-- 按天分区
CREATE TABLE orders_realtime (
    order_id BIGINT,
    user_id BIGINT,
    amount DECIMAL(18,2),
    create_time DateTime,
    dt String  -- 分区字段
) ENGINE = MergeTree()
PARTITION BY dt  -- 按天分区
ORDER BY (create_time, order_id)
TTL create_time + INTERVAL 90 DAY;  -- 90 天后自动删除

-- 插入数据
INSERT INTO orders_realtime
SELECT 
    order_id,
    user_id,
    amount,
    create_time,
    toYYYYMMDD(create_time) as dt
FROM orders_source;

Doris 示例:

-- 按范围分区(时间)
CREATE TABLE orders_realtime (
    order_id BIGINT,
    user_id BIGINT,
    amount DECIMAL(18,2),
    create_time DATETIME
) ENGINE=OLAP
UNIQUE KEY(order_id, create_time)
PARTITION BY RANGE(create_time) (
    FROM ('2026-01-01') TO ('2026-04-01') INTERVAL 1 DAY
)
DISTRIBUTED BY HASH(order_id) BUCKETS 32
PROPERTIES (
    "replication_num" = "3",
    "storage_medium" = "SSD"
);

分桶设计(Bucket)

原则:

1. 选择高基数列
   用户 ID、订单 ID 等,避免数据倾斜

2. 分桶数量适中
   - 太少:并行度不够
   - 太多:小文件太多

3. 计算公式
   分桶数 = 数据量 / 单桶大小(1-10GB)
   
   示例:
   - 100GB 数据:10-100 个桶
   - 1TB 数据:100-1000 个桶

ClickHouse 示例:

-- 按用户 ID 分桶(通过 ORDER BY 实现)
CREATE TABLE user_behavior (
    event_id BIGINT,
    user_id BIGINT,
    event_type String,
    event_time DateTime
) ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(event_time)
ORDER BY (user_id, event_time);  -- 先按 user_id 排序

-- 查询时利用分桶
SELECT user_id, count()
FROM user_behavior
WHERE user_id = 12345  -- 快速定位
GROUP BY user_id;

Doris 示例:

-- 哈希分桶
CREATE TABLE orders (
    order_id BIGINT,
    user_id BIGINT,
    amount DECIMAL(18,2)
) ENGINE=OLAP
UNIQUE KEY(order_id)
PARTITION BY RANGE(create_time) (...)
DISTRIBUTED BY HASH(order_id) BUCKETS 64;  -- 64 个桶

-- 随机分桶(均匀分布)
DISTRIBUTED BY RANDOM BUCKETS 64;

索引设计

ClickHouse 索引:

-- 主键索引(ORDER BY 字段)
CREATE TABLE orders (
    order_id BIGINT,
    user_id BIGINT,
    create_time DateTime
) ENGINE = MergeTree()
ORDER BY (create_time, order_id);  -- 主键索引

-- 跳数索引(MinMax)
CREATE TABLE orders (
    ...
) ENGINE = MergeTree()
ORDER BY create_time
SETTINGS index_granularity = 8192;  -- 跳数索引粒度

-- 自定义索引
CREATE TABLE orders (
    order_id BIGINT,
    user_id BIGINT,
    amount DECIMAL(18,2),
    INDEX idx_amount (amount) TYPE minmax GRANULARITY 1024
) ENGINE = MergeTree()
ORDER BY create_time;

-- 使用索引
SELECT * FROM orders
WHERE amount > 1000;  -- 使用 idx_amount 索引

Doris 索引:

-- 前缀索引(自动创建)
CREATE TABLE orders (
    order_id BIGINT,
    user_id BIGINT,
    create_time DATETIME,
    INDEX idx_user (user_id) USING INVERTED
) ENGINE=OLAP
UNIQUE KEY(order_id)
...;

-- 倒排索引(全文检索)
CREATE INDEX idx_product_name ON orders(product_name)
USING INVERTED;

-- 位图索引(用户画像)
CREATE INDEX idx_user_tags ON orders(user_tags)
USING BITMAP;

🔧 查询优化技巧

物化视图(预聚合)

ClickHouse 物化视图:

-- 创建物化视图
CREATE MATERIALIZED VIEW mv_user_daily_gmv
ENGINE = SummingMergeTree()
PARTITION BY dt
ORDER BY (user_id, dt)
AS SELECT
    user_id,
    toYYYYMMDD(create_time) as dt,
    SUM(amount) as gmv,
    COUNT(1) as order_count
FROM orders_realtime
GROUP BY user_id, dt;

-- 查询物化视图(自动命中)
SELECT user_id, SUM(gmv) as total_gmv
FROM orders_realtime
WHERE dt >= '20260301'
GROUP BY user_id;

-- 手动查询物化视图
SELECT user_id, SUM(gmv) as total_gmv
FROM mv_user_daily_gmv
WHERE dt >= '20260301'
GROUP BY user_id;

Doris 物化视图:

-- 创建物化视图
CREATE MATERIALIZED VIEW mv_user_gmv
AS SELECT
    user_id,
    DATE(create_time) as stat_date,
    SUM(amount) as gmv,
    COUNT(1) as order_count
FROM orders_realtime
GROUP BY user_id, DATE(create_time);

-- 查询(自动命中)
SELECT user_id, SUM(gmv)
FROM orders_realtime
WHERE create_time >= '2026-03-01'
GROUP BY user_id;

-- 查看物化视图状态
SHOW MATERIALIZED VIEW FROM orders_realtime;

预计算(Rollup)

Doris Rollup:

-- 创建 Rollup(多版本聚合)
ALTER TABLE orders_realtime
ADD ROLLUP rollup_user_daily (
    user_id,
    DATE(create_time),
    SUM(amount),
    COUNT(1)
);

-- 创建 Rollup(多维度)
ALTER TABLE orders_realtime
ADD ROLLUP rollup_category_daily (
    category_id,
    DATE(create_time),
    SUM(amount),
    COUNT(1)
);

-- 查询自动选择最优 Rollup
SELECT 
    user_id,
    DATE(create_time),
    SUM(amount)
FROM orders_realtime
GROUP BY user_id, DATE(create_time);

Join 优化

大表 Join 小表(广播):

-- ClickHouse
SELECT 
    o.order_id,
    u.user_name,
    o.amount
FROM orders_realtime AS o
INNER JOIN users AS u
ON o.user_id = u.user_id
SETTINGS join_algorithm = 'auto';  -- 自动选择算法

-- Doris Hint
SELECT /*+ BROADCAST(u) */
    o.order_id,
    u.user_name,
    o.amount
FROM orders_realtime AS o
JOIN users AS u
ON o.user_id = u.user_id;

大表 Join 大表(分桶):

-- 确保 Join Key 分桶一致
-- orders 表
DISTRIBUTED BY HASH(user_id) BUCKETS 64

-- users 表
DISTRIBUTED BY HASH(user_id) BUCKETS 64

-- Join 时数据无需 Shuffle
SELECT o.order_id, u.user_name
FROM orders o
JOIN users u ON o.user_id = u.user_id;

⚙️ 参数调优

ClickHouse 参数

<!-- config.xml -->

<!-- 内存限制 -->
<max_memory_usage>16000000000</max_memory_usage>  <!-- 16GB -->
<max_memory_usage_for_all_queries>32000000000</max_memory_usage_for_all_queries>

<!-- 并发控制 -->
<max_concurrent_queries>100</max_concurrent_queries>
<max_threads>16</max_threads>

<!-- 查询超时 -->
<max_execution_time>30</max_execution_time>

<!-- 合并优化 -->
<merge_tree>
    <max_parts_in_total>50000</max_parts_in_total>
    <max_parts_to_merge_at_once>100</max_parts_to_merge_at_once>
</merge_tree>

Doris 参数

-- 会话级别
SET exec_mem_limit = 4G;  -- 单查询内存限制
SET parallel_fragment_exec_instance_num = 16;  -- 并行度
SET enable_profile = true;  -- 开启性能分析

-- 全局级别
ADMIN SET FRONTEND CONFIG ("max_query_timeout" = "300000");  -- 5 分钟
ADMIN SET BACKEND CONFIG ("disable_storage_page_cache" = "false");

🏭 生产环境完整案例

案例:实时大屏优化(30 秒 → 0.3 秒)

背景:

某电商公司实时大屏:
- 展示实时 GMV、订单量、用户数
- 数据量:1 亿条/天
- 查询延迟:30 秒
- 并发:20 人同时访问

问题分析:

-- 原始查询(慢)
SELECT 
    DATE_FORMAT(create_time, '%H:%i') as time_slot,
    SUM(amount) as gmv,
    COUNT(1) as order_count
FROM orders_realtime
WHERE create_time >= NOW() - INTERVAL 2 HOUR
GROUP BY DATE_FORMAT(create_time, '%H:%i')
ORDER BY time_slot;

-- 问题:
-- 1. 全表扫描(2 小时数据约 800 万条)
-- 2. 实时计算聚合(CPU 密集)
-- 3. 无索引可用

优化方案:

-- 方案 1:物化视图(预聚合)
CREATE MATERIALIZED VIEW mv_realtime_gmv
ENGINE = SummingMergeTree()
ORDER BY time_slot
AS SELECT
    toStartOfMinute(create_time) as time_slot,
    SUM(amount) as gmv,
    COUNT(1) as order_count
FROM orders_realtime
GROUP BY time_slot;

-- 优化后查询(自动命中物化视图)
SELECT 
    DATE_FORMAT(time_slot, '%H:%i') as time_slot,
    SUM(gmv) as gmv,
    SUM(order_count) as order_count
FROM mv_realtime_gmv
WHERE time_slot >= now() - INTERVAL 2 HOUR
GROUP BY time_slot;

-- 优化效果:
-- 扫描数据:800 万 → 120 条(2 小时×60 分钟)
-- 查询时间:30 秒 → 0.3 秒
-- 方案 2:滚动窗口(Flink 实时聚合)
-- Flink SQL
CREATE VIEW realtime_gmv AS
SELECT
    TUMBLE_START(create_time, INTERVAL '1' MINUTE) as time_slot,
    SUM(amount) as gmv,
    COUNT(1) as order_count
FROM orders_realtime
GROUP BY TUMBLE(create_time, INTERVAL '1' MINUTE);

-- 输出到 ClickHouse
INSERT INTO mv_realtime_gmv
SELECT time_slot, gmv, order_count
FROM realtime_gmv;

-- 查询直接查聚合结果
SELECT time_slot, gmv, order_count
FROM mv_realtime_gmv
WHERE time_slot >= now() - INTERVAL 2 HOUR;

-- 优化效果:
-- 查询时间:30 秒 → 0.1 秒

案例:用户画像查询优化(10 秒 → 0.5 秒)

背景:

用户画像标签查询:
- 标签数量:500 个
- 用户数量:1 亿
- 查询:圈选符合条件的人群
- 原始查询:10 秒

优化方案:

-- 使用 Bitmap 索引(Doris)
CREATE TABLE user_tags (
    user_id BIGINT,
    tag_id INT,
    tag_value STRING
) ENGINE=OLAP
UNIQUE KEY(user_id, tag_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 128
PROPERTIES (
    "bitmap_index" = "true"
);

-- 创建 Bitmap 索引
CREATE INDEX idx_tag_value ON user_tags(tag_value)
USING BITMAP;

-- 人群圈选查询
SELECT user_id
FROM user_tags
WHERE tag_value IN ('北京', '上海', '广州')  -- 地区
  AND tag_value IN ('高消费', '高价值')      -- 消费水平
GROUP BY user_id
HAVING COUNT(DISTINCT tag_value) = 5;  -- 同时满足 5 个标签

-- 优化效果:
-- 扫描数据:1 亿 → 使用 Bitmap 直接计算
-- 查询时间:10 秒 → 0.5 秒

案例:高并发查询优化(10 → 200 QPS)

背景:

数据服务 API:
- 提供用户订单查询接口
- 并发:10 QPS(峰值超时)
- 目标:100+ QPS

优化方案:

-- 方案 1:读写分离
-- 主集群:写入 + 复杂查询
-- 从集群:简单查询(API)

-- 方案 2:查询缓存
-- Redis 缓存热点数据
SET enable_query_cache = true;
SET query_cache_ttl = 60;  -- 缓存 60 秒

-- 方案 3:连接池
-- 使用连接池(如 HikariCP)
-- 避免频繁创建连接

-- 方案 4:资源隔离
-- 小查询队列(API)
-- 大查询队列(分析)
CREATE USER api_user SETTINGS max_execution_time = 5;
CREATE USER analyst_user SETTINGS max_execution_time = 300;

📋 最佳实践清单

表设计

  • 合理分区(按时间)
  • 合理分桶(高基数列)
  • 选择合适引擎(MergeTree/OLAP)
  • 设置 TTL(自动清理)

索引优化

  • 主键索引(ORDER BY)
  • 跳数索引(MinMax)
  • 倒排索引(全文检索)
  • Bitmap 索引(用户画像)

查询优化

  • 使用物化视图
  • 使用预聚合(Rollup)
  • Join 优化(广播/分桶)
  • 开启查询缓存

资源管理

  • 读写分离
  • 资源隔离(队列)
  • 连接池
  • 监控告警

📌 总结

核心要点

优化方向技术性能提升
表设计分区/分桶10-100x
索引Bitmap/倒排10-50x
预聚合物化视图/Rollup50-500x
Join 优化广播/分桶5-20x
资源管理隔离/缓存10-20x

实践原则

1. 设计先行
   表设计决定性能上限

2. 预聚合优先
   能预计算的不实时算

3. 索引得当
   合适的索引事半功倍

4. 监控持续
   持续优化,没有终点

💡 实时查询优化是系统工程,建议从表设计开始就做好规划!


👋 感谢阅读!


🔗 系列文章

  • [01-SQL 窗口函数从入门到精通](./01-SQL 窗口函数从入门到精通.md)
  • [02-Spark 性能优化 10 个技巧](./02-Spark 性能优化 10 个技巧.md)
  • 03-数据仓库分层设计指南
  • 04-维度建模实战
  • [05-Flink 实时数仓实战](./05-Flink 实时数仓实战.md)
  • [06-Kafka 消息队列实战指南](./06-Kafka 消息队列实战指南.md)
  • [07-Hive 性能优化实战](./07-Hive 性能优化实战.md)
  • [08-Linux 大数据开发必备工具](./08-Linux 大数据开发必备工具.md)
  • [09-缓慢变化维 SCD Type 2 详解](./09-缓慢变化维 SCD Type2 详解.md)
  • [10-Flink 时间语义与 Watermark 详解](./10-Flink 时间语义与 Watermark 详解.md)
  • [11-Hadoop 集群搭建完整教程](./11-Hadoop 集群搭建完整教程.md)
  • [12-Spark SQL 进阶实践](./12-Spark SQL 进阶实践.md)
  • 13-数据血缘与元数据管理
  • 14-指标体系设计与产品手册
  • 15-实时数据查询优化实战(本文)

🎉 系列完成!

16 篇大数据技术博客全部完成!

类别篇数文章
SQL 基础2 篇01 窗口函数、12 Spark SQL
数仓设计3 篇03 分层设计、04 维度建模、09 SCD
实时计算3 篇05 Flink 数仓、10 Watermark、15 查询优化
离线计算3 篇02 Spark 优化、07 Hive 调优、11 Hadoop
数据集成2 篇06 Kafka、08 Linux 工具
数据治理2 篇13 元数据、14 指标体系
面试题库1 篇00 面试题库

总字数:约 10 万字

下一步:

  • 发布到 CSDN/知乎/掘金
  • 收集读者反馈
  • 持续更新迭代

祝博客大受欢迎!🚀