用了几年SQL之后我最大的感受是:不管你是做后端、搞数据分析,还是兼职运维数据库,真正需要“背下来”的常用SQL就那么几块。剩下的绝大多数场景,都是在这几个基础语法上排列组合而已。这篇汇总里我不会事无巨细地罗列手册内容,而是把最常用、最容易踩坑、检索频率最高的SQL语言知识点按实际使用逻辑重新拼一遍,包含查询套路、多表关联、数据清洗、建表改表、慢SQL定位,以及SQL Server安装和连接相关的高频报错。不管你是刚上手SQL的新人,还是碰到问题想快速翻答案的老手,这份清单都能直接当速查工具用。
1. 搞清楚SQL语言的五大家族的用途,比背语法更重要
1.1 一句话区分DQL、DML、DDL、DCL、TCL
很多人一开始学SQL就把注意力全放在SELECT写法和各种函数上,等到真要建库建表、授权、开事务的时候反而反应不过来。实际上SQL虽然看起来命令很多,但它内部是有明确分工的。按我平时的工作习惯,我一般把它们分成五组看,对应关系非常清晰:
| 分类 | 英语全称 | 干什么用 | 常见关键字 | 使用频率 |
|---|---|---|---|---|
| DQL | Data Query Language | 查数据 | SELECT | 最高 |
| DML | Data Manipulation Language | 增、删、改数据 | INSERT、UPDATE、DELETE | 高 |
| DDL | Data Definition Language | 定义库、表结构 | CREATE、ALTER、DROP、TRUNCATE | 中 |
| DCL | Data Control Language | 权限控制 | GRANT、REVOKE | 低 |
| TCL | Transaction Control Language | 事务控制 | COMMIT、ROLLBACK、SAVEPOINT | 中 |
有了这个分类,你看到一条陌生SQL时就能有个初步判断:它到底是在“改数据”还是在“改结构”,两者带来的影响完全不同。例如DELETE删的是几行数据,事务里还能回滚;而DROP、TRUNCATE这类DDL操作会直接影响表本身,很多数据库默认不可回滚,用之前一定要想清楚。
1.2 先建立“数据世界”的基础认知
任何一个SQL语句,最终都逃不过“库、表、行、列、索引”这五个层次。数据库就是一个文件夹,表就是文件夹里的Excel文件,行就是每个记录,列就是每一列的属性字段,索引就是加速查找的目录。
理解了这个模型,你再去记SQL语法会轻松很多。比如:
- 建表:先想清楚有哪些字段、每个字段用什么类型、哪些字段做索引、哪几个字段联合组成唯一键。
- 查询:无非就是从某个表里按条件圈出某些行,再取某些列,再做聚合或排序。
- 修改:先定位你要改的行,再赋值,改了之后确认提交。
数据库里还有个很核心的东西是主键。主键就是每一行的唯一身份证号,理论上不应该为空、不应该重复。实践中最常见的主键类型有三种:自增整数、UUID随机字符串、业务编号。自增整数在MySQL里常用,UUID在分布式场景下常见,业务编号则要结合实际业务保证唯一性。
1.3 常用SQL快速学习路径建议
根据我带新人的经验,最快的学习路径是:
- 学会看的表结构:先能看懂一个表有哪些字段、字段类型。
- 写简单SELECT:查询指定列、WHERE过滤、ORDER BY排序。
- 学会聚合:GROUP BY + COUNT/SUM/AVG,这是数据分析的分水岭。
- 学会多表关联:内连接、左连接。
- 学会子查询和CTE:把复杂需求拆解成小块。
- 学会建表和增删改:能完整搭建一个简单库表。
- 学点窗口函数:解决排名、累计值、环比等复杂统计。
把这个路径走完,你已经能应付绝大多数企业的日常数据需求了。
2. 查询语句背熟“四步走”,所有SELECT都不慌
2.1 SELECT语法的书写顺序和执行顺序是两回事
SELECT是SQL中使用频率最高的语句,几乎每天都要写。但很多人在记语法时只记住了“SELECT字段FROM表WHERE条件”,一旦出现GROUP BY、HAVING、ORDER BY、LIMIT就乱套。
其实你只需要记住两组顺序:
书写顺序:
SELECT 列 FROM 表 WHERE 筛选行 GROUP BY 分组列 HAVING 筛选分组 ORDER BY 排序列 LIMIT 限制行数执行顺序:
FROM 表 WHERE 筛选行 GROUP BY 分组 HAVING 筛选分组 SELECT 取列 ORDER BY 排序 LIMIT 分页这里最容易被忽略的是HAVING和WHERE的区别:WHERE是在分组之前过滤原始行,HAVING是在分组之后再过滤分组结果。举个例子:
SELECT department_id, COUNT(*) FROM employee WHERE salary > 5000 GROUP BY department_id HAVING COUNT(*) > 3这条SQL的意思是:先只看月薪大于5000的员工,再按部门分组,最后只留下人数大于3人的部门。如果你把salary > 5000放到HAVING里,虽然结果可能碰巧一样,但逻辑上已经变了。
2.2 WHERE筛选的常用模板:等于、范围、空值、模糊、集合
日常筛选条件无外乎下面几种,我直接给你整理成模板:
| 场景 | 写法 | 注意点 |
|---|---|---|
| 等值查询 | WHERE status = 1 | 注意字符类型要加引号 |
| 区间查询 | WHERE score BETWEEN 60 AND 90 | 包含边界值 |
| 集合查询 | WHERE city IN ('北京','上海') | 字段类型要匹配 |
| 模糊查询 | WHERE name LIKE '张%' | %在右边通常能走索引 |
| 空值查询 | WHERE remark IS NULL | 不能用= NULL |
| 非空查询 | WHERE remark IS NOT NULL | 空字符串也不是NULL |
| 多条件组合 | WHERE a=1 AND (b=2 OR c=3) | 多条件一定要加括号 |
关于NULL我想多说一句:NULL表示“未定义”,它不等于0,不等于空字符串,任何与NULL直接比较的结果都是NULL,不是TRUE也不是FALSE。所以过滤空值只能用IS NULL,不能写成= NULL。这个错误在小白阶段几乎人人都会犯,哪怕写了好几年SQL,偶尔也会被NULL坑一下。
2.3 GROUP BY与聚合函数:把明细变成汇总
聚合函数是统计需求里绕不开的一组函数,最常用的有:
COUNT(*):统计行数COUNT(字段):统计某字段非NULL个数SUM(字段):求和AVG(字段):求平均MAX(字段):最大值MIN(字段):最小值
它们的共同特点是从多行数据计算出一个值,所以通常会跟GROUP BY搭配使用。如果没有GROUP BY,整张表会被当成一个大组。
最常见的错误是在SELECT里同时出现聚合列和非聚合列。像下面这条SQL在MySQL的默认配置下会直接报错,而某些数据库虽然不报错,返回的数据也是不严谨的:
SELECT name, COUNT(*) FROM employee GROUP BY department_id;你应该做到:SELECT出来的列,要么被GROUP BY包含,要么本身就是聚合函数。
2.4 排序和分页:不同数据库写法差异很大
排序几乎没什么争议,就是ORDER BY。需要注意的一点是:多个排序字段时用逗号分隔,每个字段可以分别指定ASC或DESC。
分页就很值得单独提一句了,因为不同数据库的分页写法完全不同:
| 数据库 | 写法 |
|---|---|
| MySQL / PostgreSQL | LIMIT 20 OFFSET 40 |
| SQL Server | OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY |
| Oracle | FETCH FIRST 20 ROWS ONLY(12c以上)或ROWNUM |
如果做跨数据库开发,这部分一定要封装好,否则换个数据库就裂开。尤其是SQL Server老版本的TOP写法是SELECT TOP 20 * FROM table,但不支持优雅的跳过前40行这种操作,要到2012版本之后才有OFFSET FETCH。
3. 多表关联和子查询:复杂业务的解题核心
3.1 JOIN的正确理解方式:不是画线,是集合运算
很多初学SQL的人一看到多表关联就脑子里一团乱。我的建议是:把JOIN想象成两个集合的组合过程。
假设有两张表:A表存用户,B表存订单。
- INNER JOIN:取交集,只有两边都能匹配上的行。
- LEFT JOIN:左表全保留,右表匹配不上就填NULL。
- RIGHT JOIN:右表全保留,左表匹配不上就填NULL。
- FULL OUTER JOIN:两边都保留,MySQL不支持但可用UNION模拟。
实际业务中,用得最多的是INNER JOIN和LEFT JOIN。RIGHT JOIN我几乎不用,因为把两个表换一下位置就能变成LEFT JOIN,语义更清晰。
JOIN写法的标准模板:
SELECT a.user_name, b.order_id FROM user a INNER JOIN order b ON a.user_id = b.user_id;关于ON后面关联条件的判定,有个隐藏点要注意:如果两个表存在一对多关系,关联后会导致数据成倍增加。比如一个用户有三个订单,JOIN之后这个用户的行会变成三行。假如你又去COUNT(DISTINCT a.user_id)可能没事,但COUNT(b.order_id)就会得到3而不是1。所以写多表SQL之前,先确认关联关系是一对一还是一对多。
3.2 IN与EXISTS其实各有优势
子查询最常见的写法有两种:IN和EXISTS。
SELECT * FROM user WHERE user_id IN (SELECT user_id FROM order); SELECT * FROM user u WHERE EXISTS (SELECT 1 FROM order o WHERE o.user_id = u.user_id);从语义上讲,IN是把子查询结果都查出来再做匹配,EXISTS是逐行判断是否存在。早期的MySQL版本里,子查询性能容易有问题,一般建议用EXISTS。现在版本优化之后,两者在很多场景下差距不大,真正决定性能的关键是有没有索引。
如果你只关心“是否存在”,推荐写EXISTS (SELECT 1 ...),性能上更稳定,可读性也更好。
3.3 CTE:把SQL写成读起来像文章的代码
CTE(Common Table Expression)是用WITH语句定义的临时命名结果集。我第一次接触时觉得这是把简单问题复杂化,后来在复杂SQL里用多了才发现,它才是提高SQL可读性的最重要的工具之一。
举例:我想统计每个部门的员工数,并且找出人数超过10人的部门。用CTE可以这样写:
WITH dept_cnt AS ( SELECT department_id, COUNT(*) AS cnt FROM employee GROUP BY department_id ) SELECT * FROM dept_cnt WHERE cnt > 10;更重要的是,CTE可以链式引用,一个WITH里定义多个结果集,后面的结果集依赖前面的结果集:
WITH a AS (SELECT ... FROM table1), b AS (SELECT ... FROM a, table2), c AS (SELECT ... FROM b) SELECT * FROM c;这种写法比层层嵌套的子查询清晰太多了,我强烈建议任何经常写复杂SQL的人摒弃“老式嵌套子查询”,优先使用CTE。CTE在SQL Server、PostgreSQL、MySQL 8.0、Oracle里都支持,使用门槛并不高。
3.4 一个典型业务场景:取用户最近一笔订单
遇到“取每个用户最近订单”这种需求,如果你的第一反应是GROUP BY再加上LIMIT,那大概率会写错。正确做法是使用窗口函数,或者先排序再按用户分组取第一行。
窗口函数写法如下:
WITH ranked AS ( SELECT user_id, order_id, order_time, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_time DESC) AS rn FROM orders ) SELECT * FROM ranked WHERE rn = 1;PARTITION BY的意思是按用户分组,ORDER BY是在组内排序,ROW_NUMBER()会给每组内每一行编一个序号,序号为1的就是这笔订单。这种方法在MySQL 8.0及以上版本、SQL Server、Oracle、PostgreSQL都适用,是标准窗口函数,非常通用。
4. 数据清洗和统计中常用的SQL处理套路
4.1 去重的完整解决方案:DISTINCT只是入门
很多新人遇到去重就只会写DISTINCT,它确实简单好用:
SELECT DISTINCT city FROM user;但DISTINCT有三个局限:
- 它只能去除查询结果的行重复,不能告诉你哪行该留、哪行该删。
- 在多个字段都重复时才有效,如果只想按某一列去重,其他列又要保留,它做不到。
- 在COUNT里写
COUNT(DISTINCT city)还行,但如果数据量大且没索引,性能压力会很大。
真正解决“按某字段去重并保留其他字段”的正规方案是用窗口函数。以删除重复用户为例:
WITH t AS ( SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM user ) DELETE FROM user WHERE id IN (SELECT id FROM t WHERE rn > 1);如果你想大致探查一下有多少重复数据,可以先用统计写法验证:
SELECT email, COUNT(*) FROM user GROUP BY email HAVING COUNT(*) > 1;4.2 空值的处理:清洗数据避不开的话题
我在前面已经说过了NULL不能用=判断。这里再补充几个空值处理高频函数:
| 场景 | 写法 | 说明 |
|---|---|---|
| 空值替换 | COALESCE(字段, 默认值) | 依次取第一个非NULL值 |
| 简单替换 | IFNULL(字段, 0) | MySQL专用 |
| 空转0 | NULLIF(字段, 0) | 把0值转成NULL |
| 空值参与统计 | SUM(CASE WHEN 字段 IS NOT NULL THEN 字段 ELSE 0 END) | 不遗漏NULL带来的误差 |
COALESCE是标准的SQL函数,几乎所有数据库都支持,它在处理多层默认值时特别方便。比如:
SELECT COALESCE(nickname, real_name, mobile, '匿名用户') AS display_name FROM user;4.3 窗口函数速查表:统计高手和小白的分界线
窗口函数这几年热度很高,因为很多统计场景用普通GROUP BY根本写不出来。它最大的特点是:不改变原有行数,而是给每一行附加一个统计结果。
常用窗口函数:
| 函数 | 作用 | 适用场景 |
|---|---|---|
| ROW_NUMBER() | 顺序编号 | 取第N名、去重 |
| RANK() | 排名,有并列时跳过 | 排行榜 |
| DENSE_RANK() | 排名,有并列时不跳过 | 价格区间排行 |
| SUM() OVER | 移动累计 | 月度累计利润 |
| LAG() | 取上一行值 | 环比计算 |
| LEAD() | 取下一行值 | 同比/下一步状态 |
举个例子,计算每个员工在本部门的薪资排名:
SELECT name, department_id, salary, RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS dept_rank FROM employee;注意RANK和DENSE_RANK的区别:假设薪资前三名分别是10000、9000、9000、8000,RANK排名是1、2、2、4,DENSE_RANK排名则是1、2、2、3。看你业务需要是哪种排名,选对应的。
4.4 CASE WHEN:SQL的if-else逻辑
很多统计需求都需要“按条件打标”,这时CASE WHEN就是标配。它的完整写法是:
SELECT user_id, CASE WHEN order_amount >= 1000 THEN '高消费' WHEN order_amount >= 100 THEN '普通消费' ELSE '低消费' END AS consumption_level FROM orders;CASE WHEN还能和聚合函数组合,实现“多条件统计”。比如统计每个部门男女比例:
SELECT department_id, SUM(CASE WHEN gender = '男' THEN 1 ELSE 0 END) AS male_cnt, SUM(CASE WHEN gender = '女' THEN 1 ELSE 0 END) AS female_cnt FROM employee GROUP BY department_id;5. 建表、增删改、事务:从“只会查”到“能干活”
5.1 常用建表语句模板
建表是DDL,相比SELECT来说,日常改动频率低,但要格外严谨。建完再改的成本远比一开始设计好要高。下面这个建表语句包含了常见字段类型、默认值、注释、主键、索引:
CREATE TABLE user_info ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_name VARCHAR(50) NOT NULL COMMENT '用户名', email VARCHAR(100) DEFAULT NULL COMMENT '邮箱', age INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_user_name (user_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';这里特别提醒几个点:
- 字符集尽量用utf8mb4,不要再用utf8,因为utf8在MySQL里存不下emoji等四字节字符。
- 不要把业务逻辑中的“是/否”字段存成字符串,直接建
TINYINT或BOOLEAN更合适。 - 日期字段能用DATETIME就不要用VARCHAR存,节省空间也便于后续查询。
5.2 增删改的常见注意事项
INSERT、UPDATE、DELETE三个DML语句看似简单,真正用起来有不少细节:
INSERT INTO user_info (user_name, email) VALUES ('张三', 'zhangsan@example.com');还可以用“从查询结果插入”的方式:
INSERT INTO user_info (user_name, email) SELECT name, email FROM temp_user WHERE status = 1;UPDATE的正确姿势是尽量先用WHERE圈定小的范围。我见过不少线上事故是UPDATE漏写了WHERE,导致全表数据被更新成同一个值,那种感觉简直让人怀疑人生:
UPDATE user_info SET age = age + 1 WHERE id = 1001;DELETE也一样,删之前先SELECT查看一下影响范围:
SELECT * FROM user_info WHERE created_at < '2020-01-01'; -- 确认无误后 DELETE FROM user_info WHERE created_at < '2020-01-01';5.3 事务基础:别让数据改一半
多表同时更新或插入数据时,事务就是安全兜底。事务的核心是ACID,但日常工作中你要记住的就一条:一组SQL要么全部成功,要么全部回滚。
START TRANSACTION; INSERT INTO account_flow (account_id, amount) VALUES (1, -100); UPDATE account SET balance = balance - 100 WHERE id = 1; COMMIT; -- 如果中途出问题,执行 ROLLBACK;在SQL Server里对应的是BEGIN TRANSACTION和COMMIT,Oracle里是BEGIN...END包起来。跨数据库写事务时,语法细节要提前确认。
6. 网络热词和报错清单:SQL真正难的地方在实战
6.1 慢SQL优化:一条SQL跑半小时的教训
关于性能调优,很多文章喜欢上来就讲“要加索引”,但实际排查慢SQL有一套完整的流程。根据我自己的经验,可以按照这四步来:
首先,找出慢SQL。MySQL里用慢查询日志,SQL Server里用DMV查询,或者数据库监控平台直接看执行耗时排行。
其次,看执行计划。MySQL用EXPLAIN,SQL Server用SET SHOWPLAN_ALL ON,核心关注type、key、rows三个字段。如果type是ALL,说明全表扫描,大概率有问题。
然后,看索引是否生效。最常见的问题是“函数包裹索引列”导致索引失效。例如:
WHERE YEAR(created_at) = 2022这种写法看起来很自然,但会导致created_at上的索引失效。为了触发索引,更推荐写成范围查询:
WHERE created_at >= '2022-01-01' AND created_at < '2023-01-01'最后,优化SQL本身。不要用SELECT *,只取需要的字段;减少不必要的大表JOIN;能用一次查询解决的不要拆成多次在代码里循环执行,那种N+1查询在ORM场景下尤其可怕。
6.2 SQL注入是怎么来的,怎么防
搜索热词里既有“sql注入”又有“sql注入万能密码绕过”,说明安全生产这件事是所有SQL使用者绕不开的痛点。
“万能密码”这类注入的基本原理是用户输入被拼进了SQL,改变了SQL的语义。比如登录判断:
SELECT * FROM user WHERE user_name = 'admin' AND password = 'xxx'如果用户在密码框里输入' OR 1=1 --,拼出来就变成:
SELECT * FROM user WHERE user_name = 'admin' AND password = '' OR 1=1 --'后半段永远为真,于是直接绕过校验。
解决办法非常简单而且必须坚持:永远不要用拼接字符串的方式组装SQL,一律使用参数化查询。Java里用PreparedStatement,Python里用cursor.execute(sql, [参数]),MyBatis里用#{}而不是${}。只有动态排序字段名、表名等必须用白名单方式处理。
6.3 SQL Server连接时报SSL相关错误怎么办
这些年“sql server”相关搜索里,有一类报错出现频率特别高,尤其集中在ODBC Driver 18和SQL Server连接SSL加密的提示上。典型报错信息类似:
驱动程序无法通过使用安全套接字层(SSL)加密与 SQL Server 建立安全连接。错误:证书链是由不受信任的颁发机构颁发的或者:
SSL 提供程序:证书链是由不...这通常不是SQL Server本身挂了,而是客户端默认强制加密,但服务器端证书不被客户端信任。常见解决方案有以下三个:
- 在连接字符串里关闭或降级加密要求:
Encrypt=False- 如果确实需要加密,但要跳过证书校验,可以加:
TrustServerCertificate=True- 如果公司有正规证书,把CA证书导入到客户端信任根目录,并保持
Encrypt=True。
我记得自己第一次用Docker部署SQL Server时,就因为这个证书问题卡了很久。后来查清楚原因之后,每次写连接串都会默认加上TrustServerCertificate=True,本地开发和测试环境都很好用,生产环境再根据安全策略调整为正式证书。
6.4 更换版本和安装SQL Server时的高频疑问
与SQL Server相关的搜索里,安装类和“共存”类问题也很常见。比如“sql server 2008可以和ssms2022共存吗”,答案是:SQL Server数据库引擎和SSMS管理工具是两个独立的安装项,SSMS 2022连接SQL Server 2008实例,在很多场景下是可以的,但要注意SSMS 2022有些新功能在旧版本引擎上用不了,管理工具和引擎版本差得太多也可能出现部分GUI功能失效。
再比如“sql server 2012的数据库备份2008能用吗”,SQL Server的备份还原原则是低版本不能向上还原高版本备份。也就是说:2008实例还原不了2012的备份,但2012实例可以还原2008的备份。如果你只有高版本备份,又必须还原到低版本,通常只能通过脚本导出表结构和数据的方式来迁移。
还有人在装SQL Server 2008R2时遇到“对秘钥无访问权限”的提示,这种情况多数是安装目录权限或注册表残留问题。解决方法通常是:管理员身份运行安装包、检查目标安装目录权限、清理旧版本残留,必要时把Windows的用户账户控制临时调低。
6.5 把SQL转成ER图:维护老项目的妙招
最后聊一个工具类技巧。老项目数据库表一多,很多表的关联关系就没人说得清了,接手的程序员看着几十张表一脸懵。这时候“sql转er图”就特别有用。
很多数据库客户端自带这个功能:Navicat、DataGrip、SQL Server Management Studio都可以直接生成数据库关系图。如果你手上只有建表SQL脚本,也可以用一些在线工具,把DDL语句贴进去自动生成ER图,帮你看清楚外键关系和依赖链条。
我的建议是:接手一个老项目后,第一件事不是急着写业务代码,而是先把所有表结构导出成SQL脚本,再生成一份ER图,把关键表的主外键关系理解透。这一步做好,后续写查询SQL会少走很多弯路。
7. 最后再分享一个小习惯
我只是建议你把上面这些内容当作“字典”,而不是“课文”。不要坐在那里一遍一遍背SQL语法,SQL本来就是用来解决问题的工具,最好的学习方式就是拿着真实业务问题去查、去写、去运行。遇到不会的再翻一翻常用语言汇总,翻的次数多了,常用写法自然就刻在脑子里了。
我在实际工作里,还有一个很适合自己的小习惯:准备一本自己的“SQL错题本”。每次写完一条比较复杂但成功的SQL,或者踩了一个数据库相关的坑,就把它贴到笔记里,标注使用场景和踩坑原因。时间长了以后,这本笔记的价值甚至超过各种速查表。你的前缀是"一键搞定SQL常用语言[汇总]",所以这份汇总的目的不是让你把SQL当八股文背,而是让你真正把它当成手边随用随查的工具。希望这一篇就能帮你省下以后到处翻文档的时间。