☰
SQL基础速查手册:从查询到窗口函数的常用语句与避坑指南
2026/9/28 6:42:59 网站建设 项目流程

1. 关于这份SQL速查手册

很多人查SQL语句都是现用现百度,搜到一条复制过去,能跑就完事。我以前也这么干,直到有一次把MySQL的LIMIT语法直接贴在SQL Server里执行,被报错搞得怀疑人生,才意识到:SQL虽然叫"结构化查询语言",但每个数据库都往里面塞了自己的方言。今天把基础部分整理成一份速查手册性质的博文,持续更新,就是为了让我们这种经常在多个数据库之间切换的人,有一份自己能看懂的底稿。

这份手册不是什么教科书,不打算把SQL标准的历史讲一遍,也不会事无巨细覆盖所有冷门语法。它更像是我个人的SQL常用语句笔记,覆盖了从SELECT、INSERT、UPDATE、DELETE到建表、连接、函数、去重、空值处理这些日常最常用的操作。适合刚入门不久、写SELECT都会卡壳的新手,也适合准备面试前想快速过一遍基础语法的同学。你拿到之后可以把它当成一份目录,按图索骥去找自己需要的部分,遇到不会的再回来翻。

1.1 为什么建议你整理一份自己的SQL手册

我见过太多人收藏了十来个SQL知识点合集,真到写的时候还是到处搜。问题不在于收藏不够多,而在于收藏的东西没有变成自己的。我自己整理这个手册的过程,其实就是一个重新理解SQL的过程。你只抄代码,永远不会知道为什么有的地方要加双引号,为什么同样的语句在MySQL能跑、在SQL Server就不能跑。把这些"为什么"搞清楚,才算真正掌握。

整理手册还有一个实际好处:你可以按自己的使用频率排优先级。比如我主要做数据分析,SELECT、聚合函数、窗口函数用得最多,就放最前面;建表改表用得相对少,但也必须有,等用到的时候翻一下不慌。时间久了,这本手册会变成你自己的知识索引,面试前翻一遍,心里特别踏实。

1.2 覆盖范围与阅读须知

本手册覆盖的基础内容包括:查询(SELECT)、过滤(WHERE)、排序(ORDER BY)、去重(DISTINCT)、插入(INSERT)、更新(UPDATE)、删除(DELETE)、建表(CREATE TABLE)、改表(ALTER TABLE)、连接(JOIN)、子查询、常用函数(聚合、字符串、日期、窗口函数),以及常见问题排查。每个部分我都会尽量给出两样东西:能直接抄的语句结构,以及我写代码时会特别注意的坑。

有一点要提前说明:SQL方言很多。本文尽量采用主流数据库通用的写法,遇到MySQL、SQL Server、PostgreSQL有明显差异的地方,我会单独标注出来。你动手验证的时候,最好先确认自己用的是哪个数据库,别拿着SQL Server的写法去MySQL里跑,反过来也一样。这一点说起来轻巧,实际踩过一次就记住了。

2. 查询语句:先学会把数据取出来

查询是整个SQL里最常用的部分,没有之一。你写的所有SQL,十个里有八个是SELECT相关的。这一章我把查询相关的常用语句按使用频率拆开来讲,每一段都能直接上手验证。

2.1 SELECT基础:字段选择与别名

最基础的查询语法是SELECT 列1, 列2 FROM 表名。这里有一个容易忽略的点:尽量只取你需要的列,不要一上来就SELECT *。SELECT *虽然省事,但如果表有几十个字段,数据传输和内存开销都会变大,后续排查也看不清你到底想拿什么数据。很多慢查询的起点,就是从一句看似无害的SELECT *开始的。

-- 基础查询:只取需要的列 SELECT id, name, price FROM products; -- 给列起别名,让结果更易读 SELECT name AS product_name, price, price * 1.1 AS price_tax_included FROM products;

别名用不用AS都可以,MySQL里甚至可以省略,但我习惯把AS写出来。别小看这个习惯,当你的SQL越来越长时,清晰的别名就是给下一个读代码的人——通常是三个月后的你自己——留的提示。特别是多表联查时,如果不给表起别名,写出来的条件又长又难读,维护起来特别痛苦。

