Oracle SQL 复杂查询案例

Oracle SQL 复杂查询案例

适用版本:Oracle Database 10g / 11g / 12c / 19c / 23ai 文档版本:v1.0 / 2026-07


1. 概述

复杂查询案例汇总[1]:

详细见:Oracle 数据库高级 SQL 技巧Oracle 高级分析函数


2. 累计计算

2.1 累计求和

SELECT sale_date, amount,
  SUM(amount) OVER (ORDER BY sale_date) AS cum_total
FROM sales
ORDER BY sale_date;

2.2 移动平均

SELECT sale_date, amount,
  AVG(amount) OVER (
    ORDER BY sale_date 
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ) AS ma7
FROM sales;

2.3 同比环比

SELECT sale_date, amount,
  LAG(amount, 1) OVER (ORDER BY sale_date) AS prev_month,
  (amount - LAG(amount, 1) OVER (ORDER BY sale_date)) / 
   LAG(amount, 1) OVER (ORDER BY sale_date) AS growth_rate
FROM monthly_sales;

3. 排名

3.1 Top N

-- 每部门 Top 3
SELECT * FROM (
  SELECT e.*,
    ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn
  FROM employees e
)
WHERE rn <= 3;

3.2 排名

SELECT name, salary,
  RANK() OVER (ORDER BY salary DESC) AS rnk,
  DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rnk,
  ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn
FROM employees;

3.3 百分比

SELECT name, salary,
  PERCENT_RANK() OVER (ORDER BY salary) AS pct_rank,
  CUME_DIST() OVER (ORDER BY salary) AS cume_dist,
  NTILE(4) OVER (ORDER BY salary) AS quartile
FROM employees;

详细见:Oracle 高级分析函数


4. 树形查询

4.1 层级

SELECT employee_id, name, manager_id, LEVEL,
  SYS_CONNECT_BY_PATH(name, '/') AS path
FROM employees
START WITH manager_id IS NULL
CONNECT BY PRIOR employee_id = manager_id
ORDER SIBLINGS BY name;

4.2 递归 CTE

WITH org_chart(id, name, mgr_id, lvl) AS (
  SELECT id, name, manager_id, 0
  FROM employees
  WHERE manager_id IS NULL
  
  UNION ALL
  
  SELECT e.id, e.name, e.manager_id, oc.lvl + 1
  FROM employees e, org_chart oc
  WHERE e.manager_id = oc.id
)
SELECT * FROM org_chart;

详细见:Oracle 层次查询Oracle CTE 与递归查询


5. 行列转换

5.1 PIVOT

SELECT * FROM (
  SELECT dept_id, job_id, salary FROM employees
)
PIVOT (
  SUM(salary) FOR job_id IN ('IT_PROG' AS it, 'SA_REP' AS sales)
);

5.2 UNPIVOT

SELECT * FROM sales_pivot
UNPIVOT (
  amount FOR quarter IN (q1, q2, q3, q4)
);

详细见:Oracle SQL 行列转换详解


6. 去重

6.1 ROWID

DELETE FROM employees WHERE ROWID IN (
  SELECT rid FROM (
    SELECT ROWID rid, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) rn
    FROM employees
  ) WHERE rn > 1
);

6.2 DISTINCT

SELECT DISTINCT dept_id FROM employees;

6.3 GROUP BY

SELECT dept_id FROM employees GROUP BY dept_id;

7. 连续值

7.1 连续 N 天

SELECT user_id, login_date
FROM (
  SELECT user_id, login_date,
    login_date - ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) AS grp
  FROM logins
)
GROUP BY user_id, grp
HAVING COUNT(*) >= 7;

7.2 Islands

-- 连续区间
SELECT MIN(val) AS start_val, MAX(val) AS end_val
FROM (
  SELECT val, 
    val - ROW_NUMBER() OVER (ORDER BY val) AS grp
  FROM numbers
)
GROUP BY grp
ORDER BY start_val;

8. 中位数

8.1 PERCENTILE_CONT

SELECT dept_id,
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary) AS median
FROM employees
GROUP BY dept_id;

8.2 分析函数

SELECT dept_id, salary,
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary) 
    OVER (PARTITION BY dept_id) AS median
FROM employees;

9. 自连接

9.1 同表比较

-- 比同部门平均高
SELECT e.name, e.salary, e.dept_id, d.avg_sal
FROM employees e,
  (SELECT dept_id, AVG(salary) AS avg_sal FROM employees GROUP BY dept_id) d
WHERE e.dept_id = d.dept_id AND e.salary > d.avg_sal;

9.2 间隔

-- 间隔 N 行
SELECT a.id, b.id AS next_id
FROM employees a, employees b
WHERE b.id = (SELECT MIN(id) FROM employees WHERE id > a.id);

10. EXISTS

10.1 存在

SELECT d.id, d.name
FROM departments d
WHERE EXISTS (SELECT 1 FROM employees e WHERE e.dept_id = d.id);

10.2 不存在

SELECT d.id, d.name
FROM departments d
WHERE NOT EXISTS (SELECT 1 FROM employees e WHERE e.dept_id = d.id);

详细见:Oracle 子查询与 EXISTS


