SELECT 查询基础
问题
SQL 中 SELECT 语句怎么用?WHERE、ORDER BY、LIMIT 分别有什么作用?
答案
SELECT 是 SQL 中使用频率最高的语句,用来从数据库中查询数据。你可以把它理解为"向数据库提问"——告诉数据库你想要什么数据、满足什么条件、按什么顺序排列。
SELECT 基本语法
一条完整的 SELECT 语句结构如下:
SELECT [DISTINCT] 列名1, 列名2, ... -- 选择哪些列
FROM 表名 -- 从哪张表查
WHERE 条件 -- 过滤哪些行
ORDER BY 列名 [ASC|DESC] -- 怎么排序
LIMIT 行数 OFFSET 偏移量; -- 取多少条
这些部分都是按需使用的,只有 SELECT 和 FROM 是必须的。
用一个实际例子来理解
假设有一张 employees 表:
| id | name | department | salary | hire_date |
|---|---|---|---|---|
| 1 | 张三 | 技术部 | 15000 | 2020-03-15 |
| 2 | 李四 | 市场部 | 12000 | 2021-06-01 |
| 3 | 王五 | 技术部 | 18000 | 2019-01-10 |
| 4 | 赵六 | 市场部 | 11000 | 2022-09-20 |
| 5 | 孙七 | 技术部 | 20000 | 2018-05-08 |
| 6 | 周八 | 人事部 | 13000 | 2021-11-30 |
查询所有数据
-- 查询所有列所有行
SELECT * FROM employees;
-- 查询指定列
SELECT name, salary FROM employees;
-- 使用别名(AS 可以省略)
SELECT name AS 姓名, salary AS 月薪 FROM employees;
SELECT name 姓名, salary 月薪 FROM employees; -- 省略 AS
SELECT * 存在以下问题:
- 查询不需要的列浪费 I/O 和内存
- 影响查询优化器选择覆盖索引
- 表结构变化可能导致程序出错
在日常分析中使用 SELECT * 来快速看数据没问题,但正式查询建议明确指定列名。
WHERE 条件过滤
WHERE 子句用来筛选满足条件的行。
比较运算符
-- 等于
SELECT * FROM employees WHERE department = '技术部';
-- 不等于
SELECT * FROM employees WHERE department != '技术部';
SELECT * FROM employees WHERE department <> '技术部'; -- 同样效果
-- 大于/小于/大于等于/小于等于
SELECT * FROM employees WHERE salary > 15000;
SELECT * FROM employees WHERE salary >= 15000;
SELECT * FROM employees WHERE hire_date < '2021-01-01';
逻辑运算符
-- AND:同时满足多个条件
SELECT * FROM employees
WHERE department = '技术部' AND salary > 15000;
-- 结果:王五(18000)、孙七(20000)
-- OR:满足其中一个条件
SELECT * FROM employees
WHERE department = '技术部' OR department = '市场部';
-- NOT:取反
SELECT * FROM employees WHERE NOT department = '技术部';
范围查询
-- BETWEEN:闭区间范围 [12000, 18000]
SELECT * FROM employees WHERE salary BETWEEN 12000 AND 18000;
-- 等价于:WHERE salary >= 12000 AND salary <= 18000
-- IN:匹配列表中的任意值
SELECT * FROM employees WHERE department IN ('技术部', '市场部');
-- 等价于:WHERE department = '技术部' OR department = '市场部'
-- NOT IN:不在列表中
SELECT * FROM employees WHERE department NOT IN ('技术部');
模糊查询
-- LIKE + 通配符
-- %:匹配零个或多个任意字符
-- _:匹配恰好一个任意字符
SELECT * FROM employees WHERE name LIKE '张%'; -- 以"张"开头
SELECT * FROM employees WHERE name LIKE '%三'; -- 以"三"结尾
SELECT * FROM employees WHERE name LIKE '%五%'; -- 包含"五"
SELECT * FROM employees WHERE name LIKE '张_'; -- "张" + 恰好一个字符
NULL 判断
-- 判断是否为 NULL(不能用 = NULL)
SELECT * FROM employees WHERE department IS NULL;
SELECT * FROM employees WHERE department IS NOT NULL;
ORDER BY 排序
-- 默认升序(ASC 可省略)
SELECT * FROM employees ORDER BY salary;
SELECT * FROM employees ORDER BY salary ASC;
-- 降序
SELECT * FROM employees ORDER BY salary DESC;
-- 多列排序:先按部门升序,同部门内按薪资降序
SELECT * FROM employees ORDER BY department ASC, salary DESC;
-- 按列别名排序
SELECT name, salary * 12 AS annual_salary
FROM employees
ORDER BY annual_salary DESC;
-- 按列位置排序(不推荐,可读性差)
SELECT name, salary FROM employees ORDER BY 2 DESC; -- 按第2列(salary)降序
LIMIT 分页
-- 取前 3 条
SELECT * FROM employees ORDER BY salary DESC LIMIT 3;
-- 分页:跳过前 2 条,取接下来的 3 条
SELECT * FROM employees ORDER BY id LIMIT 3 OFFSET 2;
-- MySQL 简写:LIMIT 偏移量, 行数
SELECT * FROM employees ORDER BY id LIMIT 2, 3;
-- 常见分页公式:第 page 页,每页 pageSize 条
-- LIMIT pageSize OFFSET (page - 1) * pageSize
面试中可能会问"LIMIT 分页在数据量大时有什么性能问题?"
问题:LIMIT 1000000, 10 需要先扫描 1000010 行再丢弃前 100 万行。
优化方案:使用"游标分页",基于上一页最后一条记录的 ID:
-- ❌ 慢:深度分页
SELECT * FROM employees ORDER BY id LIMIT 10 OFFSET 1000000;
-- ✅ 快:游标分页(假设上一页最后的 id 是 1000000)
SELECT * FROM employees WHERE id > 1000000 ORDER BY id LIMIT 10;
DISTINCT 去重
-- 单列去重
SELECT DISTINCT department FROM employees;
-- 结果:技术部、市场部、人事部
-- 多列去重(根据组合去重)
SELECT DISTINCT department, salary FROM employees;
-- 只去掉 department 和 salary 完全相同的行
-- COUNT + DISTINCT:统计不重复的值的数量
SELECT COUNT(DISTINCT department) AS dept_count FROM employees;
-- 结果:3
DISTINCT 是针对整行结果去重,不是只对紧跟在后面的列去重。SELECT DISTINCT a, b 是对 (a, b) 组合去重。
计算列与函数
算术运算
-- 基本运算
SELECT name, salary, salary * 12 AS annual_salary FROM employees;
SELECT name, salary * 1.1 AS 加薪后 FROM employees; -- 加薪10%
-- COALESCE:处理 NULL(返回第一个非 NULL 的值)
SELECT name, COALESCE(bonus, 0) + salary AS total_pay FROM employees;
-- 如果 bonus 为 NULL,用 0 代替
字符串函数
| 函数 | MySQL | PostgreSQL | 说明 |
|---|---|---|---|
| 拼接 | CONCAT(a, b) | a || b | 字符串连接 |
| 长度 | LENGTH() / CHAR_LENGTH() | LENGTH() | 字符/字节数 |
| 截取 | SUBSTRING(s, pos, len) | SUBSTRING(s FROM pos FOR len) | 子字符串 |
| 替换 | REPLACE(s, old, new) | REPLACE(s, old, new) | 替换 |
| 大小写 | UPPER() / LOWER() | UPPER() / LOWER() | 转换大小写 |
| 去空格 | TRIM() | TRIM() | 去首尾空格 |
-- 实用示例
SELECT name,
CONCAT(department, '-', name) AS full_label,
LENGTH(name) AS name_length,
UPPER(SUBSTRING(name, 1, 1)) AS first_char
FROM employees;
日期函数
-- 当前日期/时间
SELECT NOW(), CURDATE(), CURTIME();
-- 提取日期部分
SELECT name, hire_date,
YEAR(hire_date) AS hire_year,
MONTH(hire_date) AS hire_month,
DAY(hire_date) AS hire_day
FROM employees;
-- 日期计算
SELECT name, hire_date,
DATEDIFF(CURDATE(), hire_date) AS days_worked, -- 工作天数
TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) AS years_worked -- 工作年数
FROM employees;
-- 日期格式化(MySQL)
SELECT DATE_FORMAT(hire_date, '%Y年%m月%d日') FROM employees;
-- 日期格式化(PostgreSQL)
SELECT TO_CHAR(hire_date, 'YYYY年MM月DD日') FROM employees;
-- 日期加减
SELECT DATE_ADD(CURDATE(), INTERVAL 30 DAY); -- 30天后
SELECT DATE_SUB(CURDATE(), INTERVAL 1 MONTH); -- 1个月前
条件表达式
CASE WHEN(SQL 的 if-else)
CASE WHEN 在数据分析中极其常用,用于创建派生字段、分桶、条件计数等:
-- 用法一:简单 CASE(匹配具体值)
SELECT name,
CASE department
WHEN '技术部' THEN 'Tech'
WHEN '市场部' THEN 'Marketing'
WHEN '人事部' THEN 'HR'
ELSE 'Other'
END AS dept_en
FROM employees;
-- 用法二:搜索 CASE(匹配条件表达式,更灵活)
SELECT name, salary,
CASE
WHEN salary >= 18000 THEN '高薪'
WHEN salary >= 13000 THEN '中等'
ELSE '待提升'
END AS level
FROM employees;
-- 用法三:CASE WHEN + 聚合 = 行转列
SELECT
department,
COUNT(CASE WHEN salary >= 15000 THEN 1 END) AS high_salary_count,
COUNT(CASE WHEN salary < 15000 THEN 1 END) AS low_salary_count
FROM employees
GROUP BY department;
IF 函数(MySQL 特有简写)
-- IF(条件, 真值, 假值) —— 相当于简化版 CASE WHEN
SELECT name, IF(salary >= 15000, '高薪', '一般') AS level FROM employees;
-- IFNULL(值, 替代值) —— NULL 替换
SELECT name, IFNULL(bonus, 0) AS bonus FROM employees;
COALESCE 函数
-- 返回第一个非 NULL 的值
SELECT COALESCE(nickname, name, 'unknown') AS display_name FROM users;
-- 优先用昵称,没有就用名字,都没有用 'unknown'
数据分析实战场景
场景 1:用户分群
-- 根据注册时间和消费金额对用户分群
SELECT
user_id,
total_orders,
total_amount,
CASE
WHEN total_orders >= 10 AND total_amount >= 5000 THEN 'VIP'
WHEN total_orders >= 5 THEN '活跃用户'
WHEN total_orders >= 1 THEN '普通用户'
ELSE '沉默用户'
END AS user_segment
FROM (
SELECT user_id, COUNT(*) AS total_orders, SUM(amount) AS total_amount
FROM orders
GROUP BY user_id
) user_stats;
场景 2:日期筛选
-- 查询最近 7 天的数据
SELECT * FROM events WHERE event_date >= DATE_SUB(CURDATE(), INTERVAL 7 DAY);
-- 查询本月数据
SELECT * FROM events
WHERE YEAR(event_date) = YEAR(CURDATE())
AND MONTH(event_date) = MONTH(CURDATE());
-- 更高效的写法(可以走索引)
SELECT * FROM events
WHERE event_date >= DATE_FORMAT(CURDATE(), '%Y-%m-01')
AND event_date < DATE_FORMAT(CURDATE() + INTERVAL 1 MONTH, '%Y-%m-01');
场景 3:条件统计
-- 统计不同状态的订单数量(一行展示)
SELECT
COUNT(*) AS total,
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS paid_count,
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) AS cancelled_count,
ROUND(SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) AS paid_rate
FROM orders
WHERE order_date >= '2024-01-01';
场景 4:数据质量检查
在数据分析工作中,拿到新数据集时应先做质量摸底,了解空值率、重复率等基本情况,再决定如何处理:
-- 一次性检查 users 表的空值率和重复率
SELECT
COUNT(*) AS total_rows,
-- 空值统计:用 CASE WHEN 计数为 NULL 的行
SUM(CASE WHEN email IS NULL THEN 1 ELSE 0 END) AS email_null_count,
-- 空值率:乘以 100.0 强制浮点除法,避免整数截断
ROUND(SUM(CASE WHEN email IS NULL THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) AS email_null_rate,
-- 去重计数:COUNT(DISTINCT ...) 统计不重复的值
COUNT(DISTINCT email) AS unique_emails,
-- 重复行数 = 总行数 - 不重复行数
COUNT(*) - COUNT(DISTINCT email) AS duplicate_emails
FROM users;
实际工作中可以把多个字段的空值率合并到一个查询,或者用 COUNT(列名) 的特性(自动忽略 NULL)做简洁写法:
-- COUNT(列名) 会忽略 NULL,等价于 SUM(CASE WHEN col IS NOT NULL THEN 1 ELSE 0 END)
SELECT
COUNT(*) AS total,
COUNT(*) - COUNT(email) AS email_null_count, -- 总行数 - 非NULL行数 = NULL行数
COUNT(*) - COUNT(phone) AS phone_null_count,
COUNT(*) - COUNT(name) AS name_null_count
FROM users;
常见面试问题
Q1: SELECT 语句的执行顺序是什么?
答案:
SQL 的书写顺序和执行顺序不同。执行顺序为:
FROM— 确定数据来源WHERE— 行级过滤GROUP BY— 分组HAVING— 组级过滤SELECT— 选择列、计算表达式DISTINCT— 去重ORDER BY— 排序LIMIT— 限制行数
常见陷阱:WHERE 执行在 SELECT 之前,所以 WHERE 中不能使用 SELECT 中定义的列别名。
理解执行顺序能帮你避开很多常见错误:
-
WHERE 中不能用 SELECT 别名:
SELECT salary * 12 AS annual定义的annual在 WHERE 阶段还不存在,WHERE annual > 100000会报错;应改用WHERE salary * 12 > 100000,或用子查询/CTE 包一层。 -
HAVING 才能用聚合函数:
WHERE AVG(salary) > 15000会报错,因为 WHERE 发生在分组之前,聚合还没计算;正确写法是HAVING AVG(salary) > 15000。 -
ORDER BY 可以用 SELECT 别名:ORDER BY 在 SELECT 之后执行,所以
ORDER BY annual DESC是合法的,这是 ORDER BY 和 WHERE 的关键区别。
Q2: WHERE 和 HAVING 有什么区别?
答案:
| 对比 | WHERE | HAVING |
|---|---|---|
| 过滤对象 | 过滤行 | 过滤分组 |
| 执行时机 | 在 GROUP BY 之前 | 在 GROUP BY 之后 |
| 聚合函数 | 不能使用 | 可以使用 |
-- WHERE 先过滤行,再分组
SELECT department, AVG(salary)
FROM employees
WHERE salary > 10000 -- 先排除低于1万的员工
GROUP BY department;
-- HAVING 先分组,再过滤组
SELECT department, AVG(salary) AS avg_sal
FROM employees
GROUP BY department
HAVING avg_sal > 15000; -- 只保留平均薪资>1.5万的部门
Q3: NULL 在 WHERE 条件中有什么陷阱?
答案:
NULL 表示"未知",任何值与 NULL 比较的结果都是 NULL(而非 TRUE/FALSE),因此:
-- ❌ 这样查不到 NULL 值
SELECT * FROM employees WHERE department = NULL;
-- ✅ 正确方式
SELECT * FROM employees WHERE department IS NULL;
-- ❌ NOT IN 遇到 NULL 的陷阱
SELECT * FROM employees WHERE department NOT IN ('技术部', NULL);
-- 结果:返回 0 行!因为任何值 != NULL 都是 NULL
-- ✅ 安全做法
SELECT * FROM employees
WHERE department NOT IN ('技术部')
AND department IS NOT NULL;
Q4: LIKE '%keyword' 为什么性能差?怎么优化?
答案:
LIKE '%keyword'(前缀有 %)会导致全表扫描,因为数据库索引是从左往右匹配的(类似字典查找——你知道开头才能翻到对应页数),前缀不确定就无法使用索引。
优化方案:
- 全文索引:MySQL 的
FULLTEXT索引或 PostgreSQL 的tsvector - Elasticsearch:专业搜索引擎
- 反转存储:如果经常搜后缀(如邮箱域名),可以额外存一列反转后的字符串
Q5: LIMIT 1000000, 10 为什么很慢?怎么优化?
答案:
LIMIT offset, count 的工作方式是:先读取 offset + count 行,然后丢弃前 offset 行。当 offset 很大时(深度分页),需要扫描大量无用数据。
优化方案:
-- 方案 1:游标分页(最推荐)
-- 前端传来上一页最后一条记录的 ID
SELECT * FROM employees WHERE id > 1000000 ORDER BY id LIMIT 10;
-- 方案 2:延迟关联(利用覆盖索引)
SELECT e.* FROM employees e
INNER JOIN (SELECT id FROM employees ORDER BY id LIMIT 1000000, 10) t
ON e.id = t.id;
-- 子查询只查 id(走覆盖索引,更快),再用 id 关联获取完整数据
Q6: 什么是 SQL 注入?数据分析师需要了解哪些防护知识?
答案:
SQL 注入(SQL Injection)是指攻击者通过在输入中插入恶意 SQL 代码,改变原本 SQL 语句的逻辑,从而非法读取、篡改甚至删除数据库数据。
典型攻击示例:
-- 假设程序拼接 SQL:
-- "SELECT * FROM users WHERE name = '" + input + "'"
-- 用户输入:' OR '1'='1
-- 拼接结果变成:
SELECT * FROM users WHERE name = '' OR '1'='1';
-- '1'='1' 恒为真,导致返回所有用户数据!
防护方法(数据分析师需知):
-- ❌ 危险:字符串拼接(任何语言中都不要这么做)
query = "SELECT * FROM users WHERE id = " + user_input
-- ✅ 安全:参数化查询(用占位符,驱动层自动转义)
-- Python + pymysql
cursor.execute("SELECT * FROM users WHERE id = %s", (user_input,))
-- ✅ 安全:使用 ORM(SQLAlchemy、Django ORM 默认参数化)
User.objects.filter(id=user_input)
数据分析师虽然通常不写业务代码,但在编写报表工具、数据导出接口或BI 平台动态查询时需注意:
- 永远不要把用户输入直接拼进 SQL 字符串
- 对输入做白名单校验(如列名、排序方向只允许枚举值)
- 利用
LIMIT限制返回行数,防止意外全表导出
Q7: 如何不用窗口函数查询每个部门薪资第 2 高的员工?
答案:
这是一道经典 SQL 面试题,考察相关子查询(Correlated Subquery)的掌握程度。核心思路:对于某部门中的某员工,如果比他薪资高的同部门员工恰好只有 1 个,那他就是第 2 名。
-- 相关子查询方式:
-- 对外层每一行,内层子查询统计"同部门中薪资严格大于我的人数"
-- 如果该人数 = 1,说明只有 1 人比我高,我就是第 2 高
SELECT e1.name, e1.department, e1.salary
FROM employees e1
WHERE (
SELECT COUNT(DISTINCT e2.salary) -- DISTINCT 处理同薪情况
FROM employees e2
WHERE e2.department = e1.department -- 同部门
AND e2.salary > e1.salary -- 比当前员工薪资高
) = 1; -- 恰好只有 1 人比我高 → 我是第 2 名
- 若要查第 N 名,把
= 1改为= N - 1即可 - 上述写法用
DISTINCT处理了同薪问题(薪资相同视为同名次);如果要求同薪算不同名次,去掉DISTINCT - 现代 SQL 更推荐用窗口函数
DENSE_RANK()/ROW_NUMBER()解决此类问题,语义更清晰
Q8: WHERE 1=1 是什么意思?有什么用?
答案:
1=1 是一个恒为真的条件,对查询结果没有任何过滤效果。单独看它毫无意义,但在动态拼接 SQL 场景中非常实用:
场景:报表工具根据用户勾选的筛选条件动态生成 SQL,过滤条件数量不确定。
-- ❌ 没有 WHERE 1=1 时,第一个条件是 WHERE,后续是 AND,需要判断"我是不是第一个条件"
-- 拼接逻辑麻烦,容易出错:
SELECT * FROM orders WHERE status = 'paid' AND amount > 100
-- 如果 status 条件不勾选,就变成:
SELECT * FROM orders WHERE amount > 100 -- 得手动去掉 AND
-- ✅ 有了 WHERE 1=1,所有动态条件都统一用 AND 拼接,代码更简洁
SELECT * FROM orders WHERE 1=1
-- 程序逐条追加(Python 示例):
-- if status_filter: sql += " AND status = 'paid'"
-- if amount_filter: sql += " AND amount > 100"
-- 最终结果:SELECT * FROM orders WHERE 1=1 AND status = 'paid' AND amount > 100
绝大多数数据库的查询优化器会自动识别并消除 1=1 这类常量条件,不会产生性能损耗。