SQL 单表操作全解
本文所有语法和实例,均基于开发最常用的users用户表,表结构完全符合生产规范,后续所有操作均围绕此表展开:
sql
CREATE TABLE IF NOT EXISTS users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID,主键',
username VARCHAR(50) NOT NULL COMMENT '用户名',
email VARCHAR(100) DEFAULT '' COMMENT '用户邮箱',
password CHAR(32) NOT NULL COMMENT '加密密码',
age TINYINT UNSIGNED DEFAULT 0 COMMENT '年龄',
city VARCHAR(20) DEFAULT '' COMMENT '所在城市',
gender TINYINT DEFAULT 0 COMMENT '性别:0-未知,1-男,2-女',
is_delete TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除:0-未删除,1-已删除(逻辑删除)',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-- 单表索引(后续优化章节详解)
PRIMARY KEY (id),
UNIQUE KEY uk_username (username),
KEY idx_city_age (city, age),
KEY idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
单表操作的本质,就是对一张二维表的两个维度的精准控制,以及基于筛选结果的附加加工:
- 列维度(垂直筛选) :通过
SELECT控制要查询的字段,只提取需要的列; - 行维度(水平筛选) :通过
WHERE控制要查询的记录,只过滤符合条件的行; - 结果加工:对筛选后的结果做排序、分页、去重等格式化处理;
- 数据计算:对筛选后的结果做聚合、分组、统计等数值计算;
- 数据操纵:对表中的数据进行新增、更新、删除操作。
列维度筛选:SELECT 语法全解
SELECT是单表操作的入口,核心作用是精准提取需要的字段 ,生产环境有一条铁律:严禁使用SELECT *查询全字段,仅查询业务需要的列,避免敏感字段泄露、性能损耗和表结构变更带来的业务报错。
基础指定字段查询
sql
-- 语法:SELECT 字段1, 字段2, 字段3 FROM 表名;
-- 实例:查询用户的非敏感基础信息(忽略密码等敏感字段)
SELECT id, username, email, age, city FROM users;
字段别名:AS
用于简化字段名、解决字段重名、自定义返回列名,提升结果可读性,别名支持中文(生产建议用英文)。
sql
-- 语法:SELECT 字段 AS 别名 FROM 表名;
-- 实例:给字段起业务别名
SELECT
id AS user_id,
username AS user_name,
email AS user_email,
age AS user_age
FROM users;
去重查询:DISTINCT
用于剔除查询结果中的重复行,核心规则:DISTINCT对查询的所有字段组合去重,而非单个字段。
sql
-- 语法:SELECT DISTINCT 字段1, 字段2 FROM 表名;
-- 实例1:查询表中所有不重复的城市(单字段去重)
SELECT DISTINCT city FROM users;
-- 实例2:查询城市+年龄的不重复组合(多字段组合去重)
SELECT DISTINCT city, age FROM users;
常量列与计算列
用于给结果补充固定值,或对字段做简单的数值计算,无需修改表结构即可实现业务需求。
sql
-- 实例1:补充常量列,给所有用户标记固定的用户类型
SELECT
id,
username,
'正式用户' AS user_type -- 常量列,所有行均为该固定值
FROM users;
-- 实例2:计算列,对数值字段做简单计算
SELECT
id,
username,
age,
age + 1 AS next_year_age, -- 计算用户明年的年龄
2026 - age AS birth_year -- 计算用户出生年份
FROM users;
行维度筛选:WHERE 语法全解
WHERE是单表精准查询的核心,作用是按条件过滤符合业务规则的行,避免全表扫描,是开发中使用频率最高的语法之一。
常用条件运算符
| 运算符类型 | 具体运算符 | 作用说明 | 开发高频度 |
|---|---|---|---|
| 比较运算符 | =、>、<、>=、<=、!=/<> |
数值、字符串、日期的等值 / 不等值比较 | ★★★★★ |
| 逻辑运算符 | AND、OR、NOT |
多条件组合,AND优先级高于OR,可用括号调整优先级 |
★★★★★ |
| 范围运算符 | BETWEEN ... AND ...、IN |
区间匹配、集合匹配 | ★★★★☆ |
| 模糊匹配 | LIKE |
模糊查询,%匹配任意字符,_匹配单个字符 |
★★★★☆ |
| 空值判断 | IS NULL、IS NOT NULL |
空值判断,NULL 不能用=/!=判断 |
★★★★☆ |
sql
-- 1. 基础等值查询:查询指定ID的用户
SELECT id, username, email FROM users WHERE id = 100;
-- 2. 多条件组合查询:查询北京的成年、未删除的男性用户
-- 注意:AND优先级高于OR,复杂条件必须用括号明确执行顺序
SELECT id, username, age, city FROM users
WHERE age >= 18
AND city = '北京'
AND gender = 1
AND is_delete = 0;
-- 3. 范围查询:查询年龄在18-30岁之间的用户
SELECT id, username, age FROM users
WHERE age BETWEEN 18 AND 30;
-- 4. 集合匹配:查询北上广深四个城市的用户
SELECT id, username, city FROM users
WHERE city IN ('北京', '上海', '广州', '深圳');
-- 5. 模糊查询:查询姓张的用户(前缀匹配,可命中索引)
SELECT id, username FROM users
WHERE username LIKE '张%';
-- 6. 模糊查询:查询用户名包含「三」的用户(中缀匹配,无法命中索引,大表慎用)
SELECT id, username FROM users
WHERE username LIKE '%三%';
-- 7. 空值判断:查询未填写邮箱的用户
SELECT id, username FROM users
WHERE email IS NULL OR email = '';
-- 8. 取反查询:查询除北京之外的用户
SELECT id, username, city FROM users
WHERE NOT city = '北京';
必避坑
NULL值判断必须用IS NULL/IS NOT NULL,NULL = ''、NULL = NULL的结果均为UNKNOWN,不会匹配到任何行;AND优先级高于OR,例如WHERE a=1 OR b=2 AND c=3,实际执行逻辑是a=1 OR (b=2 AND c=3),如需调整顺序,必须用括号包裹;- 模糊查询
%xxx%中缀匹配无法命中索引,大表查询会触发全表扫描,生产环境慎用。
结果集加工:排序、分页、去重
对SELECT + WHERE筛选后的结果,做格式化处理,满足业务的展示、分页、排序需求,是列表类接口的必用语法。
排序:ORDER BY
用于对结果集按指定字段排序,ASC为升序(默认值,可省略),DESC为降序,支持多字段组合排序。
sql
-- 语法:SELECT 字段 FROM 表名 [WHERE 条件] ORDER BY 字段1 ASC/DESC, 字段2 ASC/DESC;
-- 实例1:按用户创建时间倒序,查询最新注册的用户
SELECT id, username, create_time FROM users
WHERE is_delete = 0
ORDER BY create_time DESC;
-- 实例2:多字段排序:先按城市升序,同城市内按年龄降序
SELECT id, username, city, age FROM users
WHERE is_delete = 0
ORDER BY city ASC, age DESC;
分页:LIMIT
用于限制返回的行数,避免一次性查询全表海量数据导致内存溢出,是后台列表、前端分页接口的核心语法。
sql
-- 语法:LIMIT 偏移量, 每页条数; 偏移量计算公式:(页码-1) * 每页条数
SELECT 字段 FROM 表名 [WHERE 条件] [ORDER BY 排序] LIMIT 偏移量, 条数;
-- 实例:用户列表第2页,每页10条数据,按注册时间倒序
SELECT id, username, city, create_time FROM users
WHERE is_delete = 0
ORDER BY create_time DESC
LIMIT 10, 10; -- 第2页:(2-1)*10=10,偏移量10,取10条
-- 简化语法:查询前10条数据,无需偏移量
SELECT id, username FROM users LIMIT 10;
分页优化提示
大表深分页(如LIMIT 100000, 10)性能极差,优化方案:用主键过滤替代偏移量,示例:
sql
-- 优化前:深分页,需扫描100010行数据
SELECT id, username FROM users ORDER BY id DESC LIMIT 100000, 10;
-- 优化后:主键过滤,仅扫描10行数据
SELECT id, username FROM users WHERE id < 100000 ORDER BY id DESC LIMIT 10;
单表聚合与分组统计
用于对筛选后的结果做数值计算、分组统计,是报表、数据看板、业务统计类需求的核心语法。
常用聚合函数
聚合函数会对多行数据做计算,最终返回单个结果,聚合函数会自动忽略NULL值的行。
| 函数 | 核心作用 | 开发高频度 |
|---|---|---|
COUNT(*) |
统计结果集的总行数,包含NULL值行、空值行 |
★★★★★ |
COUNT(字段) |
统计指定字段非空的行数,排除NULL值行 |
★★★★☆ |
SUM(字段) |
对数值字段求和,非数值字段返回 0 | ★★★★☆ |
AVG(字段) |
对数值字段求平均值,排除NULL值行 |
★★★★☆ |
MAX(字段) |
求字段的最大值,支持数值、日期、字符串 | ★★★★☆ |
MIN(字段) |
求字段的最小值,支持数值、日期、字符串 | ★★★★☆ |
sql
-- 实例1:统计平台总用户数(未删除)
SELECT COUNT(*) AS total_user FROM users WHERE is_delete = 0;
-- 实例2:统计北京用户的平均年龄、最大年龄、最小年龄
SELECT
AVG(age) AS avg_age,
MAX(age) AS max_age,
MIN(age) AS min_age
FROM users
WHERE city = '北京' AND is_delete = 0;
-- 实例3:统计已填写邮箱的用户数量
SELECT COUNT(email) AS email_user_count FROM users WHERE email IS NOT NULL AND email != '';
分组统计:GROUP BY
将结果集按指定字段分组,对每个分组单独做聚合计算,实现「分维度统计」的业务需求。
MySQL 5.7+ 默认开启ONLY_FULL_GROUP_BY模式,SELECT后的非聚合字段,必须全部出现在GROUP BY的分组字段中,否则会直接报错。
sql
-- 语法
SELECT 分组字段, 聚合函数 FROM 表名
[WHERE 行过滤条件]
GROUP BY 分组字段
[ORDER BY 排序];
-- 实例1:统计每个城市的用户数量,按用户数倒序
SELECT
city,
COUNT(*) AS user_count
FROM users
WHERE is_delete = 0
GROUP BY city
ORDER BY user_count DESC;
-- 实例2:多字段分组:统计每个城市+性别的用户数量
SELECT
city,
gender,
COUNT(*) AS user_count
FROM users
WHERE is_delete = 0
GROUP BY city, gender
ORDER BY city ASC;
分组过滤:HAVING
用于对GROUP BY分组后的结果做过滤,是新手最容易和WHERE搞混的语法,核心区别如下:
| 特性 | WHERE | HAVING |
|---|---|---|
| 执行时机 | 分组前执行,先过滤行,再分组 | 分组后执行,先分组聚合,再过滤组 |
| 聚合函数 | 不支持,不能在 WHERE 中使用聚合函数 | 支持,核心作用就是过滤聚合结果 |
| 作用对象 | 表中的行,按行过滤 | 分组后的结果集,按组过滤 |
sql
-- 语法
SELECT 分组字段, 聚合函数 FROM 表名
[WHERE 行过滤条件]
GROUP BY 分组字段
HAVING 分组后过滤条件
[ORDER BY 排序];
-- 实例:统计每个城市的用户数,仅保留用户数大于100的城市,按用户数倒序
SELECT
city,
COUNT(*) AS user_count
FROM users
WHERE is_delete = 0 -- 先过滤掉已删除的用户(行过滤)
GROUP BY city
HAVING user_count > 100 -- 再过滤掉用户数不足100的城市(组过滤)
ORDER BY user_count DESC;
-- 实例2:统计平均年龄大于25岁的城市
SELECT
city,
AVG(age) AS avg_age
FROM users
WHERE is_delete = 0
GROUP BY city
HAVING avg_age > 25;
单表查询完整执行顺序
理解执行顺序,就能彻底搞懂WHERE和HAVING的区别,单表查询的完整执行顺序如下:
plaintext
FROM 表名 → 确定要操作的表
WHERE 行过滤条件 → 过滤符合条件的行
GROUP BY 分组字段 → 对过滤后的行进行分组
聚合函数计算 → 对每个分组做聚合计算
HAVING 分组过滤条件 → 对分组后的结果做过滤
SELECT 字段/别名 → 提取最终要返回的列
ORDER BY 排序 → 对最终结果排序
LIMIT 分页 → 限制返回的行数
单表数据操纵:增删改(INSERT/UPDATE/DELETE)
核心是对单表中的数据进行写入、修改、删除操作,生产环境有严格的使用规范,必须重点关注。
新增数据:INSERT
单行插入
生产环境必须指定插入的字段,避免表结构变更(如新增字段)导致插入报错。
sql
-- 语法
INSERT INTO 表名(字段1, 字段2, 字段3)
VALUES(值1, 值2, 值3);
-- 实例:新增单个用户
INSERT INTO users(username, email, password, age, city, gender)
VALUES('张三', 'zhangsan@example.com', 'e10adc3949ba59abbe56e057f20f883e', 25, '北京', 1);
批量插入(高性能推荐)
一次性插入多条数据,远高于循环单行插入的性能,减少数据库 IO 次数,是批量导入数据的首选。
sql
-- 语法
INSERT INTO 表名(字段1, 字段2, 字段3)
VALUES(值1, 值2, 值3), (值4, 值5, 值6), (值7, 值8, 值9);
-- 实例:批量新增3个用户
INSERT INTO users(username, email, password, age, city, gender)
VALUES
('李四', 'lisi@example.com', 'e10adc3949ba59abbe56e057f20f883e', 28, '上海', 1),
('王五', 'wangwu@example.com', 'e10adc3949ba59abbe56e057f20f883e', 22, '广州', 2),
('赵六', 'zhaoliu@example.com', 'e10adc3949ba59abbe56e057f20f883e', 30, '深圳', 1);
幂等插入:INSERT IGNORE
插入时如果唯一键(如 username、id)重复,不会报错,直接忽略本次插入,避免重复插入导致的业务异常。
sql
-- 实例:用户名重复则忽略,不报错
INSERT IGNORE INTO users(username, email, password, age)
VALUES('张三', 'zhangsan_new@example.com', 'e10adc3949ba59abbe56e057f20f883e', 26);
插入或更新:ON DUPLICATE KEY UPDATE
开发高频语法,插入时如果唯一键重复,则执行更新操作;不重复则执行插入,完美实现「有则更新,无则新增」的业务需求。
sql
-- 实例:用户存在则更新邮箱和年龄,不存在则新增
INSERT INTO users(id, username, email, password, age)
VALUES(1, '张三', 'zhangsan_update@example.com', 'e10adc3949ba59abbe56e057f20f883e', 26)
ON DUPLICATE KEY UPDATE
email = VALUES(email),
age = VALUES(age),
update_time = NOW();
更新数据:UPDATE
- UPDATE 必须加 WHERE 条件,严禁无 WHERE 的全表更新;
- 执行更新前,先用
SELECT语句确认 WHERE 条件匹配的行数,避免误更新; - 大表批量更新必须拆分,避免长事务锁表。
sql
-- 语法
UPDATE 表名 SET 字段1=值1, 字段2=值2 [WHERE 条件];
-- 实例1:更新指定ID用户的邮箱和年龄
UPDATE users
SET email = 'zhangsan_new@example.com', age = 26, update_time = NOW()
WHERE id = 1;
-- 实例2:批量更新:将北京的未成年用户的性别重置为0(未知)
UPDATE users
SET gender = 0, update_time = NOW()
WHERE city = '北京' AND age < 18;
删除数据:DELETE & 逻辑删除
- DELETE 必须加 WHERE 条件,严禁无 WHERE 的全表删除;
- 执行删除前,先用
SELECT语句确认 WHERE 条件匹配的行数,避免误删; - 生产环境优先使用逻辑删除,而非物理删除,避免数据无法恢复。
物理删除:DELETE
直接从表中删除数据,删除后无法直接恢复。
sql
-- 语法
DELETE FROM 表名 [WHERE 条件];
-- 实例:删除指定ID的用户
DELETE FROM users WHERE id = 100;
逻辑删除(生产推荐)
不实际删除数据,而是通过is_delete字段标记数据为已删除状态,查询时过滤掉已删除的数据,保证数据可追溯、可恢复,是互联网业务的通用规范。
sql
-- 实例:逻辑删除指定ID的用户(更新is_delete字段为1)
UPDATE users SET is_delete = 1, update_time = NOW() WHERE id = 1;
-- 业务查询时,固定过滤is_delete=0,只查询未删除的数据
SELECT id, username FROM users WHERE is_delete = 0 AND id = 1;
清空全表:TRUNCATE
sql
TRUNCATE TABLE users;
TRUNCATE与DELETE的核心区别:
| 特性 | DELETE | TRUNCATE |
|---|---|---|
| 条件过滤 | 支持 WHERE | 不支持,直接清空全表 |
| 事务回滚 | 支持 | 不支持 |
| 自增主键 | 不重置 | 重置为初始值 |
| 执行效率 | 大表慢(逐行记录日志) | 大表极快 |
| 触发器 | 会触发 | 不会触发 |
单表操作性能核心:索引优化
单表查询的性能瓶颈,99% 都来自于索引使用不当,这里聚焦单表场景下的索引核心知识,完全贴合单表操作的优化需求。
单表常用索引类型
| 索引类型 | 核心作用 | 单表适用场景 |
|---|---|---|
| 主键索引 | 基于主键创建,唯一、非空,一张表只能有一个 | 主键 id 查询,精准定位单行数据 |
| 普通索引 | 最基础的索引,仅提升查询速度 | WHERE、ORDER BY、GROUP BY 后的高频查询字段 |
| 唯一索引 | 保证字段值唯一,查询性能极高 | 用户名、邮箱等唯一标识字段 |
| 联合索引 | 基于多个字段创建的索引,性能最优 | 多字段组合查询的场景,如WHERE city=? AND age=? |
单表索引基础操作
sql
-- 1. 创建普通索引
CREATE INDEX idx_age ON users(age);
-- 2. 创建唯一索引
CREATE UNIQUE INDEX uk_email ON users(email);
-- 3. 创建联合索引(高区分度字段放最左侧)
CREATE INDEX idx_city_age_gender ON users(city, age, gender);
-- 4. 查看表中所有索引
SHOW INDEX FROM users;
-- 5. 删除索引
DROP INDEX idx_age ON users;
单表场景索引生效核心规则
-
最左前缀原则 :联合索引会按创建时的字段顺序,从左到右依次匹配,遇到范围查询会停止匹配。例如联合索引
idx_city_age_gender(city, age, gender):- ✅ 生效:
WHERE city='北京'、WHERE city='北京' AND age=25、WHERE city='北京' AND age=25 AND gender=1 - ❌ 失效:
WHERE age=25、WHERE gender=1、WHERE city LIKE '北%' AND age=25
- ✅ 生效:
-
ORDER BY/GROUP BY的字段尽量匹配索引,避免文件排序,提升性能; -
精准查询优先使用主键索引、唯一索引,性能远高于普通索引。
单表查询高频索引失效场景
- 索引字段使用函数、表达式计算:
WHERE YEAR(create_time) = 2026 - 索引字段发生隐式类型转换:
WHERE phone = 13800138000(phone 是 VARCHAR 类型) - 模糊查询以
%开头:WHERE username LIKE '%三' - 使用
NOT、!=、<>反向查询(优化器评估全表扫描更快时,会放弃索引) OR连接非索引字段:WHERE username='张三' OR age=25(age 无索引)- 使用
IS NOT NULL对低区分度字段查询
单表索引最佳实践
- 优先为
WHERE、ORDER BY、GROUP BY后的字段创建索引; - 联合索引优先把高区分度字段放在最左侧(如 username、city,区分度远高于 gender、is_delete);
- 单表索引数量控制在 5 个以内,避免过多索引影响增删改性能;
- 小表(数据量 < 1000 行)无需创建索引,全表扫描比索引查询更快;
- 避免创建冗余索引,如已有联合索引
idx_city_age(city,age),无需再为 city 单独建索引; - 上线前必须用
EXPLAIN查看 SQL 执行计划,确认索引是否生效。
单表操作规范
查询规范
- 严禁使用`SELECT *`,必须指定业务需要的字段;
- 大表查询必须加 WHERE 条件,严禁无过滤的全表查询;
- 避免使用`SELECT COUNT(*) FROM 大表`统计总行数,可使用统计表替代;
- 禁用`SELECT ... FOR UPDATE`行锁操作,除非强一致性场景,且必须控制锁范围。
增删改规范
- `UPDATE`、`DELETE`必须加 WHERE 条件,执行前先用 SELECT 确认条件;
- 生产环境优先使用逻辑删除,而非物理删除;
- 大表批量操作必须拆分,避免长事务锁表,如批量删除 10 万条数据,分 10 次每次删除 1 万条;
- 严禁在循环中执行单条 INSERT/UPDATE,必须使用批量操作。
语法规范
- 避免在 WHERE 条件中对索引字段做函数计算、类型转换;
- 多条件组合时,用括号明确执行优先级,避免 AND/OR 优先级混乱;
- NULL 值判断必须用 IS NULL/IS NOT NULL,禁止用 =/!= 判断 NULL;
- GROUP BY 语句严格遵守`ONLY_FULL_GROUP_BY`规则,非聚合字段必须出现在 GROUP BY 中。