2.2 WHERE过滤:条件优先级是最隐蔽的坑

WHERE负责把行过滤出来,语法本身不难,难在条件组合。比较运算、IN、BETWEEN、LIKE、NULL判断这些都要掌握,但最容易出错的其实是运算符优先级:AND的优先级高于OR。我记得自己刚写SQL时,想查"状态为启用且价格大于100,或数量小于5"的商品,直接写成了WHERE status = 'active' AND price > 100 OR quantity < 5,结果发现查出来的数据完全不对,就是因为后半段的OR把前面的AND条件绕过去了。

-- 正确写法:用括号明确优先级 SELECT id, name, status, price, quantity FROM products WHERE status = 'active' AND (price > 100 OR quantity < 5); -- 字符串模糊匹配:LIKE + 通配符 SELECT id, name FROM products WHERE name LIKE '苹果%'; -- 以"苹果"开头 WHERE name LIKE '%手机%'; -- 包含"手机" WHERE name LIKE '_Pro'; -- 任意一个字符后接Pro

模糊匹配里的通配符%和_也很关键。%表示任意多个字符,_表示一个字符,用错了查出来的结果会差很多。还有一个隐藏坑:LIKE对中文也生效,但要注意数据库连接字符集,连接字符集不对时,中文匹配可能查不出结果。遇到这种情况,优先检查连接参数里的characterEncoding或charset配置。

2.3 DISTINCT去重:为什么有时候去重不干净

去重是日常工作中出现频率很高的需求。SELECT DISTINCT 列1, 列2可以去掉查询结果中完全相同的行。这里很多新人会有误区:以为DISTINCT只对第一列去重。实际上,DISTINCT是对你SELECT出来的这一整组列做组合去重,而不是某一列单独去重。

-- 对 brand 和 category 组合去重 SELECT DISTINCT brand, category FROM products; -- 只去重某个字段,但还想看到其他字段的值 -- 这个写法不对:name虽然去重了,但id、price不是唯一的 SELECT DISTINCT name, id, price FROM users;

如果你只想看一个列表,比如"有哪些品牌",SELECT DISTINCT brand FROM products就够了。但如果你想"去掉重复的name,保留每个name对应的完整记录",DISTINCT做不到,这种场景要交给窗口函数,我在后面第6章会专门讲。还有一个细节:DISTINCT对NULL去重时,所有NULL会被视为同一个值,只保留一行。

2.4 ORDER BY与分页:排序和取数范围控制

排序用ORDER BY,默认升序,降序要加DESC。多列排序时,每一列都可以单独指定方向。这里有一个具体问题:NULL值在不同数据库里的排序位置不一样。MySQL和SQL Server里NULL通常被视为最小,升序排在最前;PostgreSQL默认把NULL当作最大值,升序排在最后。如果你写了一个报表,发现NULL总是出现在奇怪的位置,先查这个,别怀疑是排序条件写错了。

-- 多列排序:先按价格降序,再按库存升序 SELECT id, name, price, quantity FROM products ORDER BY price DESC, quantity ASC; -- MySQL/PostgreSQL:LIMIT + OFFSET 分页 SELECT id, name FROM products ORDER BY id LIMIT 20 OFFSET 0; -- 第一页,每页20条 -- SQL Server:OFFSET...FETCH 分页(2012以上版本支持) SELECT id, name FROM products ORDER BY id OFFSET 0 ROWS FETCH NEXT 20 ROWS ONLY;

分页的SQL在不同数据库差异很大,这也是我最初整理手册的直接原因。SQL Server的OFFSET...FETCH必须配合ORDER BY使用,不写ORDER BY直接报错;MySQL则是可以没有ORDER BY,但没排序的分页结果不稳定,翻页时可能出现重复数据。所以不管在哪个数据库,分页前先把排序条件写好,这条没得商量。

2.5 CASE WHEN:在查询里做条件判断

CASE WHEN相当于在SQL里写if-else,属于基础但极其常用的语法。写法和编程语言里的switch很像,不过要注意:CASE表达式返回的是一个值,所以一般用在SELECT里生成新列,或者放在WHERE里做复杂条件。

