news 2026/10/6 13:22:30

MySQL慢SQL优化:Explain执行计划关键字段与实战调优

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL慢SQL优化:Explain执行计划关键字段与实战调优

很多搞后端的朋友第一次接触Explain,是在慢SQL压测被领导叫过去的时候。我也不例外:线上有个订单统计页面,运营点一下要等十几秒,一翻日志全是同一条SELECT。当时我做的第一件事不是改代码,而是把这条SQL丢进工具里跑了EXPLAIN——把SQL的执行计划打出来,一看type列是ALL,rows预估500万,心里基本就有数了。这篇分享就围绕SQL的Explain展开,从执行计划里每一个关键字段怎么读,到真实慢SQL怎么一步步调优,再到Explain看不到的盲区,全部用踩过的坑讲一遍,适合刚接手慢SQL优化的后端开发,也适合想系统理解执行计划的DBA新人。

1. 慢SQL找上门时,Explain到底在告诉你什么

做了几年数据库相关的工作,我最大的感受是:慢SQL本身不可怕,可怕的是你不知道它慢在哪。很多时候业务方只丢给你一句"这条SQL很慢",然后你对着几十行SQL发呆,不知道从哪下手。Explain的存在,就是把数据库优化器做决策的过程"透明化"——它不帮你优化,但会清清楚楚告诉你:这条SQL最终打算怎么执行。

1.1 一条SQL从编写到执行,中间发生了什么

先理一下基础。我们写的SQL提交到数据库后,会经历几个阶段:词法解析、语法解析、语义检查、优化器生成执行计划、执行器执行。前几步都是标准动作,真正影响性能的是优化器。优化器会根据表结构、索引信息、统计信息、连接顺序等条件,枚举出它认为"成本最低"的一系列执行方案。

这个过程很像你用导航软件规划路线:起点终点一样,但导航会给你好几条路,有的走高速、有的走国道、有的绕远但少收费。数据库优化器也一样,它要在多个执行路径里挑一个成本最低的。Explain打印出来的,就是优化器最终选中的那条"路线图"。

注意一个关键点:优化器选路线,依赖的是它手里的"地图数据",也就是统计信息和索引信息。如果这张地图本身是旧的、不准的,那它选出来的"最优路线"就可能是个大坑。这也是为什么后面我会强调,Explain的结果需要结合真实执行情况来验证。

1.2 Explain能回答的三个核心问题

我在实际排查中,会把Explain要解决的事情归纳成三个问题,所有字段都是围绕这三个问题展开:

第一,SQL先访问哪张表、以什么顺序访问。多表JOIN时,驱动表和被驱动表的顺序不同,性能差异能到几十倍。Explain的输出行顺序,以及id字段,能帮你还原优化器选择的连接顺序。

第二,每张表是怎么被访问的。是全表扫描,还是走索引?走的是主键索引、普通索引,还是索引范围扫描?这个问题的答案在type字段。type上如果出现ALL,基本就是慢SQL的头号嫌疑。

第三,访问之后还要做多少额外工作。比如要不要排序、要不要建临时表、能不能只用索引就拿到数据。这些看Extra字段。Using filesort、Using temporary一旦出现,就说明数据库在做"扫描之外"的额外功,通常很贵。

这三个问题搞明白,一条慢SQL的病灶基本就浮出水面了。

1.3 什么时候应该习惯性用Explain

很多人只在SQL出问题的时候才想起Explain,我的建议是把它变成一种"下意识动作":

  • 新功能上线前,凡是涉及核心表查询的SQL,先Explain一轮,确认没有全表扫描再合代码。
  • 索引变更之后,旧SQL的执行计划可能说变就变,跑一次Explain做前后对比,比看执行时间更可靠。
  • 慢查询日志里出现的SQL,不要只看日志,把SQL复制出来Explain一下,定位"慢"在哪一步。
  • 代码评审时别人问你"这个查询性能怎么样",拿Explain结果说话,比拍胸脯有说服力得多。

慢查询日志只告诉你"这条SQL花了3秒",Explain告诉你"这3秒花在了哪一行操作上"。两者的关系,一个是体检报告上的指标异常,一个是医生给你开的详细检查单。

2. 读懂执行计划的关键字段:先抓住四个“病根”位置

