# 05 · 受约束 SQL 生成实现

> 目标：基于本体推理结果 + 映射，用「模板 + 参数填充」生成 SQL，并做口径校验。LLM 不写 SQL，只填参数。

## 1. 核心原则

**LLM 不写 SQL，只填参数。** SQL 结构由指标模板固化，口径固化在模板里。

## 2. 指标 SQL 模板（针对账单表 dwd_bill）

```java
public class MetricSqlTemplate {
    // GMV 模板：口径固化（status='paid' AND refunded=false）
    public static final String GMV =
        "SELECT {region_col}, SUM(bill_amount) AS gmv " +
        "FROM dwd_bill " +
        "WHERE status='paid' AND refunded=false " +   // ← 口径固化
        "AND dt BETWEEN '{start_dt}' AND '{end_dt}' " +
        "{region_filter} " +
        "GROUP BY {region_col}";

    // 订单数模板
    public static final String ORDER_COUNT =
        "SELECT SUM(bill_count) AS cnt FROM dwd_bill " +
        "WHERE status='paid' AND dt BETWEEN '{start_dt}' AND '{end_dt}' {region_filter}";
}
```

## 3. 参数填充流程

```
① 本体推理结果：
   - 指标：GMV（等价识别后）
   - 维度展开：华东区 → [上海,江苏,浙江,安徽,福建,江西,山东]
   - 时间：上个月 → 2026-07-01 ~ 2026-07-31
② 选模板：根据指标选 GMV 模板
③ 填参数：
   region_filter = "AND region_id IN (SELECT region_id FROM dim_region WHERE region_name IN ('上海','江苏',...))"
   start_dt = "2026-07-01", end_dt = "2026-07-31"
④ 生成 SQL（确定性拼接，无 LLM 参与）
```

```java
public String generateSql(String metric, List<String> regions, TimeRange time) {
    String template = selectTemplate(metric);
    String regionFilter = regions.isEmpty() ? "" :
        "AND region_id IN (SELECT region_id FROM dim_region WHERE region_name IN (" +
        regions.stream().map(r -> "'" + r + "'").collect(Collectors.joining(",")) + "))";
    return template
        .replace("{region_col}", "region_id")
        .replace("{region_filter}", regionFilter)
        .replace("{start_dt}", time.start())
        .replace("{end_dt}", time.end());
}
```

## 4. 口径校验（公理校验 SQL）

```java
public class CaliberValidator {
    /** 校验生成的 SQL 是否满足指标口径 */
    public boolean validate(String metric, String sql) {
        // 从本体读口径公理（whereClause 数据属性）
        String requiredClause = ontologyService.getWhereClause(metric);
        // GMV 的 requiredClause = "status='paid' AND refunded=false"
        // 校验 SQL 是否包含必需的过滤条件
        for (String clause : requiredClause.split(" AND ")) {
            if (!sql.contains(clause)) return false;
        }
        return true;
    }
}
```

## 5. 完整链路

```java
@Service
public class MetricQueryService {
    // ① 等价识别
    String metric = ontologyService.resolveEquivalentMetric("成交额"); // → GMV
    // ② 维度展开
    List<String> regions = ontologyService.expandRegion("华东区");     // → 7省
    // ③ 生成 SQL
    String sql = sqlGenerator.generateSql(metric, regions, time);
    // ④ 口径校验
    if (!caliberValidator.validate(metric, sql)) {
        throw new CaliberException("SQL 违背指标口径");
    }
    // ⑤ 执行（复用 MaxComputeQueryService）
    return maxComputeQueryService.query(sql);
}
```

## 6. 关键注意

- **口径固化在模板**：`WHERE status='paid' AND refunded=false` 就是口径，LLM 碰不到
- **双层防护**：模板固化（第一道）+ 公理校验（第二道）
- **维度展开是参数不是 SQL**：华东区展开成省份，是填进 IN 的参数，不是让 LLM 拼 SQL
- **维度 join**：账单表存 region_id，展开的省份名要 join dim_region 转成 region_id
- **复合指标特殊**：客单价等复合指标，模板里是子查询（先查 GMV、订单数再相除）
