尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SQL快速参考手册:从基础语法到性能优化的实战指南

SQL快速参考手册:从基础语法到性能优化的实战指南 最近后台收到不少朋友的私信问我能不能整理一份“SQL 快速参考”说网上的教程要么太散、要么太深真正上手写查询的时候还是抓瞎。我做了这么多年数据库相关的工作自己也踩过不少坑今天干脆把我日常最常用、也最容易被问到的 SQL 知识点全部梳理成一篇可以直接查、可以直接抄的参考手册。这份内容适合刚接触 SQL 的新手快速建立知识框架也适合写过一段时间 SQL 但总在某些细节上卡壳的开发者当字典用哪怕你只是临时要写一条查询、排查一个慢 SQL也能在这里找到对应的套路。我不会讲太多晦涩的理论重点放在“怎么写”“为什么这么写”“踩过什么坑”这三件事上。1. SQL 核心语法速览与执行顺序1.1 SELECT 查询的基本骨架很多朋友刚开始写 SQL 的时候习惯从网上一段段复制粘贴结果一遇到稍微复杂的查询就晕。其实所有查询都逃不出一个基本骨架SELECT 字段列表 FROM 表名 WHERE 过滤条件 GROUP BY 分组字段 HAVING 分组后的过滤条件 ORDER BY 排序字段 LIMIT 限制条数;这个骨架看起来简单但绝大多数人不知道的是SQL 的逻辑执行顺序和书写顺序并不一样。数据库并不是从第一行 SELECT 开始读的。它实际执行的顺序是FROM确定数据来源表先找到要查哪张表WHERE对源数据做行级过滤GROUP BY按指定字段分组HAVING对分组后的结果做过滤SELECT计算并投影出最终要返回的字段ORDER BY对结果集排序LIMIT截取指定行数这个执行顺序直接决定了很多“看着好像没毛病但一跑就报错”的问题。比如在 WHERE 子句里你不能用 SELECT 里刚起的别名做过滤因为 WHERE 执行时 SELECT 还没算出来。我见过太多人在 WHERE 里写类似WHERE 总金额 100而总金额其实是 SELECT 里刚定义的别名结果数据库直接报“未知列”其实就是没搞懂执行顺序。1.2 多表 JOIN 的选型与细节多表关联是 SQL 里最常用的操作也是最容易写出慢查询的地方。JOIN 的种类其实就四种INNER JOIN内连接、LEFT JOIN左连接、RIGHT JOIN右连接、FULL OUTER JOIN全外连接。写 JOIN 的时候我建议新手记住一句话先搞清楚哪张表是“主表”再决定用什么 JOIN。比如你要查“所有用户以及他们的订单”那么用户就是主表应该先写FROM 用户表 LEFT JOIN 订单表。如果你只想要“有订单的用户”那就是INNER JOIN。有一个细节特别容易踩坑LEFT JOIN 时如果右表有多条匹配记录左表的数据会被复制多份这就是“数据膨胀”。比如一个用户下了三笔订单LEFT JOIN 之后用户信息会出现三次。这时候你如果对左表字段做 SUM 去统计某个指标结果就会偏大。我在实际项目里遇到过好几次这种“统计翻倍”的问题最后定位下来都是 JOIN 的数据膨胀导致的。解决办法通常是先对右表做去重聚合再 JOIN 主表或者使用ROW_NUMBER()窗口函数保留一条记录。2. 高频 SQL 场景速查去重、空值、时间函数2.1 去重查询的正确姿势去重是后台开发里最常见的需求我看到热搜词里也有“sql语句去重查询”“清洗---sql语句去重”。去重有两个层级一个是去掉完全重复的行一个是去掉某一字段重复的行这两种写法完全不一样。去掉完全重复的行直接加DISTINCTSELECT DISTINCT user_id, order_date FROM orders;但注意DISTINCT是对后面所有字段组合去重不是只对第一个字段去重。如果你只想按user_id去重、但同时要保留其他字段的值DISTINCT就做不到了。这种情况一般用窗口函数SELECT user_id, product_name, order_date FROM ( SELECT user_id, product_name, order_date, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date DESC) AS rn FROM orders ) t WHERE rn 1;这个写法表示按 user_id 分组组内按 order_date 倒序排取每组的第一条。这也是实际工作中“保留每个用户最近一条订单”的标准写法。别用 GROUP BY 去硬凑这种场景因为 GROUP BY 之后非分组字段的取值在严格模式下会直接报错即便不报错取到的值也是不确定的。2.2 空值处理NULL 的三大坑NULL 是 SQL 里最容易让人翻车的东西。很多新手刚接触时想不通为什么WHERE 字段 NULL查不到数据为什么NULL NULL返回的是不成立。首先要明确NULL 表示“未知”不等于空字符串也不等于 0。判断一个字段是否为 NULL只能用IS NULL或IS NOT NULL不能用或!。如果你想在查询结果里把 NULL 替换成默认值可以使用COALESCE函数它接受多个参数返回第一个非 NULL 的值SELECT user_id, COALESCE(phone, 未填写手机号) AS phone_display FROM users;COALESCE 比IFNULLMySQL 里的写法更通用各大数据库都支持。另外在做聚合统计时要注意COUNT(字段名)会自动忽略 NULL 值而COUNT(*)会统计所有行。如果某个字段大量为 NULLCOUNT(该字段)和COUNT(*)的结果会差很多这在报表统计时是个隐形的大坑。我在做数据清洗时通常会先跑一条SELECT COUNT(*), COUNT(关键字段) FROM 表看一眼差异有多大心里先有个底。2.3 时间函数一网打尽热搜词里“sql server 时间函数”被频繁搜索确实时间处理是 SQL 中使用频率极高、但各个数据库方言差异也最大的部分。这里我先说通用写法再单独列 SQL Server 的常见用法。标准 SQL 里获取当前时间戳是CURRENT_TIMESTAMP。取日期部分用CAST(字段 AS DATE)或DATE(字段)MySQL。做时间加减在 MySQL 里是DATE_ADD(日期, INTERVAL 1 DAY)在 SQL Server 里是DATEADD(DAY, 1, 日期)。SQL Server 里最常用的时间函数我给整理成了一张速查表功能SQL Server 写法说明当前时间GETDATE()返回当前日期和时间当前 UTC 时间GETUTCDATE()返回 UTC 时间取年份YEAR(日期)返回整数年份取月份MONTH(日期)返回 1-12取日DAY(日期)返回 1-31日期加减DATEADD(DAY, 7, 日期)加 7 天计算两个日期差DATEDIFF(DAY, 开始日期, 结束日期)返回间隔天数格式化日期FORMAT(日期, yyyy-MM-dd)注意性能开销较大特别提醒一下DATEDIFF的第三个参数是结束日期第二个是开始日期顺序搞反了会得到负数。另外在 SQL Server 里FORMAT函数虽然好用但它走的是 .NET 的格式化逻辑数据量大时性能很差。生产环境做日期格式化建议还是用CONVERT配合样式码比如CONVERT(VARCHAR(10), 日期, 120)性能能差出一个数量级。3. 慢 SQL 优化与 EXPLAIN 核心指标解读3.1 慢 SQL 是怎么产生的热搜词里“慢sql优化 explain主要看哪些信息”“慢sql优化”热度很高说明大家在性能优化上确实有痛点。我做了这么久数据库优化发现慢 SQL 的原因其实高度集中在几个方向没有走索引全表扫描查询条件字段上有函数运算导致索引失效大表 JOIN 没有合适的索引SELECT *查了大量不需要的列深分页导致的性能问题比如OFFSET 1000000 LIMIT 10隐式类型转换导致无法命中索引其中最常见的就是第一条没走索引。怎么确认一条 SQL 有没有走索引看执行计划。MySQL 里在 SQL 前面加EXPLAIN关键字即可。3.2 EXPLAIN 到底要看哪些字段我面试别人的时候经常问你用 EXPLAIN 主要看哪些信息能完整答出来的人不多。EXPLAIN 的输出字段不少但真正需要重点关注的其实就这几项type访问类型。从好到差依次是system const eq_ref ref range index ALL。看到ALL就说明是全表扫描基本可以判定这条 SQL 有问题。key实际用到的索引名。如果这一列是 NULL说明没走索引。rows预估扫描行数。这个数字越小越好如果 millions 级别的 rows 出现在核心接口的 SQL 上肯定要优化。Extra额外信息。如果出现Using filesort表示排序没走索引出现Using temporary表示使用了临时表这两种情况在大数据量下都很致命。我自己的排查习惯是先看 type 是不是 ALL再看 key 是不是 NULL然后看 rows 数量级。这三项能在十秒内判断出一条 SQL 是否健康。如果 rows 在万级以内且 type 是 ref 或 range一般不用太担心。如果 type 是 ALL 且 rows 到了百万级那就是必须优化的高危 SQL。3.3 几个实战优化手段优化手段上最常见的就是建索引。但建索引不是万能的而且建多了还会拖慢写入。我一般遵循以下原则区分度高的字段适合建索引比如订单号、手机号区分度低的字段比如性别建索引基本没用。联合索引遵循“最左前缀”原则查询条件里必须包含联合索引最左边的字段才会走索引。不要在索引列上做计算。WHERE DATE(create_time) 2024-01-01会导致索引失效应该改成WHERE create_time 2024-01-01 AND create_time 2024-01-02。深分页优化可以用“延迟关联”或“游标分页”。延迟关联的思路是先查出主键再用主键关联回原表取完整数据。举个例子一个常见的大表分页查询-- 慢offset 越深越慢 SELECT * FROM orders ORDER BY id LIMIT 1000000, 20; -- 快延迟关联 SELECT o.* FROM orders o INNER JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 20) t ON o.id t.id;第二种写法让内层子查询只查主键走覆盖索引速度能提升好几倍。这类优化手段在数据量千万级时效果尤其明显。4. 窗口函数进阶分组排名与累计计算4.1 窗口函数解决了什么问题“sql窗口函数”能上热搜一点都不奇怪因为窗口函数确实是 SQL 进阶路上的一道分水岭。之前我们做分组统计只能用 GROUP BY但 GROUP BY 有一个硬伤它会合并行导致你无法同时看到明细数据和汇总数据。窗口函数则可以在不合并行的前提下为每一行计算基于某个窗口范围的聚合值。窗口函数的基本语法是窗口函数名(字段) OVER ( PARTITION BY 分组字段 ORDER BY 排序字段 窗口范围 )PARTITION BY相当于把数据切分成多个小组ORDER BY决定窗口内数据的排序窗口范围则决定每一行计算时能看到多少行数据。4.2 三个最常用的窗口函数场景排名类需求是窗口函数用得最多的地方。常见的三个排名函数ROW_NUMBER()、RANK()、DENSE_RANK()它们的区别在于处理并列名次的方式ROW_NUMBER()相同分数强制分先后名次不重复RANK()相同分数名次相同但会跳号。比如两个并列第一下一个是第三名DENSE_RANK()相同分数名次相同且不跳号。两个并列第一下一个是第二名写一个“按部门给员工工资排名”的典型例子SELECT department, employee_name, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS row_num, RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rank_num, DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dense_num FROM employees;另一类高频场景是累计计算。比如计算“截至当前行的累计销售额”SELECT order_date, amount, SUM(amount) OVER (ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumulative_amount FROM orders;这里ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW表示从分组内第一行到当前行的范围是累计计算的经典写法。如果在 OVER 里什么都不写默认就是从开头到当前行结果一样。但一旦涉及移动平均这类需求就一定要显式写出窗口范围比如“最近 7 天平均”是ROWS BETWEEN 6 PRECEDING AND CURRENT ROW。窗口函数还有一个很实用的场景是“组内占比”。用SUM(amount) OVER (PARTITION BY category)算出每个分类的总值再拿当前行的值去除以它就能得到每行在其分类中的占比。这种写法在报表开发中非常常见而且性能远好于同表多次 JOIN 自关联。5. SQL 注入攻击与万能密码原理5.1 什么是 SQL 注入热搜词里“sql注入”“sql注入万能密码绕过”“ctfshow web入门 sql注入”频繁出现说明很多人对这个话题既好奇又陌生。我直接说结论SQL 注入的本质是外部输入被当作 SQL 代码拼接执行了。举个例子一个最简单的登录逻辑很多初学者会写成这样SELECT * FROM users WHERE username 用户输入 AND password 用户输入;如果用户在用户名输入框里填的是admin --拼接后的 SQL 就变成SELECT * FROM users WHERE username admin -- AND password xxx;在 SQL 里--是注释符后面的内容全部被注释掉了。这条 SQL 最终只校验了用户名没有校验密码。这就是传说中的“万能密码”绕过登录。更严重的是如果攻击者在输入框里填入; DROP TABLE users; --拼接后整张表都可能被删掉。5.2 如何防御 SQL 注入理解了原理防御思路就很清晰了永远不要把用户输入直接拼接到 SQL 字符串里而是要使用参数化查询。在 Java 的 JDBC 里是PreparedStatementString sql SELECT * FROM users WHERE username ? AND password ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password);在 Python 里是参数占位符cursor.execute(SELECT * FROM users WHERE username %s AND password %s, (username, password))参数化查询的原理是数据库会把 SQL 模板和参数值分开传输参数永远只被当作“值”来处理无论里面写了什么特殊字符都不会被解析成 SQL 语句的一部分。这是防御 SQL 注入最有效、最根本的手段。除了参数化查询还有几层防线可以加对用户输入做严格的类型校验比如数字参数强制转 int对数据库账号分配最小权限应用账号只给 CRUD 权限不给 DDL 权限以及通过白名单校验排序字段名等等。做安全的思路要当成纵深防御来做不能指望单点。6. 实用工具与安装避坑心得6.1 IDEA 插件拼装 SQL 的便利与陷阱热搜词里“idea 插件拼装sql”这个搜索词让我挺有共鸣。日常开发中很多人会用 IDEA 自带的 Database 工具或一些插件比如 MyBatis 相关插件来自动生成 SQL、格式化 SQL这确实能提升效率。但我必须提醒一个坑自动生成的 SQL 往往带有特定数据库方言换库就不兼容。比如从 MySQL 迁到 SQL Server自动生成的分页 SQLLIMIT就会直接报错。用插件拼 SQL 的正确姿势是在开发环境用它快速生成基础 CRUD 语句然后检查两个点——字段有没有冗余、WHERE 条件有没有走索引。插件帮你省去打字的时间但对执行计划的分析它帮不了你最终还是要回到 EXPLAIN 或执行计划上做判断。6.2 SQL Server 安装过程的常见问题热搜词里关于 SQL Server 安装的搜索非常多“sql server 2008 不能删除数据库”“安装sql server 报错无法启动 Windows Management Instrumentation (WMI) 服务”“警告26003 无法卸载”“2008 R2 补丁是否已经安装”。这些我在早期折腾环境时几乎全遇到过。先解释“2008 不能删除数据库”的问题。SQL Server 2008 删除数据库时报错最常见的原因是数据库处于“正在还原”或“正在使用”状态。处理方式很直接ALTER DATABASE 数据库名 SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE 数据库名;第一句强制把数据库切换为单用户模式并回滚所有未完成事务再执行删除就不会卡住。如果是“文件占用导致无法删除”的问题检查是否有别的进程连到了该库。再说 WMI 服务报错的问题。SQL Server 安装过程中会依赖 Windows Management Instrumentation 服务这个服务如果启动失败安装基本进行不下去。遇到这个报错我当时的处理顺序是先打开服务管理器services.msc确认 WMI 服务的启动类型是否为“自动”尝试手动启动如果启动报错去事件查看器看具体错误通常是 WMI 存储库损坏导致。修复方法是在管理员命令行执行winmgmt /verifyrepository winmgmt /salvagerepository分别用于检查仓库完整性和修复仓库。做完之后重启 WMI 服务再重新执行 SQL Server 安装程序。还有那个“警告26003”的卸载问题本质上是安装程序支持文件与现有实例状态不一致导致一般出现在彻底卸载不干净又重装的时候。6.3 用 SQL 转 ER 图的思路最后提一下“sql转er图”这个需求。有现成工具比如 MySQL Workbench、Navicat 都能根据数据库反向生成 ER 图。如果只是想快速看某几张表的关联关系可以在允许读写的情况下直接用数据库管理工具连接实例让工具自动读取表结构和外键关系生成图表。如果没有外键关系很多历史系统压根没建外键手工梳理字段命名也能判断关联一般按表名_id或id对xxx_id来识别。这个思路在数据库文档缺失的老项目里特别实用我接手过不少没人维护的系统都是靠反向 ER 图先梳理清结构再动手改代码的。7. 一份可直接抄作业的常用语句速查7.1 日常开发高频 SQL 场景示例我把日常工作中真正高频使用的 SQL 场景整理成一个速查列表每一行都是可以直接改表名和字段名就能用的。-- 1. 按条件统计数量 SELECT COUNT(*) FROM orders WHERE status 已完成; -- 2. 分组统计 SELECT category, COUNT(*) AS cnt, AVG(price) AS avg_price FROM products GROUP BY category HAVING COUNT(*) 10; -- 3. 多表关联查询 SELECT u.name, o.order_no, o.amount FROM users u INNER JOIN orders o ON u.id o.user_id WHERE o.created_at 2024-01-01; -- 4. 分页查询 SELECT * FROM products ORDER BY id LIMIT 20, 20; -- 5. 批量更新 UPDATE users SET status vip WHERE points 5000; -- 6. 删除重复数据保留 id 最小的一条 DELETE FROM users WHERE id NOT IN ( SELECT MIN(id) FROM users GROUP BY email ); -- 7. 某字段是否存在重复值 SELECT email, COUNT(*) AS cnt FROM users GROUP BY email HAVING COUNT(*) 1;其中第 6 条要特别提醒MySQL 不允许在 DELETE 子查询中直接引用目标表会报“You cant specify target table for update in FROM clause”。解决方法是套一层临时表DELETE FROM users WHERE id NOT IN ( SELECT id FROM ( SELECT MIN(id) AS id FROM users GROUP BY email ) t );7.2 慢 SQL 排查清单最后送大家一份慢 SQL 排查清单是我每次处理线上问题都会过一遍的检查项用 EXPLAIN 看 type是否为 ALL 全表扫描检查 WHERE 条件字段是否有索引检查索引列上是否做了函数运算或隐式类型转换看 SELECT 的字段是否是全部列尝试只查必要字段看是否涉及大表 JOINJOIN 字段是否有索引看是否深分页尝试延迟关联或减少偏移量检查数据量级和 rows 预估是否匹配是否统计信息过期如果上述都没问题考虑从业务侧优化比如拆分查询、加缓存维护 SQL 知识本来就是一个长期积累的过程没有谁一开始就能记住所有函数和写法。关键是把底层的执行逻辑搞明白把高频场景的写法练熟碰到不会的知道去哪里查、按什么思路排查这个能力比死记硬背函数列表重要得多。希望这份快速参考能帮你少走一些弯路。
返回列表