刚接触Explain的人,很容易被输出的一大堆字段吓到。MySQL的EXPLAIN结果里有id、select_type、table、partitions、type、possible_keys、key、key_len、ref、rows、filtered、Extra,一共十几个字段,全看懂当然最好,但如果你想快速定位问题,优先级最高的只有四个:type、key、rows、Extra。其他字段更多是辅助判断。

2.1 核心字段速查表

先给一张我自己常用的速查表,标注每个字段的实际用途:

字段含义排查时怎么用
id查询中各个SELECT的执行顺序编号id相同,从上往下执行;id不同,id越大越先执行
select_typeSELECT的类型(SIMPLE、PRIMARY、SUBQUERY、DERIVED等)判断是否存在子查询、临时表等复杂结构
table当前行操作的目标表多表JOIN时区分哪张表出了问题
type访问类型,性能从好到差依次是system > const > eq_ref > ref > range > index > ALL出现ALL或index时,先想办法让它变成range或ref
possible_keys优化器认为可能用到的索引如果一个都没显示,说明这个查询根本没索引可用
key优化器实际选中的索引key为NULL,说明没走任何索引;key和possible_keys不一致,说明有索引但优化器没用
key_len用到的索引字节数联合索引中判断实际用了几个字段,比如key_len=4,通常只用到了第一个字段
rows优化器预估要扫描的行数只是个预估值,不是实际值,但能反映量级
filtered表条件过滤后剩余行数的百分比结合rows看最终返回的行数级
Extra补充的执行信息,如Using index、Using filesort、Using temporary这里出现filesort/temporary,往往比全表扫描还值得警惕

2.2 type字段:一张表究竟是怎么被访问的

type是执行计划里最核心的一个字段,它直接决定了这张表的访问效率。网上很多文章把它叫"访问类型",我更喜欢叫它"扫描方式"。它的性能排序从好到坏,大致是这样:

type级别含义典型场景
system表只有一行记录系统表或极限情况,基本见不到
const使用主键或唯一索引等值匹配,最多一行WHERE id = 1
eq_refJOIN时被驱动表使用主键或唯一索引等值匹配常见于多表JOIN,性能极佳
ref使用普通二级索引等值匹配,返回多行WHERE user_id = 100
range使用索引做范围扫描BETWEEN、>、<、IN等条件
index扫描整棵索引树,但不需要回表覆盖索引的查询仍会扫全索引
ALL全表扫描,整张表逐行读取没走索引的查询,最大性能杀手

我在优化SQL时有个不成文的规矩:如果执行计划里出现两个以上type为ALL的表,几乎可以断定这条SQL要重写或者要加索引了。尤其是几千行的小表全表扫描还好说,一旦表到百万级,ALL就是灾难。

2.3 rows是"预估值",别拿它当真实值

很多新手有个误区:看到rows=100,就以为这条SQL真的只扫了100行。实际上rows是优化器基于统计信息推算出来的预估值,不是真实扫描行数。比如一张表前天统计信息记录是500万行,你昨晚删了400万,今天Explain出来的rows可能还是500万,因为统计信息还没更新。

这就解释了为什么有时候Explain看着很漂亮,实际执行却慢得离谱。优化器拿着错误的统计信息,很可能选错索引、算错成本。遇到这种情况,先对表执行ANALYZE TABLE刷新统计信息,再重新Explain,往往会有惊喜。

顺便说一句,MySQL 8.0之前InnoDB对rows的估算误差是出了名的,尤其是范围查询、多表JOIN时误差可能达到一个数量级。所以在关键场景下,我会用EXPLAIN ANALYZE(8.0.18+)去看真实的执行行数和耗时,这个后面专门讲。

2.4 Extra字段:隐藏的额外成本全在这

Extra字段平时最容易被忽略,但恰恰是它揭示了很多"看不见的工作"。几种高频出现的值,我结合场景解释一下:

Using filesort:这是排序操作。注意,这里的filesort不代表一定用了磁盘文件,也可能在内存里做排序。但它意味着MySQL需要额外做一次排序,而不是直接利用索引的有序性。SQL中有ORDER BY但排序字段不满足索引顺序时就会出现。一旦出现,就要检查能不能通过调整索引让排序字段也用上索引。

