3、SQL基础回顾:SELECT、WHERE、JOIN、GROUP BY、HAVING、ORDER BY在金融数据中的应用
在组合风险绩效归因实践中,SQL是提取、清洗、聚合和关联金融数据的核心工具。本节将系统回顾SQL中最常用的六个子句——SELECT、WHERE、JOIN、GROUP BY、HAVING、ORDER BY,并结合持仓表、收益表、基准表等金融数据场景,展示每个子句的具体用法与执行逻辑。
3.1 核心子句功能速览
下表总结了六个子句在金融数据分析中的典型角色与执行顺序:
| 子句 | 执行顺序 | 金融数据典型用途 |
|---|---|---|
SELECT |
5 | 选择证券代码、市值、收益率等字段;支持聚合函数与表达式 |
FROM + JOIN |
1 | 关联持仓表与收益表、基准表与因子表,构建分析宽表 |
WHERE |
2 | 过滤特定日期区间、资产类别、风险等级的数据行 |
GROUP BY |
3 | 按行业、因子暴露、投资组合分组计算风险指标 |
HAVING |
4 | 筛选分组后的聚合结果,如波动率超过阈值的行业组 |
ORDER BY |
6 | 按风险贡献、收益率、权重降序排列输出结果 |
执行顺序要点: SQL的逻辑执行顺序与书写顺序不同。
FROM/JOIN最先执行,确定数据源;WHERE逐行过滤;GROUP BY分组;HAVING过滤分组;SELECT投影列;ORDER BY最后排序。理解这一顺序对调试复杂归因查询至关重要。
3.2 SELECT:字段投影与计算列
SELECT用于指定输出列,支持字段别名、算术运算和聚合函数。在绩效归因中,常用来计算日收益率、超额收益、权重占比等衍生指标。
-- 查询组合持仓的市值占比与权重
SELECT
portfolio_id,
security_code,
market_value,
market_value / SUM(market_value) OVER (PARTITION BY portfolio_id) AS weight_pct,
ROUND(market_value / 1000000, 2) AS market_value_mm
FROM daily_holdings
WHERE trade_date = '2024-12-31';
实践建议: 在归因分析中,尽量在
SELECT阶段使用ROUND、CAST等函数统一数值精度,避免后续聚合时因浮点误差导致归因结果偏差。
3.3 WHERE:金融数据行级过滤
WHERE在分组前对原始数据进行逐行筛选,常用于限定时间窗口、资产类别、风险等级等维度。过滤条件越早执行,查询性能越好。
-- 筛选2024年Q4的权益类持仓数据
SELECT
trade_date,
security_code,
sector,
daily_return
FROM portfolio_returns
WHERE trade_date BETWEEN '2024-10-01' AND '2024-12-31'
AND asset_class = 'Equity'
AND daily_return IS NOT NULL;
| 过滤场景 | WHERE条件示例 | 说明 |
|---|---|---|
| 日期范围 | trade_date >= '2024-01-01' |
限定回测或绩效周期 |
| 资产类别 | asset_class IN ('Equity','Fixed Income') |
多资产归因时按类别过滤 |
| 异常值剔除 | ABS(daily_return) < 0.20 |
过滤数据错误或极端事件 |
| 非空约束 | factor_exposure IS NOT NULL |
确保因子暴露值完整 |
3.4 JOIN:多表关联构建归因宽表
组合风险归因通常需要将持仓表、收益表、基准表、因子表关联为一张宽表。JOIN是实现这一目标的核心机制。金融数据中常用INNER JOIN和LEFT JOIN。
-- 将持仓表与日收益表关联,计算每只证券的贡献收益
SELECT
h.portfolio_id,
h.trade_date,
h.security_code,
h.weight,
r.daily_return,
h.weight * r.daily_return AS contribution_return
FROM daily_holdings h
INNER JOIN daily_returns r
ON h.security_code = r.security_code
AND h.trade_date = r.trade_date
WHERE h.trade_date = '2024-12-31';
关联陷阱: 金融数据中同一证券在不同表内可能使用不同代码(如Bloomberg Ticker vs ISIN)。关联时必须确认键值一致,否则会导致数据丢失或笛卡尔积。建议在关联前先通过
DISTINCT检查键值匹配情况。
3.5 GROUP BY:分组聚合计算风险指标
GROUP BY将数据按一个或多个维度分组,配合聚合函数计算组内统计量。在风险归因中,常用于按行业、因子、投资组合计算波动率、夏普比率、VaR等指标。
-- 按行业分组计算波动率与平均收益
SELECT
sector,
COUNT(*) AS num_securities,
ROUND(STDDEV(daily_return), 6) AS vol_daily,
ROUND(AVG(daily_return), 6) AS avg_return,
ROUND(SUM(weight * daily_return), 6) AS sector_contribution
FROM portfolio_returns
WHERE trade_date BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY sector
ORDER BY vol_daily DESC;
| 聚合函数 | 风险归因用途 | 示例 |
|---|---|---|
COUNT |
统计分组内证券数量 | COUNT(DISTINCT security_code) |
SUM |
计算权重和、贡献收益和 | SUM(weight * daily_return) |
AVG |
平均收益率、平均因子暴露 | AVG(factor_exposure) |
STDDEV |
波动率(风险度量核心) | STDDEV(daily_return) |
MAX/MIN |
最大回撤、峰值/谷值 | MAX(cumulative_return) |
3.6 HAVING:分组后过滤风险阈值
HAVING与WHERE类似,但作用于分组后的聚合结果。在风险归因中,常用HAVING筛选出超过风险容忍度的行业组或因子组。
-- 筛选年化波动率超过20%的行业组
SELECT
sector,
ROUND(STDDEV(daily_return) * SQRT(252), 4) AS vol_annual,
ROUND(AVG(daily_return) * 252, 4) AS return_annual
FROM portfolio_returns
WHERE trade_date BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY sector
HAVING STDDEV(daily_return) * SQRT(252) > 0.20
ORDER BY vol_annual DESC;
WHERE vs HAVING 对比:
WHERE在分组前过滤原始行,不能使用聚合函数;HAVING在分组后过滤组,必须与聚合函数配合使用。在归因查询中,通常先用WHERE剔除异常日期,再用HAVING筛选高风险组。
3.7 ORDER BY:排序输出归因结果
ORDER BY控制最终结果的排序方式,支持升序(ASC,默认)和降序(DESC)。在绩效归因报告中,通常按风险贡献、超额收益或权重降序排列,以便快速定位主要风险来源。
-- 按风险贡献降序输出行业归因结果
SELECT
sector,
ROUND(SUM(weight * daily_return), 6) AS risk_contribution,
ROUND(STDDEV(daily_return), 6) AS vol
FROM portfolio_returns
WHERE trade_date BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY sector
ORDER BY risk_contribution DESC, vol DESC;
3.8 综合案例:组合风险归因查询
以下综合查询将上述六个子句串联,完成一个完整的行业风险归因分析:
-- 行业风险归因:计算各行业对组合总风险的贡献
SELECT
h.sector,
COUNT(DISTINCT h.security_code) AS num_holdings,
ROUND(SUM(h.weight), 4) AS sector_weight,
ROUND(STDDEV(r.daily_return) * SQRT(252), 4) AS sector_vol_annual,
ROUND(SUM(h.weight * r.daily_return) * 252, 4) AS sector_return_annual,
ROUND(SUM(h.weight * r.daily_return) / NULLIF(STDDEV(r.daily_return), 0) * SQRT(252), 4) AS sector_sharpe
FROM daily_holdings h
INNER JOIN daily_returns r
ON h.security_code = r.security_code
AND h.trade_date = r.trade_date
WHERE h.trade_date BETWEEN '2024-01-01' AND '2024-12-31'
AND h.portfolio_id = 'PF001'
AND r.daily_return IS NOT NULL
GROUP BY h.sector
HAVING COUNT(DISTINCT h.security_code) >= 3
AND STDDEV(r.daily_return) * SQRT(252) > 0.05
ORDER BY sector_vol_annual DESC, sector_sharpe DESC;
查询解读: 该查询首先通过
INNER JOIN关联持仓与收益数据,WHERE限定时间窗口和组合ID,GROUP BY按行业分组,HAVING过滤掉证券数量不足3只或年化波动率低于5%的行业,最后按波动率和夏普比率降序输出,帮助分析师快速定位高风险与高收益行业。
3.9 常见错误与避坑指南
| 错误类型 | 错误示例 | 正确做法 |
|---|---|---|
| WHERE中使用聚合函数 | WHERE SUM(weight) > 0.5 |
改用HAVING SUM(weight) > 0.5 |
| SELECT非聚合列未在GROUP BY中 | SELECT sector, trade_date, AVG(return) 且GROUP BY sector |
将trade_date加入GROUP BY或使用聚合函数包裹 |
| JOIN条件遗漏日期字段 | ON a.code = b.code(无日期) |
添加AND a.date = b.date避免跨期匹配 |
| ORDER BY使用别名但执行顺序错误 | ORDER BY vol_annual 但vol_annual在SELECT中定义 |
MySQL/PostgreSQL支持别名排序;SQL Server需用列位置或完整表达式 |