11. 跨行计算

11.1 前后值

SELECT sale_date, amount,
  LAG(amount, 1) OVER (ORDER BY sale_date) AS prev,
  LEAD(amount, 1) OVER (ORDER BY sale_date) AS next,
  amount - LAG(amount, 1) OVER (ORDER BY sale_date) AS diff
FROM sales;

11.2 第一最后

SELECT dept_id,
  FIRST_VALUE(name) OVER (PARTITION BY dept_id ORDER BY salary DESC) AS top_earner,
  LAST_VALUE(name) OVER (PARTITION BY dept_id ORDER BY salary DESC 
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS low_earner
FROM employees;

12. 多维分析

12.1 ROLLUP

SELECT dept_id, job_id, SUM(salary)
FROM employees
GROUP BY ROLLUP (dept_id, job_id);
-- 小计 + 总计

12.2 CUBE

SELECT dept_id, job_id, SUM(salary)
FROM employees
GROUP BY CUBE (dept_id, job_id);
-- 所有组合

12.3 GROUPING SETS

SELECT dept_id, job_id, SUM(salary)
FROM employees
GROUP BY GROUPING SETS ((dept_id), (job_id), ());
-- 指定组合

13. CASE

13.1 条件聚合

SELECT dept_id,
  SUM(CASE WHEN gender = 'M' THEN 1 ELSE 0 END) AS male_count,
  SUM(CASE WHEN gender = 'F' THEN 1 ELSE 0 END) AS female_count
FROM employees
GROUP BY dept_id;

13.2 分类

SELECT name, salary,
  CASE 
    WHEN salary >= 10000 THEN 'High'
    WHEN salary >= 5000 THEN 'Medium'
    ELSE 'Low'
  END AS level
FROM employees;

14. MERGE

MERGE INTO target t
USING source s ON (t.id = s.id)
WHEN MATCHED THEN 
  UPDATE SET t.name = s.name
  DELETE WHERE t.status = 'INACTIVE'
WHEN NOT MATCHED THEN 
  INSERT (id, name) VALUES (s.id, s.name);

详细见:Oracle MERGE 语句详解


15. 复杂示例

15.1 销售排行

WITH dept_sales AS (
  SELECT dept_id, SUM(amount) AS total
  FROM sales
  WHERE sale_date >= TRUNC(SYSDATE, 'YYYY')
  GROUP BY dept_id
)
SELECT d.dept_name, ds.total,
  RANK() OVER (ORDER BY ds.total DESC) AS rank
FROM dept_sales ds
  JOIN departments d ON ds.dept_id = d.id
ORDER BY rank;

15.2 同比

SELECT 
  EXTRACT(YEAR FROM sale_date) AS yr,
  EXTRACT(MONTH FROM sale_date) AS mon,
  SUM(amount) AS total,
  LAG(SUM(amount), 12) OVER (ORDER BY EXTRACT(YEAR FROM sale_date), EXTRACT(MONTH FROM sale_date)) AS last_year,
  ROUND((SUM(amount) - LAG(SUM(amount), 12) OVER (ORDER BY EXTRACT(YEAR FROM sale_date), EXTRACT(MONTH FROM sale_date))) / 
        LAG(SUM(amount), 12) OVER (ORDER BY EXTRACT(YEAR FROM sale_date), EXTRACT(MONTH FROM sale_date)) * 100, 2) AS yoy_pct
FROM sales
GROUP BY EXTRACT(YEAR FROM sale_date), EXTRACT(MONTH FROM sale_date)
ORDER BY yr, mon;

15.3 用户留存

WITH first_login AS (
  SELECT user_id, MIN(login_date) AS first_date
  FROM logins
  GROUP BY user_id
),
retention AS (
  SELECT 
    f.first_date,
    COUNT(DISTINCT f.user_id) AS day_0,
    COUNT(DISTINCT CASE WHEN l.login_date = f.first_date + 1 THEN f.user_id END) AS day_1,
    COUNT(DISTINCT CASE WHEN l.login_date = f.first_date + 7 THEN f.user_id END) AS day_7,
    COUNT(DISTINCT CASE WHEN l.login_date = f.first_date + 30 THEN f.user_id END) AS day_30
  FROM first_login f
    LEFT JOIN logins l ON f.user_id = l.user_id
  GROUP BY f.first_date
)
SELECT * FROM retention ORDER BY first_date;

16. 性能

16.1 索引

- 高选择性列
- 覆盖
- 复合

详细见:Oracle 索引优化策略详解

16.2 执行计划

EXPLAIN PLAN FOR ...;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY));

详细见:Oracle 执行计划详解

16.3 统计

EXEC DBMS_STATS.GATHER_TABLE_STATS(...);

17. 最佳实践

  1. 分析函数:复杂
  2. CTE:清晰
  3. FETCH:分页
  4. MERGE:UPSERT
  5. PIVOT:转换
  6. 树形查询:层级
  7. 索引:性能
  8. 执行计划:验证
  9. 测试:完整
  10. 文档化:说明

18. 参考资料

[1] Oracle Database SQL Tuning Guide 19c https://docs.oracle.com/en/database/oracle/oracle-database/19/tgsql/