让业务人员直接"问"数据库:Spring AI Alibaba NL2SQL 实战指南

# 让业务人员直接"问"数据库:Spring AI Alibaba NL2SQL 实战指南

不用学 SQL,不用找开发排期,用大白话就能查数据——这不是未来,而是现在就能落地的方案。


# 一个真实的痛点

你是公司的运营负责人,想知道"上个月华东地区复购率最高的三个品类"。

在传统模式下,这个需求的链路是这样的:

你提需求 → 开发排期(3天后)→ 写 SQL → 测试 → 上线 → 你拿到数据
1

如果第二天你又想知道"按周维度的趋势变化"?再来一轮。

NL2SQL(Natural Language to SQL) 要解决的正是这个问题——让不懂 SQL 的人,也能用自然语言直接查询数据库。

但 NL2SQL 不是新概念。早在 ChatGPT 出现之前,学术界就在研究这个问题。为什么以前的方案没有普及?核心障碍有三个:

  1. Schema 理解不准:模型不知道你的业务表结构,容易"张冠李戴"
  2. 复杂查询搞不定:多表关联、窗口函数一上,生成质量断崖式下降
  3. 执行不可控:生成的 SQL 可能有语法错误,甚至误删数据

Spring AI Alibaba 的 NL2SQL 模块,正是针对这三个痛点,将阿里云析言 GBI 产品中的核心能力模块化、开源化。


# 一、整体架构:三步走

Spring AI Alibaba NL2SQL 的核心链路可以概括为三步:

┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  用户自然语言  │───►│ Schema 召回   │───►│  SQL 生成     │
│  "上月华东区  │    │ 找到相关表     │    │  大模型翻译    │
│   复购率Top3" │    │ 和字段         │    │  为 SQL       │
└──────────────┘    └──────────────┘    └──────┬───────┘
                                               │
                                               ▼
                                        ┌──────────────┐
                                        │  SQL 执行      │
                                        │  返回结果      │
                                        │  + 错误处理    │
                                        └──────────────┘
1
2
3
4
5
6
7
8
9
10
11
12

# 每一步解决什么问题?

步骤 核心问题 解决方案
Schema 召回 数据库可能有上百张表,模型怎么知道该用哪张? 将表结构向量化,根据用户问题语义匹配最相关的表和字段
SQL 生成 自然语言怎么变成准确的 SQL? 大模型 + 数据库方言适配 + 复杂查询优化
SQL 执行 生成的 SQL 能直接跑吗?安全吗? 连接池管理、只读保护、错误反馈

# 二、环境准备

# 2.1 前置条件

  • JDK 17+:Spring Boot 3.x 的最低要求
  • MySQL 8.0(或其他 JDBC 兼容的数据库)
  • DashScope API Key:用于调用通义千问等大模型

设置 API Key:

export AI_DASHSCOPE_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx
1

# 2.2 准备测试数据

为了演示效果,我们先准备一个简单的电商数据库:

CREATE DATABASE ecommerce_demo;
USE ecommerce_demo;

-- 商品表
CREATE TABLE products (
    product_id INT PRIMARY KEY AUTO_INCREMENT,
    product_name VARCHAR(100),
    category VARCHAR(50),
    price DECIMAL(10, 2)
);

-- 订单表
CREATE TABLE orders (
    order_id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    product_id INT,
    quantity INT,
    order_date DATE,
    region VARCHAR(50),
    FOREIGN KEY (product_id) REFERENCES products(product_id)
);

-- 插入测试数据
INSERT INTO products VALUES 
    (1, 'iPhone 15', '手机', 5999),
    (2, 'MacBook Pro', '电脑', 12999),
    (3, 'AirPods Pro', '耳机', 1899);

INSERT INTO orders VALUES 
    (1, 101, 1, 2, '2024-01-15', '华东'),
    (2, 102, 2, 1, '2024-02-20', '华北'),
    (3, 101, 3, 3, '2024-03-10', '华东'),
    (4, 103, 1, 1, '2024-04-05', '华南');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 三、项目搭建

# 3.1 引入依赖