Using temporary:使用了临时表。通常出现在GROUP BY、DISTINCT、UNION这类操作中,数据量一大,临时表甚至会落到磁盘上,性能断崖式下跌。常见优化手段是让GROUP BY的字段用上索引,或者改写SQL减少中间结果集。

Using index:这是一个好信号,表示查询用到了覆盖索引,直接扫索引树就能拿到全部字段,不需要回表。在追求极致性能的SQL里,这是我最希望看到的Extra值之一。

Using where:表示存储引擎返回记录后,Server层又做了条件过滤。但如果前面几列用了索引,这里通常是正常的。要注意的是,如果type=ALL且Extra里同时出现Using where,这意味着全表扫描加逐行过滤,大概率是个问题SQL。

Using index condition:也就是ICP(索引条件下推),存储引擎层在索引上先过滤一部分记录,减少回表次数。这个在联合索引场景下很常见,一般不需要额外干预。

我见过太多人只盯着key列看有没有用索引,忽略了Extra里的filesort。实际上一条SQL可能走了索引,但由于ORDER BY字段不在索引里,MySQL要把几千行数据重新排序,这个排序成本有时比扫描本身还高。

3. 一条订单统计慢SQL的完整Explain排查链路

讲完字段,来一个完整案例。这是我在一个电商项目里真实处理过的问题,出于脱敏我把表名和字段名做了替换,但执行计划的特点完全保留。

背景是这样的:运营要做一个订单统计报表,需求是查最近30天订单,按照用户昵称模糊匹配关键词,按订单金额倒序展示前30条。最初SQL长这样:

SELECT o.order_id, u.nickname, o.total_amount, o.created_at FROM orders o LEFT JOIN users u ON o.user_id = u.id WHERE u.nickname LIKE '%小明%' AND o.created_at >= '2024-06-01' ORDER BY o.total_amount DESC LIMIT 30;

orders表当时大概500万行,users表100万行。这条SQL线上执行耗时12.8秒,直接把运营后台拖到崩溃。

3.1 第一步:先跑一次Explain,拿到原始现场

我拿到这条SQL的第一时间,直接在测试库跑了一次EXPLAIN:

EXPLAIN SELECT o.order_id, u.nickname, o.total_amount, o.created_at FROM orders o LEFT JOIN users u ON o.user_id = u.id WHERE u.nickname LIKE '%小明%' AND o.created_at >= '2024-06-01' ORDER BY o.total_amount DESC LIMIT 30;

结果的关键行大致是这样:

表typekeyrowsExtra
ordersALLNULL5008450Using where; Using filesort
usersALLNULL998122Using where

看到这个结果,几个问题非常明显:两张大表都是ALL全表扫描,orders还要做filesort排序。为什么明明orders表有10多个索引,优化器却一个都没用上?只看字段还不够,还得结合数据特性来推断。

3.2 第二步:拆解第一个痛点——主表为何全表扫

orders表是500万行的大表,type=ALL意味着逐行扫500万条记录,这本身就已经是灾难。为什么有索引还不用?问题出在WHERE条件的结构上。

o.created_at >= '2024-06-01'是范围条件,如果orders表有created_at的单列索引,理论上是能走range扫描的。但这个SQL里还有个u.nickname LIKE '%小明%',它作用在被驱动表users上,而且使用了前导通配符"%"。

注意,MySQL的优化器在选择驱动表时,会估算各种连接顺序的成本。由于LIKE '%小明%'无法走索引,users表被认定为"必须全表扫",而orders表又是LEFT JOIN的被驱动侧,优化器最终选择了先全表扫orders,再去匹配users。结果两张大表都成了ALL。

3.3 第三步:先解决主表扫描,让created_at索引生效

定位到问题后,第一步优化很明确:先让orders表尽量缩小扫描范围。检查发现orders表确实没有created_at的单列索引,于是先补上一个:

ALTER TABLE orders ADD INDEX idx_created_at (created_at);

再次EXPLAIN,orders表的type从ALL变成了range,rows从500万下降到40多万,Extra里的Using where还在,但Using filesort依旧存在。这个改善很大,但还远远不够——40万行依然不少,而且排序的问题没解决。

