ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

中级SQL进阶实战:窗口函数、CTE与慢SQL优化核心要点

中级SQL进阶实战:窗口函数、CTE与慢SQL优化核心要点 所谓“Intermediate-SQL”说人话就是你已经不是那个靠背SELECT * FROM和WHERE混日子的小白了但又还没到能对着执行计划谈笑风生的程度。这个阶段特别容易让人纠结——会写一点JOIN、会套一点子查询可一遇到“去重”就头疼一碰到“窗口函数”就发怵一封邮件里别人甩给你一段要优化的慢 SQL你盯着它很想说“这功能我没用过”。这篇不是基础教程而是把我自己在实际项目里从“会写”到“写明白”的过程做一次完整复盘涉及窗口函数、CTE、去重、时间函数、慢 SQL 优化、常见安装与工具坑尽量做到每个点都能直接拿去用。1. 中级 SQL 的定位与核心思路1.1 什么才叫“中级水平”很多人对“Intermediate-SQL”有误解觉得能写出三条以上的JOIN、背得下LEFT JOIN和RIGHT JOIN的区别就算中级了。但我在真实项目里见过太多“能跑但完全不能看”的 SQL——有的是用子查询套了三层跑一次全表扫描有的是用DISTINCT硬顶去重结果把索引全废了还有的是在WHERE里对索引列做函数运算导致查询计划直接放弃索引。中级水平的真正标准我认为有两个第一能选出正确的结果第二能说清楚这个结果是怎么高效跑出来的如果慢知道该看什么地方。这个阶段的核心关键词是“拆解”。拿到一个需求不是马上写SELECT而是先把问题拆成数据形态、过滤逻辑、分组维度、排序规则、去重策略几个层面。比如最常见的“统计每个分类下最近 7 天的订单去重后的金额”听起来简单但你先要想清楚去重是按订单号还是按客户最近 7 天是自然日还是滚动窗口“金额”是订单表直接有的还是需要从明细表聚合这些细节不搞清楚SQL 写出来不是错就是慢。1.2 不同数据库的“方言陷阱”要先认清楚中级水平另一个躲不过去的问题是SQL 不是只有一种。热搜词里能明显看出大家同时在关注 MySQL、SQL Server、Hive SQL 和 Oracle。我在公司就经历过这种场景——同一个需求MySQL 里用LIMIT 10很简单到了 SQL Server 就得换成SELECT TOP 10Hive SQL 里写窗口函数没问题但很多 OLAP 函数版本一低就不支持Oracle 的字符串拼接用的是||MySQL 却写成CONCAT。我个人建议在练中级 SQL 时不用贪多先固定一个主力数据库把思路打通比如 SQL Server 或 MySQL然后再通过对比迁移的方式学 Hive SQL。重要的是理解“关系型数据库背后的逻辑是一致的只是表达方式不同”。比如取分组内前 N 条MySQL 8.0、SQL Server 2005、Hive 都支持窗口函数那就直接把ROW_NUMBER()用熟比记一堆方言写法管用得多。2. 必须掌握的几个核心语法细节2.1 窗口函数从排序到分组内比较的利器中级 SQL 和初级最大的分水岭就是对窗口函数的敏感度。我刚工作那会儿遇到“取每个班级成绩前三名”这种需求第一反应是写相关子查询后来发现代码又慢又难读。窗口函数的写法其实非常固定核心就三块函数名()OVER()PARTITION BY / ORDER BY。比如SELECT student_id, subject, score, ROW_NUMBER() OVER (PARTITION BY subject ORDER BY score DESC) AS rn FROM student_scores;这个rn就是每个科目里按成绩降序排的序号。要取前三名外面套一层子查询过滤一下WHERE rn 3就行。注意ROW_NUMBER()、RANK()、DENSE_RANK()三者的差别在遇到并列成绩时非常明显ROW_NUMBER()不管同分不同分都强行给一个唯一序号RANK()会跳号比如两个并列第一后下一个是第三DENSE_RANK()不跳号下一个是第二。实际项目里做榜单、取 TopN 都爱用ROW_NUMBER()但如果你要的是“并列名次”就得换RANK()或DENSE_RANK()。窗口函数不只是排序用聚合函数也能配合OVER()用比如SUM(amount) OVER (PARTITION BY user_id ORDER BY create_time)这就是经典的“累计值”写法。我在做用户充值流水分析时经常要算“每个用户截至当前时间的累计充值金额”如果不用窗口函数要么写自连接要么写子查询性能都不理想。用窗口函数一行搞定而且代码很直观。要注意一个特坑的地方窗口函数执行在WHERE和GROUP BY之后。所以你不能在同一个SELECT里直接对窗口函数的结果做过滤因为这时候过滤条件还没生效。正确姿势是先用子查询算出窗口函数结果再在上一层过滤比如前面说的rn 3。我在带新人时发现90% 的人第一次都会在这个地方翻车。2.2 CTE 公共表表达式把复杂查询拆成人话WITH开头的 CTECommon Table Expression算是中级 SQL 的“整理术”。新手喜欢一层层套子查询结果括号一多自己都分不清哪层是哪层。CTE 的好处是给每一段结果起个名字让查询像搭积木一样一层一层往上垒。比如先找出活跃用户再从活跃用户里找下单金额最后统计每个类别的消费在 SQL Server、MySQL 8.0、PostgreSQL、Hive 里都能写成WITH active_users AS ( SELECT user_id FROM user_login_log WHERE login_date 2024-01-01 GROUP BY user_id HAVING COUNT(*) 10 ), user_spending AS ( SELECT u.user_id, SUM(o.amount) AS total_amount FROM orders o INNER JOIN active_users u ON o.user_id u.user_id WHERE o.status SUCCESS GROUP BY u.user_id ) SELECT CASE WHEN total_amount 5000 THEN 高价值 WHEN total_amount 1000 THEN 中价值 ELSE 低价值 END AS value_segment, COUNT(*) AS user_count FROM user_spending GROUP BY value_segment;这种写法不仅好读还好排查。如果中间某段数据不对你只要单独SELECT * FROM active_users就能验证。热词里专门有“sql 语句复习”和“sql 必知必会 pdf”我建议如果想深入就让学习围绕 CTE 展开因为它能帮你把复杂需求拆成多个简单块这本身就是中级 SQL 最重要的思维方式。递归 CTE 是另一个常被忽略的能力。比如组织架构里找所有下属部门、BOM 里找子件、评论表里找所有层级回复都需要递归。SQL Server 和 MySQL 8.0 都能写WITH RECURSIVE。我处理过一次分类树的全路径查询数据只有几百条用递归 CTE 一次就能把所有层级关系拉平比在代码里循环查数据库要高效得多。2.3 去重不是你想象的那么简单热搜词里“清洗---sql 语句去重”和“sql 语句去重查询”出现频率很高说明去重是中级 SQL 绕不开的高频场景。很多人去重只会一句SELECT DISTINCT但 DISTINCT 有两个问题一是它对所有选出的列整体去重如果字段组合特别宽作用很有限二是它没法告诉你“到底该保留哪一条”。真正业务上去重更多是“同一客户、同一订单、保留最新一条”这种场景。遇到这种情况我一般有两种处理方式。一种是配合ROW_NUMBER()WITH ranked AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY updated_at DESC) AS rn FROM order_histories ) SELECT * FROM ranked WHERE rn 1;这就是“按订单号分组保留更新时间最新的一条”。另一种是用GROUP BY配合聚合函数取MAX/MIN但这样你只能拿到聚合后的值没法把整行数据带出来。如果数据量在百万以内我推荐ROW_NUMBER()方案语义清晰还能灵活调整保留规则。万一某些大宽表或者历史表没有唯一主键去重前建议先排查有没有重复数据SELECT order_id, COUNT(*) FROM orders GROUP BY order_id HAVING COUNT(*) 1;这个查询不会动数据但能让你对数据质量心里有数。我在数据清洗项目里第一步永远是跑这种探查语句因为“不知道有什么脏数据”才是最大的风险。2.4 时间函数统计报表的基础底座热词里有两个典型的 SQL Server 时间函数需求sql server 时间函数和sql server 2008 r2。时间处理是中级 SQL 的必修课因为绝大多数业务分析和报表都离不开日、周、月、季度的统计。在 SQL Server 里GETDATE()拿当前时间DATEADD(day, -7, GETDATE())算 7 天前DATEDIFF(day, start_date, end_date)算两个日期差多少天DATEPART(week, date)取周数这几个函数基本覆盖 80% 场景。MySQL 则习惯用NOW()、DATE_ADD()、DATEDIFF()、DATE_FORMAT()。比如按月分组统计MySQL 里可以直接DATE_FORMAT(create_time, %Y-%m)SQL Server 可以用CONVERT(char(7), create_time, 120)或者FORMAT(create_time, yyyy-MM)。但要注意在WHERE里对时间列做函数运算非常影响索引使用。如果你遇到一个“昨天至今”的报表别直接写WHERE DATEDIFF(day, create_time, GETDATE()) 1而是先算出边界值WHERE create_time DATEADD(day, -1, CONVERT(date, GETDATE())) AND create_time CONVERT(date, GETDATE())这样数据库能对create_time用索引扫描数据量大时性能差距是数量级的。时间函数还有个大坑是时区和非标准日期格式。我接手过一个系统日期字段存的是字符串2024-03-15 21:30:00看起来没毛病但一排序就乱因为有人往里存了2024-3-5 9:00:00这种格式。最后先清理数据统一转成DATETIME2类型再建索引问题才彻底解决。所以看到日期字段是varchar时心里一定要拉响警报。3. 从“能跑”到“跑得快”慢 SQL 优化实践3.1 先学会看执行计划热搜词里“慢 SQL 优化”出现很多次但我发现很多人第一步就错了——他们不看执行计划先瞎猜加索引。正确的做法是拿到一条慢 SQL先把它的执行计划打开。MySQL 里是EXPLAINSQL Server 里是“显示估计的执行计划”按钮或者也可以用SET STATISTICS IO ON; SET STATISTICS TIME ON;看实际 IO 和 CPU 消耗。执行计划最核心看几样东西有没有出现Table Scan或Clustered Index Scan全表扫描有没有出现Key Lookup回表连接方式是Nested Loop、Hash Match还是Merge Join实际行数和你估计的行数是否差距巨大。比如 MySQL 的EXPLAIN输出里type字段从system、const、ref、range到all越往后越差rows是预估扫描行数数值越大越危险。我优化过一个实时报表接口原始 SQL 在 MySQL 里跑 12 秒接口一直超时。打开执行计划一看一个小表关联一个大表大表走了全表扫描而且Where条件里有个DATE_FORMAT(create_time, %Y-%m-%d) 2024-03-18索引直接失效。改成create_time 2024-03-18 00:00:00 AND create_time 2024-03-19 00:00:00后再看执行计划走的是索引范围扫描接口响应直接降到 200 毫秒以内。改动很小效果却天差地别这让我后来养成一个习惯任何慢 SQL先解释执行计划再谈优化方案。3.2 索引用了但效率还是不行怎么办有一种情况很气人明明加了索引也用了索引但 SQL 就是慢。这时候你要检查“索引选择性”和“回表”问题。比如你在性别列上建索引但数据里男女比例接近 1:1优化器觉得还不如全表扫直接放弃索引这是选择性太差。再比如覆盖索引没建好查SELECT *索引里没有的列需要回表拿数据十万行回表也够呛。解决方案是把常用查询字段放进联合索引做成“覆盖索引”减少回表。还有一种常见场景是分页太深LIMIT 100000, 20这种写法前面十万行数据都扫描掉了只为了返回最后 20 行。优化方式有几种一种是在查询条件里带上时间或其他范围条件减少初始结果集一种是“延迟关联”先查主键再用主键去关联原表取数据SELECT t.* FROM orders t INNER JOIN ( SELECT id FROM orders WHERE status PAID ORDER BY create_time DESC LIMIT 100000, 20 ) tmp ON t.id tmp.id;这种写法在 MySQL 里效果非常明显因为子查询只在索引页上操作大大减少了回表次数。SQL Server 里也可以用OFFSET FETCHSQL Server 2012来替代ROW_NUMBER() OVER()分页性能各有优劣需要实测。3.3 Hive SQL 里的几个特殊优化点在大数据场景用 Hive SQL和传统数据库的优化思路很不一样。Hive 的慢很多时候不是 SQL 写得差而是数据倾斜。比如一个GROUP BY user_id里某个超级用户占了 80% 数据这个 reduce 任务就会拖很久。解决办法常见有几种加DISTRIBUTE BY手动分散数据或者对热点 key 加随机前缀打散后再聚合。在 Hive 里做数据清洗和报表窗口函数的用法跟 MySQL 差不多但要注意版本对函数的支持COUNT(DISTINCT ...)在超大结果集上很容易成为性能瓶颈因为会产生大量 distinct 计算。这时可以先用子查询去重再在外面算COUNT(1)比直接COUNT(DISTINCT)要稳。另外 Hive 的MAPJOIN//* BROADCAST */hint 也很重要。小表关联大表时把小表广播到每个 map 端能省掉 shuffle 过程大幅度缩短运行时间。我处理过一张 5 千万行的订单表和一张只有几千行的城市维表关联没用 hint 之前跑了 40 分钟加上 broadcast 不到 5 分钟就完成了效果立竿见影。但注意不要对大表用 broadcast否则会把内存撑爆。4. 环境搭建与踩坑实录SQL Server 及其他常见坑4.1 SQL Server 2022 Express 下载与安装热搜词里“sql server 2022 download”“sql server 2022 express”出现频率极高我自己的经验是学习用 SQL Server直接下 Developer 版或 Express 版就够Express 免费、功能完整适合练习大部分 SQL。安装时有个容易忽略的点是实例配置里的“混合模式”学习阶段建议在安装向导里选Windows 身份验证模式以后想换再加SQL Server 身份验证。也可以直接默认 Windows 身份验证用 SSMS 登录时选 Windows 认证即可没必要一上来就折腾用户名密码。SQL Server 2022 的安装向导相比 2008 R2 和 2012 已经智能很多但仍有几个细节要留意路径不要带中文和空格防火墙要放行1433端口仅当需要远程连接时功能选择时如果只想练 SQL勾选“数据库引擎服务”和“客户端工具连接”即可不需要装报表服务和集成服务。假如你的电脑是 Windows 11装旧版 SQL Server 2008 R2 很容易遇到兼容性问题我的强烈建议是别在 Win11 上折腾 2008 R2直接上 2022 Express学习成本更低踩坑更少。4.2 常见安装与连接报错处置SQL Server 安装和连接过程中我遇到过三个高频问题基本都在热搜词里能对上号。第一个是“安装报错无法启动 Windows Management Instrumentation (WMI) 服务”。这个大概率是系统服务层面的问题先在服务管理器里看Windows Management Instrumentation服务的状态如果没启动手动启动时如果报错通常是因为Software Protection或Winmgmt的依赖项失效。最简单粗暴的办法是打开管理员命令行执行winmgmt /verifyrepository和winmgmt /salvagerepository然后重启服务。我处理过好几次修复后安装就正常了。第二个是“ODBC Driver 18 for SQL Server命名管道提供程序: 无法打开连接”。这个报错我一开始也很懵后来发现是连接字符串里没有指定EncryptFalse或者TrustServerCertificateTrue。新版 ODBC Driver 18 默认强制加密如果你的服务端证书配置不那么严格连接就会失败。解决办法是在连接字符串里加上EncryptFalse;TrustServerCertificateTrue;或者用 SSMS 登录时在“选项”里把加密设为可选。第三个是“sql server 2008 不能删除数据库”。在 SQL Server 里删库失败最常见原因是数据库正被会话占用。这个时候用 SSMS 把数据库改成单用户模式再删除或者先杀掉占用进程ALTER DATABASE [YourDB] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [YourDB];WITH ROLLBACK IMMEDIATE会强制回滚并断开所有连接操作前一定确认没有重要事务在跑。4.3 从一个报错看如何排查定位问题热搜里还有一条很典型的报错“could not add role column to users table sql: you have an error in your sql”。这不是纯 SQL 问题而是某个项目里用 ORM 自动迁移时生成的 SQL 在目标数据库里语法不被支持。我遇到过的类似情况是在一个 MySQL 5.7 环境里跑ALTER TABLE users ADD COLUMN role varchar(50)按说没问题但如果你把 MySQL 8.0 的迁移脚本拿到 5.7 执行或者把 SQL Server 的ALTER TABLE ... WITH (ONLINEON)拿到 MySQL 执行就会报“you have an error in your SQL syntax”。排查这类问题最好的办法是打开 ORM 的 SQL 日志把实际执行的 SQL 复制出来放到对应版本的数据库客户端里单独执行看具体报错位置。不要盯着框架堆栈猜。我建议所有做开发的朋友至少在 IDE 里装一个 MyBatis Log Plugin如果你们用 Java/MyBatis或者开启 Hibernate 的show_sql配置这样能直接看到最终生成的 SQL排查效率翻倍。热词里“idea 插件拼装 sql”说的就是这类工具。4.4 DBeaver 执行 SQL 文件的小技巧DBeaver 是很多人的主力数据库客户端处理大 SQL 文件时注意几个点一是 SQL 文件编码一定要看清UTF-8 和 GBK 混用会导致中文乱码甚至语法报错二是执行整个文件时注意脚本里的DELIMITER处理如果是 MySQL 的存储过程、函数DBeaver 不一定支持DELIMITER需要改成它自己的脚本编辑方式三是跑大批量插入时建议用事务分批执行避免一条出错回滚整个文件。具体操作右键连接 → “SQL 编辑器”打开文件然后可以先CtrlShiftEnter执行当前选中语句确认没问题后再整文件执行。5. SQL 注入与安全防御中级玩家必须跨过的红线5.1 为什么拼接 SQL 这么危险热词里“sql注入”和“sql注入万能密码绕过”都很靠前我得说清楚了解注入原理最重要的目的是防御而不是攻击。SQL 注入的本质是用户输入被拼接到 SQL 语句后变成了 SQL 语法的一部分。最典型的万能密码样例是用户输入 OR 11拼接后查询条件变成SELECT * FROM users WHERE username admin AND password OR 11;因为OR 11恒为真整个条件就成了真于是攻击者可以绕过密码校验。这种场景在旧项目里真实出现过教训极其深刻。防御的核心不是写一堆过滤函数而是用参数化查询。无论是 Python 的cursor.execute(sql, params)、Java 的PreparedStatement还是 .NET 的SqlCommand你都应该把参数作为参数传进去让数据库驱动帮你做类型检查和转义。我检查代码时有个习惯搜索所有字符串拼接 SQL 的地方一旦发现fSELECT * FROM ... WHERE id {id}这种写法直接判为高危要求立刻改掉。项目里哪怕只有一个这样的漏洞风险也是不可控的。5.2 抓写法和查隐患的基础方法除了参数化查询平时自查代码还能用几条“味道很重”的规则禁止SELECT *出现在生产代码里禁止把前端传参直接拼进ORDER BY或LIMIT禁止用存储过程拼接EXEC(_sql)。ORDER BY和表名这类不能参数化的场景建议使用白名单校验比如把允许排序的字段名和数据库列名做成映射表。如果你接手一个老项目想快速摸清有没有 SQL 注入隐患可以搜索这些关键词execute(、rawQuery、String sql 、SELECT然后人工检查哪些 SQL 里有外部变量直接拼接。这种排查虽然累但往往能发现一两个历史遗留的高风险点。我自己就曾经在一个内部管理系统的导出功能里发现filename参数直接拼到了 SQL 的注释里虽然是低危但说明代码规范并不可靠。5.3 安全本身也是一项“技能债”关于注入靶场和练习环境我的建议是本地搭建一个实验环境研究原理没问题但千万别在真实系统上做未授权的测试。我做安全防御培训时经常用一个小库演示构造一张用户表故意写一条拼接 SQL再输入恶意字符串让大家亲眼看到绕过效果然后再改成参数化写法对比两者的区别。这种“破坏后再修复”的学习方式记忆点非常强。说实话安全这部分很多中级开发都觉得跟自己没关系觉得自己只是写业务 CRUD。但 SQL 注入往往就发生在最简单的登录接口里因为你把用户输入直接拼进了 SQL而后端没有人在 review 环节拦住你。所以我把这条放在“中级 SQL 必须跨过”的位置它比优化更影响系统性安全。6. 实战案例用一句话需求串起全部技能6.1 需求描述与表结构假设为了把前面讲的东西串起来我构造一个常见的实战需求。假设有一个订单系统表结构如下以 SQL Server 语法为例customers(customer_id, customer_name, register_date)orders(order_id, customer_id, order_date, total_amount, status)order_items(order_item_id, order_id, product_name, quantity, unit_price)需求统计 2024 年每一个月首次下单客户数量、月下单客户数、月订单金额并且要求同一个订单状态为CANCELED的不计入统计。这个需求如果完全没有中级 SQL 思维很容易写成一坨多层子查询而且首次下单客户的定义可能搞错。我的思路是先算每个客户的首单月份再按月汇总新客数每月订单金额则直接从有效订单里聚合最后用月份字段做LEFT JOIN拼起来。6.2 SQL 实现与逐步解释第一步计算每个客户的首单月份WITH first_order AS ( SELECT customer_id, MIN(order_date) AS first_order_date FROM orders WHERE status ! CANCELED GROUP BY customer_id )这里用MIN(order_date)取每个客户最早的有效订单时间就是首单时间。第二步统计每月的首次下单客户数同时统计总订单数和金额WITH monthly_stats AS ( SELECT YEAR(order_date) AS order_year, MONTH(order_date) AS order_month, SUM(total_amount) AS month_amount, COUNT(DISTINCT customer_id) AS active_customers FROM orders WHERE status ! CANCELED GROUP BY YEAR(order_date), MONTH(order_date) )这一步要注意两点COUNT(DISTINCT customer_id)对活跃客户数去重按年月分组时GROUP BY和SELECT里的表达式要一致。第三步把首单月份和每月统计通过月份拼接起来WITH first_order AS (...), monthly_stats AS (...) SELECT m.order_year, m.order_month, m.month_amount, m.active_customers, COUNT(f.customer_id) AS new_customer_count FROM monthly_stats m LEFT JOIN first_order f ON YEAR(f.first_order_date) m.order_year AND MONTH(f.first_order_date) m.order_month GROUP BY m.order_year, m.order_month, m.month_amount, m.active_customers ORDER BY m.order_year, m.order_month;这里用LEFT JOIN连接条件是首单的年份和月份等于当前统计月份。因为一个客户的首单只会出现在一个月COUNT(f.customer_id)不重复就能算出该月新客数。整个查询如果不顺手可以用同一个 CTE 单独验证 first_order 和 monthly_stats很好排查。这个例子其实就是中级的日常拆解需求、用WITH组织逻辑、用窗口或聚合函数处理分组去重、最后统一汇总。不需要什么高深语法但每一步都要明确自己“为什么要这么写”而不是瞎凑结果。6.3 从 SQL 转 ER 图到方便排查热词里有一条“sql 转 er 图”这在中级阶段也很有用。当你接手一个老项目时数据库表多、关系乱光看建表脚本很难快速理解业务。现在很多工具可以自动生成 ER 图DBeaver 里选中几张表右键“查看 ER 图”即可Navicat 的模型功能也可以反向同步数据库如果想用代码方式管理还可以用mysql-workbench的逆向工程或 SQL Server 的“数据库关系图”。我常用的做法是先把核心业务表的关系理清楚再手动补充外键说明因为工具生成的关系图经常差外键约束只能靠字段自己猜。不过要提醒一点ER 图是给“理解结构”用的不是给“优化性能”用的。真正排查慢 SQL 时还是要回到执行计划和索引上别被关系图带偏了。7. 常见问题与排查技巧速查7.1 一张表总结高频报错与对策现象常见原因解决思路安装 SQL Server 时 WMI 服务无法启动Windows 服务组件异常管理员命令行执行winmgmt /verifyrepository、winmgmt /salvagerepositorySQL Server 使用 ODBC Driver 18 连不上新版驱动默认强制加密连接串加EncryptFalse;TrustServerCertificateTrue;2008 R2 数据库无法删除数据库被会话占用ALTER DATABASE ... SET SINGLE_USER WITH ROLLBACK IMMEDIATE;后删库数据库连接超时SQL 查询慢缺索引或扫描数据太多先看执行计划确认走索引后再优化 SQL 结构某个字段排序混乱字段是字符串类型或含空格统一类型、清理空格必要时增加排序列去重后结果和预期不一致只用了 DISTINCT没明确保留规则改用ROW_NUMBER() OVER(PARTITION BY ... ORDER BY ...)WHERE里对索引列使用函数索引失效查询变慢把函数计算移到条件值一侧或新增加冗余列这张表基本覆盖了我这几年在群里、评论区和项目中见过的高频问题。建议收藏或者按照自己项目的表结构补充成团队速查手册。7.2 我觉得比语法更重要的几个习惯排查和优化 SQL 这么久我有几个顿悟时间比较长的习惯这里一并写出来。第一个习惯是“先看数据再看写 SQL”。拿到一张表先SELECT TOP 100 *看真实的字段值长什么样尤其是时间字段、状态字段和客户字段有没有异常值。见过有人因为状态字段存了大小写混合的值Success和success导致WHERE status SUCCESS漏数据排查了大半天。第二个习惯是“写查询时加上时间边界除非你有明确的理由不限制”。生产中很多慢查询都是因为查询一开始就扫全表程序员觉得数据量不大但一旦跑批几个月后数据量翻几十倍原来能跑的 SQL 就挂了。哪怕是临时分析也尽量把时间范围写上保护自己也保护数据库。第三个习惯是“每一个复杂 SQL 都留一个验证版”。比如你要写 5 个 CTE 嵌套的汇总那就先单独跑前两个 CTE看一下中间结果是否符合预期。不要写完一大坨再跑出错时根本不知道是哪一层出了问题。我用这种“小步调试”的方法至少节省过几十个小时的排查时间。第四个习惯是“遇到慢查询别急着加索引”。先问业务是否真的需要这种方式查数据量能不能缩小SQL 能不能改写比如热词里的“并行 sql 优化”有时候把并行度调大能解决一部分场景但真正稳妥的还是把 SQL 逻辑简化把数据扫描量降下来。索引是银弹但不是万能药加多了反而影响写入性能。7.3 应对“问答不清晰”的业务需求中级 SQL 经常遇到的不是语法问题而是业务口径问题。一句话描述“统计用户数”但没说是“活跃用户数”“付费用户数”还是“新增用户数”写出来结果完全不一样。我现在的习惯是拿到需求后先反问三个问题统计口径是什么统计的时间范围是什么结果集需要的粒度是什么这三个问题问完SQL 的骨架基本就出来了。如果对方说不清楚就把它拆成可量化的子问题比如“同一个用户重复下单算一个人还是算多笔订单”这种问题在需求评审时问清楚比事后改 SQL 要省力得多。8. 写在最后的中级经验中级 SQL 这个阶段最容易掉进的坑就是把注意力全放在“背函数”上。函数背得再多遇到真实数据还是会手足无措。我个人的经验是用一套固定的流程去面对任何 SQL 需求——先确认表结构与业务口径再动手‘拆需求’为多个独立逻辑块优先用 CTE 组织然后逐步验证中间结果最后再考虑性能与索引。这个过程熟练之后你基本就跨过中级门槛了。最后再分享一个小技巧平时可以把写过的复杂 SQL 整理成自己的“SQL 工具箱”按场景分类比如去重类、累计类、TopN 类、同比环比类、递归类。下次再遇到类似需求直接拿旧代码改一改比从零开始写要快得多而且经过验证的逻辑不容易出错。我自己的工具库现在已经有几十条常用模板很多业务分析任务都是直接在模板上调整参数完成的。这本“个人手册”比任何一本 SQL 教材都更有价值。
返回列表