SELECT id, name, price, CASE WHEN price < 100 THEN '便宜' WHEN price < 500 THEN '中等' ELSE '昂贵' END AS price_level FROM products;

我通常会把CASE嵌套进聚合函数里,比如统计各价格段的数量:SUM(CASE WHEN price < 100 THEN 1 ELSE 0 END)。这种写法在报表里非常常见,属于SQL基础阶段必须掌握的组合拳。用的时候记得每条分支都考虑清楚,特别是ELSE分支,漏掉的话不符合条件的数据会被置成NULL,统计出来数量会对不上。

3. 增删改:让数据动起来

如果说SELECT是读,那INSERT、UPDATE、DELETE就是写。写操作比读操作危险得多,尤其是UPDATE和DELETE,一不留神就把全表改了。这一章我会把每种写操作的正确姿势讲清楚,也会分享我是怎么防止手滑的。

3.1 INSERT:插入单行和多行数据

INSERT的基本语法是INSERT INTO 表名(列1, 列2) VALUES(值1, 值2)。如果你插入所有列,可以省略列名,但我不建议这么干,因为表结构一变,代码就废了。多行插入在MySQL、PostgreSQL、SQL Server 2008以上版本都支持,直接写多组VALUES,中间用逗号隔开就行。

-- 单行插入 INSERT INTO products (name, price, quantity) VALUES ('无线鼠标', 129, 50); -- 多行插入 INSERT INTO products (name, price, quantity) VALUES ('机械键盘', 399, 30), ('显示器支架', 149, 80);

还有一类很实用:把查询结果插入另一张表。我遇到过一次要从线上库拷贝一张表的部分数据到本地方便排查,用的就是INSERT INTO ... SELECT,一条语句搞定,不用走导出导入流程。如果想完整复制整个表,CREATE TABLE new_table AS SELECT * FROM old_table在部分数据库里也能用,但不同类型和约束不会全部带过来,正式场景还是要看清楚需求再选。

3.2 UPDATE:改数据前先数一下行数

UPDATE容易出事,但出事不是语法问题,而是忘了加WHERE。UPDATE products SET price = 0这条语句,没有WHERE,全表价格清零。这个场景听起来像段子,但在真实工作里发生过太多次了。我的习惯是:写UPDATE之前,先写一条等价的SELECT确认影响范围,心里有数再动手。

-- 先查:确认要改哪些行 SELECT COUNT(*) FROM products WHERE name LIKE '无线%'; -- 再改:把"无线"系列价格上调10% UPDATE products SET price = price * 1.1 WHERE name LIKE '无线%';

如果要更新另一个表关联的数据,比如按订单表统计每个用户的累计消费金额,回写到用户表,MySQL和SQL Server写法略有差异。MySQL可以直接UPDATE 表1 JOIN 表2,SQL Server则是UPDATE 表1 SET ... FROM 表1 INNER JOIN 表2。这种跨表更新的SQL,我先跑SELECT验证JOIN结果对不对,再转换成UPDATE执行,能避免很多隐性错误。

3.3 DELETE与TRUNCATE:删除也有讲究

DELETE用于删除指定行,语法和UPDATE类似,同样要小心WHERE。TRUNCATE用于清空整张表,这两个看起来都是删数据,实际差别很大。DELETE是DML,可以带WHERE,会逐行删除,可以配合事务回滚;TRUNCATE是DDL,不带WHERE,直接释放存储空间,很多数据库里TRUNCATE之后没法回滚。

-- 删除指定行 DELETE FROM products WHERE id = 123; -- 清空整张表(慎用) TRUNCATE TABLE products;

我的建议是:生产环境里,能不用TRUNCATE就不用。清空表之前先确认有没有外键依赖,有没有备份。即使是DELETE大量数据,也最好分批删,比如每次只删一万行,循环执行,避免一次性删除导致表锁和事务日志暴涨。这种小技巧听起来笨,但数据库出问题时,能救你一条命。

3.4 事务控制:给写操作上一道保险

事务是写操作安全的关键。BEGIN、COMMIT、ROLLBACK这三个命令,在手动维护数据时几乎是必备。我经常看到新手直接在正式环境执行UPDATE,执行完才知道条件写错了,数据已经改了。如果提前开启事务,发现不对还能回滚。