这里要提醒一点:给表加索引不是随便加的,要考虑现有索引是否已经覆盖了查询场景。如果orders表已经有一个包含created_at的联合索引,就不需要另建单列索引,避免索引冗余。

3.4 第四步:把LIKE的坑和JOIN方向一起处理

再看users表的全表扫描。u.nickname LIKE '%小明%'是前导通配符模糊匹配,B+Tree索引是按前缀匹配的,前导通配符会导致索引失效。这一点要记住:LIKE以%开头,索引必然用不上。

按照当时的业务需求,运营要求的是"昵称包含关键词",不能简单改成前缀匹配。可选项有几个:一是改成前缀匹配LIKE '小明%',但会损失需求;二是在users表引入冗余的搜索字段,配合全文索引(MySQL 5.7+支持中文ngram全文解析器)来处理模糊搜索;三是如果允许改表结构,干脆把用户昵称相关的搜索条件拆出来独立处理。

我们最终选了全文索引方案,在nickname字段上建了全文索引,把LIKE条件改写为MATCH...AGAINST。这一步之后,users表的type从ALL提升为fulltext,性能有了质的改善。如果你遇到类似问题,但数据量不大、并发不高,也可以考虑用前面加索引加前缀匹配的方式做取舍,不必上全文索引。

3.5 第五步:消除filesort,重新设计联合索引

最后剩下的痛点是Using filesort。ORDER BY o.total_amount DESC要求按金额倒序,而orders表目前没有任何索引能让created_at过滤和total_amount排序同时生效。

这里要用到联合索引的设计思路。联合索引遵循最左前缀原则,我们要让"过滤条件"和"排序条件"命中同一个索引。当前查询对orders的过滤条件是created_at范围,排序条件是total_amount,联合索引可以设计成:

ALTER TABLE orders ADD INDEX idx_created_amount (created_at, total_amount);

注意一个细节:范围查询之后,联合索引后面字段的有序性会被破坏。也就是说idx_created_amount中created_at用于range过滤,total_amount在范围过滤后不一定能保证全局有序,MySQL 5.7及以下对这种情况通常还是会文件排序。真正稳妥的做法,是把过滤条件做成等值,再让排序字段接力。

所以我把SQL改写了一下:先把最近30天的时间范围细化为具体的日期分组,让created_at尽量用等值条件匹配,再按total_amount排序。对业务影响不大,但执行计划立刻就变了。加上MySQL 8.0支持降序索引,ORDER BY total_amount DESC可以设定索引字段为降序存储,进一步压榨执行效率。

3.6 优化后的执行计划对比

经过几轮调整,最终SQL的执行计划是这样的:

表typekeyrowsExtra
ordersrangeidx_created_amount4185Using index condition
userseq_refPRIMARY1NULL

和最初的执行计划放在一起看,差异非常直观:

项目优化前优化后
orders访问类型ALLrange
users访问类型ALLeq_ref
预估扫描行数500万+100万约4200行
Extra排序Using filesort无
线上真实耗时12.8秒约80毫秒

不要只看rows从600万变成4000多这个数字,执行时间的差距更能说明问题。扫描行数差了三个数量级,文件排序又消失,性能自然就回来了。

3.7 进阶验证:EXPLAIN ANALYZE看真实执行

MySQL 8.0.18之后提供了一个更强的工具:EXPLAIN ANALYZE。它不只是输出执行计划,还真正执行SQL,并返回每一步的实际耗时、实际行数、循环次数,用了pretty tree格式展示。

比如对优化后的SQL执行EXPLAIN ANALYZE,输出会包含类似这样的信息:

-> Limit: 30 row(s) (actual time=76.8..78.9 rows=30 loops=1) -> Nested loop inner join (actual time=0.2..77.4 rows=42 loops=1) -> Index range scan on orders using idx_created_amount (actual time=0.1..2.1 rows=4200 loops=1) -> Single-row index lookup on users using PRIMARY (actual time=0.00..0.01 rows=1 loops=4200)

这比普通EXPLAIN直接把预估rows替换成了actual rows,一眼就能看出真实扫描了多少行、每一步耗时多少。我在优化慢SQL的后期,基本都会用它做最终验证,避免被预估误差带偏。

4. 当Explain显示“一切正常”,问题可能藏在哪

