SQL基础回顾:SELECT语法、WHERE过滤、ORDER BY排序、LIMIT分页、DISTINCT去重、基础运算符与逻辑条件
SQL(Structured Query Language)是与关系型数据库交互的核心语言。无论你未来要处理多复杂的查询、多庞大的数据量,最终都要回归到最基础的SELECT语句上。本章将系统梳理SELECT的完整执行逻辑、WHERE过滤机制、排序与分页规则、去重原理,以及运算符和逻辑条件的优先级。这些内容不仅是日常开发的高频工具,更是后续学习索引优化、执行计划分析的基石。理解本章内容,你将能够写出逻辑清晰、性能可控的基础查询语句,并为复杂查询打下扎实的语法与思维基础。
核心原理:SELECT的完整执行链路
很多初学者将SELECT语句视为简单的“取数”操作,但实际上,一条完整的SELECT查询在数据库内部会经历一个严格的逻辑执行顺序。理解这个顺序,是排查查询错误、优化查询性能的第一步。SQL的书写顺序与逻辑执行顺序并不一致,这是最容易混淆的地方。
逻辑执行顺序如下:首先从FROM子句确定数据源(表或视图);接着WHERE子句对每一行进行条件过滤,剔除不满足条件的行;如果存在GROUP BY,则对过滤后的数据进行分组;HAVING对分组后的结果进行二次过滤;随后进入SELECT投影阶段,计算列、表达式、聚合函数;DISTINCT对投影结果去重;ORDER BY对最终结果集排序;最后LIMIT截取指定行数。这一顺序决定了哪些操作可以使用别名,哪些操作不能。
关键认知:WHERE是在原始数据行上过滤,因此不能使用SELECT中的列别名;而ORDER BY在SELECT之后执行,所以可以使用别名。例如:SELECT salary * 12 AS annual_salary FROM employees ORDER BY annual_salary DESC; 是合法的,但 SELECT salary * 12 AS annual_salary FROM employees WHERE annual_salary > 50000; 会报错,因为WHERE执行时annual_salary还不存在。
WHERE过滤:精确控制数据范围
WHERE子句是查询中最常用的过滤工具,它支持多种运算符和逻辑组合。从性能角度看,WHERE条件的写法直接影响索引的利用率。一个常见的误区是:在WHERE中对索引列使用函数或计算,导致索引失效。例如 WHERE YEAR(hire_date) = 2020 会阻止索引使用,应改写为 WHERE hire_date >= '2020-01-01' AND hire_date < '2021-01-01'。
WHERE支持的基础运算符包括:比较运算符(=、>、<、>=、<=、<>或!=)、范围运算符(BETWEEN AND)、集合运算符(IN)、空值判断(IS NULL、IS NOT NULL)、模糊匹配(LIKE,配合%和_通配符)。逻辑条件使用AND、OR、NOT连接,其中AND优先级高于OR,建议使用括号明确逻辑分组。
| 运算符类型 | 示例 | 说明 | 性能提示 |
|---|---|---|---|
| 比较 | age > 18 | 基础行过滤 | 可走索引 |
| 范围 | price BETWEEN 100 AND 200 | 闭区间,等价于 >= 和 <= | 可走索引 |
| 集合 | city IN ('北京','上海') | 等价于多个OR | 可走索引 |
| 空值 | email IS NULL | 不能用 = NULL 判断 | 索引使用需注意 |
| 模糊 | name LIKE '张%' | %匹配任意字符,_匹配单字符 | 前缀模糊可走索引,后缀模糊不行 |
风险提示:对于 LIKE '%关键词' 这类后缀模糊查询,数据库无法使用B+树索引进行快速定位,会触发全表扫描。在数据量大的表上,这种查询可能造成严重的性能瓶颈。如果业务确实需要后缀模糊搜索,建议考虑全文索引或Elasticsearch等方案。
ORDER BY排序与LIMIT分页:结果呈现与数据裁剪
ORDER BY用于对结果集按一列或多列排序,默认升序(ASC),可指定降序(DESC)。多列排序时,先按第一列排序,若第一列相同,再按第二列排序。排序操作在SELECT之后执行,因此可以使用别名。但要注意,排序通常需要额外的内存或磁盘空间,对大数据集排序可能成为性能瓶颈。
LIMIT用于限制返回的行数,语法为 LIMIT offset, count 或 LIMIT count OFFSET offset。offset表示跳过的行数,count表示返回的行数。分页查询的经典写法是 LIMIT (page-1)*page_size, page_size。然而,深分页(如LIMIT 100000, 20)会导致数据库扫描大量无用的行,性能急剧下降。
-- 基础排序与分页示例
SELECT product_id, product_name, price
FROM products
WHERE category_id = 5
ORDER BY price DESC, product_id ASC
LIMIT 10 OFFSET 20;
-- 含义:取分类5的商品,按价格降序(同价按ID升序),跳过前20条,返回10条
优化建议:对于深分页场景,不要使用 OFFSET 直接跳转,而应使用“键集分页”(Keyset Pagination)。例如:WHERE price < 100 ORDER BY price DESC LIMIT 20,通过记住上一页最后一条记录的排序字段值来获取下一页,这样可以利用索引避免全表扫描。
DISTINCT去重:理解其代价与替代方案
DISTINCT用于去除查询结果中的重复行。它的工作原理是对SELECT投影后的所有列进行组合去重。这意味着,如果SELECT包含多列,DISTINCT会对多列的组合进行去重,而不是单独对某一列去重。使用DISTINCT时,数据库需要对结果集进行排序或哈希操作,这在大数据集上会产生额外的CPU和内存开销。
在实际业务中,很多DISTINCT的使用场景可以通过更精准的WHERE条件或GROUP BY替代。例如,查询“有哪些不同的部门ID”可以使用 SELECT DISTINCT dept_id FROM employees,但如果需要统计每个部门的员工数,则应使用 SELECT dept_id, COUNT(*) FROM employees GROUP BY dept_id。DISTINCT与GROUP BY在去重功能上等价,但GROUP BY更灵活,可以配合聚合函数。
实战辨析:假设要查询“有订单记录的用户ID”,如果直接 SELECT DISTINCT user_id FROM orders,在订单表数据量巨大时,去重操作可能较慢。如果orders表在user_id上有索引,且业务允许,可以改写为 SELECT user_id FROM orders GROUP BY user_id,两者执行计划可能不同,但GROUP BY在某些数据库优化器下能更高效地利用索引。
基础运算符与逻辑条件:构建复杂过滤的基石
运算符包括算术运算符(+、-、*、/、%)、比较运算符和逻辑运算符。在SELECT投影中,可以使用算术运算符对列进行计算,生成新的派生列。在WHERE中,逻辑运算符AND、OR、NOT用于组合多个条件。理解运算符的优先级至关重要:NOT > AND > OR。如果不确定,务必使用括号明确优先级,这不仅能避免逻辑错误,还能提高代码可读性。
另一个常见陷阱是NULL值的处理。任何与NULL进行的比较运算结果都是“未知”(UNKNOWN),在WHERE中会被视为不满足条件。例如 WHERE salary > 10000 OR salary <= 10000 并不能筛选出所有员工,因为salary为NULL的行在两个条件中都不成立。必须显式使用 IS NULL 或 IS NOT NULL 来处理空值。
-- 运算符与逻辑条件综合示例
SELECT
employee_id,
first_name,
salary,
salary * 1.1 AS adjusted_salary
FROM employees
WHERE (department_id = 10 OR department_id = 20)
AND salary > 5000
AND (commission_pct IS NOT NULL OR salary < 8000)
ORDER BY adjusted_salary DESC;
案例拆解:从需求到SQL的完整推演
下面通过一个实际业务场景,完整演示如何将需求转化为正确的SQL查询,并规避常见陷阱。
业务背景:某电商平台的运营人员需要分析“2023年第四季度(10月1日至12月31日)下单且实付金额大于200元的VIP客户(等级为'A'或'B')的订单明细,按订单金额降序排列,并分页展示第一页(每页15条)”。数据表结构如下:
- customers 表:customer_id, customer_name, vip_level('A','B','C')
- orders 表:order_id, customer_id, order_date, actual_amount
初始目标:获取符合条件的订单列表,包含客户姓名、订单日期、实付金额。
分析过程:
- 确定数据源:需要关联customers和orders两张表,使用INNER JOIN。
- WHERE过滤:订单日期范围
order_date >= '2023-10-01' AND order_date < '2024-01-01';金额条件actual_amount > 200;VIP等级vip_level IN ('A','B')。 - 排序:按
actual_amount DESC。 - 分页:第一页15条,即
LIMIT 15 OFFSET 0。
-- 最终SQL
SELECT
c.customer_name,
o.order_id,
o.order_date,
o.actual_amount
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
WHERE o.order_date >= '2023-10-01'
AND o.order_date < '2024-01-01'
AND o.actual_amount > 200
AND c.vip_level IN ('A', 'B')
ORDER BY o.actual_amount DESC
LIMIT 15 OFFSET 0;
结果与复盘:该查询正确返回了所需数据。但复盘时发现,如果orders表数据量极大,且order_date上有索引,上述查询在过滤日期范围时能有效利用索引。然而,如果后续需要支持“按客户名称模糊搜索”或“按订单金额区间搜索”,则需要额外评估索引策略。此外,如果VIP等级字段在customers表上,关联查询时驱动表的选择会影响性能,通常应选择过滤后数据量较小的表作为驱动表。
风险提示:在关联查询中,如果忘记为customers表的customer_id主键或orders表的customer_id外键建立索引,会导致嵌套循环连接(Nested Loop Join)时频繁扫描内表,性能极差。建议在关联列和WHERE过滤列上建立合适的复合索引,例如 (order_date, actual_amount) 或 (customer_id, order_date)。
常见误区与总结复盘
以下是本章内容中初学者最容易犯的错误,务必牢记:
| 误区 | 错误示例 | 正确做法 |
|---|---|---|
| WHERE中使用别名 | WHERE annual_salary > 50000 | 使用原始列表达式 |
| NULL判断用= | WHERE email = NULL | WHERE email IS NULL |
| AND/OR优先级混淆 | WHERE a OR b AND c | WHERE (a OR b) AND c |
| 深分页OFFSET过大 | LIMIT 100000, 20 | 使用键集分页 |
| DISTINCT多列误解 | SELECT DISTINCT col1, col2 | 理解是对组合去重 |
| LIKE后缀模糊 | LIKE '%keyword' | 考虑全文索引或改写 |
总结来说,SELECT语法虽然基础,但每一个子句都有其内在的执行逻辑和性能影响。掌握逻辑执行顺序是分析问题的钥匙;WHERE过滤要兼顾正确性与索引利用;ORDER BY和LIMIT要警惕深分页陷阱;DISTINCT要权衡去重代价;运算符和逻辑条件要特别注意NULL值和优先级。在实际工作中,建议养成先写清晰逻辑、再检查执行计划、最后验证数据正确性的习惯。只有将基础打牢,才能在后续的复杂查询优化中游刃有余。