关于 sql-advanced
面向开发者和数据从业者的高级 SQL 查询与优化参考技能,涵盖 CTE、窗口函数、执行计划、索引和常见查询模式。
主要功能
- 01CTE 与递归查询
- 02窗口函数用法
- 03分析 EXPLAIN 执行计划
- 04索引与查询优化
- 05透视与时态查询
适用场景
- 01通过执行计划分析慢查询
- 02计算排名和累计值
- 03查询层级或时序数据
MCP 相关
该技能指向 `mcp__documentation__fetch_docs`,用于获取完整的 SQL 文档。
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill sql-advanced安装后在 Claude Code 中输入/sql-advanced确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
quick-ref/cte.mdoptimization.mdrecursive-queries.mdwindow-functions.md详细介绍
SQL 进阶核心知识
深入了解:使用技术栈为
sql的mcp__documentation__fetch_docs获取完整文档。
公用表表达式(CTE)
基本 CTE
WITH active_users AS (
SELECT id, name, email
FROM users
WHERE status = 'active'
)
SELECT u.name, COUNT(o.id) as order_count
FROM active_users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.name;
多个 CTE
WITH
active_users AS (
SELECT id, name FROM users WHERE status = 'active'
),
user_orders AS (
SELECT user_id, COUNT(*) as order_count, SUM(total) as total_spent
FROM orders
WHERE status = 'completed'
GROUP BY user_id
),
high_value_users AS (
SELECT u.*, uo.order_count, uo.total_spent
FROM active_users u
JOIN user_orders uo ON uo.user_id = u.id
WHERE uo.total_spent > 10000
)
SELECT * FROM high_value_users ORDER BY total_spent DESC;
递归 CTE
-- Hierarchical data (org chart, categories)
WITH RECURSIVE org_chart AS (
-- Base case: top-level employees
SELECT id, name, manager_id, 1 as level, ARRAY[name] as path
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- Recursive case
SELECT e.id, e.name, e.manager_id, oc.level + 1, oc.path || e.name
FROM employees e
JOIN org_chart oc ON e.manager_id = oc.id
)
SELECT * FROM org_chart ORDER BY path;
-- Generate series
WITH RECURSIVE numbers AS (
SELECT 1 as n
UNION ALL
SELECT n + 1 FROM numbers WHERE n < 100
)
SELECT * FROM numbers;
-- Date range
WITH RECURSIVE dates AS (
SELECT DATE '2024-01-01' as date
UNION ALL
SELECT date + INTERVAL '1 day' FROM dates WHERE date < '2024-12-31'
)
SELECT * FROM dates;
物化 CTE(PostgreSQL 12+)
-- Force CTE to be materialized (evaluated once)
WITH active_users AS MATERIALIZED (
SELECT * FROM users WHERE status = 'active'
)
SELECT * FROM active_users WHERE id = 1
UNION ALL
SELECT * FROM active_users WHERE id = 2;
-- Force CTE to be inlined (not materialized)
WITH active_users AS NOT MATERIALIZED (
SELECT * FROM users WHERE status = 'active'
)
SELECT * FROM active_users WHERE id = 1;
深入了解窗口函数
分区计算
SELECT
department,
name,
salary,
-- Within department
SUM(salary) OVER (PARTITION BY department) as dept_total,
AVG(salary) OVER (PARTITION BY department) as dept_avg,
salary - AVG(salary) OVER (PARTITION BY department) as diff_from_avg,
-- Percentage of department total
ROUND(100.0 * salary / SUM(salary) OVER (PARTITION BY department), 2) as pct_of_dept
FROM employees;
累计计算
SELECT
date,
amount,
-- Running totals
SUM(amount) OVER (ORDER BY date) as running_total,
SUM(amount) OVER (ORDER BY date ROWS UNBOUNDED PRECEDING) as same_as_above,
-- Moving averages
AVG(amount) OVER (ORDER BY date ROWS 6 PRECEDING) as moving_avg_7d,
AVG(amount) OVER (ORDER BY date ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING) as centered_avg,
-- Cumulative stats
COUNT(*) OVER (ORDER BY date) as cumulative_count,
MIN(amount) OVER (ORDER BY date) as running_min,
MAX(amount) OVER (ORDER BY date) as running_max
FROM daily_transactions;
间隙与岛屿分析
-- Find consecutive sequences (islands)
WITH numbered AS (
SELECT
date,
value,
ROW_NUMBER() OVER (ORDER BY date) as rn,
date - (ROW_NUMBER() OVER (ORDER BY date) * INTERVAL '1 day') as grp
FROM daily_data
)
SELECT
MIN(date) as island_start,
MAX(date) as island_end,
COUNT(*) as days_in_sequence
FROM numbered
GROUP BY grp
ORDER BY island_start;
-- Find gaps in sequence
SELECT
id,
LEAD(id) OVER (ORDER BY id) as next_id,
LEAD(id) OVER (ORDER BY id) - id - 1 as gap_size
FROM items
WHERE LEAD(id) OVER (ORDER BY id) - id > 1;
分组中的首项/末项
-- Get first and last values per group
SELECT DISTINCT ON (department)
department,
name as highest_paid,
salary
FROM employees
ORDER BY department, salary DESC;
-- With window functions
SELECT DISTINCT
department,
FIRST_VALUE(name) OVER w as highest_paid,
LAST_VALUE(name) OVER w as lowest_paid
FROM employees
WINDOW w AS (
PARTITION BY department
ORDER BY salary DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
);
查询优化
EXPLAIN 基础
-- Show query plan
EXPLAIN SELECT * FROM users WHERE email = '[email protected]';
-- Show actual execution stats
EXPLAIN ANALYZE SELECT * FROM users WHERE email = '[email protected]';
-- More details
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM users WHERE email = '[email protected]';
-- JSON output for tools
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)
SELECT * FROM users WHERE email = '[email protected]';
阅读 EXPLAIN 输出
Seq Scan on users (cost=0.00..155.00 rows=1 width=100) (actual time=0.015..0.842 rows=1 loops=1)
Filter: (email = '[email protected]'::text)
Rows Removed by Filter: 4999
| 术语 | 含义 |
|---|---|
Seq Scan | 全表扫描(通常不理想) |
Index Scan | 使用索引(良好) |
Index Only Scan | 使用覆盖索引(最佳) |
Bitmap Index Scan | 使用多个索引 |
cost=0.00..155.00 | 预估启动成本..总成本 |
rows=1 | 预估返回的行数 |
actual time=0.015..0.842 | 实际启动时间..总时间(毫秒) |
Rows Removed by Filter | 已读取但未返回的行 |
常见性能问题
缺少索引
-- Problem: Seq Scan
EXPLAIN SELECT * FROM users WHERE email = '[email protected]';
-- Solution: Add index
CREATE INDEX idx_users_email ON users(email);
未使用索引
-- Problem: Function on column prevents index use
SELECT * FROM users WHERE LOWER(email) = '[email protected]';
-- Solution: Expression index
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
N+1 查询问题
-- Problem: Querying in a loop (application code)
-- For each user: SELECT * FROM orders WHERE user_id = ?
-- Solution: Single query with JOIN
SELECT u.*, o.*
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.id IN (1, 2, 3, 4, 5);
索引策略
-- Composite index (column order matters!)
-- Good for: WHERE a = ? AND b = ?
-- Good for: WHERE a = ? ORDER BY b
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at DESC);
-- Covering index (all needed columns in index)
CREATE INDEX idx_orders_user_covering ON orders(user_id) INCLUDE (status, total);
-- Partial index (filtered)
CREATE INDEX idx_active_orders ON orders(user_id) WHERE status = 'active';
-- Expression index
CREATE INDEX idx_orders_year ON orders(EXTRACT(YEAR FROM created_at));
查询重写
-- Avoid: Subquery in SELECT
SELECT
name,
(SELECT COUNT(*) FROM orders WHERE user_id = users.id) as order_count
FROM users;
-- Better: JOIN with aggregation
SELECT u.name, COALESCE(o.order_count, 0) as order_count
FROM users u
LEFT JOIN (
SELECT user_id, COUNT(*) as order_count
FROM orders GROUP BY user_id
) o ON o.user_id = u.id;
-- Avoid: OR conditions on different columns
SELECT * FROM users WHERE email = '[email protected]' OR phone = '123';
-- Better: UNION (can use indexes)
SELECT * FROM users WHERE email = '[email protected]'
UNION
SELECT * FROM users WHERE phone = '123';
-- Avoid: NOT IN with NULLs (tricky behavior)
SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM banned);
-- Better: NOT EXISTS
SELECT * FROM users u WHERE NOT EXISTS (
SELECT 1 FROM banned b WHERE b.user_id = u.id
);
高级模式
透视/逆透视
-- Pivot: Rows to columns
SELECT
product_id,
SUM(CASE WHEN month = 1 THEN sales END) as jan,
SUM(CASE WHEN month = 2 THEN sales END) as feb,
SUM(CASE WHEN month = 3 THEN sales END) as mar
FROM monthly_sales
GROUP BY product_id;
-- PostgreSQL: crosstab
SELECT * FROM crosstab(
'SELECT product_id, month, sales FROM monthly_sales ORDER BY 1,2'
) AS ct(product_id INT, jan INT, feb INT, mar INT);
-- Unpivot: Columns to rows (PostgreSQL)
SELECT product_id, month, sales
FROM products,
LATERAL (VALUES
('jan', jan_sales),
('feb', feb_sales),
('mar', mar_sales)
) AS t(month, sales);
去重
-- Keep first occurrence
DELETE FROM users a USING users b
WHERE a.id > b.id AND a.email = b.email;
-- With CTE (safer)
WITH duplicates AS (
SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at) as rn
FROM users
)
DELETE FROM users WHERE id IN (
SELECT id FROM duplicates WHERE rn > 1
);
时态查询
-- Events active at specific time
SELECT * FROM events
WHERE start_time <= '2024-06-15 10:00:00'
AND end_time > '2024-06-15 10:00:00';
-- Overlapping periods
SELECT a.*, b.*
FROM reservations a, reservations b
WHERE a.id < b.id
AND a.room_id = b.room_id
AND a.start_time < b.end_time
AND a.end_time > b.start_time;
-- Fill gaps with generate_series
SELECT
d.date,
COALESCE(s.revenue, 0) as revenue
FROM generate_series(
'2024-01-01'::date,
'2024-12-31'::date,
'1 day'
) d(date)
LEFT JOIN daily_sales s ON s.date = d.date;
何时不应使用此技能
- 基础 SQL(SELECT、JOIN、INSERT)- 使用
sql-fundamentals技能 - PostgreSQL 特性(数组、JSONB)- 使用
postgresql技能 - MySQL 特性(存储过程)- 使用
mysql技能 - ORM 查询 - 使用
prisma、typeorm或相关 ORM 技能
反模式
| 反模式 | 问题 | 解决方案 |
|---|---|---|
| 相关子查询 | 性能慢、N+1 | 使用 JOIN 或窗口函数 |
| 在索引列上使用函数 | 未使用索引 | 使用函数索引 |
| 不受限制的深度递归 | 堆栈溢出 | 添加递归深度限制 |
| CTE 中缺少 WHERE | 处理不必要的数据 | 在 CTE 中尽早筛选 |
| 过度使用窗口函数 | 内存压力 | 先限制结果集 |
| 未分析 EXPLAIN 输出 | 未能发现慢查询 | 始终检查执行计划 |
快速故障排除
| 问题 | 诊断 | 修复 |
|---|---|---|
| CTE 执行缓慢 | EXPLAIN ANALYZE | 添加 MATERIALIZED 提示或重写查询 |
| 内存使用量高 | 检查排序/哈希操作 | 增大 work_mem,优化查询 |
| 超出递归限制 | 检查递归深度 | 添加 LIMIT,重新设计查询 |
| 窗口函数缓慢 | 检查 PARTITION BY 基数 | 在分区列上添加索引 |
| 查询计划发生变化 | 对比 EXPLAIN 输出 | 更新统计信息,固定查询计划 |
参考文档
评论
支持 Markdown