数据可视化基础:从MySQL到ECharts的完整数据链路
发布时间:2026/9/30 12:08:13来源:尧图网络
做了几个完整的数据可视化项目之后我慢慢发现一个规律可视化项目最后翻车大多数不是图表画得不好看而是数据本身就站不住脚。图表颜色可以调布局可以改但数据口径混乱、字段值千奇百怪、缺的缺失的失——这些才是真正让人头疼的地方。数据可视化基础说白了就一句话把MySQL这类关系型数据库里存储的结构化数据通过清洗和加工用图表把数据内在规律呈现出来。所以这篇文章的重点放在“数据可视化基础与MySQL的关系”上聊聊我从存储到展示这条链路上的实操经验——MySQL在可视化项目里到底承担什么角色、数据准备与清洗应该怎么做、pandas怎么和MySQL衔接、Flask加ECharts怎么把数据变成能看的图表以及这中间最容易踩的几个坑。如果你正准备做可视化项目或者正在被脏数据折磨这篇应该能帮你少走不少弯路。1. MySQL负责“存”可视化负责“读”这层关系别搞反了1.1 结构化数据的本质先定规则再装数据在动手写SQL之前我觉得有必要先把MySQL和结构化数据的关系讲清楚。MySQL是关系型数据库它的核心不是“能存多少数据”而是“数据必须按照约定好的结构来存”。建表的时候先定义字段类型和约束比如价格字段用DECIMAL(10,2)那它就只能存两位小数日期字段用DATE那它就必须是标准日期格式。这就是结构化数据和散装数据的区别——Excel里你可以随便写同一列里既有数字又有文本也不会报警但MySQL不允许这种混乱。但现实比理想残酷得多。我实际接手的项目里MySQL表里存“非结构化”内容的例子太常见了日期字段里存了“2023年8月1日”价格字段里存了“约12.5元/斤”手机号字段里混着固话号码。所以“结构化数据”只是起点数据准备与清洗才是决定数据能不能被可视化正确解读的关键。一张表建得再规范只要数据录入端不可控图表就一定会有问题。1.2 从存储到展示整条链路可以拆成五段我把一个可视化项目的完整流程拆成这样业务数据产生和采集订单、行情、日志等持续写入MySQL数据准备与清洗在MySQL或pandas里修正格式、去重、补缺、过滤异常值聚合加工把明细数据变成报表口径的统计结果后端接口把结果暴露成JSON供前端调用前端图表ECharts等工具拉取数据并完成渲染这里最关键的一点每一段都有自己的职责别让前端去承担清洗或者统计。我见过太多人图省事把明细数据一股脑返回给前端让JavaScript去算平均值、过滤异常值。短期看能跑等数据量上来、统计口径要调整的时候你会发现前端代码改得想骂人。而MySQL侧多写一条SQL就能解决的问题没必要扔给浏览器。1.3 为什么可视化项目的底座通常是MySQL而不是Excel有个问题经常被问到数据可视化为什么非得配MySQL用Excel/CSV不行吗我的回答是如果你只是画两三张临时图表Excel完全够但稍微正式一点的可视化项目有三件事Excel做不好。第一数据量。Excel到几万行就开始卡MySQL用索引配合聚合查询千万级数据也能做到秒级返回。第二并发和增量。业务系统在持续写入Excel没法保证多人同时读写时数据一致CSV更是连个事务都没有。第三数据口径统一。MySQL可以用视图和存储过程把清洗逻辑固定下来所有人都从同一张“净化后的表”取数Excel副本一多口径必然分叉。所以数据可视化基础里最重要的第一步其实是把MySQL这个数据源管好。2. 数据准备与清洗质量问题的根子大多在MySQL里就能解决2.1 清洗前先定标准数据质量就五大类我给自己定过一个检查维度表接到任何可视化需求先拿这张表去对照核心表基本能发现八成问题维度典型表现对可视化的影响完整性字段为空、缺少关键记录图表缺数据、折线断点唯一性同一业务记录重复计数虚高、柱状图失真准确性数值溢出、格式混乱、乱码走势图出现离谱尖峰一致性单位/口径不统一图表结论互相矛盾时效性过期数据、未来日期混入分析结论失真很多人拿到数据就急着画图结果折线图上突然冒出一个尖到天上的峰值那就是异常值没处理饼图所有扇区加起来超过100%那是重复记录没去重。这些问题如果不回到MySQL这一层去治理后面无论用多漂亮的图表工具都是在垃圾上做装修。2.2 MySQL里可以直接做的四种清洗动作MySQL本身就是一个很强悍的数据清洗工具很多操作根本不用等数据导出来再处理。我常用的四类清洗SQL给你列一下。第一去重。重复订单、重复采集记录最常见。下面这句SQL是按商品和市场分组保留report_date最新的那条DELETE t1 FROM price_records t1 INNER JOIN price_records t2 ON t1.product_name t2.product_name AND t1.market t2.market WHERE t1.report_date t2.report_date;注意这种写法走的是自连接数据量大之前一定先在关联字段上建好索引否则DELETE能把表锁到怀疑人生。第二空值。要么直接UPDATE填充要么查询时用COALESCE兜底UPDATE price_records SET price 0 WHERE price IS NULL; SELECT product_name, COALESCE(price, 0) AS price FROM price_records;第三类型修正。把字符串日期统一成DATE类型这是可视化项目里最常见的动作。用STR_TO_DATE指定输入格式UPDATE price_records SET report_date STR_TO_DATE(report_date, %Y-%m-%d) WHERE report_date IS NOT NULL AND STR_TO_DATE(report_date, %Y-%m-%d) IS NOT NULL;如果你不确定字段里有多少种日期格式可以先跑一句探查SQLSELECT report_date, COUNT(*) FROM price_records GROUP BY report_date ORDER BY COUNT(*) DESC;把枚举值全看一遍再决定用哪种格式去转这个习惯能帮你避免大量返工。第四异常值过滤。数值字段先看MAX/MIN再用WHERE把不合理的区间过滤掉SELECT * FROM price_records WHERE price 0 AND price 1000;负价格、几千元一斤的“天价”数据基本都来自录入错误这种异常值不处理图表X轴Y轴都会被迫拉出很丑的间距。2.3 用视图把清洗结果固化一份逻辑全体复用最让我受不了的一种做法是每个报表SQL里都把清洗条件重写一遍。日期转换、空值过滤、单位换算五个报表写五次后面想改口径得一个个找。我的做法是建“可视化专用视图”把清洗逻辑彻底固化下来。CREATE OR REPLACE VIEW v_price_clean AS SELECT product_name, market, STR_TO_DATE(report_date, %Y-%m-%d) AS report_date, CAST(REPLACE(price, 元/斤, ) AS DECIMAL(10,2)) AS price FROM price_records WHERE report_date IS NOT NULL AND price IS NOT NULL AND price AND STR_TO_DATE(report_date, %Y-%m-%d) IS NOT NULL;以后所有可视化查询都只对v_price_clean这个视图操作业务表再怎么脏视图这一层已经把脏数据挡在外面。这样做还有个额外好处业务字段改名时只需要改视图不用动报表SQL。视图不是物理表大数据量场景性能会有损耗这个问题后面第5章会专门讲。2.4 案例复盘农产品价格数据从混乱到规整结合我自己做过的农产品价格可视化项目来说。原始数据来自几个批发市场的每日价格采集表字段是product_name、market、report_date、price看似正常实际脏得一塌糊涂。价格字段有三种形态“3.5元/斤”“12500元/吨”“约2元/公斤”。日期字段混着“2023-08-01”“2023/08/01”“2023年8月1日”。市场名“新发地”和“北京新发地”同时存在。这种数据直接进图表折线图会平白多出好几个尖峰。处理思路是先在MySQL里把单位和市场名归一化UPDATE price_records SET price CASE WHEN price LIKE %元/公斤 THEN CAST(REPLACE(price, 元/公斤, ) AS DECIMAL(10,2)) / 2 WHEN price LIKE %元/斤 THEN CAST(REPLACE(price, 元/斤, ) AS DECIMAL(10,2)) WHEN price LIKE %元/吨 THEN CAST(REPLACE(price, 元/吨, ) AS DECIMAL(10,2)) / 2000 ELSE CAST(price AS DECIMAL(10,2)) END; UPDATE price_records SET market 新发地 WHERE market IN (新发地, 北京新发地);这两个UPDATE跑完再配合去重和异常值过滤清洗后的数据就能进视图供后面Flask和ECharts使用。整个清洗过程不用写一行PythonMySQL全搞定。这就是我强调“在MySQL里就要先动手”的原因。3. pandas接力MySQL数据接出来后还要再做一轮精细活3.1 连接MySQL的三种常见方式选哪种更合适MySQL侧的清洗做完数据依然不一定能直接画图。有些清洗在SQL里写起来非常痛苦比如正则提取、跨行计算、按业务规则修正单个字段值这时候我会把数据接出来交给pandas。先看三种连接方式方式适合场景优点缺点pymysql一次性脚本、小工具轻量直接用原生SQL要手动管理连接SQLAlchemy create_enginepandas read_sql、常用查询连接复用、与pandas无缝多一层封装DBUtils连接池Flask接口频繁取数连接复用高并发稳定配置稍复杂日常做数据分析我基本用SQLAlchemy一行代码就能把MySQL表读成DataFramefrom sqlalchemy import create_engine import pandas as pd engine create_engine( mysqlpymysql://root:password127.0.0.1:3306/mydb?charsetutf8mb4 ) df pd.read_sql(SELECT * FROM v_price_clean, engine)注意连接串里必须带charsetutf8mb4否则中文大概率乱码后面第5章我会专门说这个问题。3.2 清洗六件套类型、重复、空值、异常一步都不能省pandas里的清洗动作我习惯叫“清洗六件套”每一步都有明确意图。先做类型修正。MySQL里存的字段类型和pandas推出来的类型经常对不上日期字段尤其明显df[report_date] pd.to_datetime(df[report_date]) df[price] pd.to_numeric(df[price], errorscoerce)然后去重。按业务主键去重不一定是表主键df df.drop_duplicates( subset[product_name, market, report_date], keeplast )接着处理空值。关键业务字段直接drop数值字段用fillna补一个业务上合理的值df df.dropna(subset[product_name]) df[price] df[price].fillna(0)最后过滤异常值。这就是SQL里不好写、但pandas里一行搞定的事df df[(df[price] 0) (df[price] 1000)]为什么很多场景要拿到pandas里做而不是全在MySQL里做因为pandas是内存计算逻辑写起来自由得多尤其当你需要结合业务规则做判断、写循环、或者对接一个复杂的Excel规则表时pandas的灵活性是SQL没法比的。我的分工原则是能用SQL快速解决的粗清洗留在MySQL需要复杂逻辑的精清洗交给pandas。3.3 图表要什么数据形状就聚合到什么程度清洗完明细数据下一步是聚合。很多人容易忽略ECharts并不是直接展示明细数据它要的是“刚好够画图”的形状。折线图要时间数值比如近一年每个月的平均价格柱状图要分类数值比如各市场月均价格对比饼图要分类占比比如各品类销售占比热力图/地图要区域数值比如各省订单量分布用groupby做聚合是常规操作monthly ( df.groupby(df[report_date].dt.to_period(M))[price] .mean() .reset_index() ) monthly[report_date] monthly[report_date].astype(str)这里把Period对象转成字符串是因为JSON序列化不支持Period类型转成“2024-01”这样的字符串才能在接口里正常返回。做多维度对照时用pivot_table更直观pivot df.pivot_table( indexreport_date, columnsmarket, valuesprice, aggfuncmean )聚合后的数据要么以CSV/Parquet落地要么写回MySQL给报表系统用。我的习惯是中间结果用CSV临时保存最终给业务系统用的落回MySQL。4. 从MySQL到图表Flask ECharts 的完整落地过程4.1 架构选择Flask做接口层ECharts做展示层清洗和聚合做完接下来就是把数据送到前端。我做过最简单的方案也做过复杂的企业级方案但中小型可视化项目里Flask ECharts依然是最顺手的组合。Flask负责做接口层不负责渲染页面ECharts负责在前端消费JSON并画图。为什么要单独接口层因为数据要复用。同一个价格走势接口今天放在大屏上明天可能要加到企业报表里后天移动端也要调。接口单独拆出来所有端共用这才叫“从存储到展示的完整流程”。4.2 Flask端把清洗后的MySQL数据变成JSONFlask接口写起来非常轻。我通常用SQLAlchemy引擎在接口函数里读数据、做必要的格式处理、返回JSONfrom flask import Flask, jsonify from sqlalchemy import create_engine import pandas as pd app Flask(__name__) engine create_engine( mysqlpymysql://root:password127.0.0.1:3306/mydb?charsetutf8mb4 ) app.route(/api/price/trend) def price_trend(): df pd.read_sql( SELECT report_date, AVG(price) AS avg_price FROM v_price_clean WHERE report_date DATE_SUB(CURDATE(), INTERVAL 1 YEAR) GROUP BY report_date ORDER BY report_date , engine ) df[report_date] df[report_date].astype(str) return jsonify(df.to_dict(orientrecords))注意df.to_dict(orientrecords)出来的结构是数组套对象正好是ECharts最喜欢的格式。日期字段在返回前统一转成字符串避免JSON序列化时出现“2024-01-01T00:00:00”这种带T的格式省得前端再处理。4.3 ECharts端消费JSON数据并完成渲染前端这块儿先引ECharts的CDN再fetch接口最后setOption。一个典型的价格走势页面长这样!DOCTYPE html html head meta charsetutf-8 title农产品价格趋势/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script /head body div idchart stylewidth: 800px; height: 500px;/div script fetch(/api/price/trend) .then(function(response) { return response.json(); }) .then(function(data) { var chart echarts.init(document.getElementById(chart)); chart.setOption({ title: { text: 近一年农产品均价走势 }, tooltip: { trigger: axis }, xAxis: { type: category, data: data.map(function(item) { return item.report_date; }) }, yAxis: { type: value, name: 元/斤 }, series: [{ name: 均价, type: line, data: data.map(function(item) { return item.avg_price; }) }] }); }); /script /body /html这套逻辑的核心就是后端把数据清洗好、聚合好、以友好结构输出前端只做映射。如果后端返回的数据对不上前端改起来非常别扭。我就遇到过后端把日期返回成嵌套对象前端拿到data.map后全是[object Object]页面一片白板。所以前后端数据契约一定要简单宁可多几个字段也不要嵌套层次太深。4.4 大数据量场景离线清洗回写的架构更稳上面这套FlaskECharts适合中小项目但订单量到千万级以后直接让Flask去查原始MySQL明细表做实时聚合响应时间很容易超过3秒大屏就会卡。我参与网约车大数据可视化项目时采用的是一套“离线清洗回写”的架构原始订单明细先交给Spark/MapReduce做清洗和聚合把结果回写到MySQL的报表表里Flask只查这些已经聚合好的报表表ECharts再展示。链路变成MySQL业务库 → Spark/MapReduce清洗聚合 → 结果回写MySQL报表表 → Flask接口 → ECharts大屏这样做的核心逻辑是能用离线框架批量跑的聚合就不要让在线接口实时算。MySQL里只放结果表行数从几千万降到几千行接口自然就快了。如果你也在做这类项目记住这个原则明细归离线结果归在线。5. MySQL侧最容易踩的坑和我的排错记录5.1 安装与连接e0434352、SSL错误、初始密码先说安装。Windows下用官方安装器装MySQL 8有时候会报e0434352这个错误多半是.NET Framework相关组件导致的。我的建议是绕开安装器直接用zip包解压部署解压后执行mysqld --initialize-insecure初始化再启动服务。虽然少了一点图形化引导但过程清晰出问题也好排查。再说连接。Python连MySQL 8经常遇到SSL相关报错比如“SSL connection error”或者证书协商失败。开发环境可以显式关闭SSLengine create_engine( mysqlpymysql://root:password127.0.0.1:3306/mydb?charsetutf8mb4ssl_disabledTrue )服务端如果开启了强制SSL那就得在MySQL配置里确认对应的ssl参数两边对齐。还有初始密码。Linux上装完MySQL初始密码一般在日志里grep temporary password /var/log/mysqld.log拿到初始密码再ALTER USER rootlocalhost IDENTIFIED BY 新密码。5.2 默认值0与sql_mode小问题能卡一整天MySQL里给字段设置默认值为0有时候会报ERROR 1067 (42000): Invalid default value这个问题非常隐蔽。原因多半是当前sql_mode里带了NO_ZERO_DATE选项MySQL不允许默认值中出现“0000-00-00”这样的零日期。解决方式有两种要么临时放宽SET SESSION sql_mode ALLOW_INVALID_DATES;要么在my.cnf里把sql_mode里NO_ZERO_DATE剔除然后重启服务。生产环境改sql_mode要谨慎最好先在测试库验证。我见过有人因为这个报错绕了一大圈去改字段类型最后才发现只是sql_mode的问题。5.3 中文乱码按链路逐层查做可视化项目最怕的不是SQL写错而是页面出来之后中文变成“???”。乱码问题要从四个层面排查。数据库连接层连接串必须带charsetutf8mb4pymysql/SQLAlchemy都一样。数据库与表结构层面建库建表时显式声明CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;接口返回层Flask那边flask的JSON响应默认是UTF-8但pandas的to_json方法默认会把中文转成ASCII这时候要设置force_asciiFalsejson_str df.to_json(orientrecords, force_asciiFalse)前端页面层HTML文件的meta标签里要有meta charsetutf-8四层全对中文基本就没问题。哪一层断了乱码就出现在哪里。5.4 完整排错记录图表空白不一定是前端问题最后分享一次真实的排错过程。网约车可视化项目里折线图突然有一天完全不显示数据从后端到前端查了一遍问题出在MySQL的一行SQL上。第一步看接口返回。打开浏览器Network发现接口返回的是空数组[]说明问题在后端不是前端。第二步在MySQL里手动执行接口同款的SQL结果却有一条数据。那为什么接口返回空对比后发现问题出在一个隐藏的条件接口SQL里写了order_time 2024-01-01而库里的order_time是字符串存的是“2024/01/01 10:00:00”。字符串比较时“2024/01/01”和“2024-01-01”没法正确比较条件永远为false。第三步用STR_TO_DATE把字符串日期转成标准时间再比较接口立刻恢复WHERE STR_TO_DATE(order_time, %Y/%m/%d %H:%i:%s) 2024-01-01第四步顺带用EXPLAIN检查了执行计划发现这条查询因为格式转换做不了索引走了全表扫描。后来在原始字段的表达式上没法建索引就改成了在数据入库时先统一格式再在order_time字段上建普通索引问题才算根治。这个案例给我最大教训是接口返回空不代表“没有数据”很可能是“SQL条件把数据全过滤了”。排查顺序永远是——数据有没有到接口 → SQL查不查得出 → 字段里到底是什么值。最后再分享一个我自己的习惯接手任何一个可视化项目我不会急着搭前端而是先花半天时间在MySQL里做“数据体检”——查看每个必填字段的空值率、枚举值的分布、时间字段的最早最晚日期、数值字段的极值。这几个探查SQL写完整张表的质量问题基本就清楚了。很多同事问我为什么画图这么快其实不是画图快是数据已经被我摸透了。数据准备与清洗这件事看着不起眼却是从MySQL到可视化这条链路里最值得花时间的环节。
网站建设高端定制企业官网