ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MySQL查询优化实战:从去重、排序到索引与锁的全面排查指南

MySQL查询优化实战:从去重、排序到索引与锁的全面排查指南 1. 从热搜词里看大家最常摔跤的地方这段时间帮几个团队看MySQL问题顺手刷了一圈大家搜得最多的查询相关内容sql语句去重查询in查询语句报错mysql排序mysql将字符串转为日期mysql中int5这些词高频出现。看下来挺感慨——很多人觉得查询语句嘛能跑出结果就行但恰恰是这些能跑就行的写法在数据量上来之后成了慢查询重灾区有些甚至直接报错。先说个真实场景。有个朋友问我说一条去重查询跑了快三秒表也就二十万行他百思不得其解。我一看SQLSELECT DISTINCT user_id, user_name, create_time FROM orders;这就是典型的DISTINCT三个字段误解。他以为DISTINCT只对user_id去重实际上DISTINCT作用于后面所有字段的组合只要user_name或create_time不同user_id相同也会被当成不同记录返回。这种查询根本没法走索引覆盖还要做临时表排序去重不慢才怪。去重要分清楚需求你是要某个字段的唯一值还是要多字段组合下的唯一记录前者直接SELECT DISTINCT user_id FROM orders;或者更灵活一点SELECT user_id FROM orders GROUP BY user_id;后者才需要DISTINCT多字段。另外在数据量大的时候GROUP BY通常比DISTINCT更好优化因为GROUP BY可以配合索引下推而DISTINCT在某些版本里优化器处理得不够聪明。实际项目中我默认优先GROUP BY除非只是临时查个数。排序也是同样的问题。很多人喜欢对非索引字段排序然后数据一大就发现排序好慢。这里要记住一个底层逻辑MySQL的排序有两种一种走索引利用B树天然有序的特征直接返回一种是生成临时表做filesort。你给不了索引MySQL只能硬着头皮排没有别的出路。2. 一条慢查询的完整排查链路先看全局再开刀处理查询问题我的习惯路径是固定的先查当前哪些SQL在跑、跑多久了再去看具体某条SQL的执行计划最后才决定是改SQL还是加索引。SHOW FULL PROCESSLIST;这条命令排第一。它能列出当前所有连接正在执行的SQL、状态、执行时间。如果看到某个Query状态是Sending data或者Copying to tmp table执行时间还很离谱那基本就是慢查询现场。注意这里有个细节——SHOW FULL PROCESSLIST和SHOW PROCESSLIST的区别在于FULL会显示完整SQL文本不截断。排查问题时候必须用FULL不然SQL长一点就只能看到前半截还得靠猜。网上大量搜show full processlist killed基本是想把卡死的SQL杀掉。杀会话很简单KILL 12345;但我要强调KILL是治标不治本更不是第一选择。你应该先去搞明白它为什么卡。如果是因为表锁杀查询不如去查锁如果是因为某条SQL本身太烂杀掉了下次还会以同样方式出现。我见过不止一个团队每天定时KILL慢查询然后问题永远在这种亡羊补牢的方式没法沉淀任何东西。杀完之后对这条SQL做EXPLAINEXPLAIN SELECT user_name, COUNT(*) FROM orders WHERE create_time 2024-01-01 GROUP BY user_name;EXPLAIN输出里我最关注这几列type、key、rows、Extra。type从好到差大致是system const eq_ref ref range index ALL。看到ALL意味着全表扫描除非表极小否则这就是优化信号。rows是估计扫描行数Extra里如果出现Using filesortUsing temporary基本可以断定排序和去重没有好日子过——都在临时表里完成。有一次给客户看一条查询SELECT * FROM users WHERE phone 13800138000;phone字段是varchar类型结果发现type是ALL全表扫。我让DBA查了一下表结构phone确实是varchar但查询里用数字比较MySQL会做隐式类型转换把字段值转成数字再比较这种情况下索引直接失效。改写成SELECT * FROM users WHERE phone 13800138000;type立刻变成ref扫描行数从十万降到个位数。这种坑在真实环境里非常多——不只是字符串和数字还有字段的字符集不一致导致的隐式转换也走不了索引。关于索引我说个扎心的现实给查询建索引不是越多越好而是够用就好。每个索引都有代价写入要更新索引内存要存索引。实践中我会用一个小原则——单表索引控制在五六个以内联合索引能覆盖的查询绝不加第二个单列索引。最典型的就是SELECT * FROM orders WHERE user_id 123 ORDER BY create_time;如果你单独建了user_id索引、单独建了create_time索引这条SQL依然可能慢因为MySQL的优化器在5.x时代很难把两个单列索引自动merge。正确做法是建联合索引(user_id, create_time)这样等值匹配user_id之后create_time天然有序一次索引扫描搞定排序。3. 连接与环境的坑从socket报错到SSL配置再到版本选择热搜词里error 2002 (hy000): cant connect to local mysql server through socket /tmp/mysql.sock被搜了很多次说明这个报错困扰了一大批人。这台报错的本质是客户端按默认的socket文件路径去找MySQL服务端结果没找到。常见原因有三个第一MySQL服务没起来本来就没socket文件可连第二MySQL起来了但socket文件路径不是默认的/tmp/mysql.sock你配置文件里自定义了别的位置第三权限问题客户端用户根本访问不到那个socket文件。排查顺序我建议这样来# 先确认服务在不在 ps -ef | grep mysqld # 服务在的话用ss或netstat确认监听状态 ss -tlnp | grep 3306 # 看socket文件到底在哪 find / -name *.sock 2/dev/null # 确认MySQL配置里的socket路径 cat /etc/my.cnf | grep -i socket如果服务没起systemctl start mysqld或者按你的发行版对应方式启动如果没起成功去看错误日志通常位置在/var/log/mysqld.log或/var/log/mysql/error.log。很多情况下日志里会直接告诉你原因——比如磁盘满了、权限不对、配置项写错。记得我有一次帮人排查折腾半天最后发现是磁盘满了InnoDB写不了redo log服务反复崩溃socket文件起起灭灭。这种情况你光去看socket是没用的得从日志挖根因。关于mysql ssl连接错误这词热度不低。MySQL的SSL默认是开启的但有些老版本的客户端或者某些第三方工具连接时会因为证书校验失败而报错。连接串里常见的两个参数usessl和sslmode很多人分不清楚。简单说usessltrue只是告诉驱动使用SSL加密通道但如果服务端证书不受信任照样会报错。sslmode是更高阶的控制选项值有DISABLED、PREFERRED、REQUIRED、VERIFY_CA、VERIFY_IDENTITY等。VERIFY_CA和VERIFY_IDENTITY会强制校验服务端证书的合法性这是最严格也最安全的模式但配置成本也最高需要给客户端提供CA证书。我的建议是生产环境一定要用SSL但证书校验维度要结合你的运维能力来定。如果你搞清楚了证书链路用VERIFY_CA如果只是想加密传输但不想处理证书信任链至少用REQUIRED级别。千万别图省事把sslmode设为DISABLED明文传输的MySQL连接在公网上等于裸奔。JDBC连接串一个简单的示例jdbc:mysql://127.0.0.1:3306/dbname?useSSLtruerequireSSLtrueverifyServerCertificatefalse注意verifyServerCertificatefalse是让步策略只加密不验证书适合内部网络、证书体系还没搭建起来的场景。版本选择也是一个绕不开的问题。热搜里好几个词是mysql下载哪个版本mysql下载官网mysql下载安装教程。现在主流就是5.7和8.0两大版本。8.0相比5.7有几个重要变化默认字符集从latin1变成utf8mb4这直接影响中文存储窗口函数和CTE语法支持写复杂统计查询方便太多caching_sha2_password默认认证插件用老客户端连接会报认证错误。如果你是新项目我明确建议直接上8.0别在新项目里抱5.7不放。5.7已经在2023年结束了官方更新支持安全补丁都没有了。如果你维护的是存量系统那5.7继续用着但一定要关注字符集问题——旧的latin1库碰到中文场景迟早是要还账的。Workbench和Navicat这些工具的热搜词也说明了现实大部分人不是直接用命令行而是用GUI工具管理数据库。工具只是个入口真正的功力还是在你能不能写对SQL、能不能看懂执行计划。工具选哪个根据自己习惯来Navicat功能全面但是商业版Workbench免费官方出品且支持8.0的诸多新特性用起来都行。4. 存储过程与锁从声明到卡死现场的处理思路存储过程这个词也进了热搜。很多从JavaWeb项目转过来的开发者对存储过程的第一印象是能用Java代码解决的事为什么要写SQL里这话不是没道理但存储过程在特定场景下确实有不可替代的价值——比如好多条SQL需要在一个事务里完成、数据核对、批量历史数据归档等存储过程可以减少客户端与数据库之间的网络往返逻辑直接落在数据库层事务边界也更好控制。先看一个标准的声明结构DELIMITER // CREATE PROCEDURE sp_archive_orders(IN days INT) BEGIN DECLARE v_count INT DEFAULT 0; DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SELECT Error Occurred AS ErrorMsg; END; START TRANSACTION; INSERT INTO orders_archive SELECT * FROM orders WHERE create_time DATE_SUB(NOW(), INTERVAL days DAY); DELETE FROM orders WHERE create_time DATE_SUB(NOW(), INTERVAL days DAY); COMMIT; END // DELIMITER ;几个容易出错的点DELIMITER是关键MySQL的客户端默认用分号作为语句分隔符而存储过程内部就有大量分号不临时改分隔符MySQL会在第一个分号处就错误地认为CREATE PROCEDURE语句已经结束。声明变量用DECLARE必须在BEGIN之后、任何操作语句执行之前不然语法报错。异常处理我习惯加上EXIT HANDLER FOR SQLEXCEPTION这样过程体里出任何SQL错误都会进入回滚逻辑避免数据不一致。调用存储过程CALL sp_archive_orders(90); CREATE PROCEDURE 用 CALL 调用不用 SELECT。存储过程的报错排查比较难受因为MySQL不像某些数据库那样把编译错误提示得明明白白。我的做法是先在存储过程体外把每一条SQL单独跑一遍确保逻辑没问题再把语句组装回过程体。报错信息里带的所谓call stack in connection.php line 528这种其实不是MySQL存储过程的报错而是PHP代码里调用数据库连接时的报错跟存储过程本身关系不大。原理是PDO连接MySQL时如果构造的DSN字符串有问题或者MySQL服务端拒绝连接就会在new PDO这个位置抛出连接异常。看到这个错误你应该去查数据库地址、端口、用户名密码和DB名而不是纠结于那一行PHP代码。锁这块热搜词出现mysql锁原理及面试题不意外锁是面试常客更是生产环境的痛点。先理清一个概念锁和事务是绑定的在InnoDB默认的REPEATABLE READ隔离级别下普通的SELECT走的是MVCC快照读不加重读锁但SELECT ... FOR UPDATE和UPDATE/DELETE操作会申请行锁Record Lock如果条件里的字段没有索引行锁会升级为间隙锁甚至表级别的锁。有一次一个用户跟我抱怨系统并发一高就死锁。我看日志两条UPDATE语句-- 会话A UPDATE inventory SET stock stock - 1 WHERE product_id 1; -- 会话B UPDATE inventory SET stock stock - 1 WHERE product_id 1;这看起来就是同一行的更新怎么会死锁再往下看发现inventory表上product_id没有索引。这就麻烦了——没有索引InnoDB无法确定要锁哪些行只能把可能涉及的范围都锁上两个会话互相等待对方的锁释放死锁就出现了。解决办法也很简单给product_id加上索引让锁精确定位到行。所以说锁等待频繁的时候第一反应不是去调事务隔离级别而是去看WHERE条件用到的字段有没有索引。锁的监控手段也很重要SHOW ENGINE INNODB STATUS;这条命令的输出里会有一块LATEST DETECTED DEADLOCK里面记录了死锁相关的SQL语句、持有锁和等待锁的细节是排查死锁的第一手证据。我处理死锁的经验顺序先看这块识别是哪两条SQL互相冲突再看它们的WHERE条件字段是否有合适索引最后才考虑业务逻辑上能不能调整SQL顺序来规避。另外还有个操作上的小心得如果你在命令行里看到了类似SHOW FULL PROCESSLIST;然后发现某个会话一直在Waiting for table metadata lock通常意味着有人开了一个事务但没有提交DDL语句在后面排队等锁。这种问题跟SQL本身关系不大本质是事务管理不规范。解决方式是找到那个长期持有事务的会话确认业务确实结束后杀掉它同时要在代码层杜绝开了事务忘记提交的隐患——这也是为什么我强烈建议事务代码必须用try-finally或者注解式事务保证commit/rollback成对出现。5. 工程化落地连接池、主从复制与面试高频坑这部分聊聊工程实践层面的东西对应热搜里的mysql的数据库连接池怎么使用mysql 主从复制javaweb项目完整案例mysqlmysql jdbc usessl 与 sslmode 使用。连接池这个点很多人只停留在听说过层面。连接池解决的是MySQL连接建立成本高的问题。MySQL的每条连接都要走TCP握手、认证、权限判断等流程频繁建立和销毁连接在高并发下会拖垮服务端。连接池的核心是维护一组固定的连接复用用完了还回去而不是真的断开。常见的有HikariCP、Druid、C3P0。性能上HikariCP现在基本是Spring Boot默认的选择Druid有监控和SQL防火墙这些附加功能看团队需求选。连接池参数有几个必须理解initialSize是初始化连接数minIdle是池中最小空闲连接数maxActive是最大活跃连接数maxWait是获取连接的最大等待时间。生产环境我一般把maxActive设定为业务并发峰值的估算值计算公式大致是数据库预计活跃连接数 QPS × 每条SQL平均执行时间。比如系统每秒大概500个请求每条SQL平均20ms那活跃连接数约为500×0.0210maxActive设置在20到30比较合理留足余量但又不至于撑爆数据库连接上限。注意maxActive设得再大数据库端max_connections就是天花板两边要对齐否则连接池排队排在数据库拒绝之后应用就会报连接超时。连接池这事的常见坑是连接泄漏。如果代码里获取了连接却没有释放连接池里的连接会被慢慢耗尽服务表现为越来越慢最终卡死。排查方法很简单——连接池工具一般会提供活跃连接数和空闲连接数能看到活跃连接数一直涨而不回落基本就是泄漏了。有条件的可以用Druid的监控页面看哪些SQL拿连接后一直没还。主从复制是另一个高频话题。MySQL主从复制的原理其实不复杂主库把数据变更写进二进制日志binlog从库I/O线程把binlog拉过来写到自己的中继日志relay log从库SQL线程再拿中继日志去重放完成数据同步。把远程库的某张表同步到本地这个需求我碰到过很多次。整体思路分两步先做一次全量初始化把当前数据导过来再开启实时增量同步靠binlog。具体操作步骤我整理一下在主库上准备好复制账号CREATE USER repl% IDENTIFIED BY your_password; GRANT REPLICATION SLAVE ON *.* TO repl%;主库配置开启binlog修改my.cnf[mysqld] server-id1 log-binmysql-bin binlog_formatROW注意server-id在主从两端必须不同。从库同样配置server-id为不同值重启并开始初始化# 在主库上锁定表并获取当前binlog位置 FLUSH TABLES WITH READ LOCK; SHOW MASTER STATUS;注意生产环境锁表时要结合业务低峰期执行不然数据写入会被阻塞。用mysqldump全量备份在另一个会话里执行mysqldump -h主库IP -u备份用户 -p --single-transaction --master-data2 库名 表名 table.sql然后释放锁UNLOCK TABLES;注意--single-transaction在InnoDB下可以做到一致性快照而不锁表加--master-data2会在dump文件里记录binlog位置信息省得你手工记。备份完成后把表导入从库mysql -h127.0.0.1 -u用户名 -p 库名 table.sql配置从库同步并启动CHANGE MASTER TO MASTER_HOST主库IP, MASTER_PORT3306, MASTER_USERrepl, MASTER_PASSWORDyour_password, MASTER_LOG_FILEmysql-bin.000001, MASTER_LOG_POS107; START SLAVE; SHOW SLAVE STATUS\G关注Slave_IO_Running和Slave_SQL_Running都得是YesSeconds_Behind_Master表示延迟秒数。这里有个容易被人忽略的点如果只同步一张表可以设定复制过滤规则。在从库的配置文件里加replicate-wild-do-table库名.表名这样从库只重放这一张表的变更其他表的binlog到了从库后会被过滤掉。不过要注意谨慎使用过滤因为它可能导致从库的数据不完整后面如果业务又要同步别的表得重新初始化。主从同步延迟也是个常见问题。出现延迟常见原因有三个从库机器性能不如主库同步的是串行重放而主库是并发写从库上有长事务或慢查询卡住SQL线程。如果是5.7以上可以开启并行复制slave_parallel_typeLOGICAL_CLOCK slave_parallel_workers4这个配置能让从库按逻辑时钟并行重放事务延迟能明显下降。面试这块热搜里mysql面试题mysql锁原理及面试题也说明大家的需求。我根据实际面试经验总结几个真正常考且需要理解的题InnoDB和MyISAM的核心区别事务支持、行级锁、外键、崩溃恢复能力。InnoDB是默认标准MyISAM在只读场景下偶尔出现。索引为什么用B树不用哈希表或二叉树B树叶子节点有序、范围查询友好、树高较矮减少IO次数。这是核心。什么情况下索引会失效隐式类型转换、对索引列使用函数、LIKE前置通配符、联合索引不满足最左前缀原则。这条基本每个面试官都会问而且会追问你举例子。事务ACID是怎么实现的原子性靠undo log持久性靠redo log隔离性靠锁和MVCC一致性靠约束加前面三者配合。能把这个逻辑链条讲清楚才算真正理解。另外热搜里还有python skill 读取mysql这种词Python读MySQL一般就是PyMySQL或SQLAlchemy关键要记住连接本身也是个资源别忘了关闭或交给连接池否则一样有连接泄漏的隐患。写法上基本是这样import pymysql conn pymysql.connect( host127.0.0.1, userroot, passwordyour_password, databasetest_db, charsetutf8mb4 ) try: with conn.cursor() as cursor: cursor.execute(SELECT id, name FROM users WHERE status %s, (1,)) rows cursor.fetchall() conn.commit() finally: conn.close()注意这里用了参数化查询而不是字符串拼接这是防止SQL注入最基本的姿势。用%s占位符把变量传进去PyMySQL会帮你做转义不要图省事用f-string拼SQL。mysql中int5这个词我猜测是想问怎么让某个整数字段在查询中加5展示比如SELECT price 5 AS adjusted_price FROM products;这个写法没问题但要注意如果字段是NULL加出来的结果还是NULL可能需要用IFNULL包一层SELECT IFNULL(price, 0) 5 AS adjusted_price FROM products;顺带提一句mysql将字符串转为日期是另一个常见需求。方法主要有STR_TO_DATE、CAST、DATE_FORMAT的反向使用SELECT STR_TO_DATE(2024-12-01 14:30:00, %Y-%m-%d %H:%i:%s); SELECT CAST(2024-12-01 AS DATE);这些转换的隐患在于如果你在查询条件里对日期字段做转换索引就废了。比如SELECT * FROM orders WHERE DATE_FORMAT(create_time, %Y-%m-%d) 2024-12-01;看起来没毛病但date_format把索引列的每个值都计算了一遍MySQL没法走索引。正确写法是SELECT * FROM orders WHERE create_time 2024-12-01 00:00:00 AND create_time 2024-12-02 00:00:00;这种范围写法既能走索引语义也更准确。6. 最后聊点实际干活的经验写到最后分享几个我在真实项目里反复用到、但书上很少专门写的小经验。第一个是关于安装和版本。热搜里大量词是mysql安装教程linux安装mysqlrpm安装mysqldocker安装mysql——这说明很多人第一步就被环境卡住了。我个人的建议是能用Linux发行版官方源安装那就优先官方源比官网下载tar包省心得多想省事就Docker跑官方镜像一条命令的事docker run -d \ --name mysql8 \ -e MYSQL_ROOT_PASSWORDyour_password \ -p 3306:3306 \ -v /my/own/datadir:/var/lib/mysql \ mysql:8.0但无论哪种方式装完第一件事都是跑一下mysql_secure_installation把匿名用户、默认测试库、root远程访问这些安全隐患一次性清掉。不要嫌麻烦这一步能省掉后面无数个安全问题。第二个是关于SQL查询的最小化原则。查询时只取需要的字段不要动不动SELECT *。这个不只是因为SELECT *可能拖慢查询更重要的是它会破坏覆盖索引——当索引里的字段已经能满足查询需求时InnoDB可以直接从索引返回结果连表数据都不用回。你一旦多取一个不在索引里的字段整个优化就前功尽弃。我之前在项目里有一条统计查询改成覆盖索引写法后耗时从180ms降到8ms同样的索引只是select列表变了这就是差异。第三个是关于日常巡检。MySQL不是装上就能躺平的工具它需要你关注它的健康状态。我给团队定的简单巡检表每天看几条命令-- 看连接数占用 SHOW STATUS LIKE Threads_connected; -- 看是否有慢查询堆积 SHOW STATUS LIKE Slow_queries; -- 看关键缓冲池命中率 SHOW STATUS LIKE Innodb_buffer_pool_read%;Threads_connected如果长期接近max_connections上限说明连接管理已经瓶颈了Slow_queries突然暴增那就该去慢查询日志里翻翻最近写了什么烂SQL。慢查询日志的开启配置slow_query_log1 long_query_time1 slow_query_log_file/var/log/mysql/slow.loglong_query_time我建议设成1秒小于1秒的一般不值得人工介入超过1秒的都应该有合理理由。从查询语句的写法到执行计划分析再到连接环境、存储过程、锁和复制这些内容表面上看是散点但实际上有一条主线贯穿数据库的很多问题表面是SQL写法问题深层是索引、事务、锁这三件套的协同关系没理清。写SQL的时候多想想它对应到存储引擎层面是怎么执行的遇到问题就沿着执行计划-索引-锁-日志的路径去排查数据库没那么神秘它只是有一套自己的运行逻辑罢了。
返回列表