
很多人写SQL查询都是用到哪查到哪SELECT后面跟着一堆星号WHERE条件随手拼上几个AND、OR结果一跑数据要么多了一堆重复行要么少了几条关键记录。尤其是刚开始接触数据库的朋友最容易卡在DISTINCT到底怎么去重、WHERE和OR混用的时候结果不符合预期这两件事上。今天就把这组最基础也最要命的查询子句拆开揉碎了讲清楚SELECT、SELECT DISTINCT、WHERE、AND和OR。这篇东西适合刚入门SQL的读者也适合那些写了几个月查询但老在边缘case上翻车的人。读完之后你不仅知道怎么写还能明白数据库到底是怎么执行的——理解了那层逻辑写错的可能性会小很多。1. 查询语句的核心骨架先搞清楚数据是怎么被“拿”出来的1.1 SELECT的完整语法顺序比你想象中更重要SELECT是SQL里出场率最高的关键字但很多人对它的理解停留在“从表里挑几列出来”。这句话没错但不完整。一条完整的查询语句书写的顺序是有严格规定的你可以这样记SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT。在实际开发里这个顺序经常被写错。比如有人会在GROUP BY后面直接用WHERE筛选分组后的结果或者试图在WHERE里使用SELECT子句中定义的别名。这些错误说明一个问题很多人把“书写顺序”和“执行顺序”混为一谈了。数据库引擎真正干活的时候顺序是这样的FROM先确定数据源WHERE先过滤行GROUP BY再分组HAVING过滤分组SELECT挑列ORDER BY排序最后LIMIT截断。这就意味着WHERE里不能用SELECT里定义的别名。比如你写了SELECT salary * 12 AS annual_salary FROM employee WHERE annual_salary 100000数据库会直接报错因为在WHERE执行的那一步annual_salary这个别名还不存在。正确的写法是在WHERE里重复写一遍salary * 12或者用子查询包一层。这个坑几乎所有写过SQL的人都踩过理解了执行顺序就不会再犯。我不建议一上来就背各种语法规则更好的方式是自己亲手建一张表、插几行数据然后反复用不同的子句组合实验。数据量不用大六到十行就够但字段要设计得有代表性。下面的章节里我们全程用一张员工表作为例子来拆解这样每一步执行效果你都能看得见、摸得着。1.2 准备一张拿来练手的员工表先说明一下为了演示方便我设计了一张比较典型的员工表字段涵盖了数字、字符串、日期和空值后面讲的每个坑都能在这张表上复现。建表语句和插入数据的SQL放在下面你自己本地跑一遍也就一分钟的事CREATE TABLE employee ( emp_id INT PRIMARY KEY, emp_name VARCHAR(50), dept_id INT, salary DECIMAL(10,2), gender CHAR(1), hire_date DATE ); INSERT INTO employee VALUES (1, 张伟, 101, 12000.00, M, 2020-03-15), (2, 李娜, 102, 15000.00, F, 2019-07-22), (3, 王强, 101, 9800.00, M, 2021-01-10), (4, 赵敏, 103, 13500.00, F, 2018-11-05), (5, 刘洋, 102, 11000.00, M, 2022-06-01), (6, 陈静, 101, NULL, F, 2020-09-30), (7, 周磊, 103, 14200.00, M, 2017-04-18), (8, 吴芳, 102, 10800.00, F, 2021-12-12), (9, 孙明, 101, 12500.00, M, 2019-02-27), (10, 郑红, NULL, 9900.00, F, 2023-05-20);这张表里我故意埋了几颗“雷”第6行陈静的salary是NULL第10行郑红的dept_id是NULL。后面演示WHERE、DISTINCT的各种边界情况时这两行数据就是关键的“试金石”。很多查询结果看起来“不对劲”十有八九都是空值在捣鬼。建好表之后先跑一句最简单的验证一下环境SELECT * FROM employee;。能把这十行数据都查出来说明环境没问题就可以跟着后面的章节一步步往下走了。2. SELECT DISTINCT的去重原理以及那些让你怀疑人生的特殊情况2.1 DISTINCT到底是怎么工作的内存里的排序去重SELECT DISTINCT的作用是去掉查询结果里完全相同的行。注意是“完全相同的行”——也就是说只要查出来的结果里有任何一列数据不同这两行就不算重复都会被保留下来。很多初学者会以为DISTINCT是“针对某一列”去重这是一个非常普遍的错误认知。比如你想查一下公司里有哪些部门编号写了SELECT DISTINCT dept_id FROM employee;这个没问题。但如果你写SELECT dept_id, gender FROM employee;结果里出现了101, M和101, F那这两行都算不同的记录。DISTINCT处理的是你查询出来的整行组合不是单独的某一列。从执行层面看数据库实现DISTINCT的方式一般是先把查询结果在内存里做一次排序或者哈希然后挨个比对相邻行是否相同。这个操作是有额外成本的数据量小的时候感觉不到等表里有个几千万行滥用DISTINCT会让查询慢到怀疑人生。我见过有的同事习惯性在每个查询前面都加一个DISTINCT理由是“这样数据更干净”——这是典型的坏习惯。除非你确实需要去重否则不要用它不是一个“保险措施”而是实实在在的额外开销。那到底怎么判断自己的查询需不需要去重我的经验是先想清楚业务上“重复”的定义是什么。是同一个dept_id算重复还是dept_id gender的组合算重复定义清楚了再决定是用DISTINCT还是用GROUP BY还是用ROW_NUMBER()窗口函数。顺序搞反了写出来的SQL就会反复改来改去。2.2 多列去重和NULL的坑两个最常见的DISTINCT翻车场景多列去重的正确用法是SELECT DISTINCT dept_id, gender FROM employee;。这个查询会返回所有不同的(dept_id, gender)组合。如果表里有一行是(101, M)另一行也是(101, M)它们会被合并成一行但如果有一行是(101, M)另一行是(101, F)这两个都会保留。这就解释了很多开发者的困惑“我明明只想去重dept_id为什么出来那么多行”因为你的查询条件里还带着gender数据库压根不知道你想“忽略”这一列。另一个容易翻车的地方是NULL值。在DISTINCT的世界里所有的NULL会被当成同一个值来处理。比如你执行SELECT DISTINCT dept_id FROM employee;张伟、王强、陈静、孙明所在的101只出现一次赵敏、周磊的103只出现一次李娜、刘洋、吴芳的102只出现一次而郑红的NULL也会出现在结果里占一行。如果你不希望结果里出现这个NULL行就得在查询里显式排除SELECT DISTINCT dept_id FROM employee WHERE dept_id IS NOT NULL;。这里还要强调一个实战经验用DISTINCT排查数据质量问题是特别方便的。比如你怀疑某张表的dept_id字段存在脏数据直接SELECT DISTINCT dept_id FROM employee;把所有不重复的值拉出来看一眼有没有异常值一目了然。这比写复杂的统计SQL要快得多是我日常用得最多的“数据体检”命令之一。3. WHERE条件过滤执行顺序、运算符和 NULL 陷阱全解析3.1 逻辑执行顺序决定WHERE的威力边界WHERE子句的作用是“行级过滤”——在分组和计算之前先把不满足条件的行扔掉。理解这句话非常重要因为它直接决定了你能在WHERE里干什么、不能干什么。沿用前面提到的执行顺序FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT。WHERE排在FROM之后、GROUP BY之前意味着两件事第一WHERE只能引用数据源里真实存在的列不能引用SELECT里算出来的别名第二WHERE的过滤发生在分组之前所以你无法在WHERE里使用聚合函数。举个例子你想找出薪资高于部门平均水平的员工。如果写WHERE salary AVG(salary)数据库会直接报错因为AVG()是聚合函数必须在GROUP BY和HAVING阶段才能用。正确的思路是要么用子查询先把部门平均值算出来要么换一种写法在HAVING里做筛选。很多初学者在这里被卡住总觉得SQL语法怎么这么“死板”——不是语法死板而是WHERE和HAVING的分工原本就不同WHERE过滤原始行HAVING过滤分组后的结果。有了这层理解你在写查询的时候就应该形成一个自检习惯我要的这个条件是作用在原始数据行上的还是作用在分组结果上的前者用WHERE后者用HAVING。这个判断一次做对了后面就不会反复调试。WHERE条件里最常用来过滤数据的运算符大致可以分成几类比较运算符,,,,,、范围运算符BETWEEN AND、集合运算符IN,NOT IN、模式匹配运算符LIKE、空值判断IS NULL,IS NOT NULL以及逻辑运算符AND,OR,NOT。其中和!是等价的都表示“不等于”但更推荐用可读性更好这在很多公司的编码规范里也是约定俗成的。范围判断BETWEEN是包含边界的比如BETWEEN 10000 AND 12000等价于 10000 AND 12000。LIKE里%匹配任意长度字符串_匹配单个字符。3.2 NULL的经典三连坑等值判断、NOT IN和空字符串关于NULL如果你只记住一句话那就是NULL不等于任何值包括它自己。这句话能够帮你躲过至少三个最常见的坑。第一个坑用判断空值。很多人想查薪资为空的人写了WHERE salary NULL结果一行数据都查不出来。原因是NULL参与的等值比较结果不是“真”也不是“假”而是“未知”WHERE子句只会保留结果为“真”的行。查空值的正确写法永远是WHERE salary IS NULL查非空用WHERE salary IS NOT NULL。第二个坑NOT IN遇上NULL。这个坑更隐蔽。比如你想找出部门不在101和102的员工很自然地会写SELECT * FROM employee WHERE dept_id NOT IN (101, 102);。看着没问题但执行结果里郑红那一行消失了原因在于郑红的dept_id是NULLNULL NOT IN (101, 102)的结果是“未知”于是这一行被过滤掉了。如果你确实希望把dept_id为空的人也查出来就必须额外加一个条件WHERE dept_id NOT IN (101, 102) OR dept_id IS NULL;。第三个坑空字符串和NULL不是一回事。是一个长度为0的字符串它是一个“真实存在的值”而NULL表示“没有值”。查询时的判断逻辑完全不同WHERE emp_name 能匹配到名字为空串的但匹配不到emp_name IS NULL的。如果你的表允许这两者都存在那就要定时做数据清洗把空串统一转成NULL否则后续统计口径很容易对不上。这三个坑每一个我都见过同事在生产环境里踩过而且都是数据出问题了才回头排查出来的。提前把NULL的行为摸透能省掉很多半夜查数据的痛苦。我自己的习惯是凡是知道字段可能为空的写完WHERE条件后都会下意识多看一眼这个查询结果会不会漏掉NULL行该不该补一个IS NULL条件4. AND与OR的逻辑优先级让你少走弯路的组合条件实战4.1 为什么“不加括号”的结果总是不对劲当你需要同时过滤多个条件时AND和OR就登场了。AND表示“同时满足”OR表示“满足其一即可”。两者的优先级不同AND的优先级高于OR。这个优先级意味着当你把两者混写在一个WHERE里却不加括号时数据库会先执行AND再执行OR这往往和你的直觉相悖。举个例子你想查“部门101里薪资大于10000的人或者部门102里的所有人”。直观的写法可能是WHERE dept_id 101 AND salary 10000 OR dept_id 102;。因为AND优先数据库会把这条语句解析成(dept_id 101 AND salary 10000) OR dept_id 102。你发现没有这其实刚好也是你想要的结果但这里有个隐患假如你想表达的是“部门101里薪资大于10000的人加上部门102里薪资大于10000的人”你的第一版写法仍然是上面那条SQL语义却完全变了。更常见的翻车场景是这种需求“找出部门101或部门102的员工并且他们的薪资大于10000”。有人会写成WHERE dept_id 101 OR dept_id 102 AND salary 10000;。由于AND优先实际执行的是dept_id 101 OR (dept_id 102 AND salary 10000)——结果把部门101里所有薪资的人都查出来了包括那些低于10000的。这就是典型的优先级理解偏差导致的错误。解决这个问题的办法只有一个而且是铁律只要AND和OR混用就必须用括号把条件分组。上面那个正确需求应该写成WHERE (dept_id 101 OR dept_id 102) AND salary 10000;。加括号不是为了满足语法要求而是为了让读你代码的人包括一个月后的你自己一眼就看出条件分组的逻辑。在团队协作时这种“刻意的清晰”比任何编码技巧都重要。我在Code Review里看到过太多因为少了一对括号导致线上数据出错的事故无一例外都是这个原因——不是不懂逻辑而是不小心。4.2 IN和NOT INOR的“语法糖”但也要防坑如果你有一组离散值需要做等值判断比如“部门是101、102或103”用一串OR写出来是WHERE dept_id 101 OR dept_id 102 OR dept_id 103;。这写法本身没错但既冗长又容易出错。更简洁的写法是用INWHERE dept_id IN (101, 102, 103);。IN本质上就是多个OR的等价简写可读性更好性能上和逐个OR差不太多。NOT IN则相当于“不等于这一组值中的任何一个”。前面已经提到NOT IN配合NULL会翻车这里再补充一个更完整的例子SELECT * FROM employee WHERE dept_id NOT IN (101, 102);由于郑红那行dept_id是NULLNULL NOT IN (101, 102)的结果是未知这行会被静默丢弃。如果你想要“既不在101也不在102包括部门为空的”就必须显式补上OR dept_id IS NULL。IN和OR还有一个微妙的差异在真实业务中偶尔会遇到某些数据库优化器对IN列表会做排序和去重对OR则不一定会。不过在数据量级不大的时候这个差异基本感觉不到。我更看重的是代码可读性和可维护性所以连续三个以上的OR条件我都会建议改成IN。这不仅仅是美观问题也是降低出错的概率。当你一眼扫过去就能判断条件覆盖范围的时候逻辑错误会更容易被发现。组合条件的设计思路我建议遵循一条主线先把业务需求翻译成“几个独立条件”再用括号把每个独立条件包起来最后用AND或OR连接。比如“找出2019年以后入职、薪资大于12000的女性员工”翻译过来就是(hire_date 2019-01-01) AND (salary 12000) AND (gender F)。虽然每个括号在语法上不是必须的但这种写法让条件边界一目了然也方便后续调整。5. 常见问题速查表与几条实用的排查经验问题现象可能原因正确写法查出来的数据没有去重没用DISTINCT或者用了但多列组合不相同确认去重口径SELECT DISTINCT只去整行完全相同的明明有NULLWHERE col NULL却查不到NULL不能用于等值判断改为WHERE col IS NULLNOT IN的结果少了几行列表或字段里有NULL结果变成了未知补上OR col IS NULL或用NOT EXISTS改写AND、OR混用结果不对AND优先级高于OR条件被错误分组用括号显式写出分组(cond1 OR cond2) AND cond3WHERE里用了SELECT的别名报错WHERE先于SELECT执行别名还不存在把表达式完整写在WHERE里或用子查询WHERE里用聚合函数报错聚合应在GROUP BY之后WHERE只能过滤行改用HAVING或先子查询聚合再交WHERE过滤查日期字段用 2020-03-15查不到字段里存了时间如2020-03-15 08:30:00用DATE(hire_date) 2020-03-15或BETWEEN范围限制以上是这六个子句最常见的组合问题速查。表格里提到的几个写法差异其实在业务场景中每天都能遇到尤其是日期类型和NULL的联合坑。我的建议是把这个表格当成自己排查查询问题的入口遇到类似现象先对照一遍多半能快速定位。再分享一个排查SQL问题的习惯当你觉得查询结果“不对”的时候不要盯着整条语句发呆要把查询拆分成最小单元验证。比如先只查SELECT * FROM employee WHERE dept_id 101;确认这几行数据对不对再单独加salary 10000看看哪一步开始少了人或多了人。这种“分而治之”的方式比反反复复看整条SQL要高效得多。另外一个实用技巧是学会用格式化来提升SQL的可读性。我的个人习惯是每个子句单独起一行AND和OR放在行首括号对齐。这样别人看你的代码时可以立刻划清条件边界。有人觉得SQL格式化不重要但在一个多人维护的库里一条三千行没人敢动的丑陋SQL往往就是当初没有好好格式化才变成后来那个样子的。练到这里你把文中几个示例自己动手跑一遍对SELECT、SELECT DISTINCT、WHERE、AND和OR的理解会扎实很多。我不主张死记硬背更推荐你拿自己的业务表试着写几个组合查询。真到实际开发里你会发现这组基础子句的灵活性比想象中大得多很多复杂功能无非就是在它们的基础上不断组合、叠加而已。