AI 能写 SQL 吗?

Bruce Momjian
https://momjian.us/presentations
知识共享署名许可
最后更新:2026 年 7 月

背景

我之前的演讲《使用 Postgres 构建 MCP 服务器》涵盖了 MCP 和固定查询的 SQL 函数。
https://momjian.us/main/presentations/ai.html#mcp

大纲

  1. 是的,它能
  2. 数据库设置
  3. 机制
  4. 一般文本查询
  5. 数据查询
  6. 局限性

1. 是的,它能:以 JSON 表示的数据

为每个查询使用以下 RAG 数据:

{"order_id": 735, "product": "gears", "amount": 21, "inv_date": "2024-04-12"}
{"order_id": 4423, "product": "pencils", "amount": 13, "inv_date": "2024-02-05"}
{"order_id": 1334, "product": "glue", "amount": 5, "inv_date": "2024-03-25"}

https://momjian.us/main/presentations/ai.html#trenches

SQL 查询可以被生成

什么 SQL 查询会生成最新的发票日期?

要从表中生成最新的发票日期(假设表名为 ‘invoices’),你可以使用以下 SQL 查询:

SELECT MAX(inv_date) AS latest_invoice_date
FROM invoices;

数据库模式必须从 JSON 数据中猜测。

2. 数据库设置

选择了 Pagila 示例数据库:

  • 视频租赁店
  • 72 MB
  • 15 张表
  • 外键
  • 未使用
  • JSON
  • 语义/向量搜索
  • 分区

https://github.com/devrimgunduz/pagila

下载示例数据库

# 下载 Pagila 示例数据库
rm -rf pagila-master master.zip
wget -q https://github.com/devrimgunduz/pagila/archive/refs/heads/master.zip
unzip master.zip
# 安装 pgvector 二进制文件
cd /u/src/gen/pgvector/pgvector/
git pull origin
make clean
make install
cd -
# 创建 pagila 数据库
su postgres -c 'dropdb pagila'
su postgres -c 'createdb pagila'

https://github.com/pgvector/pgvector

安装示例数据库

su postgres <<END
export PGUSER='postgres'
export PGDATABASE='pagila'
# 安装 pgvector 扩展
psql -c 'CREATE EXTENSION VECTOR' pagila
# 加载数据库
psql -f pagila-master/pagila-schema.sql
psql -f pagila-master/pagila-data.sql
psql -f pagila-master/pagila-schema-jsonb.sql
pg_restore -U postgres -d pagila pagila-master/pagila-data-apt-jsonb.backup

安装示例数据库(续)

# 删除未使用的表
psql -c 'DROP TABLE payment CASCADE;'
psql -c 'DROP TABLE packages_apt_postgresql_org;'
psql -c 'DROP TABLE packages_yum_postgresql_org;'
# 启用查询日志
psql -c "ALTER SYSTEM SET log_statement = 'all';"
psql -c "SELECT pg_reload_conf();"
END

Pagila 的表

\dt
List of tables
Schema | Name | Type | Owner
--------+-----------------------------+-------+----------
public | actor | table | postgres
public | address | table | postgres
public | category | table | postgres
public | city | table | postgres
public | country | table | postgres
public | customer | table | postgres
public | film | table | postgres
public | film_actor | table | postgres
public | film_category | table | postgres
public | film_embedding | table | postgres
public | inventory | table | postgres
public | language | table | postgres
public | rental | table | postgres
public | staff | table | postgres
public | store | table | postgres

Pagila 模式

https://dbdiagram.io

Pagila 主表

Pagila 电影表

Pagila 商店表

3. 机制

将文本查询转换为 SQL 查询需要访问:

  • 表名
  • 列名
  • 列约束
  • PRIMARY KEY、UNIQUE
  • 外键、REFERENCES
  • CHECK
  • 对象注释,特别是列注释 *
  • 数据库和模式名称
  • 表数据

https://www.youtube.com/watch?v=S5ezVVJhQmE

  • https://momjian.us/main/blogs/pgblog/2026.html#March_16_2026

RAG

在系统上下文中传递数据库模式,例如 pg_dump --schema-only

生成式 AI 提示

系统用户
上下文用户请求
限制
个性

https://blog.promptlayer.com/system-prompt-vs-user-prompt-a-comprehensive-guide-for-ai-prompts/

MCP

Postgres MCP Pro 提供的工具

  • 对象信息
    • list_schemas()
    • list_objects()
    • get_object_details()
  • SQL
    • execute_sql()
  • 性能分析
    • explain_query()
    • get_top_queries()
    • analyze_workload_indexes()
    • analyze_query_indexes()
    • analyze_db_health()

https://github.com/crystaldba/postgres-mcp
https://momjian.us/main/presentations/ai.html#mcp

SQL 访问

