1. 先把MySQL跑起来:连接、建库、建表的基本功
接触MySQL增删改查之前,我发现很多人其实卡在最前面的几步——装好了MySQL,却不知道怎么连上去,也不知道建表时该设什么字段类型。这篇文章不打算讲那些高大上的架构设计,就围绕我们每天都会用到的增删改查操作,把里面的细节、坑和习惯掰开揉碎聊一遍。
1.1 命令行连接到本地MySQL,这几种方式都要会
连接本地MySQL,最直接的方式就是命令行。
mysql -u root -p输入密码之后就能进入MySQL交互界面。如果连接远程数据库,需要指定主机和端口:
mysql -h 192.168.1.10 -P 3306 -u root -p这里有个细节容易被忽略:-P是大写的P,表示端口;-p是小写的p,表示密码。我第一次用的时候把端口参数写成了小写p,结果系统提示语法错误,排查了半天。还有连接时如果不想在命令行里明文输入密码,可以这样:
mysql -u root -p回车后手动输入密码,这样历史记录里就不会留下密码信息。生产环境一定不要用-p123456这种明文密码方式。
连接成功后,可以先用这几个命令确认环境状态:
SELECT VERSION(); SHOW DATABASES; SELECT CURRENT_USER();SELECT VERSION()可以查看MySQL版本,不同版本的语法和行为会有差异,比如8.0版本和5.7版本在窗口函数、CTE(公共表表达式)等方面的支持就不同。SHOW DATABASES;查看当前实例上有哪些数据库,SELECT CURRENT_USER();确认当前登录用户,这在排查权限问题时很有用。
1.2 建库建表时常见的三个决定:字符集、排序规则、字段类型
增删改查的前提是有一张表。很多人上来就CREATE TABLE,对字符集、排序规则这些完全没有概念,等写入中文出现乱码才开始头疼。
建库时我习惯明确指定字符集:
CREATE DATABASE IF NOT EXISTS demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;为什么用utf8mb4而不是utf8?因为utf8在MySQL里最多存3个字节的字符,像emoji表情这类4字节字符就存不进去。utf8mb4是utf8的超集,能完整支持所有Unicode字符。排序规则utf8mb4_unicode_ci和utf8mb4_general_ci的区别在于精确度和排序规则,前者按Unicode标准排序,更准确;后者排序更快,但个别字符的排序结果可能不够准确。日常业务建议用utf8mb4_unicode_ci。
建表时字段类型的选择也是有讲究的,这里列出一些常见的对比:
| 数据类型 | 存储大小 | 适用场景 | 注意事项 |
|---|---|---|---|
| INT | 4字节 | 用户ID、数量、年龄 | 最大值为2147483647,超了要用BIGINT |
| BIGINT | 8字节 | 订单号、时间戳 | 自增主键建议直接上BIGINT |
| VARCHAR(n) | 实际长度+1~2字节 | 姓名、地址、描述 | n必须指定,最大65535字节 |
| CHAR(n) | 固定n字节 | 固定长度编码如手机号 | 查询性能比VARCHAR略好,但浪费空间 |
| DECIMAL(m,d) | 变长 | 金额、单价 | 永远不要用FLOAT存金额,会有精度问题 |
| DATETIME | 8字节 | 业务时间 | 存储范围1000-01-01到9999-12-31 |
| TIMESTAMP | 4字节 | 日志时间 | 范围到2038年,且受时区影响 |
| TEXT | 变长 | 长文本内容 | 无法设置默认值,索引需要指定前缀长度 |
一个典型的用户表设计可以这样:
CREATE TABLE `user` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` VARCHAR(50) NOT NULL COMMENT '用户名', `phone` CHAR(11) DEFAULT NULL COMMENT '手机号', `balance` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '余额', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1启用,0禁用', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`), KEY `idx_phone` (`phone`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';这里有几个细节值得展开说说。BIGINT UNSIGNED意味着不允许负数,能存到的最大值为18446744073709551615,对绝大多数业务来说完全够用。AUTO_INCREMENT配合主键,让每条数据有唯一标识。DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP这两句非常实用:插入数据时自动写入当前时间,更新数据时自动更新时间,省掉了在业务代码里手动维护时间的麻烦。
还有一个容易忽视的点:类型后面加了UNSIGNED之后,如果UPDATE时把负数赋给该字段,MySQL 8.0默认会报错,而5.7可能只是警告并截断为0。这种版本差异在排查问题时需要留意。
1.3 自增主键到底要不要?什么场景不用
自增主键是大多数表的标准配置,因为它有两个天然优势:插入数据时只追加不移动,索引紧凑;主键值连续,查询范围时性能好。但有一类场景我不建议用自增主键——分布式系统。当多个实例同时写入数据时,自增ID无法全局唯一,这时候雪花算法、UUID才是更好的选择。
MySQL 8.0的UUID可以通过UUID()函数生成,也可以用UUID_SHORT()生成较短的数字型全局唯一ID。不过在单库单表环境下,老老实实用BIGINT AUTO_INCREMENT就够了,不用为了"看起来高大上"引入额外的复杂度。
注意:InnoDB表的主键选择直接影响插入性能。没有明确主键时,InnoDB会优先选第一个非空唯一索引作为主键,都没有时则会生成隐藏主键。这个隐藏主键没有业务意义,还会额外占用存储空间。所以每张表都要显式定义主键。
2. INSERT插入数据:从单条到批量,细节决定效率
2.1 单条插入和批量插入的性能差距
插入语句的基本格式是:
INSERT INTO `user` (`username`, `phone`, `balance`) VALUES ('张三', '13800138000', 100.00);这种写法一次插一条,语法上是没问题的。但如果要插入上万条数据,逐条INSERT的性能就很差了。每条INSERT都涉及SQL解析、权限检查、事务提交等一系列操作,相当于每次都要从头走一遍完整流程。
把多条数据合并成一次插入,性能会有数量级的提升:
INSERT INTO `user` (`username`, `phone`, `balance`) VALUES ('张三', '13800138000', 100.00), ('李四', '13900139000', 200.00), ('王五', '13700137000', 300.00);我实测过插入10万条数据的场景:逐条插入耗时约90秒,批量插入(每批1000条)耗时约3秒。这个差距在生产环境中是非常明显的。
批量插入时还要注意一个边界:单次INSERT的数据量不要太大。每次插入的数据总大小最好控制在几条MB以内,否则会占用大量内存和网络带宽。稳妥的做法是分批次,比如每批500到1000条,批次之间稍微加一点间隔。
另外补充一个写法上的细节:
INSERT INTO `user` SET `username` = '赵六', `phone` = '13600136000', `balance` = 400.00;这种列名赋值的方式在MySQL中是支持的,但标准SQL中推荐使用第一种列清单方式。平时写代码时建议保持一致,统一用列清单方式,后续维护也方便。
2.2 特殊字符、NULL与默认值,插入时最容易搞错的三件事
插入操作看似简单,但在处理数据边界情况时容易出错。
第一件容易搞错的事是特殊字符的转义。如果用户名里有单引号,直接拼接SQL就会报语法错误:
-- 这样会报错 INSERT INTO `user` (`username`) VALUES ('O'Reilly'); -- 正确写法 INSERT INTO `user` (`username`) VALUES ('O''Reilly');在SQL中,单引号通过两个单引号转义。而在实际开发中更推荐使用参数化查询,比如在Java的JDBC中使用PreparedStatement,在Python中使用pymysql的execute方法传参。参数化查询不仅能自动处理转义问题,还能防止SQL注入。自己拼SQL时用字符串替换函数去转义,永远不如参数化来得稳。
第二件容易搞错的事是NULL和默认值的处理。举一个实际业务中常见的场景:有一个字段不允许为NULL且设置了默认值0
INSERT INTO `user` (`username`, `phone`, `balance`) VALUES ('张三', '13800138000', NULL);如果balance字段没有指定默认值,且定义时加了NOT NULL,这条SQL会直接报错。但如果有默认值,插入NULL时可能报错也可能静默替换为默认值,具体取决于MySQL的SQL模式。在严格模式下(sql_mode包含STRICT_TRANS_TABLES),插入NULL到NOT NULL字段会直接报错;在非严格模式下,可能只是警告然后插入默认值。建议开启严格模式,避免数据意外被"静默更正"。
第三件容易踩坑的事是显式插入自增ID。比如下面的语句:
INSERT INTO `user` (`id`, `username`, `phone`) VALUES (1001, '张三', '13800138000');某些场景下这样做是合理的,比如数据迁移时为了保持原ID不变。但如果业务代码里混用"显式指定ID"和"不指定ID"两种方式,可能导致自增计数错乱,后续插入数据时出现"Duplicate entry"错误。因此,非必要不显式插入自增ID。
2.3 主键冲突:使用INSERT时最常见的业务场景
插入数据时主键或唯一键冲突非常常见。典型场景是:用户点击"同步"按钮,把远程API的数据同步到本地表。第一次同步时数据不存在,直接插入;第二次同步时数据已经存在,需要更新。如果先查后插,就存在竞态条件——两个请求同时查出不存在,然后同时插入,导致一个失败。更好的方案是使用MySQL的ON DUPLICATE KEY UPDATE语法:
INSERT INTO `user` (`username`, `phone`, `balance`) VALUES ('张三', '13800138000', 100.00) ON DUPLICATE KEY UPDATE `phone` = VALUES(`phone`), `balance` = VALUES(`balance`);这条SQL的含义是:如果插入时没有触发唯一键冲突,就正常插入;如果触发了,就执行后面的UPDATE操作。上面的例子中,phone和balance会被更新为当前要插入的新值。
注意,MySQL 8.0.20之后推荐使用AS NEW的别名写法,不再推荐VALUES()函数:
INSERT INTO `user` (`username`, `phone`, `balance`) VALUES ('张三', '13800138000', 100.00) AS new ON DUPLICATE KEY UPDATE `phone` = new.`phone`, `balance` = new.`balance`;这个写法的小改动是为了将来移除VALUES()函数做准备。如果用的是MySQL 8.0以上版本,建议直接使用新写法。
还有一类情况,主键冲突时希望"保持不变"。比如统计表里只有在数据变化时更新:
INSERT INTO `user` (`username`, `phone`, `balance`) VALUES ('张三', '13800138000', 100.00) ON DUPLICATE KEY UPDATE `balance` = `balance`;看起来有点奇怪,但核心思想是"冲突时不更新"。除此之外,还可以用INSERT IGNORE:
INSERT IGNORE INTO `user` (`username`, `phone`, `balance`) VALUES ('张三', '13800138000', 100.00);INSERT IGNORE遇到重复键直接跳过,不报错也不更新。如果业务逻辑是"已存在的就忽略,不存在的插入",这个语法比ON DUPLICATE KEY UPDATE更合适。
补充一个与热搜词相关的点:热搜词里出现了"mysql中int+5",其实这是UPDATE场景中给数值加固定值的写法,后面讲UPDATE时会详细展开。
3. SELECT查询:条件、排序、分页和聚合
3.1 WHERE条件的基本规则:为什么它决定了查询性能
查询是增删改查里最常用的操作。基础语法这里就不再赘述了,说几个高频问题和容易被忽视的规则。
WHERE条件中的运算符执行优先级是固定的,但书写时逻辑关系可能会让人困惑。比如:
SELECT * FROM `user` WHERE `status` = 1 OR `status` = 0 AND `balance` > 100;AND的优先级高于OR,所以这条SQL实际执行的是:
WHERE `status` = 1 OR (`status` = 0 AND `balance` > 100)这和很多人直觉上的"(status= 1 ORstatus= 0) ANDbalance> 100"完全不同。遇到复杂的组合条件,一定不要吝啬括号。括号不仅让逻辑清晰,也能防止后续维护的人误读。
WHERE条件最影响查询性能的是索引的使用。一个常见误区是:在索引列上做函数运算,会让索引失效。例如:
-- 索引失效 SELECT * FROM `user` WHERE DATE(`created_at`) = '2024-01-01'; -- 可以走索引 SELECT * FROM `user` WHERE `created_at` >= '2024-01-01 00:00:00' AND `created_at` < '2024-01-02 00:00:00';第一条会对每一行的created_at做DATE函数计算,MySQL无法直接利用索引定位;第二种写法是范围查询,索引可以正常使用。
类似的,在索引列上做运算、隐式类型转换都会破坏索引。比如phone列是VARCHAR类型,却用数字去比较:
-- 隐式类型转换,索引失效 SELECT * FROM `user` WHERE `phone` = 13800138000;MySQL会尝试把字符串转换为数字来比较,导致无法走索引。正确做法是带上引号:
SELECT * FROM `user` WHERE `phone` = '13800138000';3.2 ORDER BY和LIMIT:分页查询的实战坑
排序和分页是查询中如影随形的两个操作。
SELECT * FROM `user` ORDER BY `created_at` DESC LIMIT 10;这段SQL本身没有语法问题,但分页越到后面越慢:
SELECT * FROM `user` ORDER BY `created_at` DESC LIMIT 100000, 10;MySQL执行这条SQL时,需要先扫描出前100010条数据,然后把前100000条丢弃,只返回最后10条。扫描的数据量非常大,性能自然就差。改善方法之一是使用「延迟关联」或「基于游标的分页」。基于游标的分页思路是记住上一页最后一条记录的ID或时间,下一页只取比它更小的记录:
SELECT * FROM `user` WHERE `created_at` < '2024-01-15 10:00:00' ORDER BY `created_at` DESC LIMIT 10;这样每次查询都只需要走索引,数据量再大也能保持稳定性能。缺点是翻页时无法跳到任意页码,只能一页一页往后翻。但对于大多数"加载更多"的应用场景来说,这种分页方式完全够用。
关于排序还有一个注意点:ORDER BY与LIMIT一起用时,如果没有ORDER BY,LIMIT的结果顺序是不确定的。千万不要依赖无ORDER BY的LIMIT来"取前几条"。数据删除、插入都会影响物理存储顺序,查出来的顺序随时可能变化。
3.3 GROUP BY与聚合:热搜词里ONLY_FULL_GROUP_BY的由来
聚合查询涉及GROUP BY、COUNT、SUM、AVG、MAX、MIN等函数。最常见的报错是MySQL 5.7及以上版本开启了ONLY_FULL_GROUP_BY模式:
SELECT `username`, COUNT(*) FROM `user` GROUP BY `status`;这条SQL报错信息会提示:username没有出现在GROUP BY子句中。SQL标准的语义是:GROUP BY之后,SELECT列表中的第1列必须要么是聚合函数,要么是GROUP BY子句中的列。上面这条SQL中username既不是聚合函数,也不在GROUP BY里,因此报错。
很多人碰到这个报错第一反应是关掉ONLY_FULL_GROUP_BY,我不建议这么干。关掉之后虽然不报错,但返回的username是组内任意一条记录的,结果不确定,容易产生线上数据不一致。正确的做法是改SQL,把非聚合列去掉,或者用聚合函数包一下。
如果确实需要查出每组中的某一个具体值,MySQL 8.0推荐用窗口函数。比如要查每个状态的用户中余额最高的那个:
SELECT `username`, `status`, `balance` FROM ( SELECT `username`, `status`, `balance`, ROW_NUMBER() OVER (PARTITION BY `status` ORDER BY `balance` DESC) AS rn FROM `user` ) t WHERE t.rn = 1;这段SQL的执行逻辑是:先按status分组,在每个分组内按balance降序编号,然后只取每个分组内编号为1的记录,也就是每个状态下余额最高的用户。窗口函数是MySQL 8.0才引入的,遇到相关需求时用它能写出既清晰又高效的SQL。
3.4 JOIN:热搜词里"mysql数据库join含义"的通俗解释
JOIN的本质是把两张表的数据通过关联条件组合起来。通常见过的有INNER JOIN(内连接)、LEFT JOIN(左连接)、RIGHT JOIN(右连接)和CROSS JOIN(交叉连接)。
用生活例子来类比:一张user表存用户信息,一张order表存用户下单记录。要查每个用户名下的订单,就需要JOIN:
SELECT u.`username`, o.`order_no`, o.`amount` FROM `user` u INNER JOIN `order` o ON u.`id` = o.`user_id`;INNER JOIN只返回两边都匹配上的记录,也就是"有订单的用户及其订单"。
LEFT JOIN则以左表(user)为准,返回左表所有记录,右表没有匹配上的字段置NULL:
SELECT u.`username`, o.`order_no`, o.`amount` FROM `user` u LEFT JOIN `order` o ON u.`id` = o.`user_id`;这条SQL能查出包括"没有下过单的用户"在内的所有用户数据,订单字段为NULL。
写JOIN时常见的坑是关联条件不完整。如果一张表的主键是联合主键(比如order_id加order_line),JOIN时只写了其中一个条件,会查到大量重复数据,结果行数远超预期。另外,JOIN的字段类型要保持一致,否则会导致索引失效。VARCHAR类型的字段和INT类型的字段直接关联时,MySQL需要做类型转换,转换后索引就失效了。
关于JOIN的优化建议:控制连接的数据量。如果user表有100万条记录,order表有500万条记录,直接JOIN再过滤,查询会执行很久。更好的做法是先缩小每张表的范围再连接:
SELECT u.`username`, o.`order_no` FROM ( SELECT `id`, `username` FROM `user` WHERE `status` = 1 ) u INNER JOIN ( SELECT `user_id`, `order_no` FROM `order` WHERE `created_at` >= '2024-01-01' ) o ON u.`id` = o.`user_id`;4. UPDATE更新数据:那些年全表更新的事故
4.1 忘记WHERE条件的教训和防范措施
UPDATE `user` SET `balance` = 0;这条SQL会更新表中所有记录的balance为0。如果业务上本来就打算全表重置,那没问题。但如果在生产环境中手动执行,后果不可想象。
一个实用的习惯是:执行UPDATE之前,先用同条件的SELECT确认影响范围:
-- 先确认 SELECT COUNT(*) FROM `user` WHERE `status` = 0; -- 再更新 UPDATE `user` SET `balance` = 0 WHERE `status` = 0;MySQL客户端默认在非交互模式下执行包含WHERE的更新不需要显式确认(safe-update模式是个例外)。连接时加--safe-updates选项能开启保护模式,强制要求UPDATE/DELETE语句带WHERE或LIMIT条件,否则报错。日常开发环境建议开启:
mysql --safe-updates -u root -p4.2 热搜词"mysql中int+5"与"mysql中更新子查询"的坑
热搜词里"mysql中int+5"看起来是类似于java那套""+5的值加5写法,但其实SQL中就是普通的算术表达式。常见的用法:
UPDATE `user` SET `balance` = `balance` + 5 WHERE `id` = 123;这行语句在并发场景下是线程安全的。即使在多个事务同时执行balance = balance + 5时,MySQL的行锁会保证每次更新都是基于最新值。相比之下,如果用SELECT先查出来再加回去,可能导致丢失更新。这一点后面讲事务时会再结合具体例子展开。
"mysql中更新子查询"是另一个典型的坑。例如想把这张表里每个用户的余额更新为"订单总额最高的那个用户的订单金额":
-- 这种写法在MySQL中会报错:You can't specify target table 'user' for update in FROM clause UPDATE `user` SET `balance` = (SELECT MAX(`amount`) FROM `order` WHERE `order`.`user_id` = `user`.`id`);MySQL不允许在同一语句中,对目标表做UPDATE的同时又从同一张表中SELECT。解决方案是包一层派生表:
UPDATE `user` SET `balance` = ( SELECT max_amount FROM ( SELECT `user_id`, MAX(`amount`) AS max_amount FROM `order` GROUP BY `user_id` ) t WHERE t.`user_id` = `user`.`id` );这个报错信息在很多低版本MySQL中都会出现,新人很容易被折磨半天。
4.3 UPDATE的锁与事务:为什么更新要放在事务里
MySQL默认每个单条SQL是自动提交的,UPDATE执行完立即生效。但有时候业务需求是"要么全部成功,要么全部回滚"——比如转账操作:从A账户扣钱,给B账户加钱。这两步必须作为一个整体,任何一步失败都要把另一部分撤销。这时候就要用事务:
START TRANSACTION; UPDATE `account` SET `balance` = `balance` - 100 WHERE `account_id` = 'A'; UPDATE `account` SET `balance` = `balance` + 100 WHERE `account_id` = 'B'; COMMIT;如果在第二条UPDATE执行时发现B账户不存在,需要回滚,就执行ROLLBACK;,第一条对A账户的扣款也会被撤销。
这里深挖一下锁的问题。上面的第一步UPDATE执行后,MySQL会锁定A账户所在的行,直到提交或回滚才释放。如果别的事务在这期间也要更新A账户,就会被阻塞等待。这是InnoDB的默认行为,也是避免并发更新的关键机制。
但是在开发中常见的死锁场景是:两个事务以不同顺序更新相同的记录。比如事务1先更新A再更新B,事务2先更新B再更新A,两个事务可能互相等对方持有的锁,造成死锁。MySQL检测到死锁后会自动回滚其中一个事务。防范的关键是让多个事务以相同顺序更新记录。
事务隔离级别也会影响UPDATE的可见性。默认隔离级别是REPEATABLE READ(可重复读),在这个级别下,事务内多次SELECT同一条件,结果都是一致的。如果需要验证,可以用SELECT @@transaction_isolation;查看当前隔离级别。
5. DELETE删除数据:安全删除、逻辑删除和TRUNCATE
5.1 删除前先确认,这是保命的第一条纪律
DELETE操作的语法本身不复杂:
DELETE FROM `user` WHERE `id` = 123;但"数据一旦删除就没了"这个事实,让DELETE成为生产环境中最危险的操作之一。删数据前应该像执行UPDATE一样,先用SELECT确认:
-- 先确认影响行数 SELECT * FROM `user` WHERE `status` = 0 AND `created_at` < '2023-01-01'; -- 再删除 DELETE FROM `user` WHERE `status` = 0 AND `created_at` < '2023-01-01';如果删错了数据,MySQL没有像文件系统那样的"回收站"功能,常规操作几乎是找不回来的。即便有备份,恢复也需要时间。更稳妥的办法是在低峰期操作,而且DDL/DML语句执行后及时进行逻辑备份。
5.2 逻辑删除:为什么推荐用deleted字段代替物理删除
生产环境里,用户表、订单表这些核心业务表通常不建议做物理删除。因为数据之间有关联,删掉一张表的记录可能导致另一张表出现孤儿数据;另一方面,业务上需要保留历史数据用于分析和审计。
常见的做法是加一个is_deleted字段:
ALTER TABLE `user` ADD COLUMN `is_deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '0未删除,1已删除'; -- 逻辑删除 UPDATE `user` SET `is_deleted` = 1 WHERE `id` = 123; -- 查询时过滤掉已删除数据 SELECT * FROM `user` WHERE `is_deleted` = 0;这样数据还在,但业务查询时统一过滤。代价是每条查询都要加上is_deleted = 0条件。有些团队会在查询接口的公共层做统一处理,避免每个SQL都漏掉这个条件。
5.3 TRUNCATE和DELETE的区别:不止是速度不同
TRUNCATE是另一种"删除"操作:
TRUNCATE TABLE `user`;它和DELETE有本质区别:
| 对比项 | DELETE | TRUNCATE |
|---|---|---|
| 条件过滤 | 支持WHERE | 不支持,全表清空 |
| 事务回滚 | 可以回滚 | 隐式提交,无法回滚 |
| 自增ID | 不清零 | 重置为初始值 |
| 性能 | 逐行删除,慢 | 直接释放数据页,快 |
| 触发器 | 会触发 | 不会触发 |
| 返回结果 | 返回删除的行数 | 不返回具体行数 |
平时业务代码里几乎用不到TRUNCATE,它更像DBA层面的操作。但有些测试环境清空数据时会用到,了解它和DELETE的差异能避免误操作。
6. 新手最常踩的五个报错:现象、原因、解决方案
6.1 ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock'
这个报错的热度一直很高。现象是连接本地MySQL时提示无法通过socket文件连接。原因通常是MySQL服务未启动,或者socket文件路径不一致。
排查思路如下:
# 1. 检查MySQL服务状态 systemctl status mysqld # 或者(不同系统命令不同) /etc/init.d/mysql status # 2. 如果没启动,启动服务 systemctl start mysqld # 3. 确认socket文件存在 ls -l /tmp/mysql.sock如果服务已启动但socket路径不对,需要查看MySQL的配置文件,找到socket参数。可以让客户端通过TCP方式连接:
mysql -h 127.0.0.1 -P 3306 -u root -p跳过socket直接使用TCP连接。这个报错在通过本地命令行连接时最常见,80%的情况都是服务根本没启动,或启动失败。看到这个报错先别慌,第一步永远是检查服务状态。
6.2 root的初始密码到底是什么?安装MySQL后连不上的经典问题
很多人在Linux下装完MySQL,用mysql -u root -p登录,输入安装时设定的密码却提示Access denied。原因可能有两个:
一是MySQL 5.7及以上版本在安装时会自动生成一个临时密码,存放在/var/log/mysqld.log中:
grep 'temporary password' /var/log/mysqld.log用临时密码登录后,必须马上修改密码:
ALTER USER 'root'@'localhost' IDENTIFIED BY '新的密码';这个新密码还要满足MySQL的密码复杂度要求,比如长度至少8位且包含大小写字母、数字、特殊字符。如果不想这么严格,可以调整validate_password的相关参数,但不建议在本地学习环境之外关闭密码强度校验。
二是账户允许登录的主机范围受限。默认情况下root账号的host为localhost,也就是说只能从本机连接。如果要用Navicat等工具从另一台机器连这台服务器的MySQL,需要创建一个host允许远程访问的账号:
CREATE USER 'app'@'%' IDENTIFIED BY '密码'; GRANT ALL PRIVILEGES ON demo.* TO 'app'@'%'; FLUSH PRIVILEGES;'%'表示允许从任意IP连接。但生产环境用'%'要谨慎,更安全的方式是指定具体IP或网段,比如'app'@'192.168.1.%',只允许内网某网段连接。
6.3 数据库连接池报错与版本不兼容
热搜词里有这么一条报错:django.db.utils.NotSupportedError: MySQL 8.4 or later is required (found 8.0。这看起来是Django版本与MySQL版本不匹配导致的。新版Django要求更高版本的MySQL,但系统中装的是MySQL 8.0。
遇到这种问题,有几个思路:
- 检查Django版本,如果是新版本,可以降级到兼容MySQL 8.0的Django版本。
- 检查Python的MySQL连接驱动,比如
mysqlclient或pymysql的版本,升级驱动也可能解决问题。 - 确认连接字符串中是否指定了正确的数据库版本属性。部分ORM可以通过配置让驱动忽略版本检查。
遇到兼容性报错,最忌讳的就是盲目升级或者盲目降级。应当先查官方版本的对应关系,再决定调整哪一端。
6.4 MySQL 8.0的SSL连接错误与JDBC驱动的useSSL参数
热搜词里有"mysql ssl连接错误"和"mysql jdbc usessl 与 sslmode 使用"两条。这类问题多出现在Java应用通过JDBC连接MySQL时。
MySQL 8.0默认开启了SSL相关的支持,如果JDBC连接串没有配置SSL参数,可能报类似SSL connection error或Public Key Retrieval is not allowed的错误。常见的处理方式是修改连接串参数:
jdbc:mysql://localhost:3306/demo?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai其中useSSL=false表示不启用SSL连接,allowPublicKeyRetrieval=true允许客户端自动获取服务器公钥,这两个参数是解决此类报错最常见的方式。
但要注意:生产环境如果数据链路涉及公网传输,不建议直接关闭SSL。正确的做法是配置SSL证书并启用加密连接,而非为了省事关闭加密。内网环境安全性可控的前提下,关闭SSL提升性能可以理解,但要经过风险评估。
6.5 ONLY_FULL_GROUP_BY等SQL模式问题
这个在3.3节已详细展开过,这里只需要记住排查思路:执行SELECT @@sql_mode;查看当前模式,如果包含ONLY_FULL_GROUP_BY,且SQL中存在未聚合的非分组列,就会报错。解决方案不是移除这个模式,而是修正业务SQL。
同理,STRICT_TRANS_TABLES是另一个值得保留的严格模式,它保证写入不符合定义的数据时直接报错,而不是静默截断。所谓"严格"能让问题在第一时间暴露,减少线上数据异常。
7. 增删改查之外的三个好习惯
7.1 索引不是越多越好,但核心查询必须有
增删改查里SELECT查询性能最依赖索引。但索引不是越多越好,因为:
- 每次INSERT、UPDATE、DELETE都需要同步维护索引,索引越多写入越慢。
- 索引占存储空间。
- 优化器选择索引也需要成本。
实际项目中我遵循的原则是:
- 主键索引必须要有。
- 高频查询的WHERE条件列、ORDER BY列、JOIN关联列,尽量加索引。
- 区分度低的列(如
status只有0和1)不适合建索引。 - 联合索引要遵循最左前缀原则,查询条件从最左列开始才能命中索引。
- 冗余索引要及时清理,比如已经有索引
(a, b),就不要再建单列索引a。
以一个订单表为例,常见的查询是按用户查订单、按时间范围查订单、按订单号查订单:
ALTER TABLE `order` ADD KEY `idx_user_created` (`user_id`, `created_at`); ALTER TABLE `order` ADD UNIQUE KEY `uk_order_no` (`order_no`);idx_user_created是一个联合索引,同时覆盖了user_id和created_at。查询"某个用户最近订单"时能直接走这个索引。
7.2 执行计划EXPLAIN:用数据说话而不是靠猜
想确认一条SQL有没有走索引,最直接的方式是看执行计划:
EXPLAIN SELECT * FROM `user` WHERE `username` = '张三';执行结果里的关键列:
| 列名 | 作用 | 重点关注 |
|---|---|---|
| type | 访问类型 | 从好到差:const > eq_ref > ref > range > index > ALL |
| key | 实际使用的索引 | NULL表示没走索引 |
| rows | 预估扫描行数 | 越小越好 |
| Extra | 额外信息 | Using filesort、Using temporary 都是性能警示 |
type为ALL意味着全表扫描,数据量大时就应该想办法优化。rows只是预估行数,但能反映MySQL的"成本估算"。
排查慢SQL时,EXPLAIN是第一步,先看有没有走索引,再看走了哪个索引,最后看有没有额外的排序、临时表操作。这个习惯能省下很多性能调优的时间。
7.3 备份与恢复:不是DBA也要会的基本操作
哪怕是本地开发环境,也应该养成定期备份的习惯。MySQL的备份方式有很多,最基础的是mysqldump:
mysqldump -u root -p demo > demo_backup.sql恢复时执行:
mysql -u root -p demo < demo_backup.sqlmysqldump默认导出的是SQL语句文件,结构清晰,适合中小规模数据库。数据量大时可以考虑使用mydumper或MySQL企业版备份工具,也可以做物理备份(直接复制数据文件),但物理备份需要停机或使用xtrabackup等方式,复杂度高一些。
我个人的习惯是:重要表的增删改操作前后,都先做一次逻辑备份。写完这篇博客,我把自己常用的几条命令整理成脚本,需要时一键执行,心里踏实。
8. 最后再分享两个小技巧
写SQL这件事,门槛不高,但把SQL写好、写稳、写高效,是个需要长时间积累的过程。最后分享两个我实际工作中一直在用的小技巧。
第一个技巧是给每条核心SQL写注释。这个"注释"不是把SQL本身抄一遍,而是记录这条SQL的业务含义、涉及表的关系、调用它的业务场景。半年后再回来看,能快速理解当年为什么这样写。
第二个技巧是善于利用MySQL自带的元数据。比如查看某张表的表结构、索引信息:
SHOW CREATE TABLE `user`; DESC `user`; SHOW INDEX FROM `user`;这三个命令能快速了解一张表的设计,排查问题时非常有用。特别是SHOW CREATE TABLE,它会显示出表完整的DDL语句,包括字符集、索引、约束等,是了解表结构最快的途径。
增删改查这四个操作看似基础,实际藏着大量细节。字符集的选择、索引的设计、事务的使用、报错的排查,每一项都能单独写一篇很长的文章。希望这篇内容能帮你在使用MySQL的路上少踩一些坑,至少踩坑的时候知道怎么排查。