ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

MySQL基础操作实战:从CRUD到SQL优化与防注入

MySQL基础操作实战:从CRUD到SQL优化与防注入 搞MySQL这行SQL基础操作就是吃饭的家伙。不管你是刚入行的新人还是写了几年Java/Python想补补数据库功底的老开发SQL都是绕不过去的一道坎。很多人一开始觉得SQL就是select * from xxx真到了线上环境才发现一个慢查询能把接口拖到超时一条没加WHERE的UPDATE能把整个表改得面目全非。这篇文章不整虚的我就按自己这些年实际使用MySQL的经验从环境准备、建库建表、增删改查到JOIN、窗口函数、事务、存储过程再到Explain分析和SQL注入防御把日常开发里最常用的东西一次讲透顺便把我踩过的坑也一并列出来。标题虽然叫MySQL基础操作SQL但基础不等于简单。真正的基础是我列出的这些内容能覆盖你工作中90%以上的SQL场景而且每一个我都尽量讲清楚为什么这么做而不是只丢给你一段能跑的命令。适合谁看刚学MySQL的新手可以从头跟到尾有一定经验的开发者可以直接跳到后面看窗口函数、慢SQL优化和常见坑部分应该也有收获。1. 动手之前环境准备与整体认知1.1 MySQL 8.0 安装与版本选择先说版本。如果你现在还在犹豫装哪个版本直接上MySQL 8.0别碰5.7了。8.0是当前社区版的主流稳定大版本官方持续在维护Window函数、CTE公用表表达式这些好用的特性在8.0里都是标配5.6、5.7很多都支持得不完整。而且从8.0开始默认字符集已经是utf8mb4存储emoji和生僻字不会出现乱码问题这一点对做国内业务的开发者尤其重要。安装这块Windows用户去官网下载MySQL Installer选Server Only就行开发机不用装一堆用不上的组件。安装过程中会让你选认证方式这里有个大坑默认的caching_sha2_password认证方式如果你的程序用的是老版本的JDBC驱动或者Navicat老版本会连不上报Unable to load authentication plugin之类的错误。解决方法是安装时选Use Legacy AuthenticationMySQL 5.x兼容方式或者后面自己改认证插件。Linux服务器上安装更简单Ubuntu/Debian直接用apt装sudo apt update sudo apt install mysql-server装完先跑一下安全初始化脚本sudo mysql_secure_installation这个脚本会引导你设置root密码、删除匿名用户、禁止root远程登录。别偷懒跳过线上环境不跑这一步等于裸奔。装完8.0之后连接命令就是mysql -u root -p进去之后先看一眼版本和当前使用的存储引擎SELECT VERSION(); SHOW ENGINES;默认引擎是InnoDB线上业务也基本都用InnoDB支持事务、支持行级锁崩溃恢复能力也比老掉牙的MyISAM强太多。MyISAM除了某些只读报表场景还在用日常开发里基本可以忽略。1.2 建库建表写SQL前的第一步连接上MySQL之后第一件事肯定是建库。建库这个动作看着简单但字符集和排序规则一定要一开始就定好不然表建多了再改就是灾难。我习惯用这样一套CREATE DATABASE IF NOT EXISTS shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;utf8mb4兼容完整的Unicodeutf8mb4_general_ci是大小写不敏感的排序规则。ci就是case insensitive意味着查询时 abc 和 ABC 会被视为相等。如果你的业务有特殊的大小写区分需求可以改成utf8mb4_bin。但绝大多数业务用general_ci就够了而且它比unicode_ci性能略好。建库之后就是建表。我拿一个简单的电商订单场景来演示因为订单、用户、商品这三张表能覆盖SQL学习里绝大多数典型操作CREATE TABLE user ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, email VARCHAR(100) DEFAULT NULL, age TINYINT UNSIGNED DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE product ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, product_name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT UNSIGNED NOT NULL DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL, product_id INT UNSIGNED NOT NULL, quantity INT UNSIGNED NOT NULL DEFAULT 1, status TINYINT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user_id (user_id), KEY idx_product_id (product_id), CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES user(id), CONSTRAINT fk_order_product FOREIGN KEY (product_id) REFERENCES product(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个细节说一下。INT UNSIGNED是为了让主键能存更大的整数别小看这个等表数据量上亿的时候有没有UNSIGNED差别很大。DECIMAL(10,2)是金额字段的标准姿势千万别用FLOAT或DOUBLE存钱二进制浮点数的精度问题在金额计算上会坑死人。DATETIME和TIMESTAMP的选择简单说就是TIMESTAMP有时区转换而且2038年会溢出DATETIME没有时区概念但是范围大国内业务用DATETIME更省心。order这个表名我特意加了反引号因为ORDER是MySQL的保留字直接用会报语法错误这也是新手最常见的报错之一。外键要不要建我的建议是学习阶段必须要建能帮你理解表间关系。但到了实际高并发项目里很多团队会刻意不用外键把关系约束放到应用层去处理因为外键会让每次插入、更新都多一层一致性检查在高写入场景下性能损耗很明显。这个看团队规范没有绝对对错。2. SQL必会核心操作CRUD与查询2.1 DML操作增删改的可复制模板DML是Data Manipulation Language就是指INSERT、UPDATE、DELETE这三个操作。看着简单但实际每个操作都有值得注意的地方。插入数据单一插入和多值插入写法INSERT INTO user (username, email, age) VALUES (zhangsan, zsexample.com, 25); INSERT INTO user (username, email, age) VALUES (lisi, lisiexample.com, 30), (wangwu, wangwuexample.com, 28);多值插入在MySQL里是强烈推荐的一次网络往返批量写入性能远好于循环单条INSERT。你在写Java代码的时候用MyBatis的foreach批量插入底层也是拼这种多值SQL。更新操作最关键的是WHERE条件UPDATE user SET age 26 WHERE username zhangsan;新手最容易犯的错就是忘记写WHERE或者WHERE写得太宽。比如UPDATE user SET age 0;这会把整张表所有人的age全部改成0。如果真的发生了请立刻冷静下来MySQL默认开了事务自动提交的话数据是找不回来的。如果你提前配置好了binlog还能用binlog做时间点恢复要是没开那就只能祈祷有备份了。这也是为什么我强烈建议所有人在操作生产库之前先把那条SQL跑一遍SELECT看看到底命中哪些行确认无误再改成UPDATE或DELETE。删除操作同理DELETE FROM user WHERE id 1001;如果只是想清空表数据用TRUNCATE TABLE user。TRUNCATE是DDL操作直接删文件重建不走事务速度远快于DELETE但没法回滚谨慎使用。2.2 查询的五层结构排序、去重、条件过滤SELECT是整个SQL里最核心、最常用也是最容易写出烂SQL的部分。一个基础查询的完整语法结构长这样SELECT 字段 FROM 表名 WHERE 过滤条件 GROUP BY 分组字段 HAVING 组后过滤 ORDER BY 排序字段 LIMIT 偏移量, 数量注意一个关键认知SQL写的顺序和执行顺序是两回事。实际执行顺序是FROM先确定从哪张表取数据WHERE对每行做过滤去掉不满足条件的行GROUP BY按指定字段分组HAVING对分组后的组进行过滤SELECT计算要返回的字段ORDER BY排序LIMIT限制返回行数这个执行顺序理解透了很多诡异问题都能想明白。比如为什么WHERE里不能用SELECT中定义的别名因为WHERE执行在SELECT之前别名还没生成呢。比如为什么WHERE不能用聚合函数、HAVING才能用因为聚合发生在WHERE之后先过滤再分组这是减少计算量的关键。来一个实际例子。我想查年龄大于20岁、按年龄分组后组内人数超过1人的用户并输出每组的人数和平均年龄SELECT age, COUNT(*) AS cnt, AVG(age) AS avg_age FROM user WHERE age 20 GROUP BY age HAVING cnt 1 ORDER BY age DESC LIMIT 10;去重用DISTINCT和GROUP BY有类似效果但语义不同SELECT DISTINCT age FROM user; SELECT age FROM user GROUP BY age;DISTINCT会对返回字段做去重GROUP BY更强调分组统计。如果你只是想去重看有哪些值DISTINCT足够如果你想同时算每个值的数量、总和等指标必须用GROUP BY。这里有一个大家经常问的问题DISTINCT和GROUP BY哪个性能好在MySQL 8.0里如果都走索引二者性能差别不大。但GROUP BY的语义更强能用GROUP BY解决问题就不要纠结DISTINCT。ORDER BY排序支持单字段、多字段和方向混合SELECT username, age FROM user ORDER BY age DESC, id ASC;LIMIT分页是开发里天天碰到的。第一页没问题但大分页场景要小心比如SELECT * FROM order LIMIT 1000000, 20;这条SQL看着只是取20条数据但MySQL会先把前1000020行全部扫出来再丢掉前100万条。数据量一大这就是慢查询。优化办法是用延迟关联先只查主键再JOIN回原表取完整数据SELECT o.* FROM order o INNER JOIN (SELECT id FROM order ORDER BY id LIMIT 1000000, 20) tmp ON o.id tmp.id;或者更粗暴地记住上一页最大ID直接WHERE id 上一页最大id ORDER BY id LIMIT 20如果它的排序逻辑是主键或唯一索引效果立竿见影。3. 从基础到进阶多表查询与窗口函数3.1 别再谈JOIN色变三种关联的用法在真实业务里数据一定不会只在一张表里。订单表存user_id和product_id你想看到订单对应的用户名和商品名就必须关联user和product表。这就是JOIN存在的意义。JOIN分三种主流方式INNER JOIN内连接只返回两个表中都匹配的行LEFT JOIN左连接返回左表全部行右表没有匹配的就补NULLRIGHT JOIN右连接返回右表全部行左表没有匹配的就补NULL实际开发里RIGHT JOIN极少用能用LEFT JOIN改写就用LEFT JOIN。因为人的阅读习惯是从左往右LEFT JOIN在逻辑上更好掌握。看个例子。查询所有订单附带用户名和商品名SELECT o.id AS order_id, u.username, p.product_name, o.quantity, o.status FROM order o INNER JOIN user u ON o.user_id u.id INNER JOIN product p ON o.product_id p.id;这里我给三张表都起了别名o、u、p表名长了之后用别名可以少打很多字而且可读性更好。INNER JOIN意味着只返回有用户、有商品的订单。如果我想查所有用户包括那些一个订单都没下过的用户SELECT u.id, u.username, o.id AS order_id FROM user u LEFT JOIN order o ON o.user_id u.id;这种查出来没有下过单的用户order_id会是NULL。用这个特性就能直接筛选哪个用户没下过单SELECT u.id, u.username FROM user u LEFT JOIN order o ON o.user_id u.id WHERE o.id IS NULL;JOIN的性能核心在于ON条件能不能走索引。这个例子里的ON条件是user_id和product_id在order表里这两个字段都有索引建表时的KEY所以JOIN时就不会全表扫。如果你在关联字段上没建索引那恭喜你等着的就是全表逐行嵌套循环三层表JOIN能跑到天荒地老。3.2 窗口函数从TopN到累计值MySQL 8.0最让人欣喜的更新之一就是原生支持窗口函数。窗口函数解决的核心问题是既要聚合统计又不想丢失明细行。以前你想算每个人年龄最大的前两个用户用GROUP BY会把明细丢掉只能用各种子查询绕绕得人头大。有了窗口函数一句话就搞定了。窗口函数的基本语法是函数名() OVER ( PARTITION BY 分组字段 ORDER BY 排序字段 [ROWS 或 RANGE 窗口范围] )常用窗口函数有三大类排序类ROW_NUMBER()、RANK()、DENSE_RANK()聚合类SUM()、AVG()、COUNT() 配合OVER()取值类LAG()、LEAD()、FIRST_VALUE()、LAST_VALUE()先看排序类。ROW_NUMBER、RANK、DENSE_RANK的区别是面试官最爱问的SELECT user_id, product_id, quantity, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY quantity DESC) AS rn, RANK() OVER (PARTITION BY user_id ORDER BY quantity DESC) AS rk, DENSE_RANK() OVER (PARTITION BY user_id ORDER BY quantity DESC) AS dr FROM order;假设某个用户有三笔订单数量分别是10、10、5ROW_NUMBER结果是 1、2、3纯物理行号不重复RANK结果是 1、1、3数量相同的行并列第一但下一名直接跳号DENSE_RANK结果是 1、1、2并列第一名之后第二名紧跟着来不跳号业务里要排名不跳号就用DENSE_RANK要严格的唯一行号比如分页就用ROW_NUMBER。RANK用的场景相对最少但它适合取前三名允许并列的规则。再来看累积求和。假设我要查每个用户累计下单数量这是典型的窗口范围使用场景SELECT user_id, created_at, quantity, SUM(quantity) OVER (PARTITION BY user_id ORDER BY created_at ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumulative_qty FROM order;这段的意思是对每个用户按下单时间排序然后从最早一笔订单累加到当前行。ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW就是从分区第一行到当前行的窗口范围。这个写法在做经营分析、报表统计时非常常用。窗口函数的执行逻辑在ORDER BY之前它是在GROUP BY、HAVING之后才计算的。意思是你先用WHERE过滤掉行再分组聚合最后才轮到窗口函数在数据上跑。别在窗口函数里写WHERE那是跑不通的。4. 事务、存储过程与常用语句技巧4.1 事务多条SQL要么都成功要么都失败事务是数据库区别于文件系统的最核心能力。它保证一组SQL操作要么全部成功要么全部不生效不会出现扣了钱但订单没创建这种中间状态。事务有四个特性ACID原子性、一致性、隔离性、持久性。MySQL的InnoDB引擎默认是自动提交autocommit1每条SQL执行完就立即生效。需要手动控制事务时用BEGIN或START TRANSACTION开启用COMMIT提交用ROLLBACK回滚START TRANSACTION; UPDATE account SET balance balance - 100 WHERE user_id 1; UPDATE account SET balance balance 100 WHERE user_id 2; -- 如果一切正常 COMMIT; -- 如果中途发现第二个更新失败应该 -- ROLLBACK;这里我故意模拟一个转账场景。如果第一句扣款成功、第二句加款失败事务回滚能让第一句的扣款也取消掉确保两边账户总金额不变。这就是事务最典型的价值。开发中最常踩的坑是事务里混入DDL语句。事务能完美处理DML增删改但像CREATE TABLE、ALTER TABLE这种DDL语句MySQL会隐式提交当前事务导致你之前的操作被半路提交回滚失效。所以请记住一个铁律不要在事务中间执行DDL。隔离级别这里简单说一下。MySQL默认隔离级别是REPEATABLE READ可重复读在同一个事务里多次SELECT结果一致不会读到其他事务已提交的新数据。这是通过MVCC机制实现的。需要理解的是它会带来幻读问题InnoDB通过间隙锁gap lock部分解决了幻读。对于应用开发者90%场景用默认隔离级别就行不用过度调优。4.2 存储过程业务逻辑放进数据库存储过程就是把一段SQL逻辑预先编译好存到数据库里以后可以反复调用。简单理解就是数据库里的函数。一个打印指定用户订单数量的存储过程长这样DELIMITER // CREATE PROCEDURE count_user_orders(IN uid INT, OUT total INT) BEGIN SELECT COUNT(*) INTO total FROM order WHERE user_id uid; END // DELIMITER ;DELIMITER是MySQL特有的命令因为默认的分隔符是分号而存储过程内部也有分号所以要用//告诉MySQL整段一起编译。调用方式CALL count_user_orders(1, total); SELECT total;存储过程在写批量数据处理脚本或报表任务时确实方便但我必须提醒你别什么逻辑都往数据库里塞。存储过程的调试成本高、版本管理困难、对数据库服务器的CPU和内存占用也大。现在主流架构是轻数据库、重应用把复杂业务逻辑放在微服务或应用层里SQL只负责单纯的数据读写这样系统的可维护性和扩展性都好得多。存储过程最适合的场景是定时任务、报表统计、批量数据处理这些一次性复杂SQL放库里反而优雅。4.3 日期处理与通用表达式日常SQL写多了你会发现日期处理是另一个高频需求。MySQL里常用的日期函数有NOW()、CURDATE()、DATE_FORMAT()、DATE_ADD()等SELECT NOW(); -- 2024-11-20 14:30:00 SELECT DATE_FORMAT(NOW(), %Y-%m-%d); -- 2024-11-20 SELECT DATE_ADD(NOW(), INTERVAL 7 DAY); -- 一周后的时间 SELECT DATE_SUB(NOW(), INTERVAL 1 MONTH); -- 一个月前的时间按日期分组统计是报表中最常见的操作SELECT DATE_FORMAT(created_at, %Y-%m-%d) AS day, COUNT(*) AS order_cnt FROM order GROUP BY day ORDER BY day;8.0还支持CTE公用表表达式用WITH子句把复杂的嵌套子查询拆分得多步提升可读性。比如先算出每个用户的总消费额再从这个结果里筛出消费超过1000的用户WITH user_spending AS ( SELECT user_id, SUM(quantity * price) AS total_spent FROM order o INNER JOIN product p ON o.product_id p.id GROUP BY user_id ) SELECT * FROM user_spending WHERE total_spent 1000;这个WITH写法比层层嵌套的子查询清楚太多了。它跟临时表的区别是CTE只需要在内存里算一次不落盘性能在绝大多数场景下优于临时表。5. 性能与安全慢SQL优化与防注入5.1 Explain执行计划一句SQL怎么就慢了线上SQL变慢第一反应不是改代码而是先用EXPLAIN看一眼执行计划。EXPLAIN是MySQL分析SQL怎么执行的工具直接在SQL前面加上EXPLAIN关键字就行EXPLAIN SELECT u.username, p.product_name FROM order o INNER JOIN user u ON o.user_id u.id INNER JOIN product p ON o.product_id p.id WHERE o.status 0 ORDER BY o.created_at DESC LIMIT 10;输出结果是一个表格关键列有这几个type访问类型。从好到差大致是 system const eq_ref ref range index ALL。看到ALL就是全表扫描赶紧加索引。key实际用到的索引名。NULL表示没走索引。rows预估扫描行数。数字越大越危险。Extra额外信息出现Using filesort意味着排序没走索引文件排序出现Using temporary意味着用了临时表这两个都是性能隐患。type列里最需要记住的是ALL和ref。ALL是全表扫描表大了必然慢ref是普通索引等值匹配正常操作;eq_ref是主键或唯一索引关联是JOIN查询里最理想的方式。为什么ORDER BY查不出来因为这个查询顺序是先JOIN后排序如果排序字段没有索引就一定会走filesort。优化思路有两种一是给排序字段加索引让B树天然排好序二是减少参与排序的数据量先把WHERE条件做窄再排序。慢查询日志也要配置好。MySQL里开启慢查询日志超过阈值的SQL会被记录下来是定位线上SQL性能问题的第一手线索SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;long_query_time设为1秒意味着执行超过1秒的SQL都会被记录。在开发环境阈值可以设0.5秒线上则根据业务判断。慢查询日志通常是分析性能的起点但它只告诉你哪条SQL慢具体慢在哪还得靠EXPLAIN。5.2 SQL注入与防注入不要拼SQLSQL注入是Web安全里最经典、也是危害最大的漏洞之一。它的原理一句话总结程序把用户输入的内容直接拼接到SQL里导致用户输入的数据被数据库当成SQL代码执行了。举个例子假设登录页面处理逻辑是String sql SELECT * FROM user WHERE username username AND password password ;如果用户在用户名输入框输入admin --最终拼出来的SQL变成SELECT * FROM user WHERE username admin -- AND password xxx--是MySQL的注释符后面的密码校验直接被注释掉了。攻击者根本不需要知道密码只输入一个admin --就能以admin身份登录系统。这就是为什么要提出万能密码绕过这个概念本质上是程序把用户输入当成了代码。防御SQL注入最有效的办法是用参数化查询预编译语句。在Java的JDBC里PreparedStatement ps conn.prepareStatement( SELECT * FROM user WHERE username ? AND password ? ); ps.setString(1, username); ps.setString(2, password);问号是占位符setString传入的值会被数据库当作纯数据哪怕里面包含了单引号、注释符也只会被当作文本不会变成SQL逻辑。这个方案从根上杜绝了SQL注入因为SQL的结构在编译阶段就已经固定了用户输入再花哨也改变不了执行计划。在MyBatis里对应的就是#{}和${}的区别。#{}是预编译占位符安全${}是字符串拼接有注入风险。如果你非要用${}处理动态表名或排序字段比如ORDER BY ${sortField}那么sortField必须经过严格白名单校验绝不能直接把前端传的值拿来拼。另外给数据库账号分配最小权限也是防注入纵深防御的重要一环。哪怕查询SQL被注入了如果这个账号只有SELECT权限攻击者也做不了更多破坏。开发中应用连接数据库的账号不要用root而是建一个只给业务库增删改查权限的专用账号这是安全基线。6. 常见问题与排查技巧实录6.1 连接不上MySQL怎么办新手最常碰到的问题就是安装完MySQL后连接失败。错误五花八门最常见的是ERROR 2002 (HY000): Cant connect to local MySQL server through socket /var/run/mysqld/mysqld.sock (2)这个报错几乎可以断定是MySQL服务没启动。Linux下先看看进程状态sudo systemctl status mysql sudo systemctl start mysql如果确认服务在跑还是连不上那就要排查bind-address配置。MySQL默认只监听127.0.0.1如果你要远程连接就得改配置文件里的bind-address为0.0.0.0同时确认3306端口是放开的。而且MySQL 8.0默认是不允许root远程登录的得单独创建远程账号CREATE USER app_user% IDENTIFIED BY strong_password; GRANT ALL PRIVILEGES ON shop.* TO app_user%; FLUSH PRIVILEGES;还有一个高频问题是忘记root密码。处理办法是通过跳过权限表的方式重启MySQL但这招有风险操作时务必小心。流程大致是停掉MySQL服务在配置文件里加上skip-grant-tables重启服务后免密进入修改密码再把配置项去掉再次重启。具体步骤网上有大量教程我只强调一个重点改完密码一定要记得把skip-grant-tables删掉并重启否则你的MySQL对所有人都是免密的等于裸奔。6.2 空值和去重的坑NULL是SQL世界里最特殊的值它不是0不是空字符串而是不知道。很多新手栽在NULL上原因是拿NULL和任何值做比较结果都是NULL不是TRUE也不是FALSE。比如你想找出所有没有填邮箱的用户SELECT * FROM user WHERE email NULL;这条永远查不出数据因为email NULL的结果是NULLWHERE只接受TRUE。正确写法是SELECT * FROM user WHERE email IS NULL;判断不为空用IS NOT NULL千万不要写成email ! NULL。如果你想把NULL填充成默认值用COALESCESELECT username, COALESCE(email, no-emailexample.com) FROM user;COALESCE函数会从左到右返回第一个非NULL的值在报表里处理缺失数据时特别好用。再讲一下去重这个高频操作的几个方法对比。去重有四种常见姿势SELECT DISTINCT简单去重返回唯一行GROUP BY按字段分组适合同时做聚合统计ROW_NUMBER() OVER(PARTITION BY ...)取每组一条适合按某个字段去重但要保留完整记录的场景DELETE JOIN删除表中的重复记录属于数据清洗操作清洗表里重复记录这个场景容易踩坑。假设user表里有重复的username只保留id最小的那条DELETE u1 FROM user u1 INNER JOIN user u2 ON u1.username u2.username AND u1.id u2.id;这条SQL的意思找到所有username相同但id更大的记录把它们删掉。执行前先改成SELECT确认一下范围SELECT u1.id, u1.username, u2.id AS dup_id FROM user u1 INNER JOIN user u2 ON u1.username u2.username AND u1.id u2.id;凡是DELETE或UPDATE先把条件换成SELECT跑一遍这是我在团队里强制要求的习惯。几秒钟的确认成本能救回整张表的数据。实用操作就聊到这儿。做MySQL维护这些年最大的体会是你真正需要的不是背下所有命令而是理解SQL的执行逻辑理解数据是怎么存、怎么取、怎么关联的。今天讲的单表查询、多表关联、窗口函数、事务、存储过程、索引优化和防注入其实是同一条线串下来的——先保证数据结构合理再保证查询效率可控最后保证数据安全可靠。平时写SQL的时候多想一步这条语句是怎么执行的比刷一百道面试题都管用。真到了线上出问题时保持冷静先EXPLAIN再查慢日志最后才动代码。这样的排查顺序能帮你少走很多弯路。
返回列表