Doris 兼容 MySQL 协议,大多数 MySQL SQL 语法可以直接用。但 Doris 是 OLAP 数据库,和 MySQL 的执行模型完全不同——同一个查询,按 OLTP 的思路写和按 OLAP 的思路写,性能可能差 100 倍以上。
这篇文章从连接方式开始,逐步讲清楚 Doris 查询里最重要的几个概念:分区裁剪、聚合模型、去重函数选择、窗口函数、Join 优化,以及如何用 EXPLAIN 读懂查询计划。
一、连接方式
Doris 的 FE(Frontend)节点对外提供 MySQL 协议,默认端口 9030:
-- MySQL 客户端直接连
mysql -h fe_host -P 9030 -u root -p
-- 查看所有数据库
SHOW DATABASES;
-- 切换数据库
USE your_database;
-- 查看表结构
DESCRIBE table_name;
-- 或
SHOW CREATE TABLE table_name;
JDBC 连接串(Java 程序里):
jdbc:mysql://fe_host:9030/database?useUnicode=true&characterEncoding=utf8
Doris 还提供 HTTP API,用于流式导入或查询元数据,但日常 SQL 查询还是走 MySQL 协议更方便。
二、基础查询语法
基本 SELECT 语法和 MySQL 一致:
-- 基础查询
SELECT user_id, order_amount, create_time
FROM orders
WHERE dt = '2024-01-15'
AND order_status = 1
ORDER BY create_time DESC
LIMIT 100;
-- 别名
SELECT
user_id AS uid,
sum(order_amount) AS total_amount,
count(*) AS order_cnt,
count(distinct user_id) AS uv
FROM orders
WHERE dt = '2024-01-15'
GROUP BY user_id
HAVING total_amount > 1000;
Doris 支持 CTE(WITH 子句),复杂查询里推荐用 CTE 提升可读性:
WITH daily_stats AS (
SELECT
dt,
sum(gmv) AS total_gmv,
count(*) AS order_cnt
FROM dws_order
WHERE dt BETWEEN '2024-01-01' AND '2024-01-31'
GROUP BY dt
),
ranked AS (
SELECT
dt,
total_gmv,
order_cnt,
rank() OVER (ORDER BY total_gmv DESC) AS gmv_rank
FROM daily_stats
)
SELECT * FROM ranked WHERE gmv_rank <= 10;
三、分区裁剪:查询快慢的核心
Doris 中大多数表按时间分区(PARTITION BY RANGE),分区裁剪是性能差距最大的地方。查询时必须带上分区列的过滤条件,否则 Doris 会扫描所有分区,性能直接崩。
-- 好:明确指定分区列范围,只扫描 1 月的数据
SELECT sum(gmv)
FROM dws_order
WHERE dt BETWEEN '2024-01-01' AND '2024-01-31';
-- 坏:没有分区过滤,全表扫描
SELECT sum(gmv)
FROM dws_order
WHERE order_status = 1;
-- 查看表的分区信息
SHOW PARTITIONS FROM dws_order;
分区裁剪是否生效,可以用 EXPLAIN 确认(后面会详细讲)。裁剪生效时,EXPLAIN 里会显示 partitions=1/365 这类信息,说明只扫描了 1 个分区而不是全部 365 个。
分区列过滤的几个坑:
-- 坏:对分区列做函数运算,裁剪失效
SELECT * FROM orders WHERE date_format(dt, '%Y-%m') = '2024-01';
-- 好:改写为范围条件
SELECT * FROM orders WHERE dt >= '2024-01-01' AND dt < '2024-02-01';
-- 坏:子查询里的分区条件不一定能下推
SELECT * FROM orders
WHERE dt IN (SELECT max(dt) FROM orders);
-- 好:先算出具体值再过滤
SELECT * FROM orders WHERE dt = '2024-01-31';
除了分区裁剪,Doris 还支持按分桶列(BUCKET)的过滤,能进一步减少扫描的 Tablet 数量:
-- 如果表按 user_id 分桶,这个查询只扫描一个 Bucket
SELECT * FROM user_orders
WHERE dt = '2024-01-15' AND user_id = 12345;
四、聚合查询与聚合模型
Doris 有三种数据模型:Duplicate(明细)、Aggregate(聚合)、Unique(唯一)。Aggregate 模型的表在写入时就会做预聚合,查询时无需全量扫描。
标准聚合函数
SELECT
dt,
city,
channel,
sum(gmv) AS total_gmv,
sum(order_cnt) AS total_orders,
avg(order_amount) AS avg_amount,
max(order_amount) AS max_amount,
min(order_amount) AS min_amount,
count(*) AS row_cnt,
count(DISTINCT user_id) AS uv -- 精确去重,数据量大时慢
FROM dws_order_detail
WHERE dt = '2024-01-15'
GROUP BY dt, city, channel
ORDER BY total_gmv DESC;
GROUPING SETS / ROLLUP / CUBE
需要同时计算多个维度组合的汇总时,GROUPING SETS 比写多个 UNION 效率高(数据只扫一遍):
-- 同时计算 (city, channel)、(city) 和总计 三个粒度的聚合
SELECT
city,
channel,
sum(gmv) AS total_gmv
FROM dws_order
WHERE dt = '2024-01-15'
GROUP BY GROUPING SETS (
(city, channel),
(city),
()
);
-- ROLLUP:自动生成层级聚合(等价于上面从细到粗的维度组合)
SELECT city, channel, sum(gmv)
FROM dws_order
WHERE dt = '2024-01-15'
GROUP BY ROLLUP(city, channel);
五、去重查询:精确 vs 近似
UV(独立访客)计算是 OLAP 系统里最消耗资源的操作之一。Doris 提供了三种方案,性能和精度各有取舍。
方案一:COUNT DISTINCT(精确,慢)
-- 精确,但数据量大时很慢(需要全量去重)
SELECT dt, count(DISTINCT user_id) AS uv
FROM dws_page_view
WHERE dt = '2024-01-15'
GROUP BY dt;
方案二:Bitmap(精确,快)
适合 user_id 是整数类型的场景。表中提前存储 Bitmap 类型的列,查询时用 bitmap_union_count:
-- 前提:表里有 user_id_bitmap 列,类型为 BITMAP
-- 建表示例(Aggregate 模型):
-- user_id_bitmap BITMAP BITMAP_UNION
-- 查询:速度比 count distinct 快几倍到几十倍
SELECT
dt,
bitmap_union_count(user_id_bitmap) AS uv
FROM ads_uv_daily
WHERE dt = '2024-01-15'
GROUP BY dt;
-- 计算留存:昨天和今天都访问过的用户数
SELECT
bitmap_and_count(
today.user_id_bitmap,
yesterday.user_id_bitmap
) AS retain_uv
FROM ads_uv_daily today
JOIN ads_uv_daily yesterday
ON today.dt = '2024-01-15'
AND yesterday.dt = '2024-01-14';
-- 从明细数据实时聚合成 Bitmap(用于 DWS 层查询)
SELECT
dt,
bitmap_union(to_bitmap(user_id)) AS user_bitmap
FROM dws_page_view
WHERE dt = '2024-01-15'
GROUP BY dt;
方案三:HLL(近似,最快)
误差约 1%,数据量极大时(十亿级别)用这个:
-- 前提:表里有 user_id_hll 列,类型为 HLL
-- 建表示例(Aggregate 模型):
-- user_id_hll HLL HLL_UNION
SELECT
dt,
hll_union_agg(user_id_hll) AS approx_uv
FROM ads_uv_hll_daily
WHERE dt = '2024-01-15'
GROUP BY dt;
-- 从明细构建 HLL(动态计算,不需要预建列)
SELECT
dt,
hll_union_agg(hll_hash(user_id)) AS approx_uv
FROM dws_page_view
WHERE dt = '2024-01-15'
GROUP BY dt;
-- 或者用内置函数(更简洁)
SELECT
dt,
approx_count_distinct(user_id) AS approx_uv
FROM dws_page_view
WHERE dt = '2024-01-15'
GROUP BY dt;
选择策略:
- UV 需要精确 + 数据量 < 亿级 →
count(distinct) - UV 需要精确 + 数据量大 + user_id 是整数 → Bitmap
- UV 允许约 1% 误差 + 数据量极大 → HLL /
approx_count_distinct
六、窗口函数
窗口函数是 Doris 查询里非常常用的特性,适合计算排名、累计值、环比/同比等。
排名函数
SELECT
dt,
city,
gmv,
-- rank: 并列名次会跳号(1,1,3)
rank() OVER (PARTITION BY dt ORDER BY gmv DESC) AS rnk,
-- dense_rank: 并列名次不跳号(1,1,2)
dense_rank() OVER (PARTITION BY dt ORDER BY gmv DESC) AS dense_rnk,
-- row_number: 唯一行号,并列时按内部顺序打号
row_number() OVER (PARTITION BY dt ORDER BY gmv DESC) AS row_num
FROM dws_city_gmv
WHERE dt = '2024-01-15';
-- 取每天 GMV 最高的前 3 个城市
SELECT * FROM (
SELECT
dt, city, gmv,
row_number() OVER (PARTITION BY dt ORDER BY gmv DESC) AS rn
FROM dws_city_gmv
WHERE dt BETWEEN '2024-01-01' AND '2024-01-31'
) t
WHERE rn <= 3;
聚合窗口(累计、移动平均)
SELECT
dt,
daily_gmv,
-- 累计求和(从第一行到当前行)
sum(daily_gmv) OVER (ORDER BY dt
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_gmv,
-- 近 7 天移动平均
avg(daily_gmv) OVER (ORDER BY dt
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS ma7_gmv
FROM dws_daily_gmv
WHERE dt BETWEEN '2024-01-01' AND '2024-01-31'
ORDER BY dt;
LAG / LEAD(取前后行的值,计算环比)
SELECT
dt,
daily_gmv,
-- 取前一天的 GMV
lag(daily_gmv, 1, 0) OVER (ORDER BY dt) AS prev_day_gmv,
-- 取前 7 天的 GMV(计算周同比)
lag(daily_gmv, 7, 0) OVER (ORDER BY dt) AS prev_week_gmv,
-- 环比增长率
round(
(daily_gmv - lag(daily_gmv, 1, 0) OVER (ORDER BY dt))
/ nullif(lag(daily_gmv, 1, 0) OVER (ORDER BY dt), 0) * 100,
2
) AS wow_rate
FROM dws_daily_gmv
WHERE dt BETWEEN '2024-01-01' AND '2024-01-31'
ORDER BY dt;
七、Join 查询与性能优化
Doris 支持多种 Join 类型,但 Join 在分布式系统里是代价最高的操作,写法影响很大。
基本 Join 语法
-- INNER JOIN
SELECT o.order_id, o.gmv, u.city
FROM dws_order o
INNER JOIN dim_user u ON o.user_id = u.user_id
WHERE o.dt = '2024-01-15';
-- LEFT JOIN(保留左表所有行)
SELECT o.order_id, o.gmv, coalesce(u.city, '未知') AS city
FROM dws_order o
LEFT JOIN dim_user u ON o.user_id = u.user_id
WHERE o.dt = '2024-01-15';
Broadcast Join vs Shuffle Join
Doris 会自动选择 Join 策略,但了解这两种策略有助于分析慢查询:
- Broadcast Join:小表广播到所有节点,大表不移动。适合一大一小的 Join(小表 < 几百 MB)
- Shuffle Join(Hash Join):两张表都按 Join 键哈希重新分布,适合两张大表的 Join,但网络开销大
-- 强制指定 Broadcast Join(当 Doris 选错时手动干预)
SELECT /*+ SET_VAR(broadcast_row_count_limit = 100000000) */
o.order_id, d.product_name
FROM dws_order o
JOIN [broadcast] dim_product d ON o.product_id = d.product_id
WHERE o.dt = '2024-01-15';
Join 的几个注意事项
-- 避免:在 Join 条件里对列做函数运算(会退化为笛卡尔积或全表扫描)
-- 坏
SELECT * FROM a JOIN b ON cast(a.user_id AS varchar) = b.user_id;
-- 好:提前做类型转换,或建表时统一类型
SELECT * FROM a JOIN b ON a.user_id = b.user_id;
-- 避免:没有 Join 条件的隐式笛卡尔积(会产生巨大的中间结果集)
-- 坏
SELECT * FROM a, b WHERE a.dt = b.dt; -- 如果 dt 不是 Join 键,会爆
-- 好:明确写 JOIN ... ON
SELECT * FROM a JOIN b ON a.user_id = b.user_id WHERE a.dt = '2024-01-15';
八、EXPLAIN:读懂查询计划
遇到慢查询,第一步是用 EXPLAIN 看查询计划:
EXPLAIN SELECT sum(gmv) FROM dws_order WHERE dt = '2024-01-15';
重点看这几项:
partitions=1/365:分区裁剪是否生效,这里表示只扫了 365 个分区中的 1 个cardinality:预估的行数,用于优化器决策HASH JOIN/BROADCAST:使用了哪种 Join 策略PREAGGREGATION: ON/OFF:预聚合是否命中(Aggregate 模型表)
-- 更详细的执行计划(包含物理执行信息)
EXPLAIN VERBOSE SELECT sum(gmv) FROM dws_order WHERE dt = '2024-01-15';
-- 查询实际执行统计(需要先执行查询)
-- 在 Doris 管理页面的 Query Profile 里查看,或通过 SQL 查询 FE 日志
如果 PREAGGREGATION: OFF,说明查询没有命中预聚合,性能会下降。常见原因是 SELECT 列表里包含了非 Key 列的非聚合函数,或者 WHERE 条件对 Value 列进行了过滤。
九、物化视图加速查询
Doris 支持物化视图(Materialized View),把常用的聚合结果预先计算好存储起来,查询时自动命中,无需修改 SQL:
-- 创建物化视图(按 dt + city 预聚合)
CREATE MATERIALIZED VIEW mv_order_city_daily AS
SELECT
dt,
city,
sum(gmv) AS total_gmv,
count(*) AS order_cnt,
bitmap_union(to_bitmap(user_id)) AS user_bitmap
FROM dws_order_detail
GROUP BY dt, city;
-- 查询自动命中物化视图(不需要修改 SQL)
SELECT dt, city, sum(gmv)
FROM dws_order_detail
WHERE dt = '2024-01-15'
GROUP BY dt, city;
-- 确认是否命中物化视图(EXPLAIN 里看 TABLE 是否变成了 mv_order_city_daily)
EXPLAIN SELECT dt, city, sum(gmv)
FROM dws_order_detail
WHERE dt = '2024-01-15'
GROUP BY dt, city;
物化视图命中需要满足几个条件:查询的聚合函数和物化视图里定义的一致、GROUP BY 的维度是物化视图维度的子集、没有对 Value 列的过滤条件。
十、常用函数速查
日期函数
-- 当前日期/时间
SELECT curdate(), now(), current_timestamp();
-- 日期格式化
SELECT date_format(create_time, '%Y-%m-%d') AS dt;
-- 日期加减
SELECT date_add(curdate(), INTERVAL -7 DAY); -- 7天前
SELECT date_sub(curdate(), INTERVAL 1 MONTH); -- 1个月前
-- 提取日期部分
SELECT year(dt), month(dt), day(dt), weekday(dt);
-- 日期差(天数)
SELECT datediff('2024-01-31', '2024-01-01'); -- 返回 30
字符串函数
SELECT
concat('hello', ' ', 'world'), -- 字符串拼接
concat_ws('-', '2024', '01', '15'), -- 用分隔符拼接
substring(phone, 1, 3), -- 子字符串
left(phone, 3), -- 左取
length(name), -- 字节长度
char_length(name), -- 字符长度(中文友好)
upper(name), lower(name), -- 大小写
trim(name), ltrim(name), rtrim(name),-- 去空格
replace(str, 'old', 'new'), -- 替换
like '%关键词%', -- 模糊匹配
regexp_extract(str, '\\d+', 0); -- 正则提取
条件函数
SELECT
if(score >= 60, '及格', '不及格'),
ifnull(city, '未知'),
nullif(amount, 0), -- 如果 amount=0 返回 NULL(防止除零)
coalesce(city, province, country, '未知'), -- 返回第一个非 NULL 值
case status
when 1 then '待支付'
when 2 then '已支付'
when 3 then '已取消'
else '未知'
end AS status_name,
case
when amount >= 1000 then '大额'
when amount >= 100 then '中额'
else '小额'
end AS amount_level;
十一、总结
Doris 查询性能的关键优先级:
- 分区裁剪:每个查询必须带分区列过滤,是最重要的优化,不容妥协
- 去重函数选择:大数据量的 UV 统计用 Bitmap 或 HLL,不要无脑
count(distinct) - 物化视图:高频查询预先建物化视图,查询自动命中,改造成本为零
- EXPLAIN 先行:遇到慢查询先看执行计划,确认分区裁剪和预聚合是否生效
- JOIN 小心:大表 Join 大表前先过滤,尽量让一侧数据量小到可以 Broadcast