BEGIN; UPDATE orders SET status = 'canceled' WHERE status = 'pending' AND created_at < '2024-01-01'; -- 检查影响行数,确认无误后再提交 SELECT ROW_COUNT(); COMMIT; -- 如果发现不对,执行 ROLLBACK; 撤销本次修改

不同数据库开启事务的写法不完全一样,MySQL的START TRANSACTION更常用,但BEGIN也兼容。事务不只是手动维护数据时才用,代码里执行多个写操作时也应该包在事务里,保证要么全部成功,要么全部回滚。特别是像"先插入订单、再扣减库存"这种业务操作,一旦中间某步失败,事务回滚能避免两边数据不一致。

4. 表结构与约束:建表也是一项基础功

作为日常开发,你不可能每次都等着DBA建表。了解CREATE TABLE、ALTER TABLE和索引的基本用法,能让你少很多等待。这一章讲的都是基础,但恰恰是基础才最容易想当然。

4.1 CREATE TABLE与常用字段类型

建表语句包含字段定义和约束。字段类型我一般就记这几类:整数用INT/BIGINT,小数用DECIMAL(金额千万别用FLOAT,会有精度问题),文本用VARCHAR,日期用DATETIME/TIMESTAMP,布尔用BOOLEAN或TINYINT。给字段选择类型的原则是够用就好,能用INT就别用BIGINT,能用VARCHAR(50)就别用TEXT,过度设计只会浪费空间、拖慢索引。

CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, email VARCHAR(100) NOT NULL, balance DECIMAL(10,2) DEFAULT 0.00, status TINYINT DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );

约束这一块要单独说:NOT NULL控制字段是否允许为空,DEFAULT控制不填时的默认值,PRIMARY KEY是主键,UNIQUE是唯一约束,CHECK可以加简单的取值范围(MySQL 8.0.16以下版本实际不强制CHECK,这点注意一下)。外键约束FOREIGN KEY在基础阶段可以了解,但实际项目里很多团队刻意不用外键,靠应用层保证数据一致性,原因不外乎性能和维护成本。另外,有些系统喜欢用GUID做主键,SQL Server可以用DEFAULT NEWID(),PostgreSQL可以用DEFAULT gen_random_uuid(),MySQL 8.0.13以上支持DEFAULT (UUID()),老版本就得靠自己生成再传进去。

4.2 ALTER TABLE:改表结构的常用姿势

表结构不是建好了就不动了,加字段、删字段、改类型都是常见操作。这里我只列日常最常用的几种,每一种我都踩过或看到别人踩过坑。

-- 增加一个字段(注意:最好设置默认值,避免已有行数据异常) ALTER TABLE users ADD COLUMN age INT DEFAULT 0; -- 修改字段类型 ALTER TABLE users MODIFY COLUMN username VARCHAR(80); -- 删除字段(慎用,数据不可恢复) ALTER TABLE users DROP COLUMN age; -- 重命名字段/表 ALTER TABLE users RENAME COLUMN username TO login_name;

修改字段类型在MySQL里用MODIFY,在PostgreSQL里用ALTER COLUMN TYPE,SQL Server也有自己的写法。我踩过一个坑:给一个已经有数据的表加VARCHAR字段,没指定长度,结果默认长度太小,后面的业务数据根本存不进去,SQL报错只能干瞪眼。所以加字段时写明长度和默认值,是很必要的习惯。还有,在大表上加字段或删字段,MySQL早期版本会锁表,生产环境要评估好低峰期再操作,现在有些版本支持INSTANT算法,但别默认它一定会用。

4.3 索引:加速查询的第一把钥匙

索引是解决慢查询最常用的手段。CREATE INDEX可以给经常查询的列建索引,唯一索引可以控制在业务上的不重复。索引不是越多越好,每一条索引都会拖慢写入,所以要为高频查询建,别为"可能用到"建。很多人一遇到慢查询就加索引,结果索引越来越多,写入越来越慢,这是典型的病急乱投医。

