未记录事实核验 · 状态:维护中
记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。
warning 适用范围与待维护事项
本文适用于已有SQL、Doris建表及执行计划分析经验的读者,示例不是可直接上线的脚本。原稿未提供Doris版本、表结构、分区定义和测试数据,JOIN提示、分区提示、异步物化视图DDL及内存参数行为需在目标版本核验。行为统计不验证漏斗事件顺序;留存以日志中首次出现为起点,不一定代表注册日。提高内存上限或创建中间表前,应评估集群资源、权限与清理方案,不能据此保证提速。
Doris SQL查询与分析 - 高效数据洞察
abstract 📋 本节概览
- 🎯 学习目标:掌握Doris SQL查询特性与高效分析技巧
- 📚 前置知识:09-数据导入方案详解|数据导入方案详解
- ⏱️ 预计用时:30分钟
🎯 实际应用价值
Doris SQL查询与分析可用于聚合统计、业务指标计算与查询性能排查;实际效率改善取决于数据规模、表设计、执行计划和资源配置。
在实际工作环境中,这样的挑战每天都在发生。理解相关知识不仅能解决当前问题,更能提升整体的专业能力。
💡 Doris SQL语法特性
Doris支持标准SQL语法,同时提供了许多针对OLAP场景优化的扩展功能。
基本查询语法
-- 基本查询结构
SELECT [ALL | DISTINCT] select_expr [, select_expr ...]
FROM table_reference [, table_reference ...]
[WHERE where_condition]
[GROUP BY {col_name | expr | position} [ASC | DESC], ... [WITH ROLLUP]]
[HAVING where_condition]
[ORDER BY {col_name | expr | position} [ASC | DESC], ...]
[LIMIT {[offset,] row_count | row_count OFFSET offset}]
Doris特有SQL扩展
-- 1. 多表JOIN优化语法
SELECT /*+ SHUFFLE_JOIN(t1, t2) */
t1.id, t1.name, t2.amount
FROM table1 t1
JOIN table2 t2 ON t1.id = t2.id;
-- 2. 分区裁剪提示
SELECT /*+ PARTITION(p1, p2) */
*
FROM partitioned_table;
-- 3. 广播JOIN提示
SELECT /*+ BROADCAST_JOIN(small_table) */
big_table.id, small_table.name
FROM big_table JOIN small_table ON big_table.id = small_table.id;
🔍 常用分析操作
1. 聚合查询
-- 基本聚合
SELECT
region,
COUNT(*) AS total_orders,
SUM(amount) AS total_amount,
AVG(amount) AS avg_amount,
MIN(amount) AS min_amount,
MAX(amount) AS max_amount
FROM orders
WHERE order_date BETWEEN '2023-01-01' AND '2023-12-31'
GROUP BY region
ORDER BY total_amount DESC;
-- 多维聚合
SELECT
region,
product_category,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY region, product_category
ORDER BY region, total_amount DESC;
2. 窗口函数
-- 排名函数
SELECT
user_id,
order_date,
amount,
ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY amount DESC) AS amount_rank,
RANK() OVER(PARTITION BY user_id ORDER BY amount DESC) AS amount_rank_with_gaps,
DENSE_RANK() OVER(PARTITION BY user_id ORDER BY amount DESC) AS amount_dense_rank
FROM orders;
-- 分析函数
SELECT
product_id,
order_date,
amount,
SUM(amount) OVER(PARTITION BY product_id ORDER BY order_date) AS cumulative_amount,
AVG(amount) OVER(PARTITION BY product_id ORDER BY order_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_avg_3rows
FROM orders;
3. 复杂条件过滤
-- 多条件过滤
SELECT
*
FROM user_behaviors
WHERE user_id IN (SELECT user_id FROM high_value_users)
AND behavior_type = 'purchase'
AND behavior_time BETWEEN '2023-06-01' AND '2023-06-30'
AND product_id IN (10001, 10002, 10003);
-- HAVING子句过滤
SELECT
user_id,
COUNT(*) AS purchase_count,
SUM(amount) AS total_amount
FROM orders
WHERE order_date >= '2023-01-01'
GROUP BY user_id
HAVING purchase_count >= 5 AND total_amount > 1000;
4. 子查询与WITH语句
-- 相关子查询
SELECT
o.order_id,
o.user_id,
o.amount,
(SELECT AVG(amount) FROM orders WHERE user_id = o.user_id) AS user_avg_amount
FROM orders o
WHERE o.amount > (SELECT AVG(amount) * 2 FROM orders);
-- WITH子句(CTE)
WITH user_stats AS (
SELECT
user_id,
COUNT(*) AS order_count,
SUM(amount) AS total_spend
FROM orders
GROUP BY user_id
),
high_value_users AS (
SELECT user_id
FROM user_stats
WHERE total_spend > 10000
)
SELECT
u.user_id,
u.user_name,
s.order_count,
s.total_spend
FROM users u
JOIN high_value_users h ON u.user_id = h.user_id
JOIN user_stats s ON u.user_id = s.user_id;
5. 日期时间分析
-- 时间维度分析
SELECT
DATE_FORMAT(order_time, '%Y-%m-%d') AS order_date,
DATE_FORMAT(order_time, '%H') AS order_hour,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
WHERE order_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY order_date, order_hour
ORDER BY order_date, order_hour;
-- 时间间隔计算
SELECT
user_id,
order_id,
order_time,
LAG(order_time) OVER(PARTITION BY user_id ORDER BY order_time) AS prev_order_time,
TIMESTAMPDIFF(DAY,
LAG(order_time) OVER(PARTITION BY user_id ORDER BY order_time),
order_time) AS days_since_last_order
FROM orders;
🚀 查询优化技巧
1. 分区裁剪优化
-- 确保查询条件包含分区列
EXPLAIN SELECT * FROM sales_data
WHERE sale_date BETWEEN '2023-06-01' AND '2023-06-30';
-- 若sale_date为分区列且分区边界与该日期范围匹配,检查计划是否只扫描相关分区;未提供分区定义,不能预先断定扫描范围
2. 谓词下推
-- 尽早过滤数据
SELECT
o.order_id,
o.amount,
u.user_name
FROM orders o
JOIN users u ON o.user_id = u.user_id
WHERE o.amount > 1000
AND o.order_date > '2023-01-01'
AND u.user_type = 'VIP';
-- 对照写法:优化器可能消除这些简单派生表并生成相同计划,不能仅凭写法判定性能优劣
SELECT
o.order_id,
o.amount,
u.user_name
FROM (SELECT * FROM orders) o
JOIN (SELECT * FROM users) u ON o.user_id = u.user_id
WHERE o.amount > 1000
AND o.order_date > '2023-01-01'
AND u.user_type = 'VIP';
3. JOIN优化
-- 小表广播JOIN
SELECT /*+ BROADCAST_JOIN(dim_product) */
f.order_id,
f.product_id,
d.product_name,
f.amount
FROM fact_orders f
JOIN dim_product d ON f.product_id = d.product_id;
-- 大表Shuffle JOIN
SELECT /*+ SHUFFLE_JOIN(fact_orders_1, fact_orders_2) */
a.order_id,
a.user_id,
b.product_id
FROM fact_orders_1 a
JOIN fact_orders_2 b ON a.user_id = b.user_id;
4. 物化视图加速
-- 创建物化视图
CREATE MATERIALIZED VIEW mv_daily_sales
DISTRIBUTED BY HASH(sale_date) BUCKETS 10
REFRESH ASYNC
AS SELECT
DATE_FORMAT(sale_time, '%Y-%m-%d') AS sale_date,
product_id,
SUM(quantity) AS total_quantity,
SUM(amount) AS total_amount,
COUNT(DISTINCT user_id) AS user_count
FROM sales_detail
GROUP BY sale_date, product_id;
-- 下例直接查询物化视图,不演示基表查询的自动改写;自动改写是否发生需另行检查执行计划
SELECT
sale_date,
SUM(total_amount) AS daily_amount
FROM mv_daily_sales
GROUP BY sale_date
ORDER BY sale_date;
5. 查询计划分析
-- 使用EXPLAIN分析查询计划
EXPLAIN SELECT
u.user_id,
u.user_name,
COUNT(o.order_id) AS order_count,
SUM(o.amount) AS total_amount
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE u.register_time >= '2023-01-01'
GROUP BY u.user_id, u.user_name
HAVING order_count > 0;
-- 使用EXPLAIN VERBOSE获取更详细信息
EXPLAIN VERBOSE SELECT * FROM orders WHERE amount > 1000;
📊 高级分析场景
1. 用户行为漏斗分析
-- 当日行为覆盖统计:未验证view、add_cart、purchase的先后顺序,不是严格有序漏斗;conversion_rate为当日有行为用户中的购买用户占比
WITH user_behaviors AS (
SELECT
user_id,
SUM(CASE WHEN behavior_type = 'view' THEN 1 ELSE 0 END) AS view_count,
SUM(CASE WHEN behavior_type = 'add_cart' THEN 1 ELSE 0 END) AS cart_count,
SUM(CASE WHEN behavior_type = 'purchase' THEN 1 ELSE 0 END) AS purchase_count
FROM user_behavior_log
WHERE behavior_date = '2023-06-01'
GROUP BY user_id
)
SELECT
COUNT(DISTINCT user_id) AS total_users,
SUM(CASE WHEN view_count > 0 THEN 1 ELSE 0 END) AS viewed_users,
SUM(CASE WHEN cart_count > 0 THEN 1 ELSE 0 END) AS carted_users,
SUM(CASE WHEN purchase_count > 0 THEN 1 ELSE 0 END) AS purchased_users,
SUM(CASE WHEN purchase_count > 0 THEN 1 ELSE 0 END) / COUNT(DISTINCT user_id) AS conversion_rate
FROM user_behaviors;
2. 同环比分析
-- 销售额同比环比分析
SELECT
current_month.month_date,
current_month.total_amount AS current_amount,
last_month.total_amount AS last_month_amount,
last_year.total_amount AS last_year_amount,
(current_month.total_amount - last_month.total_amount) / NULLIF(last_month.total_amount, 0) AS mom_growth,
(current_month.total_amount - last_year.total_amount) / NULLIF(last_year.total_amount, 0) AS yoy_growth
FROM (
SELECT DATE_FORMAT(order_time, '%Y-%m') AS month_date, SUM(amount) AS total_amount
FROM orders
WHERE order_time >= DATE_SUB(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 1 MONTH)
AND order_time < DATE_FORMAT(NOW(), '%Y-%m-01')
GROUP BY month_date
) current_month
LEFT JOIN (
SELECT DATE_FORMAT(order_time, '%Y-%m') AS month_date, SUM(amount) AS total_amount
FROM orders
WHERE order_time >= DATE_SUB(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 2 MONTH)
AND order_time < DATE_SUB(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 1 MONTH)
GROUP BY month_date
) last_month ON 1=1
LEFT JOIN (
SELECT DATE_FORMAT(order_time, '%Y-%m') AS month_date, SUM(amount) AS total_amount
FROM orders
WHERE order_time >= DATE_SUB(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 13 MONTH)
AND order_time < DATE_SUB(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 12 MONTH)
GROUP BY month_date
) last_year ON 1=1;
3. 用户留存分析
-- 用户留存分析
WITH user_first_date AS (
SELECT
user_id,
MIN(DATE(login_time)) AS first_date
FROM user_login_log
GROUP BY user_id
),
daily_logins AS (
SELECT
user_id,
DATE(login_time) AS login_date
FROM user_login_log
GROUP BY user_id, login_date
)
SELECT
u.first_date,
COUNT(DISTINCT u.user_id) AS new_users,
COUNT(DISTINCT CASE WHEN DATEDIFF(d.login_date, u.first_date) = 1 THEN u.user_id END) AS day1_retained,
COUNT(DISTINCT CASE WHEN DATEDIFF(d.login_date, u.first_date) = 7 THEN u.user_id END) AS day7_retained,
COUNT(DISTINCT CASE WHEN DATEDIFF(d.login_date, u.first_date) = 30 THEN u.user_id END) AS day30_retained,
COUNT(DISTINCT CASE WHEN DATEDIFF(d.login_date, u.first_date) = 1 THEN u.user_id END) / COUNT(DISTINCT u.user_id) AS day1_retention_rate,
COUNT(DISTINCT CASE WHEN DATEDIFF(d.login_date, u.first_date) = 7 THEN u.user_id END) / COUNT(DISTINCT u.user_id) AS day7_retention_rate,
COUNT(DISTINCT CASE WHEN DATEDIFF(d.login_date, u.first_date) = 30 THEN u.user_id END) / COUNT(DISTINCT u.user_id) AS day30_retention_rate
FROM user_first_date u
LEFT JOIN daily_logins d ON u.user_id = d.user_id
WHERE u.first_date BETWEEN DATE_SUB(CURDATE(), INTERVAL 60 DAY) AND DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY u.first_date
ORDER BY u.first_date;
⚠️ 常见问题与解决方案
1. 查询性能慢
- 问题:复杂查询执行时间过长
- 解决方案:
- 使用EXPLAIN分析查询计划
- 确保利用分区裁剪
- 添加适当的JOIN提示
- 考虑创建物化视图
- 优化表结构和索引
-- 使用EXPLAIN分析慢查询
EXPLAIN SELECT
u.user_id,
u.user_name,
COUNT(o.order_id) AS order_count
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id, u.user_name;
-- 添加JOIN提示优化
SELECT /*+ BROADCAST_JOIN(users) */
u.user_id,
u.user_name,
COUNT(o.order_id) AS order_count
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id, u.user_name;
2. 内存溢出
- 问题:查询执行时出现"Memory limit exceeded"错误
- 解决方案:
- 增加查询内存限制
- 减少查询的数据量
- 优化JOIN策略
- 分阶段执行复杂查询
-- 设置会话级内存限制
SET exec_mem_limit = 8589934592; -- 8GB
-- 分阶段执行复杂查询
-- 步骤1:创建普通中间表存储结果(tmp_前缀不会使其成为临时表;使用后按权限和数据保留要求清理)
CREATE TABLE tmp_user_stats AS
SELECT
user_id,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY user_id;
-- 步骤2:基于普通中间表执行后续查询
SELECT
u.user_id,
u.user_name,
t.order_count,
t.total_amount
FROM users u
JOIN tmp_user_stats t ON u.user_id = t.user_id
WHERE t.total_amount > 1000;
3. 结果不一致
- 问题:聚合结果与预期不符
- 解决方案:
- 检查数据类型转换
- 注意NULL值处理
- 验证GROUP BY列的完整性
- 检查JOIN条件是否正确
-- 处理NULL值
SELECT
region,
COUNT(*) AS total_orders,
SUM(COALESCE(amount, 0)) AS total_amount, -- 处理NULL值
AVG(CASE WHEN amount IS NOT NULL THEN amount END) AS avg_amount -- 忽略NULL值计算平均值
FROM orders
GROUP BY region;
-- 检查JOIN条件
SELECT
o.order_id,
o.user_id,
u.user_name
FROM orders o
LEFT JOIN users u ON o.user_id = u.user_id
WHERE u.user_id IS NULL; -- 检查未匹配的记录
✅ 掌握检查
- [ ] 能熟练使用Doris SQL进行基本查询和聚合分析
- [ ] 掌握窗口函数的应用场景和用法
- [ ] 了解Doris特有的SQL优化提示
- [ ] 能分析和优化复杂查询的性能
- [ ] 掌握高级分析场景的SQL实现方法
🔗 学习连接
#Doris #SQL查询 #数据分析 #聚合分析 #窗口函数