创建 Spring Boot 项目后,在 pom.xml 中添加 NL2SQL 相关依赖:

<dependencies>
    <!-- Spring AI Alibaba 基础依赖 -->
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-starter</artifactId>
        <version>1.0.0.2</version>
    </dependency>

    <!-- NL2SQL 模块 -->
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-starter-nl2sql</artifactId>
        <version>1.0.0.2</version>
    </dependency>

    <!-- MySQL 驱动 -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <version>8.3.0</version>
    </dependency>

    <!-- Web 支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

同时需要配置 Maven 仓库(Spring Milestone 仓库):

<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>
1
2
3
4
5
6
7
8
9
10

# 3.2 配置文件

application.yml 中配置数据库和大模型连接:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/ecommerce_demo?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver

  ai:
    dashscope:
      api-key: ${AI_DASHSCOPE_API_KEY}
      chat:
        model: qwen-plus

    nl2sql:
      # 数据库方言
      database-type: mysql
      # 是否自动执行生成的 SQL(生产环境建议设为 false)
      auto-execute: true
      # Schema 召回策略
      schema-recall:
        # 召回的表数量上限
        max-tables: 10
        # 相似度阈值
        similarity-threshold: 0.6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 四、核心实现

# 4.1 Schema 注册

NL2SQL 的第一步是让模型"认识"你的数据库。Spring AI Alibaba NL2SQL 提供了 Schema 管理模块,可以自动扫描数据库结构并建立向量索引:

import com.alibaba.cloud.ai.nl2sql.schema.SchemaManager;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class SchemaInitializer implements ApplicationRunner {

    private final SchemaManager schemaManager;

    public SchemaInitializer(SchemaManager schemaManager) {
        this.schemaManager = schemaManager;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 自动扫描数据库 Schema 并建立向量索引
        schemaManager.initialize();
        System.out.println("Schema 初始化完成,已注册表和字段信息");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

这个过程会做以下几件事:

  1. 通过 JDBC 读取数据库的元数据(表名、字段名、字段类型、注释)
  2. 将元数据信息转为向量表示
  3. 存入向量数据库,供后续语义检索使用

关于字段注释的重要性: 如果你的表有完善的注释(COMMENT),Schema 召回的准确率会大幅提升。建议在数据库设计时就写好注释,例如:

ALTER TABLE orders MODIFY COLUMN region VARCHAR(50) COMMENT '订单所属地区';
1

# 4.2 NL2SQL 服务

接下来实现核心的自然语言转 SQL 功能:

import com.alibaba.cloud.ai.nl2sql.service.Nl2SqlService;
import com.alibaba.cloud.ai.nl2sql.model.Nl2SqlResult;
import org.springframework.stereotype.Service;

@Service
public class DataQueryService {

    private final Nl2SqlService nl2SqlService;

    public DataQueryService(Nl2SqlService nl2SqlService) {
        this.nl2SqlService = nl2SqlService;
    }

    /**
     * 自然语言查询数据库
     * @param naturalQuery 用户的自然语言问题
     * @return 查询结果
     */
    public Nl2SqlResult query(String naturalQuery) {
        return nl2SqlService.nl2sql(naturalQuery);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Nl2SqlResult 通常包含以下信息:

  • 生成的 SQL 语句
  • 执行结果(如果开启了自动执行)
  • 召回的 Schema 信息(用于调试和审计)
  • 错误信息(如果生成或执行失败)

# 4.3 REST 接口

对外暴露一个简洁的 HTTP 接口:

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/data")
public class DataQueryController {

    private final DataQueryService dataQueryService;

    public DataQueryController(DataQueryService dataQueryService) {
        this.dataQueryService = dataQueryService;
    }

    /**
     * 自然语言查询接口
     * GET /api/data/query?question=列出所有商品的价格
     */
    @GetMapping("/query")
    public DataQueryResponse query(@RequestParam String question) {
        try {
            var result = dataQueryService.query(question);
            return DataQueryResponse.success(
                    result.getSql(),
                    result.getExecutionResult()
            );
        } catch (Exception e) {
            return DataQueryResponse.error(e.getMessage());
        }
    }

    // 响应 DTO
    public record DataQueryResponse(
            boolean success,
            String sql,
            Object data,
            String errorMessage
    ) {
        public static DataQueryResponse success(String sql, Object data) {
            return new DataQueryResponse(true, sql, data, null);
        }

        public static DataQueryResponse error(String message) {
            return new DataQueryResponse(false, null, null, message);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

# 五、测试验证

启动应用后,用几个典型场景来测试效果:

# 场景一:简单查询

curl "http://localhost:8080/api/data/query?question=列出所有商品"
1

预期生成的 SQL:

SELECT product_id, product_name, category, price FROM products;
1

# 场景二:条件过滤 + 排序

curl "http://localhost:8080/api/data/query?question=华东地区有多少个订单"
1

预期生成的 SQL:

SELECT COUNT(*) FROM orders WHERE region = '华东';
1

# 场景三:多表关联

curl "http://localhost:8080/api/data/query?question=每个品类的商品平均价格是多少"
1

预期生成的 SQL:

SELECT category, AVG(price) as avg_price 
FROM products 
GROUP BY category;
1
2
3

# 场景四:复杂聚合

curl "http://localhost:8080/api/data/query?question=复购次数最多的用户是谁"
1

预期生成的 SQL:

SELECT user_id, COUNT(*) as order_count 
FROM orders 
GROUP BY user_id 
ORDER BY order_count DESC 
LIMIT 1;
1
2
3
4
5

# 六、生产环境关键考量

上面的代码跑通了基本流程。但在真实业务中,还需要解决以下问题:

# 6.1 安全防护

只读保护: 绝对不要让 NL2SQL 直接拥有写权限。建议:

spring:
  datasource:
    # 使用只读账号
    url: jdbc:mysql://localhost:3306/ecommerce_demo?useSSL=false
    username: readonly_user
    password: readonly_password
1
2
3
4
5
6

同时在代码层面增加 SQL 校验:

public boolean isSafeSql(String sql) {
    String upper = sql.trim().toUpperCase();
    // 只允许 SELECT 语句
    return upper.startsWith("SELECT");
}
1
2
3
4
5

# 6.2 SQL 审核与人工确认

对于关键业务场景,建议增加"生成 → 审核 → 执行"的流程:

public Nl2SqlResult queryWithReview(String naturalQuery) {
    // 第一步:生成 SQL(不执行)
    Nl2SqlResult result = nl2SqlService.generateOnly(naturalQuery);
    
    // 第二步:返回 SQL 给人工审核
    // ... 审核流程 ...
    
    // 第三步:审核通过后执行
    if (approved) {
        result = nl2SqlService.executeSql(result.getSql());
    }
    
    return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 6.3 查询超时与资源限制

防止慢查询拖垮数据库:

import java.sql.Statement;
import java.util.concurrent.*;

public Object executeWithTimeout(String sql, int timeoutSeconds) {
    ExecutorService executor = Executors.newSingleThreadExecutor();
    Future<Object> future = executor.submit(() -> {
        // 执行 SQL
        return jdbcTemplate.queryForList(sql);
    });
    
    try {
        return future.get(timeoutSeconds, TimeUnit.SECONDS);
    } catch (TimeoutException e) {
        future.cancel(true);
        throw new RuntimeException("查询超时(" + timeoutSeconds + "秒)");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 6.4 结果缓存

对于高频查询,可以加一层缓存:

import org.springframework.cache.annotation.Cacheable;

@Cacheable(value = "nl2sql", key = "#naturalQuery")
public Nl2SqlResult query(String naturalQuery) {
    return nl2SqlService.nl2sql(naturalQuery);
}
1
2
3
4
5
6

# 6.5 多数据源支持

企业通常有多个数据库(订单库、用户库、商品库)。Spring AI Alibaba NL2SQL 支持多数据源配置:

@Configuration
public class MultiDataSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.order")
    public DataSource orderDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.user")
    public DataSource userDataSource() {
        return DataSourceBuilder.create().build();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 七、提升 NL2SQL 准确率的实战技巧

# 7.1 完善数据库注释

这是最有效的优化手段。模型对字段的理解完全依赖元数据中的注释信息。

-- 好的注释
ALTER TABLE orders ADD COLUMN order_status TINYINT COMMENT '订单状态:1-待支付,2-已支付,3-已发货,4-已完成,5-已取消';

-- 不好的注释(或没有注释)
ALTER TABLE orders ADD COLUMN order_status TINYINT;
1
2
3
4
5

# 7.2 提供业务术语映射

用户的自然语言表述和数据库字段名往往不一致。可以建立术语映射表:

public class BusinessTermMapping {
    private static final Map<String, String> TERM_MAP = Map.of(
        "销售额", "price * quantity",
        "复购", "user_id 出现次数 > 1",
        "华东区", "region IN ('上海', '江苏', '浙江', '安徽')"
    );

    public static String enrichQuery(String naturalQuery) {
        for (var entry : TERM_MAP.entrySet()) {
            naturalQuery = naturalQuery.replace(entry.getKey(), entry.getValue());
        }
        return naturalQuery;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 7.3 Few-Shot 示例增强

在 Prompt 中提供几个正确的"问题 → SQL"示例,可以显著提升生成质量:

示例1:
问题:找出价格超过100元的商品
SQL:SELECT * FROM products WHERE price > 100

示例2:
问题:统计每个地区的订单数量
SQL:SELECT region, COUNT(*) FROM orders GROUP BY region

现在请回答:
问题:{user_question}
SQL:
1
2
3
4
5
6
7
8
9
10
11

# 7.4 选择合适的模型

不同模型在 NL2SQL 任务上的表现差异很大:

模型 适用场景 成本
qwen-plus 日常使用,性价比高
qwen-max 复杂查询,多表关联
XiYan-SQL(开源) 专注 NL2SQL 的专用模型 免费

对于简单的单表查询,qwen-plus 已经够用;涉及多表 JOIN、子查询、窗口函数的场景,建议使用 qwen-max 或专用的 SQL 生成模型。


# 八、常见问题排查

Q:生成的 SQL 语法错误怎么办?

A:排查顺序:

  1. 确认数据库方言配置正确(database-type: mysql
  2. 检查 Schema 是否正确注册(查看初始化日志)
  3. 尝试换用更强的模型(qwen-max)
  4. 在 Prompt 中增加 Few-Shot 示例

Q:召回的表不对,导致生成的 SQL 完全错误?

A:

  1. 检查字段注释是否完善
  2. 降低 similarity-threshold 阈值
  3. 增加 max-tables 数量
  4. 手动指定相关表(通过 API 参数)

Q:回答速度太慢?

A:NL2SQL 涉及多次 LLM 调用(Schema 召回 + SQL 生成),整体耗时通常在 3-8 秒。优化方向:

  1. 使用流式输出,先返回 SQL 再执行
  2. 缓存常见查询的结果
  3. 选择响应更快的模型

Q:依赖拉取失败?

A:确认已配置 Spring Milestones 仓库(见上文 3.1 节)。如果仍然失败,可以尝试使用快照版本或检查网络连接。


# 九、总结

NL2SQL 不是要取代 SQL,而是要降低数据查询的门槛。它的最佳应用场景是:

  • 业务人员的自助分析:不用等开发排期,自己问数据
  • 数据产品的底层引擎:为 BI 工具、智能客服提供数据查询能力
  • 开发者的效率工具:快速生成复杂 SQL,减少手写错误

Spring AI Alibaba NL2SQL 的价值在于:

  1. 开箱即用:引入依赖即可启动服务,不需要从零搭建
  2. 模块化设计:Schema 召回、SQL 生成、SQL 执行三个环节解耦,可按需组合
  3. 企业级能力:多数据源、安全防护、错误处理一应俱全
  4. 持续迭代:背靠阿里云析言 GBI 产品,能力持续增强

对于 Java 团队来说,这意味着你可以在几天内搭建一个"对话式数据查询"系统,而不需要投入大量人力去造轮子。