Explain是慢SQL排查的利器,但它不是万能的。我见过不少场景,Explain输出结果漂漂亮亮,SQL却照样慢。如果只看纸面执行计划就下结论,很容易被表象骗过去。这节就把我踩过的、Explain容易漏掉的盲区单独拿出来说。

4.1 统计信息过期:预估漂亮,实际拉胯

前面提过rows是预估,那它依据什么预估?答案是统计信息。InnoDB的表统计信息不是每次查询实时统计的,而是基于采样计算,维护在一张内部字典表里。如果一张表的数据分布发生重大变化,比如大量删行、批量插入、字段值分布剧变,统计信息却迟迟不更新,优化器就会拿着过时地图做决策。

我自己遇到过一次:一张订单明细表,正常情况下Explain走索引rows只有几百,某天突然变成全表扫描,而且性能雪崩。查了一圈发现表因为数据清理脚本,在半小时内删掉了90%的数据,optimizer统计信息还以为是原来的1000万行。执行ANALYZE TABLE之后,执行计划立刻恢复正常。

所以在Explain结果和实际性能明显不符时,先别怀疑数据库,很有可能是个统计信息过期的问题。定期对频繁变更的大表执行ANALYZE TABLE,或者开启MySQL的自动统计信息更新策略,是运维侧值得做的事。

4.2 锁等待和并发冲突:执行计划再好也救不了阻塞

Explain完全不关心锁。两条SQL的执行计划再完美,如果第三条事务拿着行锁不放,前面两条照样排队等锁。你看到的现象是SQL执行时间超长,但Explain显示type=ref、rows=10,好像一点问题都没有。

这种场景下,直接去看锁等待比看执行计划有用。查询information_schema.innodb_trx、performance_schema.data_lock_waits,或者使用SHOW ENGINE INNODB STATUS查看事务和锁的情况,能快速定位谁阻塞了谁。我处理过最典型的案例是:一个批量更新任务事务长时间不提交,把整个表的一些行锁住了,业务侧所有涉及这些行的UPDATE都卡死。Explain执行计划没变,但后端连接池被占满,系统整体假死。这类问题属于并发设计缺陷,不是索引能解决的。

4.3 深分页和LIMIT的隐藏代价

Explain对LIMIT分页的支持很有限。当你执行LIMIT 1000000, 30这类深分页查询时,Extra和type可能看起来很正常,type=range,rows预估也合适,但真实执行需要先扫描100万行,再丢掉前100万行,最后返回30行。这个"丢掉"的动作在Explain里是看不见的。

深分页的优化手段通常是延迟关联:先查出主键ID,再与原表JOIN返回完整数据。比如:

SELECT o.*, u.nickname FROM orders o INNER JOIN ( SELECT id FROM orders WHERE created_at >= '2024-06-01' ORDER BY total_amount DESC LIMIT 1000000, 30 ) tmp ON o.id = tmp.id LEFT JOIN users u ON o.user_id = u.id;

这种方式让子查询在索引上完成排序和分页,代价小得多。如果业务上允许,更推荐使用游标分页(keyset pagination),基于上一页最后一条记录的ID作为下一页起点,效率是质的飞跃。

4.4 其他数据库的"Explain"怎么用

很多人是从MySQL入门执行计划的,实际工作中可能会碰到其他数据库。概念是相通的,但命令和字段不同:

  • SQL Server:SET SHOWPLAN_ALL ON或SET STATISTICS PROFILE ON,输出结果集里能看到StmtText列里的执行计划细节。图形化客户端的"显示估计的执行计划"更直观。
  • Oracle:先EXPLAIN PLAN FOR你的SQL,然后用SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY)查看。生产环境更常用的是从共享池里取真实执行计划,用DBMS_XPLAN.DISPLAY_CURSOR还能看到实际执行统计信息。
  • PostgreSQL:EXPLAIN和EXPLAIN ANALYZE都支持,ANALYZE会真实执行SQL并输出实际行数和耗时,这一点和MySQL 8.0的EXPLAIN ANALYZE是同一个思路。

跨数据库去理解执行计划,核心不是记字段名,而是理解那四个问题:访问顺序是什么、每张表怎么访问、预估扫多少行、额外动作有哪些。只要抓住了这几个维度,换到哪个数据库都能快速上手。

