接了个带新人的活儿,让我帮忙带一带团队里的实习开发。聊了两天我发现,很多人并不是不努力,而是学数据库的方式有问题——上来就背命令、刷面试题,结果一碰到真实环境就懵。数据库这东西,说到底是一套管理数据的逻辑系统,命令只是它的外壳。这篇东西不是文档搬运,是我这些年摸爬滚打下来的实战总结。基本覆盖了一个后端开发日常90%的MySQL使用场景,包括安装部署、库表设计、增删改查、索引、锁、存储过程,还有高频报错的排查思路。
适合这几类人看:刚接触数据库的在校生、转行做开发的职场新人、学过SQL但总感觉"会了又好像没完全会"的朋友。我尽量把每个知识点都讲透,不光告诉你命令怎么写,还告诉你为什么这么写、踩过哪些坑。
1. 先搞清楚:MySQL到底是个什么东西,凭什么大家都在用它
1.1 从"数据库"这个概念说起
很多新手对数据库的理解就是"一个存数据的地方"。这话没错,但不够准确。打个比方,Excel表格大家应该都用过——有行有列,一个Sheet放一类数据,多个Sheet之间可以互相引用。关系型数据库本质上就是更严格、更高效、支持多人同时操作的Excel。
MySQL就是关系型数据库里最流行的一个。它把数据存放在"表"里,一张表有固定的列结构(字段),每一行是一条记录。多张表之间通过"关系"关联起来,比如用户表和订单表通过user_id关联。
我经常跟新人说:**先忘掉SQL语法,你先想清楚你手头的数据长什么样、有哪些字段、字段之间什么关系。**这个想明白了,SQL语句只是工具。
MySQL之所以能火这么多年,核心就几个原因:
- 开源免费:社区版够用,企业省了一大笔授权费
- 性能强劲:单表千万级数据只要索引建对了,查询依然毫秒级
- 生态成熟:各种管理工具、监控工具、云数据库服务,基本是事实标准
- 上手门槛低:相比Oracle、PostgreSQL,MySQL文法更简单直观
1.2 版本选择:5.7还是8.0,这是个问题
现在装MySQL,第一个纠结的就是版本。
MySQL 5.7是老牌稳王,前些年市面上大部分生产环境都是它。优点是稳定、资料多、踩坑经验丰富;缺点是官方已经停止更新维护,新项目不推荐。
MySQL 8.0是当前主流,性能比5.7强不少,默认字符集已经是utf8mb4(后面细说),还加入了窗口函数、CTE这些好用特性。
MySQL 9.x刚出没多久,属于尝鲜版,除非你是搞技术预研,否则不建议生产环境用。
我的建议很直接:新项目直接用8.0,你网上搜到的大部分资料、工具兼容性都是围绕8.0的。别再看那些"5.7更稳定"的旧帖子了,8.0都出了好几年了,稳定性早就经过了验证。
1.3 部署方式:直接装还是用Docker
安装方式上,不外乎三种:Windows安装包、Linux包管理器、Docker容器。
如果你是在本地Windows上学习测试,直接下载安装包就行。如果你在Linux服务器上部署,可以用apt/yum装,也可以用Docker。如果你只是想快速起一个实例来学语法,Docker是最省事的方案,后面我会专门展开说。
还有一点,很多公司现在用的都是云数据库(阿里云RDS、腾讯云DBB等),本质上还是MySQL,只是在底层帮你做了高可用、备份、监控。所以你本地学的这套东西,在云上一样适用,只是连接地址从localhost变成了公网IP或者内网域名。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 安装与连接:最容易劝退新人的环节,坑我都帮你踩过了
2.1 Windows安装:环境变量是第一个坎
Windows下安装MySQL 8.0,下载msi安装包一路Next就行,但有几个细节容易出问题。
第一个坑:安装类型选择。新手建议选Developer Default,中间会让你选是否安装MySQL Workbench,这是官方图形化工具,建议装。
第二个坑(最常见的):环境变量没配置。安装完MySQL,你在命令行输入mysql -uroot -p,却提示"mysql不是内部或外部命令"。原因就是系统找不到mysql.exe的位置。
解决办法:右键"此电脑"→属性→高级系统设置→环境变量,在系统变量里找到Path,编辑,新增一条C:\Program Files\MySQL\MySQL Server 8.0\bin(具体路径看你装哪里了)。
注意:改完环境变量要重新打开命令行窗口才生效,这个坑我见过好几个人栽过。
第三个坑:服务启动失败。安装完打开服务管理器(Win+R输入services.msc),找到MySQL80服务,右键启动。如果启动失败,八成是my.ini配置文件里的目录路径不对,或者端口3306被占用。
第四个坑:安装时设置的root密码一定记好。忘记密码的恢复步骤我后面专门讲,那叫一个折腾。
安装完成后,验证方式是在命令行输入:
bash复制mysql -uroot -p
输入你设置的密码,能看到mysql>提示符,说明连上了。
2.2 Linux安装:别在yum源上浪费时间
如果你用的是Ubuntu/Debian系:
bash复制sudo apt update
sudo apt install mysql-server
安装完默认是允许本地root免密登录的,需要手动设置密码:
bash复制sudo mysql
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的新密码';
FLUSH PRIVILEGES;
CentOS/RHEL系稍微麻烦一点,官方yum源里MySQL版本很老,需要先添加MySQL官方yum仓库:
bash复制sudo rpm -Uvh https://dev.mysql.com/get/mysql80-community-release-el7-7.noarch.rpm
sudo yum install mysql-community-server
装完启动并设置开机自启:
bash复制sudo systemctl start mysqld
sudo systemctl enable mysqld
CentOS上安装完会生成一个临时密码,在/var/log/mysqld.log里:
bash复制sudo grep 'temporary password' /var/log/mysqld.log
用临时密码登录后,第一步就是改密码,因为MySQL默认的密码策略要求密码必须够复杂(大小写字母+数字+特殊字符)。
提示:如果只是学习用,建议把密码策略调低一点。可以设置
validate_password.policy=LOW,否则你设个123456都会被拒绝。
2.3 Docker安装:效率最高的学习方式
Docker的好处是不污染宿主机环境,一条命令搞定,不要了删掉再开一个。
bash复制docker run -d --name mysql8 \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \
-e TZ=Asia/Shanghai \
mysql:8.0
解释一下几个参数:
-d:后台运行--name mysql8:容器名字,方便后续管理-p 3306:3306:把容器内的3306端口映射到宿主机,这样宿主机能直接连-e MYSQL_ROOT_PASSWORD:设置root密码-e TZ=Asia/Shanghai:设置时区,不设的话默认UTC,查时间会差8小时
进入容器操作:
bash复制docker exec -it mysql8 mysql -uroot -p
唯一要注意的是:容器删除后数据会丢失。如果想持久化数据,加一个数据卷挂载:
bash复制docker run -d --name mysql8 \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \
-v /data/mysql:/var/lib/mysql \
mysql:8.0
这样数据就存在宿主机的/data/mysql目录下,容器删了重开数据还在。
2.4 连接工具:命令行之外的图形化管理
命令行虽然炫酷,但日常开发调试我还是建议配一个图形化工具。市面上主流的几个:
| 工具 | 特点 | 适合场景 |
|---|---|---|
| MySQL Workbench | 官方出品,免费,功能全 | 新手入门、数据库设计 |
| Navicat | 功能强大,界面友好,收费 | 日常开发、多数据库管理 |
| DBeaver | 开源免费,支持几乎所有数据库 | 全栈开发、跨数据库操作 |
| dbx数据库工具 | 国产工具,界面简洁,支持多种数据库 | 国内开发者、轻量管理 |
说到Navicat,有些新手会去搜注册码,我建议如果没有公司授权就直接用DBeaver或者Workbench,别把精力花在破解上,没必要。DBeaver的社区版足够日常开发了,而且人家是真正开源免费的。
连接测试的时候,有个容易出现的误区:服务器的MySQL不允许远程连接,你在本地用工具连不上,报错Host 'xxx' is not allowed to connect to this MySQL server。解决办法:
sql复制CREATE USER '你的用户名'@'%' IDENTIFIED BY '密码';
GRANT ALL PRIVILEGES ON *.* TO '你的用户名'@'%';
FLUSH PRIVILEGES;
这句话的意思是创建一个允许从任何主机(%)登录的用户。注意开放远程连接要考虑安全性,生产环境建议只开放需要的IP。
3. 库表设计与字段类型:这里藏着90%新手都会犯的错
3.1 数据库和表的基本操作:先立规矩再动手
数据库操作之前,先搞清楚层级关系,MySQL的逻辑层级是:
code复制MySQL服务
└── 数据库(Database)
└── 表(Table)
└── 字段(Column/Field)
└── 记录(Row)
常用命令:
sql复制-- 查看所有数据库
SHOW DATABASES;
-- 创建数据库,指定字符集,这点非常重要
CREATE DATABASE mydb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 切换数据库
USE mydb;
-- 查看当前数据库的所有表
SHOW TABLES;
**为什么创建数据库必须显式指定utf8mb4?**因为MySQL 8.0之前的默认字符集是latin1,不支持中文。如果你建库时没指定,后面存中文全是乱码。8.0虽然默认改成了utf8mb4,但为了保险起见,我还是建议显式写出来,防止某些特殊情况覆盖默认值。
表的设计是这样:
sql复制CREATE TABLE user (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
password VARCHAR(255) NOT NULL COMMENT '密码',
email VARCHAR(100) COMMENT '邮箱',
age TINYINT UNSIGNED COMMENT '年龄',
status TINYINT DEFAULT 1 COMMENT '状态:1正常 0禁用',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
这个建表语句里有几个关键点:
AUTO_INCREMENT:自增主键,每插入一条记录自动+1UNSIGNED:无符号,只能存非负数,主键ID用这个能翻倍容量NOT NULL:字段不能为空,用户名这种必须有值DEFAULT CURRENT_TIMESTAMP:插入时自动填当前时间ON UPDATE CURRENT_TIMESTAMP:更新记录时自动更新时间ENGINE=InnoDB:存储引擎,8.0默认就是它,支持事务和行级锁
3.2 数据类型:别把所有数字都存成int
字段类型选型看似简单,实则影响存储空间和查询效率。新手最容易犯的错就是"不管什么字段,全都int和varchar一把梭"。
整数类型:
| 类型 | 字节数 | 取值范围 | 适用场景 |
|---|---|---|---|
| TINYINT | 1 | -128~127 | 状态值、年龄 |
| SMALLINT | 2 | -32768~32767 | 小范围计数 |
| INT | 4 | 约±21亿 | 主键、普通ID |
| BIGINT | 8 | 超大范围 | 分布式系统主键、订单号 |
浮点与定点数:
FLOAT/DOUBLE:浮点型,有精度损失,不要用于金额DECIMAL(10,2):定点数,精确存储,金额用这个
字符串类型:
CHAR(n):定长字符串,存10位就固定占10位空间,适合身份证号这种长度固定的VARCHAR(n):变长字符串,存多少占多少,适合用户名、地址这种长度不固定的TEXT:长文本,适合文章内容,注意TEXT字段不能有默认值
日期时间类型:
| 类型 | 存储范围 | 推荐场景 |
|---|---|---|
| DATETIME | 1000-01-01 到 9999-12-31 | 通用的时间存储,推荐 |
| TIMESTAMP | 1970-01-01 到 2038-01-19 | 有上限,不推荐主用 |
| DATE | 1000-01-01 到 9999-12-31 | 只存日期,比如生日 |
3.3 一个被问爆的问题:int(5)到底是什么意思
热搜词里有个"mysql中int+5",我猜很多人好奇INT(5)是啥意思。这里直接说结论:
INT(5)不是限制存储范围,而是显示宽度。
也就是说,INT(5)还是能存21亿这个数,跟INT(11)存储范围完全一样。5只是说当配合ZEROFILL时,不足5位的数字前面补零。比如INT(5) ZEROFILL存1,显示为00001。
没了ZEROFILL,这个数字就是纯装饰,不影响存储和计算。所以下次看到INT(11)、INT(5),别纠结宽度,它跟存储范围没关系。
4. 增删改查:基础语法里的高级用法,用好查询效率翻倍
4.1 SELECT查询:永远最常用的指令
增删改查是数据库的四大基本操作,其中查询占了实际开发的80%以上的工作量。从最简单的开始:
sql复制-- 查询全部字段
SELECT * FROM user;
-- 查询指定字段
SELECT id, username, email FROM user;
-- 条件过滤
SELECT id, username FROM user WHERE status = 1;
-- 分页查询
SELECT id, username FROM user LIMIT 10 OFFSET 20;
LIMIT 10 OFFSET 20的意思是跳过20条取10条,也就是第3页的数据。MySQL有简写:LIMIT 20, 10,注意这个顺序是OFFSET, LIMIT,搞反了就是另一个结果了。
条件查询的优先级,这里值得专门提醒:WHERE里可以加AND、OR、NOT,但是**AND优先级高于OR**。写复杂条件的时候,用括号明确分组,别靠记性猜。
sql复制-- 正确写法,两种条件都加上括号更清晰
SELECT * FROM user WHERE (status = 1 AND age > 18) OR (status = 0 AND age > 30);
模糊查询用LIKE,注意%代表任意多个字符,_代表一个字符:
sql复制-- 查所有姓张的用户
SELECT * FROM user WHERE username LIKE '张%';
-- 第二个字是三的用户
SELECT * FROM user WHERE username LIKE '_三%';
注意,LIKE '%xxx%'这种写法在数据量大时索引会失效,全表扫描。真要做全文搜索,应该用MySQL的全文本索引或者接ES(Elasticsearch)。
4.2 排序:不是简单的ORDER BY完事
排序是热搜词里单独出现的关键词"mysql排序",说明大家确实经常用,但经常搞错方向。
sql复制-- 按创建时间倒序(最新在前)
SELECT * FROM user ORDER BY create_time DESC;
-- 多字段排序:先按status排序,再按create_time倒序
SELECT * FROM user ORDER BY status ASC, create_time DESC;
这里有个坑:排序的NULL值处理。MySQL默认NULL在升序排最前,降序排最后。如果你想让NULL排最后:
sql复制SELECT * FROM user ORDER BY age IS NULL, age ASC;
还有一个常见技巧,按指定顺序排序,类似枚举排序:
sql复制-- 按指定状态顺序排列:1, 3, 2
SELECT * FROM user ORDER BY FIELD(status, 1, 3, 2);
这个是业务里非常常用的东西,比如你要按照"待付款、已付款、已发货、已完成"这种业务顺序展示订单状态,而不是按字母或者创建时间。
4.3 增删改:事务意识的起点
sql复制-- 插入单条
INSERT INTO user (username, password, email) VALUES ('zhangsan', '123456', 'zhangsan@example.com');
-- 插入多条
INSERT INTO user (username, password) VALUES
('lisi', '123456'),
('wangwu', '123456');
-- 更新数据,注意:一定要WHERE,否则全表更新
UPDATE user SET age = 25 WHERE username = 'zhangsan';
-- 删除数据,注意:一定要WHERE
DELETE FROM user WHERE id = 1;
关于UPDATE和DELETE,我见过的最大事故就是忘写WHERE条件。我有个朋友在测试环境执行DELETE FROM user;,然后把生产环境的表清空了。虽然最后用备份恢复了,但整个过程吓得半死。
有个好习惯:MySQL客户端开事务自动提交关闭。
sql复制SET autocommit = 0;
DELETE FROM user WHERE id = 999;
-- 发现删错了
ROLLBACK;
生产环境操作数据前先SELECT确认一下,再用WHERE条件执行UPDATE/DELETE。这是用血泪教训换来的经验。
4.4 聚合查询与GROUP BY:一个容易踩的坑
统计场景下,聚合函数是必不可少的:
sql复制-- 统计用户总数
SELECT COUNT(*) FROM user;
-- 按状态分组统计
SELECT status, COUNT(*) AS cnt FROM user GROUP BY status;
-- 过滤分组后的结果,用HAVING不是WHERE
SELECT status, COUNT(*) AS cnt FROM user GROUP BY status HAVING cnt > 10;
最重要的一个区别:WHERE在分组前过滤,HAVING在分组后过滤。WHERE过滤的是原始记录,HAVING过滤的是分组结果。
还有一个经典错误:SELECT的字段没出现在GROUP BY里。在MySQL 8.0默认开启了ONLY_FULL_GROUP_BY模式,SELECT的列必须要么在GROUP BY中,要么在聚合函数里,否则直接报错。这其实是好事,避免你查出无意义的数据。
4.5 多表查询:JOIN是所有面试题的重灾区
实际业务中,数据往往分散在多张表里。比如用户表和订单表,你要查每个用户名下的订单数,就需要关联查询。
sql复制-- INNER JOIN 内连接,只返回两表匹配的记录
SELECT u.username, o.order_no
FROM user u
INNER JOIN orders o ON u.id = o.user_id;
-- LEFT JOIN 左连接,返回左表全部记录,右表无匹配则补NULL
SELECT u.username, o.order_no
FROM user u
LEFT JOIN orders o ON u.id = o.user_id;
内连接(INNER JOIN)和左连接(LEFT JOIN)的区别,我用一句话概括:LEFT JOIN以左表为基准,左表的记录一定全部出现;INNER JOIN只要有一边不匹配就不出现。
这个知识点笔试面试必考,实际工作中也天天用。如果你用错了JOIN类型,查出来的数据多了或者少了,对业务影响很大。
5. 索引与唯一约束:从"卡死"到"秒开"就差一个索引
5.1 索引原理:用空间换时间的一本字典
索引这东西,你要理解它很简单:**它就像一本书最后的索引页,通过关键词能快速定位到页码,不用从头翻到尾。**没有索引,MySQL查询数据就是一页页翻(全表扫描),有索引则走B+树快速查找。
MySQL最常用的索引类型是B+树索引,我们平时说"建索引"默认就是建这个。它支持快速查找、范围查找、排序。
常用的索引种类:
| 索引类型 | 关键字 | 特点 |
|---|---|---|
| 普通索引 | KEY(idx_name) | 加速查询,可重复 |
| 唯一索引 | UNIQUE KEY(uk_name) | 加速查询,值不能重复 |
| 主键索引 | PRIMARY KEY(id) | 一个表只能有一个,自动创建 |
| 联合索引 | KEY(uid, status) | 多字段组合索引,注意最左前缀原则 |
| 全文索引 | FULLTEXT KEY(content) | 大文本内容搜索,InnoDB支持 |
索引不是越多越好,每个索引都会占用磁盘空间,而且插入、更新、删除时要额外维护索引结构。一般建议:查询频繁的字段建索引,更新频繁的字段慎建索引。
5.2 最左前缀原则:联合索引的灵魂
联合索引(a, b, c)相当于建了三个索引:a、a,b、a,b,c。查询条件里如果用到了a,索引就能生效;如果只用了b或c,用不到a,这个联合索引就废了。
这就像查字典时,必须从第一个字母开始查,你跳着查就翻不了索引页。所以建联合索引时,最常查询的字段放最左边。
sql复制-- 假设建了联合索引 idx_status_create_time (status, create_time)
-- 能走索引
SELECT * FROM orders WHERE status = 1 ORDER BY create_time DESC;
-- 走不了索引,因为跳过了status
SELECT * FROM orders WHERE create_time > '2023-01-01';
5.3 唯一约束和已有重复数据:如何给脏数据表加索引
热搜词"mysql设置唯一已经有重复数据库"是个非常真实的场景。比如你要给user表的email字段加唯一索引,但表里已经有两条重复的email了。直接执行会报错:
sql复制ALTER TABLE user ADD UNIQUE INDEX uk_email (email);
报错提示:Duplicate entry 'xxx@example.com' for key 'uk_email'。
处理思路分三步:
第一步:查出重复数据:
sql复制SELECT email, COUNT(*) FROM user GROUP BY email HAVING COUNT(*) > 1;
第二步:决定保留哪一条,删除多余记录。一般保留id最小的那条,删除其他:
sql复制DELETE u1 FROM user u1
INNER JOIN user u2
ON u1.email = u2.email AND u1.id > u2.id;
第三步:再执行加唯一索引的语句。
这步操作务必先备份表数据。我一般习惯先CREATE TABLE user_bak AS SELECT * FROM user;留个后路再动手。
5.4 索引失效的常见场景
花了半天建了索引,结果一查EXPLAIN发现根本没走索引,这是最让人抓狂的。常见的索引失效场景:
- 对索引字段用函数或计算:
WHERE YEAR(create_time) = 2023这样写,create_time索引就废了。应该写成WHERE create_time BETWEEN '2023-01-01' AND '2023-12-31'。 - 隐式类型转换:字段是varchar,条件传了数字,MySQL会自动转类型,索引失效。
- LIKE前置百分号:
LIKE '%abc'走不了索引,LIKE 'abc%'可以。 - OR条件:
WHERE a = 1 OR b = 2,如果b没有索引,整个查询可能放弃走索引。 - NOT IN、!=操作:可能不走索引。
排查索引是否生效,用EXPLAIN:
sql复制EXPLAIN SELECT * FROM user WHERE username = 'zhangsan';
看key列有没有显示你建的索引,type列如果是ALL说明全表扫描了。这是做SQL优化的基本功。
6. 并发控制:数据库锁与死锁,没那么神秘
6.1 为什么需要锁
多个用户同时操作同一条数据,就会产生并发问题。经典的场景:用户A和用户B同时给同一个商品下单扣库存。
- A读库存:还剩10件
- B读库存:还剩10件
- A下单,库存更新为9件
- B下单,库存也更新为9件
结果卖了2件,库存只减了1件。这就是并发写冲突。锁机制就是解决这个问题的。
MySQL的锁从粒度上分:
| 锁类型 | 说明 | 特点 |
|---|---|---|
| 表级锁 | 锁住整张表 | 开销小,但并发度低 |
| 行级锁 | 只锁被操作的行 | 并发度高,但开销大 |
| 页面锁 | 介于两者之间 | MySQL中较少用 |
InnoDB存储引擎默认行级锁,也是为什么现在都用InnoDB的原因。
从模式上分:共享锁(S锁,读锁)和排他锁(X锁,写锁)。多个共享锁可以共存,排他锁和任何锁都互斥。
sql复制-- 手动加共享锁
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;
-- 手动加排他锁(常用)
SELECT * FROM user WHERE id = 1 FOR UPDATE;
6.2 死锁是怎么产生的
死锁是指两个以上的事务互相等待对方释放锁,然后卡死。经典场景:
code复制事务A:UPDATE user SET age = 20 WHERE id = 1; -- 锁住id=1
事务B:UPDATE user SET age = 30 WHERE id = 2; -- 锁住id=2
事务A:UPDATE user SET age = 25 WHERE id = 2; -- 等待B释放id=2
事务B:UPDATE user SET age = 35 WHERE id = 1; -- 等待A释放id=1
两个事务互相等待,谁也进行不下去。
MySQL会检测到死锁,并自动回滚其中一个事务,返回类似Deadlock found when trying to get lock; try restarting transaction的报错。
避免死锁的常用手段:
- 保持一致的锁顺序:多个事务都按id从小到大加锁,就不会循环等待
- 缩短事务时间:事务里别做太多无关操作,快速提交
- 减少锁范围:能锁行别锁表,能用唯一索引锁定精确行就不用范围锁
排查死锁:
sql复制-- 查看最近一次死锁日志(需要root权限)
SHOW ENGINE INNODB STATUS;
查看LATEST DETECTED DEADLOCK部分的输出,能看到两个事务的SQL和执行时间,就能定位问题。
6.3 锁表了怎么办:一个真实场景
热搜词里"mysql锁表"确实是个高频痛点。我遇到过一个典型案例:开发者在事务里执行了一条UPDATE,但是事务一直没提交(可能代码忘记提交了),其他会话对这个表的操作全部卡住。
排查步骤:
第一步:查看当前有哪些锁等待
sql复制SELECT * FROM information_schema.innodb_trx;
看trx_state、trx_started字段,如果某个事务执行很久没提交,基本就是元凶。
第二步:杀掉阻塞的线程
sql复制-- 找到trx_mysql_thread_id
KILL 线程ID;
也可以直接:
sql复制-- 列出所有正在执行的SQL
SHOW PROCESSLIST;
-- 找到Sleep状态时间很长的连接,记下Id
KILL Id;
生产环境处理锁表,先SHOW PROCESSLIST看看哪些连接在跑,确认是僵尸事务再KILL。千万别轻易KILL正在跑大查询的连接,可能会导致回滚风暴。
7. 存储过程:什么时候该用,什么时候千万别用
7.1 存储过程是个什么东西
存储过程说白了就是一段预编译的SQL脚本,存储在数据库端,需要的时候直接调用。它可以包含变量、流程控制、循环、异常处理,像一个"数据库里的函数"。
为什么需要它?想象一个场景:用户下单后要扣库存、加积分、生成订单日志,这三个操作要同时成功或者同时失败。如果每次都在应用层写三条SQL再包一个事务,代码会非常啰嗦。存储过程可以在数据库端一次性搞定。
7.2 一个完整的存储过程示例
sql复制DELIMITER $$
CREATE PROCEDURE sp_create_order(
IN p_user_id INT,
IN p_goods_id INT,
IN p_qty INT,
OUT p_order_no VARCHAR(32)
)
BEGIN
DECLARE v_stock INT DEFAULT 0;
DECLARE v_price DECIMAL(10,2) DEFAULT 0;
-- 开启事务
START TRANSACTION;
-- 查询库存并加锁
SELECT stock, price INTO v_stock, v_price
FROM goods
WHERE id = p_goods_id
FOR UPDATE;
-- 库存不足则回滚
IF v_stock < p_qty THEN
ROLLBACK;
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '库存不足';
END IF;
-- 扣减库存
UPDATE goods SET stock = stock - p_qty WHERE id = p_goods_id;
-- 生成订单号
SET p_order_no = CONCAT(DATE_FORMAT(NOW(), '%Y%m%d%H%i%s'), LPAD(p_user_id, 6, '0'));
-- 插入订单表
INSERT INTO orders (order_no, user_id, goods_id, qty, amount, create_time)
VALUES (p_order_no, p_user_id, p_goods_id, p_qty, v_price * p_qty, NOW());
-- 提交事务
COMMIT;
END$$
DELIMITER ;
调用方式:
sql复制CALL sp_create_order(1, 100, 2, @order_no);
SELECT @order_no;
7.3 存储过程的正确打开方式
虽然存储过程功能强大,但我个人的观点是:优先在应用层实现业务逻辑,存储过程只在特定场景使用。原因:
- 存储过程不好调试,日志不直观
- 版本管理困难,代码和SQL混在数据库里,无法一起走git
- 数据库压力增大,存储过程吃CPU和连接数
- 迁移数据库时,存储过程语法不一定通用
适用场景也有:
- 复杂的报表统计:一条存储过程搞定多层嵌套查询,比应用层反复连接数据库更高效
- 定时任务:配合MySQL的事件调度器,做数据归档、清理
- 批量数据处理:比如给所有用户加积分,循环处理
总的来说,如果你不确定该不该用,那就不该用。
8. 高频报错排查实录:从"满屏红字"到"秒懂问题"
8.1 ERROR 2002:连不上socket是最常见的坑
热搜词里有一条非常具体的报错:
code复制ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)
这个报错翻译过来就是:客户端尝试通过socket文件连接本地MySQL,但是没找到这个文件。socket是Linux/Unix下进程间通信的一种方式,MySQL用它在同一台机器上通信。
排错步骤:
第一步:MySQL服务可能根本没启动。
bash复制systemctl status mysql
# 或者
ps -ef | grep mysqld
如果没启动:
bash复制systemctl start mysql
第二步:socket文件路径不对。MySQL的socket文件位置我见过在/tmp/mysql.sock、/var/run/mysqld/mysqld.sock、/var/lib/mysql/mysql.sock等不同位置。连接时显式指定:
bash复制mysql -uroot -p --socket=/var/run/mysqld/mysqld.sock
或者查看配置文件:
bash复制cat /etc/my.cnf | grep socket
第三步:权限问题。socket文件所属用户是mysql,但你用其他用户连接。确认运行mysql命令的用户对socket文件有读写权限。
如果是Docker容器里遇到的类似问题,大概率是客户端和服务器不在同一个容器里。Docker方式直接用TCP连接:
bash复制mysql -h127.0.0.1 -P3306 -uroot -p
8.2 拒绝访问和密码认证问题
sql复制ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)
这个报错是密码错误。如果曾经能连、现在突然连不上,有几个可能:
- root密码被改了
- MySQL 8.0的认证插件是
caching_sha2_password,老版本的客户端(比如Navicat 15之前的版本)不支持,也会报错
解决方式,可以把用户的认证插件改成老的(安全性略降):
sql复制ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';
FLUSH PRIVILEGES;
如果密码忘了,只能走"跳过授权表"的恢复流程。这个操作复杂且危险,生产环境务必谨慎:
bash复制# 先停掉MySQL服务
sudo systemctl stop mysql
# 安全模式启动,跳过权限验证
sudo mysqld_safe --skip-grant-tables &
# 登进去改密码
mysql -uroot
ALTER USER 'root'@'localhost' IDENTIFIED BY '新密码';
FLUSH PRIVILEGES;
# 重启MySQL
sudo systemctl restart mysql
Docker容器里忘了root密码,更简单,直接指定参数:
bash复制docker run -it --rm --name mysql_reset \
-v /data/mysql:/var/lib/mysql \
mysql:8.0 \
mysqld --skip-grant-tables
然后另开一个终端进容器改密码。
8.3 中文乱码问题
插入中文后查询显示???,这个问题现在其实不多了,但一旦遇到非常困惑。绝大多数原因就是建库建表时没指定utf8mb4。
排查和修复:
sql复制-- 查看当前数据库字符集
SHOW VARIABLES LIKE 'character_set%';
-- 查看表的字符集
SHOW CREATE TABLE user;
如果发现字段字符集是latin1,修改:
sql复制ALTER TABLE user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
注意,这个操作会锁表重建,大表操作要放低峰期。现在的MySQL 8.0默认字符集就是utf8mb4,但为了兼容未来需求,建表的习惯还是保持显式指定。
8.4 第三方软件报"访问数据库发生错误"
热搜词里有一条"multisim访问数据库发生错误怎么解决",Multisim是电子电路仿真软件,它会内嵌一个MySQL或Access数据库来管理元件库。
这种报错通常是Multisim自带的MySQL服务没启动或者ODBC驱动没装。解决思路也是先看服务、再看驱动、最后看数据源配置。这里我不展开讲Multisim,但可以说一个通用排查思路:
**第三方软件连数据库报错,先确认软件配套的数据库服务是否在运行,而不是去改软件配置。**很多时候重启电脑或者重启服务就好了。
9. 面试常考知识点:基础题和进阶题一次讲清
9.1 基础概念题
问:MySQL的存储引擎有哪些,区别是什么?
答:主要说InnoDB和MyISAM。InnoDB支持事务、外键、行级锁,崩溃恢复能力强;MyISAM不支持事务,只支持表级锁,但查询速度在某些场景更快。实际开发几乎都用InnoDB,MyISAM可以简单提一句了解即可。
问:什么是事务的ACID特性?
答:原子性(Atomicity):事务里所有操作要么全部成功要么全部失败;一致性(Consistency):事务前后数据总量保持一致;隔离性(Isolation):事务之间互不干扰;持久性(Durability):事务提交后永久生效。
问:MySQL的隔离级别有哪些?
答:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read,MySQL默认)、串行化(Serializable)。出现的问题分别是脏读、不可重复读、幻读。MySQL默认的可重复读通过MVCC(多版本并发控制)解决了一部分幻读问题。
9.2 SQL优化题
问:一条SQL执行很慢,你怎么排查?
我会按照这个顺序:
EXPLAIN看执行计划,确认有没有走索引- 看
type列:ALL是全表扫描,index是全索引扫描,ref/eq_ref/const是走了索引 - 检查是否索引失效(前面说的那些场景)
- 检查数据量,如果单表数据量巨大,考虑分库分表或归档
- 用
SHOW PROFILE看哪个阶段耗时最长
问:分页查询为什么慢,怎么优化?
LIMIT 1000000, 10这个写法MySQL要扫描1000010行再丢弃前1000000行,当然慢。
优化方案一:延迟关联,先查索引列再回表
sql复制SELECT u.* FROM user u
INNER JOIN (SELECT id FROM user ORDER BY id LIMIT 1000000, 10) tmp
ON u.id = tmp.id;
优化方案二:基于游标,记住上一页最大id
sql复制SELECT * FROM user WHERE id > 上页最大ID ORDER BY id LIMIT 10;
这是最推荐的方案,尤其适合滚动加载的场景,性能提升非常明显。
9.3 大数据量场景题
问:一张表数据量几千万了,有什么优化思路?
我一般从这几个层面回答:
- 索引优化:排查慢SQL,建合适的联合索引
- 读写分离:主库写从库读,缓解查询压力
- 数据归档:把冷数据(比如3年前订单)迁移到历史表,减小主表体积
- 分库分表:按用户ID分片或者按时间分表,这是最后一步,引入了分布式事务等复杂度
- 引入缓存:热点数据放Redis,减少数据库访问
问:MySQL主从复制的原理了解吗?
主库把数据变更写入binlog(二进制日志),从库的IO线程拉取binlog写到中继日志(relay log),然后SQL线程回放这些日志,实现数据同步。这个原理也是很多数据库同步工具的基础。
10. 写在最后:我最想告诉新手的几件事
10.1 关于工具和数据安全
我见过太多人在本机装了一堆数据库实例,密码全是一样且弱口令。如果是学习环境无所谓,但如果服务器有公网IP,务必改掉默认端口、设置复杂密码、限制远程访问IP。数据库被勒索的新闻每年都有,原因基本就一个:弱口令加开放公网访问。
10.2 关于学习路径
如果你正在自学MySQL,我的建议是:
- 第一步:把增删改查练熟,特别是JOIN和GROUP BY,这是SQL的灵魂
- 第二步:理解索引原理,学会用EXPLAIN分析SQL
- 第三步:理解事务和锁,搞懂并发控制
- 第四步:学备份恢复(mysqldump)和高可用方案
- 第五步:系统刷一遍面试题,查漏补缺
10.3 最后分享一个小技巧
每次你在MySQL里执行一条SQL,我都建议先看一眼有没有WHERE条件,再看一眼有没有带索引的列。这两眼耽误不了你两秒,但可能帮你避免一个通宵的事故。
做运维的那些年,我修复过太多"DELETE没有WHERE"的惨案了。数据库这东西,你敬畏它,它就很听话;你轻视它,它就给你上一课。把这篇文章里的基础功打扎实,日常开发基本够用了。等技术慢慢深入了,自然知道哪些知识点需要继续深挖。