-- 普通索引 CREATE INDEX idx_email ON users(email); -- 唯一索引 CREATE UNIQUE INDEX idx_username ON users(username); -- 查看查询是否走索引(EXPLAIN) EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';

关于慢SQL优化,很多人的第一反应是加索引,但正确的排查顺序应该是:先用EXPLAIN看执行计划,确认扫描行数、有没有走索引、有没有临时表、有没有filesort。索引加在WHERE条件里的列,或者JOIN的连接列上,才能发挥最大作用。但要避开一个误区:对列做了函数运算或隐式类型转换,索引经常会失效。比如WHERE YEAR(created_at) = 2024,这种写法在不少数据库里会让索引失效,应该改成范围条件WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'。

5. 多表连接:告别单表打天下

数据模型设计得再清楚,业务查询也常常要跨表。JOIN是SQL里既常用又容易出错的点。我见过的JOIN错误,八成出在分不清连接类型,以及把条件放错位置。

5.1 JOIN类型:INNER、LEFT、RIGHT、FULL怎么选

JOIN的类型说白了就是两句话:INNER JOIN只保留两边都匹配的记录,LEFT JOIN保留左边表的全部记录,右表没有匹配就用NULL补。RIGHT JOIN反过来,FULL OUTER JOIN两边都保留(MySQL不支持FULL OUTER JOIN,需要UNION拼)。实际开发里,INNER JOIN和LEFT JOIN占掉了九成以上的使用场景。

-- INNER JOIN:只返回有订单的用户 SELECT u.id, u.name, o.order_no FROM users u INNER JOIN orders o ON u.id = o.user_id; -- LEFT JOIN:返回所有用户及其订单(没有订单的用户order_no为NULL) SELECT u.id, u.name, o.order_no FROM users u LEFT JOIN orders o ON u.id = o.user_id;

用生活类比来解释:LEFT JOIN就像你手里有一份全班名单,去查他们各自的考试分数,没来考试的人分数显示为空,但名单上的人不会消失。INNER JOIN则是只把考了试的人挑出来。条件写在哪一行,决定你最终看到的数据集合。连表时还要注意表别名的命名习惯,我用u、o这种简写,SQL一长才有可读性。

5.2 ON与WHERE的坑:LEFT JOIN为什么"失灵"了

LEFT JOIN有一个非常经典的坑:把过滤右表的条件写在WHERE里,导致LEFT JOIN变成INNER JOIN的效果。问题就在于WHERE是在连接完成后,对结果集做过滤,右表没有匹配而被NULL补上的行,一旦参与WHERE判断,就会被过滤掉。

-- 写法A:过滤条件放在ON中,左表记录保留 SELECT u.id, u.name, o.order_no FROM users u LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid'; -- 写法B:过滤条件放在WHERE中,左表无匹配的记录也会消失 SELECT u.id, u.name, o.order_no FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.status = 'paid';

我在处理一个用户报表时,就遇到过写法B的问题:想统计所有用户里面已支付订单的情况,结果只查出了几个有订单的人,所有没订单的用户全被过滤掉了。排查到最后发现是条件位置的问题。从那以后,我凡是写LEFT JOIN,都先想一遍条件放ON还是WHERE,这个坑已经养成了条件反射。

5.3 子查询与EXISTS:复杂需求的两个思路

子查询可以理解为"查询里的查询"。最常用的是子查询放在WHERE里,比如查所有有订单的用户:SELECT * FROM users WHERE id IN (SELECT user_id FROM orders)。另一种写法是用EXISTS,在部分数据集下效率差异很大。

-- IN 写法 SELECT id, name FROM users WHERE id IN (SELECT user_id FROM orders); -- EXISTS 写法 SELECT id, name FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);

EXISTS只看子查询有没有返回行,不关心返回什么,所以子查询里写SELECT 1就行。还有一种使用场景是删数据前确认关系,比如删除没有订单记录的用户,NOT EXISTS会是更安全的写法。要提醒的是,子查询做深了会看不懂,能用JOIN解决的问题尽量用JOIN,会让SQL可读性好很多。遇到嵌套三层以上的子查询,我会先拆开跑一遍,看看哪一步结果不对,再慢慢复合。