4.5 中和一下:Explain应该放在排查链路的哪一步

总结这么多,我给一个比较务实的建议:Explain适用于80%的慢SQL分析场景,但它应该是排查链路的一个环节,而不是全部。

我的习惯是:拿到一条慢SQL,先看慢查询日志里的实际执行时间和扫描行数,心里有个底;然后Explain看执行计划,判断是访问类型问题、索引问题还是排序临时表问题;有疑问再用EXPLAIN ANALYZE或同类工具确认真实执行情况;最后如果所有执行计划都很正常,才回头去查锁、事务、统计信息、连接池这些执行计划之外的变量。只有把这几步连起来,才能从"SQL慢"定位到"系统慢",而不是看了个Explain就说优化完成。

最后再分享一个实战经验:我会把优化过的每条慢SQL的优化前Explain结果、优化后Explain结果、真实执行时间,整理成一个固定的对比模板,扔到项目文档里。一两个月后回头看,你会发现很多SQL的坑其实是重复的——类型转换导致索引失效、ORDER BY字段没进索引、深分页扫了太多行,翻来覆去就那么几类。把这些典型案例沉淀下来,后续团队审代码或者处理线上问题,效率会高很多。这比每次都从头拆Explain有意义得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 13:22:26

主动降噪(ANC)从原理到实战:降噪耳机如何凭空消声

先说点实在的。ANC这三个字母&#xff0c;这几年在耳机圈、手机圈几乎天天见&#xff0c;但你要是去问十个买了降噪耳机的人&#xff0c;至少有六七个其实说不清它到底是怎么把地铁轰隆隆的噪声变没的&#xff0c;更别说自己做一套可用的降噪方案了。我在这个方向断续折腾了两三…

作者头像 李华
网站建设 2026/10/6 13:21:22

对话墨子:用兼爱非攻与三表法构建AI伦理审查框架

话不多说&#xff0c;先把这个标题拆开&#xff1a; “No135: AI中国故事-对话墨子——兼爱非攻与AI伦理&#xff1a;平等主义、实用主义与技术中立” 。第一眼看到这个题目&#xff0c;我以为是又一篇蹭国潮热点的泛泛之谈&#xff0c;但真正把这个对话做下来之后&#xff0…

作者头像 李华
网站建设 2026/10/6 13:21:06

再生龙Clonezilla镜像还原实战:从启动盘到系统恢复

前阵子有个朋友打电话来&#xff0c;说公司一台存程序的服务器系统起不来了&#xff0c;里面有一堆部署好的服务和配置&#xff0c;问我该怎么办。我第一句话是&#xff1a;“你有没有做过系统镜像备份&#xff1f;”电话那头沉默了十几秒。最后我们跑了一趟机房&#xff0c;拆…

作者头像 李华
网站建设 2026/10/6 13:20:16

学成在线PSD素材切图全流程:从量尺寸到页面落地

简介&#xff1a;这是面向网页设计初学者与前端开发者的“学成在线”页面实现素材&#xff0c;源自黑马程序员及学成在线案例。整套资源将设计稿与前端代码配套呈现&#xff0c;既能用于练习 HTML/CSS 页面还原&#xff0c;也可作为课程实训或个人作品集的参考素材&#xff0c;…

作者头像 李华
网站建设 2026/10/6 13:19:44

MySQL Windows exe安装包详解:从下载配置到排错实战

1. 为什么最终我还是选了exe安装包&#xff0c;而不是zip免安装版早些年折腾MySQL&#xff0c;我一度是个zip免安装版的死忠。原因很简单&#xff0c;从官网下个压缩包&#xff0c;解压到本地&#xff0c;手动初始化data目录&#xff0c;再手工注册Windows服务&#xff0c;整个…

作者头像 李华
网站建设 2026/10/6 13:18:58

基于Python的房价数据分析与可视化大屏毕设实战

如果你正在为大数据方向的毕设选题发愁&#xff0c;我强烈建议你认真考虑一下“房价数据分析及可视化”这个方向。去年我把这个项目作为自己的毕业设计&#xff0c;从选题、采集数据、清洗、分析到最终做出可视化大屏&#xff0c;整个过程踩了不少坑&#xff0c;也积累了一套能…

作者头像 李华