直接说结论:MySQL 的 CRUD 是所有后端开发、数据分析、运维岗位绕不开的基本功,但很多人学了一堆语法就是不敢动手,或者一动手就被各种报错卡住。我见过不少转行的朋友,数据库书买了好几本,最后真正入职干活时,连一条带条件的 UPDATE 都不敢写。这篇文章不打算给你堆知识点目录,而是按我实际带新人时最常用的思路,把 CREATE、SELECT、UPDATE、DELETE 这套增删改查操作从环境搭建、建表设计写到真实排错,看完照做就行。
整份指南会分成六个部分:先把 MySQL 环境装好、连接跑通,再学建库建表,然后是读数据的 SELECT 查询,再是写数据的 INSERT、UPDATE、DELETE,接着是新人最容易踩的坑和现场排查思路,最后给一条可以直接照着练的进阶路径。适合零基础刚入门的朋友,也适合学过一点 SQL 但没系统串过一遍的人——即使你只是想把数据查出来用用,这篇也一样适用。
1. 把环境整明白:安装、连接与字符集一次到位
1.1 安装与版本选择:别让第一步变成劝退点
很多教程讲到安装就直接跳过了,但根据我带人的经验,安装这一步就能劝退掉三成新手。最常见的错误是:去下载了一个根本不匹配操作系统的安装包,或者装了一个几年前的版本,结果连工具都打不开。
我的建议非常朴素:去官网下载对应自己操作系统的社区版安装包,不要用那些绿色精简版。选择版本时,如果你没有历史项目兼容压力,直接选当前的 8.0 系列,别纠结 5.7 还是 8.0。8.0 之后的默认字符集、排序规则和窗口函数支持都要好很多,学习成本也更低。
安装过程里需要注意几个点:
- 端口默认 3306,如果没有冲突不要乱改;
- root 用户的密码要记住,不要设得太复杂然后又忘了;
- 如果是通常的服务器部署,建议创建一个专门用来跑业务的普通账号,不要所有操作都顶着 root。这不是矫情,而是你学习阶段就该养成的安全边界意识。
安装完成后,在命令行输入mysql --version能看到版本号,就算安装成功。
1.2 命令行的连接与常用全局命令
连接 MySQL 最基础的方式是把mysql命令跑到终端里:
mysql -u root -p-u指定用户,-p表示需要输入密码。输入后回车会提示Enter password,这时候输入安装时设置的密码即可(输入时不会显示,这是正常的,别以为是没敲上)。
进入 MySQL 后,最需要先记牢的不是SELECT,而是下面几条命令:
SHOW DATABASES; USE your_database; SHOW TABLES; SELECT DATABASE();SHOW DATABASES看看当前服务里有哪些数据库;USE切换到你工作的数据库;SHOW TABLES列出当前库里的所有表;SELECT DATABASE()时刻确认自己现在在哪个库。我见过不少人因为连错了库,对着错误的表折腾了半天,所以第一条经验就是:切换库之后,先跑一句SELECT DATABASE()确认一下。这个习惯能给你省掉大量无意义的排查时间。
1.3 字符集为什么重要:用 utf8mb4 作为默认选项
字符集是新手最容易忽略,又最能直接把人卡死的配置。如果你建库时没有显式指定字符集,部分默认配置会使用latin1或较低的utf8字符集,一旦你往表里写入中文、表情符号,就会出现乱码或者报错。
现在的建议很明确:所有新建的库、表,默认字符集都选utf8mb4。它不是只比utf8多了一个“mb4”,而是能完整支持四字节的 Unicode 字符,中文、日文、韩文、生僻字、表情都能存。
建库时写清楚:
CREATE DATABASE myblog DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;COLLATE是排序规则,_unicode_ci表示不区分大小写,查询时对语言排序也更准确。这里多花十分钟把字符集定好,后面所有表都会继承库的字符集,中文乱码问题能避免掉一大半。
2. 建库建表:CRUD 操作真正落地的地基
2.1 CREATE DATABASE 与 CREATE TABLE 的基础语法
建库简单,建表才是 CRUD 里“C(CREATE)”的重头戏。先看一个典型例子,我给读者演示时最喜欢用用户表users:
CREATE TABLE users ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID', username VARCHAR(50) NOT NULL COMMENT '用户名', email VARCHAR(100) NOT NULL COMMENT '邮箱', age TINYINT UNSIGNED DEFAULT 0 COMMENT '年龄', created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (id), UNIQUE KEY uk_email (email) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';这份建表语句里隐藏了几个非常关键的逻辑:
AUTO_INCREMENT配合主键PRIMARY KEY (id),让每一行数据都有一个自动递增的唯一编号,这是绝大多数业务表的标准设计;NOT NULL保证字段必须有值,避免脏数据;DEFAULT 0给可空字段设一个安全的初始值;COMMENT不是摆设,三个月后你回头看这张表,所有字段的含义全靠它;UNIQUE KEY uk_email (email)给邮箱加了唯一索引,保证同一个邮箱不会出现两行数据。
我强烈建议你把ENGINE=InnoDB作为默认引擎。它支持事务、行级锁和外键,适合绝大多数业务场景;MyISAM已经很少在实际业务中使用,学习阶段完全不用纠结。
2.2 字段类型的选择原则:先想好再建表
字段类型是新手最容易拍脑袋乱选的地方,这里我给你一套可以直接用的选择思路:
| 数据类型 | 适用场景 | 注意点 |
|---|---|---|
| INT / BIGINT | 主键、计数、存储 ID | 如果数值可能上亿,直接用 BIGINT |
| VARCHAR(n) | 用户名、邮箱、标题等变长文本 | n 设成实际业务最大值,别随手写个 255 |
| TINYINT | 状态、性别、年龄这类小范围整数 | 0/1/2 这类枚举状态很合适 |
| DECIMAL(m,d) | 金额、价格 | 绝对不要用 FLOAT 存金额 |
| DATETIME / TIMESTAMP | 时间记录 | 建表时用DEFAULT CURRENT_TIMESTAMP省心 |
| TEXT | 长文本正文 | 不适合做频繁查询排序 |
为什么金额不能用FLOAT?因为浮点数在计算机里用二进制表示,0.1 这类小数在精度上会存在误差,账目算到最后对不上,这是原则问题。
2.3 主键、外键与约束:一张表的骨架
主键不是“有了就行”,它的核心作用是唯一标识一行数据,是 DELETE、UPDATE 时最安全的条件。比如你要删除某一条记录,最稳妥的写法永远是基于主键:
DELETE FROM users WHERE id = 1;外键则是表与表之间关系的约束。比如订单表里的user_id引用用户表的id,可以在建表时声明外键,也可以只留一个普通字段。实际业务里很多团队会刻意不用数据库外键,而把关系维护放在业务层,以换取更好的插入更新性能。对新手而言,我建议你理解外键的语义,但一开始建表时不用强制使用,保持表和表之间字段语义清晰就够了。
如果你在命令行里敲错了字段名,需要重建一张表,最省事的是:
DROP TABLE IF EXISTS users;不过千万别随便在生产环境执行这条语句,学习阶段随便试,无所谓;真实环境里你可能会被同事追着打。更好的做法是用DESC users;先查看表结构,再决定是否需要删除重建。
3. SELECT 查询:用最小成本把数据读明白
3.1 SELECT 的完整执行顺序:别被书上的顺序带偏
CRUD 里真正用的最多的是 R(READ),也就是 SELECT 查询。语法看起来是:
SELECT 字段 FROM 表 WHERE 条件 GROUP BY 分组字段 HAVING 分组过滤 ORDER BY 排序字段 LIMIT 数量但你应该理解的是逻辑执行顺序,这决定了你能不能写出正确的查询:
- 从
FROM指定的表取数据; WHERE对每一行做过滤;GROUP BY分组;HAVING对分组后的结果做过滤;SELECT计算并输出字段;ORDER BY排序;LIMIT限制行数。
有个常见误区是把WHERE和HAVING混用。简单来说,WHERE在分组之前过滤行,HAVING在分组之后过滤组。统计每个年龄段的用户数,并且只保留超过 3 人的年龄段,就得这样写:
SELECT age, COUNT(*) AS user_count FROM users WHERE age > 0 GROUP BY age HAVING user_count >= 3 ORDER BY user_count DESC;3.2 WHERE 过滤:把条件写对是关键
先看一个最简单的查询:
SELECT id, username, email, age FROM users WHERE age >= 18;这里的过滤条件走的是索引或全表扫描,但更关键的是条件表达式的准确性。许多新手会把逻辑搞反,比如想查“年龄在 18 到 30 之间”的人,写成:
SELECT * FROM users WHERE age >= 18 OR age <= 30;这样整张表都被查出来了。正确写法是:
SELECT * FROM users WHERE age BETWEEN 18 AND 30;BETWEEN ... AND ...是包含两端的,和age >= 18 AND age <= 30完全等价。另外如果你需要匹配多个值:
SELECT * FROM users WHERE city IN ('北京', '上海', '广州');注意:IN列表里元素太多,查询性能会下降,学习阶段先知道处理小列表没问题,后面再学联表和子查询才是正解。
3.3 ORDER BY 与 LIMIT:排序和分页的基本功
排序的语法不难,但要注意ORDER BY的多个字段优先级:
SELECT id, username, score FROM exam_scores ORDER BY score DESC, id ASC LIMIT 10;上面的语句先按score从高到低排序,分数相同再按id从小到大排序,取前 10 条。如果你只写ORDER BY score DESC,那分数相同的记录顺序是不确定的,在需要稳定排名的场景里可能出问题。
分页是前端和后端配合时必然用到的能力:
SELECT id, username FROM users ORDER BY id LIMIT 10 OFFSET 20;LIMIT 10 OFFSET 20表示跳过 20 条,取第 21 到 30 条。页数换算公式可以这么记:第 N 页的OFFSET为(N-1) * pageSize。很多框架会自动帮你拼成这种 SQL,但你自己得看得懂才能排查问题。
3.4 聚合函数与 GROUP BY:统计需求从这里起步
只读单行数据满足不了统计,这时候要引入聚合函数:
COUNT(*)统计行数;SUM(字段)求和;AVG(字段)求平均;MAX(字段)、MIN(字段)求最大最小值。
比如统计用户总数和平均年龄:
SELECT COUNT(*) AS total_cnt, AVG(age) AS avg_age, MAX(age) AS max_age FROM users;如果想按城市分组统计人数:
SELECT city, COUNT(*) AS cnt FROM users GROUP BY city ORDER BY cnt DESC;一个常见的提醒是:SELECT中出现的普通字段,如果不在GROUP BY里,数据库的行为在不同模式下会有区别(部分模式下会报错或随机取值)。所以请培养一个习惯——“分组之后,只 select 分组字段和聚合函数”。
4. 写操作:INSERT、UPDATE、DELETE 的安全边界
4.1 INSERT:单行、多行、批量插入的取舍
插入操作最基础的是指定字段的单行插入:
INSERT INTO users (username, email, age) VALUES ('zhangsan', 'zhangsan@example.com', 28);这里建议不要省略字段名。虽然下面的写法也能执行,但表结构一旦调整,字段顺序一变化,数据很容易对错位置:
-- 不推荐 INSERT INTO users VALUES (NULL, 'lisi', 'lisi@example.com', 30, NOW());多行插入一次拼出来就行,值之间用逗号分隔:
INSERT INTO users (username, email, age) VALUES ('wangwu', 'wangwu@example.com', 22), ('zhaoliu', 'zhaoliu@example.com', 25), ('sunqi', 'sunqi@example.com', 27);如果你要一次性导入几万条,那就要考虑LOAD DATA或者拆分事务提交了。一次 INSERT 太多行可能长时间占用锁,影响其它操作。学习阶段先掌握前两种,批量导入后面独立展开。
还有个非常实用的点:INSERT 语句可以配合SELECT从别的表里取数据:
INSERT INTO user_hobby (user_id, hobby_name) SELECT id, 'coding' FROM users WHERE age > 20;这种写法的场景是“按条件批量生成关联数据”,比你在直接循环里一条条 INSERT 优雅得多。
4.2 UPDATE:带不带 WHERE 是两种命运
UPDATE 是运维同学和研发同学最怕翻车的地方。我把它拆成两部分:先看基础语法:
UPDATE users SET age = 29 WHERE username = 'zhangsan';如果你把WHERE忘了:
UPDATE users SET age = 29;那这张表里所有人的年龄都会变成 29。这不是事故概率问题,只要你忘写一次,后果就是所有数据被改。所以我在这里提出几条强制建议:
- 执行 UPDATE 前,先写一条相同
WHERE条件的SELECT,看看会命中哪些行; - 尽量用主键
id作为条件,唯一字段也可以,总之要保证条件的唯一性; - 如果确实要更新多条,先用
SELECT COUNT(*)确认影响行数; - 事务里执行,确认无误再提交。
START TRANSACTION; SELECT * FROM users WHERE id = 1 FOR UPDATE; UPDATE users SET age = 30 WHERE id = 1; -- 验证一下,确认没问题再提交 -- COMMIT; -- 有问题就回滚 -- ROLLBACK;事务的部分放到 4.4 展开,但 UPDATE 前先开事务,这是保命技能。
4.3 DELETE:物理删除之前先想清楚
DELETE 和 UPDATE 一样,核心问题是条件:
DELETE FROM users WHERE id = 10;不带WHERE的后果不需要再解释:整表清空。在学习阶段你可以随便试,在真实环境里,一条不带条件的 DELETE 基本意味着直接跑路。
业务上更值得培养的习惯是软删除:给表加一个is_deleted字段,删除时改成 UPDATE:
UPDATE users SET is_deleted = 1 WHERE id = 10;查询时统一带上WHERE is_deleted = 0。虽然查询多了一个条件,但数据还在,误删还能恢复,审计也有迹可循。很多公司对敏感业务表的态度是“删库跑路可以,但必须先走审批”,说的就是这种安全意识。
如果真的执行了物理删除并且想恢复,也不是完全没法救。后面第 5 章我会讲误删除后的止损流程。这里先记住:删除前备份,备份,再备份。
4.4 事务:给写操作加一道安全锁
MySQL 的 InnoDB 引擎天然支持事务。事务的核心意义可以用四个字母概括——ACID:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。对新手来说,最重要的是原子性:一组操作要么全部成功,要么全部失败。
举个例子,A 给 B 转账 100 元:
START TRANSACTION; UPDATE accounts SET balance = balance - 100 WHERE user_id = 1; UPDATE accounts SET balance = balance + 100 WHERE user_id = 2; -- 如果两条都成功 COMMIT; -- 如果第二条出错,执行 ROLLBACK;没有事务的话,第一条 UPDATE 成功、第二条 UPDATE 失败,钱就凭空消失了。有事务包裹,ROLLBACK会把第一条操作也撤销掉。
日常学习阶段,我建议你在命令行里手动把START TRANSACTION到COMMIT的流程敲几遍,体会一下“改了还能反悔”的感觉。这种可控感,是你在学习阶段建立写操作信心最重要的来源。
5. 新手常见错误与现场排查思路
5.1 中文乱码:问题往往不在表,而在连接
很多新人遇到中文乱码的第一反应是去改表的字符集,结果改完还是乱。实际上,乱码问题经常出现在客户端连接与服务器连接之间的字符集不一致上,而不是表本身。
排查思路是这样的:
- 先看服务器端字符集:
SHOW VARIABLES LIKE 'character_set_server';- 再看当前连接的字符集:
SHOW VARIABLES LIKE 'character_set_client';- 如果
character_set_client不是 utf8mb4,执行一条:
SET NAMES utf8mb4;然后再重新查数据。SET NAMES会同时设置客户端、连接层、结果返回的字符集,是乱码场景下最优先尝试的命令。如果每次重新连接后依旧乱码,那你还需要检查建库时有没有指定字符集,或者连接字符串里有没有加上characterEncoding=utf8mb4之类的参数。
5.2 字段名撞了关键字:用反引号兜底
order、groups、desc、select这类单词在 SQL 里都有特殊含义,但业务上偏偏喜欢用它们当字段名。我的建议是:不建这样的表,如果历史表已经这么设计了,查询时用反引号把字段名包起来:
SELECT `order`, `desc` FROM orders;反引号在绝大多数 MySQL 客户端里都能用。注意,这不是让你把普通字段也都包上,而是只在字段名与关键字冲突、或者使用了一些非常规字符时才用。表名同样适用:SELECT * FROMorder;。
5.3 误更新误删除后的止损流程
误操作谁都会遇到,关键是第一时间别慌。我自己就犯过错:一次本想只更新一张状态表里某一行,少写了WHERE,整表状态全部变了。当时手头没有任何备份,唯一的出路靠日志恢复。所以这里给出一个止损优先级:
- 服务是否还在运行?如果是,立刻停止继续执行任何新的写操作,避免数据被覆盖;
- 如果事务开启,立刻
ROLLBACK; - 如果是
binlog开着,可以用日志解析出出事语句,然后反向恢复。8.0 默认开着 binlog,这是一个极大的保命因素; - 如果表有备份文件,直接恢复备份,再把备份之后产生的写入记录重放。
从日常习惯来讲,平时做任何修改型任务之前,先做一次逻辑备份,真的不亏:
mysqldump -u root -p your_database > backup_$(date +%F).sql恢复也很简单:
mysql -u root -p your_database < backup_20240101.sql5.4 NULL 与空字符串:一个容易忽略的隐藏坑
NULL和''看起来都是“没有值”,在 MySQL 里却是两回事。NULL表示该行该字段没有被赋值,是未知状态;空字符串表示被赋值了一个长度为 0 的字符串。
这带来一个非常实际的坑:判断一个字符串字段是否为空,你不能只写WHERE field = ''。如果某些行是NULL,你得写:
SELECT * FROM users WHERE email IS NULL OR email = '';同理,COUNT(字段)统计的是非 NULL 的数量。别把COUNT(email)当成COUNT(*)来用,一张 100 万行的表,可能有 1 万行的 email 是 NULL,那COUNT(email)返回 99 万,而不是 100 万。这种细微差异在统计报表里能整出大乌龙。
6. 用一套可复用的练手路径把 CRUD 学扎实
6.1 阶段一:照着本文的语句敲,把语法肌肉练出来
光看不练等于白学。我的建议是你现在就建一个脚本库,新建一个数据库,再把 2.1 节的users表建出来,插入十行数据,然后从最简单的SELECT * FROM users;开始,一步一步往上加条件、排序、分页。每个语句敲一遍,不要复制粘贴,享受思考和报错的过程。遇到我一个一个排查,把错误信息里面包含的表名、字段名、语法提示读三遍再找解决方案。
6.2 阶段二:给自己设定一个完整业务场景
纯语法练完,你需要一个像样的场景把 CRUD 串起来。我推荐最经典的“小型订单系统”:张用户表、商品表、订单表、订单明细表。然后你可以问自己一系列问题:
- 新用户注册后插入一条用户记录(INSERT);
- 用户登录时怎么查他的信息(SELECT + WHERE);
- 修改个人信息(UPDATE);
- 用户下单时需要同时扣库存和创建订单(事务下两个 UPDATE 加一个 INSERT);
- 查某个用户最近 10 笔订单(ORDER BY 加 LIMIT);
- 统计每个商品的销售量和销售额(GROUP BY + SUM)。
把这些场景都做完一遍,你对 CRUD 的理解就不再是孤立的语法,而是能完整服务一个业务闭环。
6.3 阶段三:从 CRUD 走向索引、事务与优化
CRUD 只是起点,不是终点。当你写过的表数据量超过几百万行,你会发现慢查询开始出现了。那时候你要去理解EXPLAIN、索引失效的场景、JOIN的原理、事务隔离级别。不过这些话题都建立在一个前提下——你先对 CRUD 了如指掌。
我个人的体会是:很多所谓的“精通 MySQL”,本质上是精通了在大量数据和复杂场景下如何正确使用和优化这四类操作。所以先把基础打扎实,比如今天你花时间把WHERE条件里BETWEEN、IN、LIKE、IS NULL的细微差别弄明白,后面学索引、学执行计划都会顺很多。
最后再分享一个小技巧:在你自己的电脑上,把 MySQL 的sql_log_bin、general_log之类的参数打开一段时间,模拟线上日志环境。平时做任何修改前,先START TRANSACTION,再跑一句SELECT检查目标行,最后执行写操作并提交。这套习惯一旦养成,能让你在未来的工作里躲掉绝大多数“灾难级误操作”。