6. 常用函数:让SQL会算数、会处理文本

函数是SQL里的"工具箱"。没有函数,你只能原样取出数据;有了函数,你可以在查询里完成计算、拼接、转换、分组统计。这一章不求全,只讲我实际开发中最高频的这些。

6.1 聚合函数与GROUP BY:统计报表的基石

COUNT、SUM、AVG、MAX、MIN是五个最常用的聚合函数。注意COUNT()和COUNT(列)的区别:COUNT()统计所有行数,COUNT(列)只统计该列非NULL的行数。如果你统计用户数时不小心对可空字段做了COUNT,结果会偏小,这种错误比较隐蔽,因为数据量大的时候很难肉眼看出来。

-- 各品类商品数量、总价、平均价 SELECT category, COUNT(*) AS cnt, SUM(price) AS total_price, AVG(price) AS avg_price FROM products GROUP BY category; -- 只保留商品数超过10的品类 SELECT category, COUNT(*) AS cnt FROM products GROUP BY category HAVING COUNT(*) > 10;

GROUP BY负责把数据分组,HAVING负责过滤分组之后的结果。很多初学者分不清WHERE和HAVING:WHERE是在分组之前过滤行,HAVING是在分组之后过滤组。顺序上,数据库会先WHERE、再次GROUP BY、最后HAVING,这个执行顺序理解透了,写统计查询就不容易乱。顺带提一句,SELECT里的非聚合列,原则上都必须出现在GROUP BY里,否则很多数据库的严格模式会直接报错。

6.2 字符串与日期函数:文本和时间的常见处理

字符串处理里,CONCAT用来拼接,SUBSTRING截取,UPPER/LOWER转大小写,TRIM去空格,REPLACE替换。日期处理在报表里更是常见。想去除字符串两端的空格和杂字符,TRIM和REPLACE要配合用对。我见过一个数据清洗场景,电话号码带了一堆空格和不可见字符,先用TRIM去空格,再用REPLACE把特殊字符换掉,最后才能匹配上。

-- 字符串拼接 SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users; -- 截取、替换 SELECT SUBSTRING('hello world', 1, 5); -- hello SELECT REPLACE('hello world', 'world', 'sql'); -- hello sql -- 日期相关:MySQL与SQL Server函数名有差异 SELECT CURRENT_DATE; -- MySQL/PostgreSQL SELECT GETDATE(); -- SQL Server SELECT DATE_ADD('2024-01-01', INTERVAL 30 DAY); -- MySQL SELECT DATEADD(DAY, 30, '2024-01-01'); -- SQL Server

日期函数在不同数据库间的差异最大,写跨数据库的SQL时尽量用标准格式的日期字面量'2024-01-01',避免因为格式识别不同导致查询条件失效。做日期范围统计时,我习惯把时间字段转成日期再分组,但要注意函数(列)可能让索引失效。如果表很大,更推荐用created_at >= '2024-01-01' AND created_at < '2024-02-01'这种范围写法。

6.3 窗口函数入门:基础进阶的关键一步

窗口函数是近些年最值得掌握的基础进阶特性。ROW_NUMBER、RANK、DENSE_RANK这三个排名函数的区别,面试常问:ROW_NUMBER连续编排名次,相同值随机分配;RANK相同值并列但会跳号;DENSE_RANK相同值并列且不跳号。

-- 按分数排名 SELECT name, score, ROW_NUMBER() OVER (ORDER BY score DESC) AS rn, RANK() OVER (ORDER BY score DESC) AS rk, DENSE_RANK() OVER (ORDER BY score DESC) AS dr FROM exam_scores;

窗口函数解决去重问题更是神器。在第2章里,DISTINCT做不了"按某列去重但仍显示完整记录",窗口函数可以:

-- 去除多个重复记录,保留每组id最小的一条 WITH ranked AS ( SELECT id, name, email, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users ) SELECT id, name, email FROM ranked WHERE rn = 1;

这个写法我现在已经用得很熟练,尤其是在数据清洗阶段。注意MySQL要8.0及以上版本才支持CTE和窗口函数,如果你还在用5.7,需要换一种写法,用子查询多包一层。窗口函数的PARTITION BY可以理解为分组,ORDER BY决定组内排序,这才有了"每组第1条"的能力。理解这两个关键字,窗口函数就算入门了。