使用 Claude Code CLI、Opus 5.5

  • 在命令 shell 中运行
  • 直接通过 psql 发出 SQL 查询
  • 需要付费订阅或按 token 定价
  • 不是 Claude Desktop 的 Claude Code 选项

https://code.claude.com/docs/en/quickstart
https://www.iwoszapar.com/p/claude-code-cli-vs-desktop

强大的 SQL

Claude Code 通过 psql 可以查询和修改

  • 数据库状态,例如数据库版本和存储大小
  • 对象属性,例如数据库模式
  • 用户数据

4. 一般文本查询

告诉我关于本地托管的 pagila 数据库。

pagila 是 MySQL 的 Sakila 示例 DVD 租赁数据库的 PostgreSQL 移植版。这里的副本与标准版本相比发生了很大变化:它缺少 payment 表,某些表有更多数据,并添加了几张额外的表。

服务器

  • PostgreSQL devel,一个开发构建。它从 /usr/local/pgsql 运行,数据在 /u/pgsql/data。
  • 数据库为 72 MB,所有者为 postgres,一切都在 public 模式中。
  • 没有 claude 数据库角色。我以 postgres(超级用户)身份无密码进入,只运行了只读查询。

发出的 SQL 查询:PostgreSQL 版本

Claude Code 从命令行运行 psql:

psql -d pagila -c "select version();"

数据库大小

-- 内部查询
select pg_size_pretty(pg_database_size('pagila')) size,
pg_catalog.pg_get_userbyid(datdba) owner
from pg_database
where datname='pagila'

关系列表

-- 内部查询
select n.nspname, c.relname, c.relkind, c.reltuples::bigint est_rows,
pg_size_pretty(pg_total_relation_size(c.oid)) size
from pg_class c join pg_namespace n on n.oid=c.relnamespace
where n.nspname not in ('pg_catalog','information_schema','pg_toast') and
c.relkind in ('r','p','v','m')
order by c.relkind, pg_total_relation_size( c.oid) desc

其他查询主题

  • 获取匹配的模式
  • 获取匹配的已安装扩展
  • 获取匹配的函数
  • 获取匹配的类型
  • 获取匹配的要描述的关系
  • 获取关于一个关系的一般信息
  • 获取一个关系的每列信息
  • 获取此表的索引
  • 获取此表的外键约束
  • 获取引用此表的外键
  • 获取此表的行级策略
  • 获取此表的扩展统计信息
  • 获取发布此表的发布
  • 获取排除此表的发布
  • 获取此关系的触发器
  • 获取继承父表
  • 获取子表

5. 数据查询:员工

哪位员工卖出了最多的视频租赁?显示 SQL 查询。

SELECT s.staff_id, s.first_name, s.last_name,
s.store_id, count(*) AS rentals
FROM rental r JOIN staff s USING (staff_id)
GROUP BY s.staff_id
ORDER BY rentals DESC
LIMIT 5;

商店查询

哪家商店卖出了最多的视频租赁?显示 SQL 查询。

SELECT i.store_id, count(*) AS rentals
FROM rental r JOIN inventory i USING (inventory_id)
GROUP BY i.store_id
ORDER BY rentals DESC
LIMIT 5;

这是基于库存商店而非员工商店的报告,可能假设员工偶尔会在不同的商店工作。

电影类型查询

哪种电影类型的视频租赁最少?显示 SQL 查询。

SELECT c.category_id, c.name AS genre, count(r.rental_id) AS rentals
FROM category c LEFT JOIN film_category fc USING (category_id)
LEFT JOIN inventory i USING (film_id)
LEFT JOIN rental r USING (inventory_id)
GROUP BY c.category_id, c.name
ORDER BY rentals ASC, genre;

演员查询

哪位演员出演了我们库存中最多的电影?显示 SQL 查询。

SELECT a.actor_id, a.first_name, a.last_name,
count(DISTINCT fa.film_id) AS films_in_inventory
FROM actor a JOIN film_actor fa USING (actor_id)
WHERE EXISTS (SELECT 1 FROM inventory i WHERE i.film_id = fa.film_id)
GROUP BY a.actor_id
ORDER BY films_in_inventory DESC
LIMIT 5;

客户查询

哪位客户在 2022 年租了最多的视频?显示 SQL 查询。

SELECT c.customer_id, c.first_name, c.last_name, count(*) AS rentals_2022
FROM rental r JOIN customer c USING (customer_id)
WHERE r.rental_date >= '2022-01-01' AND
r.rental_date < '2023-01-01'
GROUP BY c.customer_id
ORDER BY rentals_2022 DESC
LIMIT 5;

视频租赁查询

哪些视频在 2023 年从未被租过?显示 SQL 查询。

SELECT f.film_id, f.title,
EXISTS (SELECT 1
FROM inventory i
WHERE i.film_id = f.film_id) AS in_inventory
FROM film f
WHERE NOT EXISTS (SELECT 1
FROM rental r JOIN inventory i USING (inventory_id)
WHERE i.film_id = f.film_id AND
r.rental_date >= '2023-01-01' AND
r.rental_date < '2024-01-01')
ORDER BY in_inventory DESC, f.title;

