我最初学 SQL 的时候,干过一件特别蠢的事:把《SQL 必知必会》从头翻到尾,语法都背下来了,结果真到公司写查询,连个去重都写得战战兢兢。后来才发现,SQL 这门东西,光看书是学不会的,得踩坑、得调优、得看执行计划、得被人指出“你这条 SQL 会把生产库查挂”,才能真正变成自己的。最近我翻了一下大家搜得最多的 SQL 相关热词,很有意思:一半是“sql server 2022 安装教程”“sql 零基础”“sql 语句大全及用法”这类入门需求,另一半是“慢 sql 优化”“sql 注入”“sql 窗口函数”“sql 面试题”这类进阶话题。这其实正好对应学 SQL 的两道坎——先把环境和基础语法搞定,然后跳到性能、安全和实际业务场景。这篇笔记我就顺着这条路径来写,把我这些年攒下的实操经验、踩过的坑、以及干活和面试时反复用到的套路,一次性整理清楚。
1. 学 SQL 之前,先弄明白选型和环境搭建
很多新手搜“sql 学习”,上来就跟着教程装个 SQL Server 2008 R2,结果装到一半卡在某个组件上,或者装完了连接不上,一天时间没了。数据库选型这件事,真的值得先花十分钟想清楚。
1.1 数据库到底怎么选
我见过不少人在选数据库时纠结很久,其实如果只是学 SQL 语法和基本功,主流关系型数据库的差别没有想象中大。选择时主要看你所在的团队和业务生态:
- SQL Server:Windows 环境下企业用得非常多,热搜词里 SQL Server 相关占了很大比重,说明很多人在用。适合 .NET 技术栈或传统企业项目,安装包大、对 Windows 服务依赖强,但 SSMS(SQL Server Management Studio)确实好用。
- MySQL:互联网项目最普及,社区资料多,安装轻量,无论是学语法还是以后找工作,基本都是首选。
- PostgreSQL:这些年势头很猛,功能丰富,对复杂查询和 JSON 支持好。如果涉及地理空间或分析类场景,PG 值得优先考虑。
- SQLite:嵌入式、轻量,适合做本地练习或小工具,连服务器都不用装,入门尝鲜成本最低。
我的建议是:如果你只是想学 SQL 本身,先用 SQLite 或 MySQL 把语法跑熟,再根据自己的职业方向去补指定数据库的高级特性。语法 80% 是通用的,换库的成本没有想象中高。但如果目标是去用 SQL Server 的企业,那直接装 SQL Server 2019 或 2022 练习也没问题。
1.2 SQL Server 版本选择的坑
热搜词里“sql server 2008 r2 下载”“sql server 2019 安装教程”“sql server 2022 安装教程”都出现了。这里我要啰嗦一句:2008 R2 那都是十几年前的老东西了,虽然经典,但微软早已停止支持,新学的人真没必要装。如果机器配置一般,装 SQL Server 2019;追求新特性、机器也扛得住,就装 2022。两个版本在基础学习和日常开发上差别不大,真正影响体验的是安装时的一项关键配置。
1.3 安装时最容易埋雷的身份验证模式
SQL Server 安装过程中,有一个“身份验证模式”的选项,默认是 Windows 身份验证模式。如果你只是本机自用,选这个没问题;但如果后续要用 sa 账号通过 JDBC、ODBC 或 DBeaver 连接,就必须选“混合模式”,并且给 sa 设置一个强密码。
这个坑我踩过不止一次:装完 SQL Server,拿 JDBC 连,死活报 用户 'sa' 登录失败(也就是热词里那个 [28000] 错误),原因就是安装时选了 Windows 身份验证,sa 账号被禁用或者没有密码策略。解决办法有两个:
- 用 Windows 身份验证登录 SSMS,在服务器属性里切到“安全性”,把身份验证模式改成“SQL Server 和 Windows 身份验证模式”,然后在“安全性 -> 登录名 -> sa”里启用账号并设置密码。
- 直接执行一条命令:
sql复制ALTER LOGIN sa ENABLE;
GO
ALTER LOGIN sa WITH PASSWORD = '你的强密码';
GO
改完记得重启 SQL Server 服务。这里多说一句:sa 是数据库超级管理员,生产环境千万别用 sa 去连业务应用,权限太大,出事就是全线崩。给应用单独建一个最小权限账号,是第一天就要养成的习惯。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础语法里最容易被绊倒的五个细节
这一节我挑的是热搜词里出现频率高、但在教科书里往往一句话带过的坑。这些坑你自己写代码时踩一次,印象能记十年。
2.1 同时出现 AND 和 OR 时的执行顺序
热搜词里有一条是“sql 当中同时有 and 和 or 的执行顺序”,问的人这么多,是因为这个坑太容易踩了。SQL 里 AND 的优先级高于 OR,这意味着:
sql复制SELECT * FROM user
WHERE status = 1 OR level = 3 AND city = '上海';
这条语句的实际逻辑是:
sql复制WHERE status = 1 OR (level = 3 AND city = '上海');
如果你本意是“status 为 1,或者 level 为 3,并且 city 为上海的用户”,那其实你不需要括号,因为 AND 已经先执行了。但如果你本意是“(status 为 1 或 level 为 3)并且 city 为上海”,就必须写成:
sql复制WHERE (status = 1 OR level = 3) AND city = '上海';
我的建议非常简单粗暴:只要条件超过两个,就一律用括号把逻辑分组写清楚。不要依赖优先级,因为三个月后的你自己看到这条 SQL,大概率也会懵。
2.2 BETWEEN AND 的边界问题
“sql between and 的用法总结”这个热词,说明很多人对边界条件不放心。BETWEEN 是 闭区间,也就是包含两端:
sql复制SELECT * FROM order_table
WHERE amount BETWEEN 100 AND 200;
等价于:
sql复制SELECT * FROM order_table
WHERE amount >= 100 AND amount <= 200;
BETWEEN 本身不复杂,坑在跟日期类型配合的时候。很多人想查“1月份的数据”,会写:
sql复制SELECT * FROM order_table
WHERE order_date BETWEEN '2024-01-01' AND '2024-01-31';
这个写法在 order_date 是纯 date 类型时没问题,但如果 order_date 是 datetime 或 timestamp,那么 2024-01-31 00:00:00 之后的数据会被漏掉——1月31日这整天都查不全。正确做法是:
sql复制SELECT * FROM order_table
WHERE order_date >= '2024-01-01'
AND order_date < '2024-02-01';
这个“左闭右开”的写法是处理日期区间的通用惯例,不管在 SQL Server、MySQL 还是 PostgreSQL 里都适用。
2.3 去重查询:DISTINCT 和 GROUP BY 不要混为一谈
热词里好几条跟去重有关:“sql 语句去重查询”“清洗---sql 语句去重”。去重场景在数据清洗时特别频繁,但很多人没搞清楚 DISTINCT 和 GROUP BY 的区别。
DISTINCT 是对查询结果整行去重,只要查出来的所有列完全一样,才合并为一行。比如:
sql复制SELECT DISTINCT city FROM user;
这是最常见的“看某个字段有哪些唯一值”。但如果你写成:
sql复制SELECT DISTINCT city, age FROM user;
它去重的依据是 city + age 的组合,不是单独看 city。很多人想要“每个城市取一个样例用户”时,这样写往往不是想要的结果。
GROUP BY 更适合“去重 + 聚合”的场景:
sql复制SELECT city, COUNT(*) FROM user GROUP BY city;
这个查询按城市分组统计人数。它的可扩展性比 DISTINCT 强很多,因为可以带聚合函数。性能上,小表两者差距不大,大表则要看执行计划和索引情况,不能一概而论。我的经验是:简单去重看字段用 DISTINCT,分组统计用 GROUP BY,两者不要互相替代。
还有个细节:如果你要按某个字段去重,同时还想查出该字段对应的其他字段,DISTINCT 做不到“随便取一个非分组字段”,这时候要么用 GROUP BY + MIN/MAX,要么上窗口函数(后面专门讲)。
2.4 空值处理:NULL 是个特立独行的东西
“sql 去除空值”这个热词背后,是很多人在数据清洗时被 NULL 折腾疯了。NULL 和空字符串不一样,NULL 表示“未知”,空字符串是“知道,但为空”。两者在排序、去重、比较时的表现完全不同。
判断一个字段是 NULL,必须用 IS NULL,不能写 = NULL。因为任何与 NULL 的常规比较,结果都是“未知”而不是真或假:
sql复制SELECT * FROM user WHERE phone IS NULL;
如果想在查询结果里把 NULL 替换成默认值,用 COALESCE 或数据库各自的 ISNULL/IFNULL:
sql复制SELECT
name,
COALESCE(phone, '未填写') AS phone_display
FROM user;
COALESCE 是标准 SQL,传多个参数,返回第一个非 NULL 值,比 ISNULL(SQL Server)或 IFNULL(MySQL)更通用。还有一个容易忽略的点:在用 COUNT(字段) 统计时,NULL 不会计入;但 COUNT(*) 会计入整行。想统计“有多少个用户填了手机号”,应该用 COUNT(phone),而统计用户总数用 COUNT(*)。
2.5 WITH AS:让复杂查询像搭积木一样清晰
“sql with as 用法”能上热词,说明这个语法确实是高频刚需。WITH AS 也就是公用表表达式(CTE),它的核心价值是把复杂查询拆成一段段有名字的中间结果,读起来像流水账,而不是一大坨嵌套子查询。
一个经典场景:先找出“每个城市的用户数”,再从中筛出“用户数超过 100 的城市”:
sql复制WITH city_user_cnt AS (
SELECT city, COUNT(*) AS user_cnt
FROM user
GROUP BY city
)
SELECT * FROM city_user_cnt
WHERE user_cnt > 100;
如果不用 CTE,要写成嵌套子查询,一眼看过去容易晕。CTE 还能递归,比如查组织结构里的层级关系,但实际工作中我会提醒一句:递归 CTE 要设置好递归深度限制,否则数据量大时跑起来非常吓人。在 SQL Server 里可以通过 OPTION (MAXRECURSION 0) 调整,但要在确认数据量可控时才敢这么做。
3. 窗口函数:让 SQL 能力上一个台阶的关键
热词里“sql 窗口函数”能上榜,说明大家已经有意识地在进阶了。窗口函数(Window Function)在我看来是区分“会写 SQL”和“写得优雅”的分水岭。很多需要用临时表加多个步骤解决的问题,一个窗口函数就搞定了。
3.1 为什么 GROUP BY 不能满足所有需求
GROUP BY 的痛点是:一旦分组,你只能查询分组字段和聚合结果,其他明细字段就消失了。比如我想看“每个部门薪资最高的员工是谁”,用 GROUP BY 很难优雅地写出来,因为需要把员工明细和分组聚合结果关联起来。窗口函数正是为了解决这类问题而生的——它可以在不折叠行的前提下,为每一行计算基于某个分组范围的聚合值。
3.2 三个排名函数:ROW_NUMBER、RANK、DENSE_RANK
这三个函数是窗口函数里最常用的,面试和工作中都逃不掉。它们的区别主要体现在对并列排名的处理上:
| 函数 | 相同值排名 | 序号是否连续 | 典型场景 |
|---|---|---|---|
| ROW_NUMBER | 强制分先后 | 连续 | 取前 N 条、分页 |
| RANK | 并列占用位次 | 可能跳号 | 并列排名,如竞赛 |
| DENSE_RANK | 并列不占用位次 | 连续 | 密集排名,如绩效等级 |
举个例子:
sql复制SELECT
department,
employee_name,
salary,
ROW_NUMBER() OVER(PARTITION BY department ORDER BY salary DESC) AS rn,
RANK() OVER(PARTITION BY department ORDER BY salary DESC) AS rk,
DENSE_RANK() OVER(PARTITION BY department ORDER BY salary DESC) AS drk
FROM employee;
如果某部门有两个人的工资相同且并列最高:
ROW_NUMBER会给其中一个人 1、另一个人 2(具体的先后顺序取决于排序的稳定性,通常不保证)。RANK会给出 1、1、3 这样的结果(如果并列的是第一名)。DENSE_RANK会给出 1、1、2。
实际干活时,只要我不需要并列排名,一律用 ROW_NUMBER,因为它的行为最可预测,配合 WHERE rn = 1 可以直接实现“分组取 Top 1”。
3.3 分组 TopN 的完整写法
“每个部门薪资最高的前三名”,这种题面试必考,我用窗口函数三行写完:
sql复制WITH ranked AS (
SELECT
department,
employee_name,
salary,
ROW_NUMBER() OVER(PARTITION BY department ORDER BY salary DESC) AS rn
FROM employee
)
SELECT department, employee_name, salary
FROM ranked
WHERE rn <= 3;
这个思路记住以后,很多变体题都是换汤不换药:按用户分组取最近一次订单、按商品分组取库存前几名,都是同一个套路。
窗口函数还可以做累计值,比如算“每月累计销售额”:
sql复制SELECT
sales_month,
sales_amount,
SUM(sales_amount) OVER(ORDER BY sales_month) AS cumulative_amount
FROM monthly_sales;
这里没有写 PARTITION BY,说明对整个结果集按月份排序做累计。如果不加 ORDER BY,那就是全表求和,每一行都显示总数——这两个写法结果完全不同,别搞混。
4. 慢 SQL 优化:不要等出事了才想起来
“慢 sql 优化”“sql 优化”“并行 sql 优化”这些热词说明大家都被慢查询折磨过。SQL 优化这件事,核心就三步:定位、看执行计划、改索引或改写。
4.1 怎么定位慢 SQL
不同数据库定位方式不同。SQL Server 里可以查系统动态管理视图(DMV),找出耗时最长的查询:
sql复制SELECT TOP 10
qs.total_elapsed_time / qs.execution_count AS avg_elapsed_time,
qs.execution_count,
SUBSTRING(st.text, (qs.statement_start_offset/2)+1,
((CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(st.text)
ELSE qs.statement_end_offset
END - qs.statement_start_offset)/2 + 1)) AS statement_text
FROM sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st
ORDER BY avg_elapsed_time DESC;
MySQL 则开启慢查询日志:
ini复制slow_query_log = ON
long_query_time = 1
然后去慢日志里捞执行时间超过 1 秒的 SQL。日常开发中,我习惯先确认是“单次跑得慢”还是“执行很多次累加起来慢”。前者多半是查询本身或索引问题,后者可能是 N+1 查询或连接池配置问题,两种的优化方向完全不一样。
4.2 执行计划告诉你真相
优化 SQL 最忌讳猜。你觉得“这条 SQL 这么简单肯定很快”,结果一跑就是几秒,问题往往在表的数据量、索引缺失或隐式类型转换上。
读执行计划时,我主要看几个东西:
- 表扫描(Table Scan / Seq Scan):表示整张表全扫了一遍,数据量一上来就跑不动。
- 索引查找(Index Seek):通过索引直接定位,通常才是我们想要的。
- 键查找(Key Lookup):通过非聚集索引找到主键,再回表查整行,如果返回行数很多,也容易成为瓶颈。
- 预估行数 vs 实际行数:如果两者差距大,统计信息可能过期,需要更新统计信息。
最典型的优化手段就是给 WHERE 条件里的字段建索引。比如:
sql复制SELECT * FROM order_table WHERE user_id = 12345;
如果 user_id 没索引,每条查询都全表扫一遍,数据量到百万级就有明显感觉了。加一个普通索引就能解决:
sql复制CREATE INDEX idx_order_user_id ON order_table(user_id);
4.3 复合索引的最左前缀原则
实际查询往往不止一个字段,比如:
sql复制SELECT * FROM order_table
WHERE user_id = 12345 AND order_date >= '2024-01-01';
这种场景建 (user_id, order_date) 复合索引比较合适。但要注意最左前缀原则:复合索引只在查询条件包含最左边的字段时才生效。如果查询只写 WHERE order_date >= '2024-01-01',上面的复合索引就用不上。索引顺序是把“等值条件”字段放前面,把“范围条件”字段放后面,这是一个很实用的设计规则。
4.4 并行 SQL 优化是什么
“并行 sql 优化”这个热词比较专业,指的是 SQL Server / Oracle 里通过并行执行计划把一个查询拆成多个线程同时跑,来缩短整体耗时。并行度不是越高越好,每个并行线程都有调度和内存开销,并发请求多的时候反而可能造成 CPU 耗尽。遇到并行导致的性能问题,可以在查询级别用提示控制最大并行度:
sql复制SELECT * FROM big_table
WHERE status = 1
OPTION (MAXDOP 4);
MAXDOP 限制了这条查询最多用 4 个线程并行。生产环境中,我的经验是:先确认系统当前的 MAXDOP 配置和并发压力再调,不要盲目全局关闭并行。很多 DBA 会把实例级 MAXDOP 设置为不超过 CPU 核数的一半,避免并行查询把服务器资源吃满。
5. SQL 注入攻防:这是安全底线,不是可选技能
热词里有好几条是关于 SQL 注入的:“sql 注入万能密码绕过”“sql 注入简单例子”“ctf sql 注入绕过登录”。看到这些搜索,我一方面觉得大家有安全意识是好现象,另一方面想强调:理解注入原理是为了防御,而不是绕过别人的系统。这里我按“原理 -> 例子 -> 防御”来讲清楚。
5.1 注入是怎么发生的
SQL 注入的本质是:应用程序把用户输入直接拼接进 SQL 字符串,改变了 SQL 原有的语法结构。它属于代码层输入校验缺失,跟数据库版本、后端语言关系不大,任何关系型数据库都可能中招。
比如一个登录逻辑,代码里这么写:
java复制String sql = "SELECT * FROM user WHERE username = '" + username + "' AND password = '" + password + "'";
当用户在用户名的输入框里输入 ' OR '1'='1,密码随便填时,拼接出来的 SQL 变成:
sql复制SELECT * FROM user
WHERE username = '' OR '1'='1' AND password = '随便填';
因为 AND 优先级高于 OR,这个条件会变成 username = '' OR ('1'='1' AND password='随便填'),只要数据库里存在任何一条记录,或者干脆 '1'='1' 恒为真,可能就绕过登录了——这就是“万能密码”的由来。注意,这只是证明漏洞存在的教学场景,用这个去尝试登录别人的系统属于越权行为,绝对不要碰。
5.2 一个更常见的注入点
登录窗口只是注入的一种入口,实际业务里更容易中招的是动态拼条件的地方,比如搜索功能:
java复制String sql = "SELECT * FROM product WHERE name LIKE '%" + keyword + "%'";
如果用户在关键词里输入 %' OR 1=1 --,SQL 会变成:
sql复制SELECT * FROM product
WHERE name LIKE '%%' OR 1=1 --%';
-- 是注释符号,后面的内容被注释掉,于是整张表都查出来了。这还只是数据越权,如果攻击者发现的是 DELETE、UPDATE 拼接点,代价就更大了。
5.3 防御第一招:参数化查询,根本不用拼字符串
防御 SQL 注入最有效的手段就是参数化查询(Prepared Statement)。它的原理是:SQL 结构先固定,用户输入以参数的形式传入,由驱动层处理,数据库只把它当数据,不参与 SQL 语法解析。
后端 Java 的写法:
java复制PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM user WHERE username = ? AND password = ?");
ps.setString(1, username);
ps.setString(2, password);
Python 里用 %s 占位或参数化 API:
python复制cursor.execute(
"SELECT * FROM user WHERE username = %s AND password = %s",
(username, password)
)
C# 的写法:
csharp复制using (var cmd = new SqlCommand(
"SELECT * FROM user WHERE username = @username AND password = @password",
conn))
{
cmd.Parameters.AddWithValue("@username", username);
cmd.Parameters.AddWithValue("@password", password);
}
这样无论用户输入什么,都只是一个字符串,无法改变 SQL 结构。任何能把用户输入拼进 SQL 的地方,一律参数化,没有任何例外。这是我写代码的铁律。
5.4 防御还要注意的其他习惯
参数化能挡住绝大多数注入,但还不够。实际工程里我还会做几件事:
- 最小权限原则:应用账号只给
SELECT权限,必要时才给INSERT/UPDATE,绝不用sa或root连业务库。这样就算被注入,破坏也被限制在可控范围。 - ORM 框架的防注入能力要用起来:比如 MyBatis 里写
${}会直接拼字符串,用#{}才是预编译占位符。新手拿别人的项目练习时,看到${}要格外警惕。 - 对特殊输入做校验:虽然参数化已经能应对主要风险,但在传参进入字符串函数、排序字段等场景时,还是建议加白名单校验,防止生成非常规 SQL。
“sql 注入”这个热词能持续这么多年,说明它从来没有消失。每次见到有人还在用字符串拼接 SQL,我就想说:你今天省的这几分钟维护成本,明天可能用一场数据灾难来还。
6. 工具链与报错排查:从安装到导入的日常必踩坑
这一部分我汇总一下热词里出现的工具类问题,包括“sql server 安装”“dbeaver 导入 sql 文件”“heidisql 导入 sql 文件”“sql server 显示行号”“sql prompt”等。
6.1 sa 登录失败后,完整的排查思路
热词那条 [28000] [microsoft][odbc driver 17 for sql server][sql server]用户 'sa' 登录 我太熟悉了。遇到这种错误,按照下面的顺序排查,基本都能解决:
- 确认身份验证模式:安装时如果选了 Windows 身份验证,
sa默认无法用 SQL 账号登录。用 Windows 身份登录 SSMS,检查服务器属性里是不是混合模式。 - 确认 sa 是否启用:右键“登录名 -> sa”,查看状态。很多安装流程里 sa 默认是禁用的。
- 确认密码策略:如果密码太简单,SQL Server 可能不允许登录;改用强密码后再试。
- 确认网络和端口:ODBC 连接时,如果在另一台机器上连,需要确保 SQL Server 的 TCP/IP 协议已启用,并且防火墙放行了 1433 端口。
顺便说个 SQL Server 里查看当前连接和进程的小技巧:
sql复制SELECT session_id, login_name, host_name, program_name
FROM sys.dm_exec_sessions;
排查连接问题的时候,看一眼这张视图能省不少时间。
6.2 DBeaver 导入 SQL 文件不要用错方式
DBeaver 是一个跨平台的免费数据库客户端,对 MySQL、PostgreSQL、SQL Server 都支持。很多人用 DBeaver 导入 SQL 文件,卡在“执行了但看不到数据变化”。
导入方式取决于你的 SQL 文件内容:
- 如果是
.sql脚本(含建表、插入语句),在左侧数据库连接上右键 -> 工具 -> 执行脚本,然后选择文件执行。 - 如果只想单条执行,打开 SQL 编辑器,把文件内容粘贴进去,按
Ctrl+Enter执行选中的语句。
导入时最容易出的问题有两个:一是文件编码不对,SQL 文件里有中文时,DBeaver 默认可能按 UTF-8 读,但文件实际是 GBK 编码,执行后中文变乱码。遇到这个,在导入前用编辑器把文件转成 UTF-8 即可。二是脚本里带了 USE database 或 CREATE DATABASE,而你在连接里没权限,会报错。建议先确认脚本是否包含建库语句,有的话要么先手动建库,要么在连接权限足够的前提下执行。
6.3 HeidiSQL 和 SQL Prompt 这类小工具的价值
HeidiSQL 是另一个轻量级数据库客户端,尤其适合 MySQL,很多人拿来替代 Navicat(收费)。它也支持导入 SQL 文件:文件 -> 运行 SQL 文件。相比 DBeaver,HeidiSQL 的界面更轻、启动更快,但跨数据库支持不如 DBeaver 广。我自己的习惯是:日常连 MySQL 用 HeidiSQL,连接多种数据库或需要一些高级功能时用 DBeaver。
SQL Prompt 则是 SQL Server 用户的效率神器,它是 Redgate 出的 SSMS 插件。最有用的特性是智能提示和自动补全,写复杂 SQL 时能少打很多字,还能帮你规范格式化。不过它是付费工具,试用期过后如果不想花钱,SSMS 自带的 Intellisense 其实也够用,只是没那么聪明。
6.4 “SQL Server 显示行号”这种小需求
热词里还有“sql server 显示行号”,这个其实是在 SSMS 的查询编辑器里设置:工具 -> 选项 -> 文本编辑器 -> Transact-SQL -> 常规,勾选“行号”。别小看这个功能,当 SQL Server 报错告诉你在第 123 行有语法错误时,没有行号就纯靠数,很崩溃。
7. SQL 在现代项目中的几种玩法
基础语法搞定以后,我更想聊点“场景感”的东西,因为现在 SQL 已经从传统报表渗透到各种框架和平台里。热词里有几条特别有意思:“spring authorization server 官方提供了标准建表 sql”“superset 可以实现在图表设计时通过 sql 计算来取数么”“agent 实现把自然语言转换成 sql”“flowable 能写 sql 吗”。这四条代表了四个完全不同的方向,我分别说一下我的理解。
7.1 Spring Authorization Server 的建表 SQL
Spring Authorization Server 是 Spring 官方做的 OAuth 2.1 / OpenID Connect 授权服务器,它确实在官方文档里提供了标准的建表 SQL,用来存储授权信息、客户端注册信息、用户同意记录等。很多人一开始想省事,让 Hibernate 自动建表,但官方文档更推荐在项目初始化时手动执行那套标准脚本,这样表结构完全可控。如果你第一次接触,建议先去官方文档找到 oauth2-authorization-server-schema.sql 这类文件,按你使用的数据库(PostgreSQL、MySQL、SQL Server)选对应版本执行。别指望一套脚本在所有数据库上无痛跑通,至少注释和字段类型会有差异。
7.2 Superset 里的 SQL 取数
Apache Superset 是一个开源的数据可视化平台,关于“能不能在图表设计时通过 SQL 计算来取数”,答案是能。Superset 有两种主要取数模式:
- 数据集模式:先写 SQL 把数据做成虚拟数据集,再在图表里拖字段。
- SQL Lab 模式:直接在 SQL Lab 里写查询,查询结果可以一键“Explorer”进入图表设计。
在图表设计页里,你还可以通过“计算字段”的方式,在已有数据集上写一些简单的 SQL 表达式,比如 SUM(sales) / COUNT(DISTINCT order_id) 来计算客单价。复杂逻辑建议在数据集 SQL 层做,不要在图表层堆太多表达式,一是维护难,二是 Superset 的缓存策略对复杂表达式并不友好。
7.3 AI Agent 把自然语言转换成 SQL
“agent 实现把自然语言转换成 sql”这个话题这两年特别火,业内一般叫 Text-to-SQL 或 NL2SQL。大的方向是用大语言模型理解用户问题,生成 SQL 查询,再把结果返回给用户。我实际体验下来,在窄业务场景里,Text-to-SQL 已经可以做到很高的准确率,比如“帮我查上个月华东区销售额前五的产品”,只要表结构清晰、字段注释规范,生成的 SQL 基本能用。
但这里有个大坑:AI 生成的 SQL 如果不加约束,可能会查询全表、忘记过滤条件,甚至绕过行级权限去取不该看的数据。所以生产环境部署这类 Agent 时,我至少要加三层防护:
- 只读模式:给 Agent 的数据库账号只授予
SELECT权限,杜绝生成DELETE、UPDATE的可能。 - SQL 白名单校验:对生成结果做正则或语法解析,拦截多语句、危险函数。
- 预置权限过滤:在系统提示词里就注入“只能查当前用户有权限的数据”的约束,同时在 SQL 层自动追加过滤条件。
技术再强,安全底线不能丢。这跟前面讲 SQL 注入的逻辑是一模一样的。
7.4 Flowable 能写 SQL 吗
Flowable 是一个工作流引擎,经常有人问“能不能在流程定义里写 SQL”。答案是:可以,但不建议直接写。Flowable 提供了查询 API 和自定义查询扩展点,更推荐通过 CustomSqlExecutionMapper 这种方式在受控环境里执行自定义 SQL。直接在流程脚本里拼 SQL 是典型的反模式:流程引擎的数据库表结构是内部实现细节,版本升级可能变,你直接操作很可能导致数据不一致。遇到复杂查询需求,正确的做法是查 Flowable 的运行时表(如 ACT_RU_TASK、ACT_HI_PROCINST),或者把业务数据单独落到自己的业务库,再通过 API 关联。
这个问题的本质其实是“工具边界”。SQL 是操作关系型数据的通用语言,但每个框架都有自己的封装逻辑和安全边界,直接写 SQL 是下策,用官方扩展点是上策。
8. SQL 面试题,我总结的几条解题思路
热词里有“sql 面试题”,平时也有人问我面试怎么准备。SQL 面试题翻来覆去就那么几类,把套路练熟了,比刷一百道题管用。
8.1 第一类:条件筛选与聚合
这类题考的是基础语法和逻辑严谨性。比如“统计每个学科成绩大于 70 分的学生人数”。很多人一上来就写 WHERE score > 70 再 GROUP BY,这没问题。但如果题目变成“统计每个学科中,学生成绩大于该学科平均分的人数”,就需要用到子查询或窗口函数:
sql复制SELECT t.course_id, COUNT(*) AS num
FROM (
SELECT
course_id,
student_id,
score,
AVG(score) OVER(PARTITION BY course_id) AS avg_score
FROM score_table
) t
WHERE t.score > t.avg_score
GROUP BY t.course_id;
这种题考的是你能不能想到“先用窗口函数在每个学科内算平均分,再筛选”,基础加一点进阶,是面试官很喜欢的题型。
8.2 第二类:分组 TopN
分组 TopN 前面已经说过,就是 ROW_NUMBER() OVER(PARTITION BY ... ORDER BY ...) 加外层过滤。面试时我建议先写清楚思路,再写代码,因为面试官看你的是思维链条,不是最终结果。
8.3 第三类:连续问题
连续性问题也是经典,比如“查连续登录 3 天以上的用户”。核心思路是先按用户分组,用日期减去行号,得到一个“分组标识”,再按这个标识聚合判断连续天数。思路比如:
sql复制WITH t AS (
SELECT
user_id,
login_date,
DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY login_date) DAY) AS grp
FROM login_log
)
SELECT user_id
FROM t
GROUP BY user_id, grp
HAVING COUNT(*) >= 3;
不同数据库对日期减法的写法有差异,但“日期减行号”这个思路是通用的。这类题目考的是对窗口函数和数据规律的理解,背题没用,理解了以后变体都能应付。
8.4 面试之外的最后一句话
面试题刷多了你会发现,SQL 的核心能力就是三块:基础语法严谨性、窗口函数熟练度、对数据业务语义的理解。前两块靠练,最后一块靠对业务的分析。人家问“查最近七天每天的活跃用户数”,你不只是会写 DATE_SUB,你还得想清楚“活跃用户数”是按用户去重还是按会话计数,这两个口径差不少。
最后分享点我的个人习惯
写了这么多,最后说点实际的。我这些年用 SQL 最深的体会是:看十篇教程,不如亲手跑一遍慢查询优化。遇到瓶颈的时候,不要急着去改代码,先看执行计划,再看索引,再考虑改写。很多问题只是缺一个索引而已,你却在子查询里翻来覆去折腾,浪费时间。
另外一个好习惯是建表的时候就把注释写清楚。字段注释是给别人(包括三个月后的自己)看的,数据库里 created_at 到底是入库时间还是业务发生时间,没注释,后面分析的每一行 SQL 都隐藏着一次灾难。现在我每建一张表,必写 COMMENT,尤其是状态字段,把每个状态的数值含义列得明明白白。
SQL 这门语言看起来简单,但天花板很高。它不像写 Java、Python 那样要记一堆框架,但你把窗口函数、执行计划、索引原理这些东西吃透之后,处理数据的能力会有质的提升。希望这篇笔记能帮你少踩几个我踩过的坑。