7. 常见问题速查:我踩过的那些坑

最后这一章,我把平时被问得最多的几个基础问题整理成一个速查,也顺便把这次整理手册过程中复习到的几个高频坑再拎一遍。这些问题都是实战里真正会遇到的,不是理论题。

7.1 去重怎么老是去不干净

去重不干净,最常见的原因是DISTINCT作用于整个结果集,你SELECT了多列,它就把多列组合去重了。另一个常见原因是想保留完整记录但只按单列去重。正确方案是:只要全部列都一样的才用DISTINCT;遇到按某一列去重并保留完整记录,直接用窗口函数加CTE处理。重复数据如果没有唯一标识列,先用ROW_NUMBER加一个序号,再筛选出每组第一条。

7.2 空值(NULL)判断:为什么=NULL查不出来

NULL代表"未知",它不等于空字符串,也不等于0。判断NULL只能用IS NULL或IS NOT NULL,用= NULL永远查不到数据。NULL参与算术运算结果也是NULL,比如price + 0.5,如果price是NULL,结果还是NULL。处理NULL常用COALESCE函数,把NULL替换成一个默认值:

SELECT COALESCE(phone, '未填写') AS phone_filled, COALESCE(NULLIF(name, ''), '匿名') AS safe_name FROM users;

NULLIF的作用是,两个值相同时返回NULL,比如空字符串''会被转成NULL,再被COALESCE换成"匿名"。这几个函数组合起来,就是一套很实用的空值清洗模板。实际做数据清洗时,"空字符串"和"NULL"区别处理,往往要先用WHERE name IS NULL OR name = ''把两类都捞出来,再统一清洗,不然统计口径会对不上。

7.3 SQL注入:一条输入毁掉一张表

把用户输入直接拼进SQL字符串,是SQL注入的温床。最常见的形式是输入' OR '1'='1,如果程序里直接拼接,这条语句就可能变成查全表的条件。正确做法是使用参数化查询或预编译语句,让数据库把用户输入当"值"而不是"SQL代码"来处理。

# Python示例:把 ? 占位符交给数据库驱动处理 cursor.execute( "SELECT * FROM users WHERE username = ? AND password = ?", (username, password) )

Java里用PreparedStatement,原理一样。这套思路不只是Web开发需要用,数据分析脚本里连数据库拼SQL也一样要防。用ORM框架时,框架内部一般已经做了参数化,但你要是写原生SQL,得自己守住这条线。任何时候都不该相信用户输入,这条原则放之四海而皆准。

7.4 慢查询排查的几个方向

慢查询原因很多,但我排查时按频率排是这样:先看是不是全表扫描,索引没走;再看是不是SELECT了不必要的大字段,比如TEXT;再看是不是隐式类型转换导致索引失效;最后看分页时OFFSET太大,拉了几十万行再丢弃。

-- 例:利用EXPLAIN确认是否走索引 EXPLAIN SELECT * FROM orders WHERE user_id = 1001;

如果发现走了全表扫描,先给WHERE和JOIN条件里的列建索引。如果已经建了索引还是慢,再看是不是写法让索引失效了,比如对索引列做函数运算,或者类型不匹配。这条排查思路基本能覆盖大部分日常慢查询。如果一条SQL已经优化到极限还慢,那就得靠分区表、缓存或者改查询逻辑来解决了,这些已经超出基础手册的范围,以后有空再单独写。

7.5 我的资料整理习惯(一点额外经验)

到了这里,手册的主体内容告一段落。说点题外话,我用了很长时间才养成"遇到新坑就往手册里补一条"的习惯。每隔一段时间,翻一翻自己整理的常用语句,会发现自己以前写的SQL有不少可以优化的地方。这份手册我会持续更新,后续计划把数据类型对比、日期函数对照、更复杂的窗口函数场景、以及备份恢复相关的常用语句也补进来。你也完全可以按照自己的节奏建一本类似的笔记,不用追求大而全,把真正用过的、踩过坑的语句记下来,比收藏任何人的合集都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询