不清楚“视频”是指所有电影还是仅指库存中的电影,因此输出按库存状态排序。

语言查询

哪种视频语言从 2022 年到 2023 年的绝对(而非百分比)租赁增长最高?显示 SQL 查询。

SELECT l.name AS language,
count(*) FILTER (WHERE r.rental_date >= '2022-01-01' AND
r.rental_date < '2023-01-01') AS rentals_2022,
count(*) FILTER (WHERE r.rental_date >= '2023-01-01' AND
r.rental_date < '2024-01-01') AS rentals_2023,
count(*) FILTER (WHERE r.rental_date >= '2023-01-01' AND
r.rental_date < '2024-01-01') -
count(*) FILTER (WHERE r.rental_date >= '2022-01-01' AND
r.rental_date < '2023-01-01') AS change
FROM language l LEFT JOIN film f ON f.language_id = l.language_id
LEFT JOIN inventory i ON i.film_id = f.film_id
LEFT JOIN rental r ON r.inventory_id = i.inventory_id
GROUP BY l.language_id, l.name
ORDER BY change DESC;

逾期租赁查询

哪些客户的视频逾期最严重?显示 SQL 查询。

WITH overdue AS (
SELECT r.customer_id, f.title, current_date -
(r.rental_date + f.rental_duration * interval '1 day')::date AS days
FROM rental r JOIN inventory i USING (inventory_id)
JOIN film f USING (film_id)
WHERE r.return_date IS NULL AND
r.rental_date + f.rental_duration * interval '1 day' < now() )
SELECT c.customer_id AS id,
c.first_name || ' ' || c.last_name AS customer,
c.email,
count(*) AS n_overdue,
max(days) AS max_days,
sum(days) AS total_days,
string_agg(title, ', ' ORDER BY days DESC) AS titles
FROM overdue JOIN customer c USING (customer_id)
GROUP BY 1, 2, 3
ORDER BY max_days DESC, n_overdue DESC, total_days DESC
LIMIT 12;

向商店添加海拔

ALTER TABLE store ADD altitude INTEGER;
UPDATE store SET altitude = random(0, 4000);

海拔查询

哪家商店海拔最高?显示 SQL 查询。

-- 内部查询
select table_name||'.'||column_name||' '||data_type
from information_schema.columns
where table_schema='public' and
table_name in ('store','address','city','country')
select table_name||'.'||column_name
from information_schema.columns
where table_schema='public' and
column_name ~* 'alt|elev|height|lat|lon|geo|loc'

海拔查询(续)

select col_description('store'::regclass,
(select attnum
from pg_attribute
where attrelid='store'::regclass and attname='altitude'))
SELECT s.store_id, s.altitude, a.address, c.city, co.country
FROM store s JOIN address a USING (address_id)
JOIN city c USING (city_id)
JOIN country co USING (country_id)
ORDER BY s.altitude DESC NULLS LAST
LIMIT 5;

没有给出单位。该列没有描述。所有 500 家商店的值从 1 到 4000,暗示是米。

添加 SQL 注释

COMMENT ON COLUMN store.altitude IS 'units are in feet';

海拔查询(带注释)

哪家商店海拔最高?显示 SQL 查询。

-- 内部查询
select col_description('store'::regclass,
(select attnum
from pg_attribute
where attrelid='store'::regclass and attname='altitude'))

海拔查询(带注释,续)

SELECT s.store_id,
s.altitude AS altitude_ft,
round(s.altitude * 0.3048) AS altitude_m,
a.address, c.city, co.country
FROM store s JOIN address a USING (address_id)
JOIN city c USING (city_id)
JOIN country co USING (country_id)
ORDER BY s.altitude DESC NULLS LAST
LIMIT 5;

使用 AI 添加 SQL 注释

向商店的海拔列添加 SQL 注释,说明单位是英尺。显示 SQL 查询。

-- 内部查询
COMMENT ON COLUMN store.altitude IS 'Altitude of the store, in feet';

6. 局限性

真实世界的数据库很复杂:

  • 更多的对象和行
  • 对象名称不太一致
  • 数据不太一致
  • 缺少约束

此外,我的示例大多是只读的,没有安全控制。

基准测试历史

2017:WikiSQL
2018:Spider
2023:BIRD
2024:Spider 2.0
2026:BIRD-Verified

https://www.youtube.com/watch?v=Hty5YMwvMsQ
https://www.flickr.com/photos/187543158@N07/

提高准确性

  1. 对象名称
  2. 引用完整性
  3. SQL 注释
  4. 数据一致性
  5. LLM 提示
  6. 机器学习

结论

https://momjian.us/presentations
https://www.flickr.com/photos/gozalewis/

Logo

更多推荐