资讯动态

列式存储如何让大数据可视化大屏秒开?——ClickHouse与ECharts实战

发布时间:2026/9/13 14:32:21 来源:尧图企业网站定制
做数据可视化最怕什么不是ECharts配置写不明白也不是大屏配色辣眼睛而是你一切准备就绪后端接口硬生生卡了十几秒才返回数据。我印象最深的一次帮朋友调一张区域销售大屏总共几亿行订单数据前端调三个聚合接口MySQL直接跑了几十秒才出结果大屏打开就在那转圈用户当场就没了耐心。后来我把存储层换成了列式存储引擎同样三个查询压到了两三百毫秒整张大屏基本秒开。这个转变让我对列式存储有了实打实的体会。市面上讲列式存储的文档很多但多数停留在“列存适合OLAP”这种概念层面。很少有人专门讲清楚列式存储到底是怎么让大数据可视化变快的在可视化场景里应该怎么选型、怎么建表、怎么调优前端ECharts怎么配合才能榨干列式存储的性能这篇文章我想用自己做过的项目为例把这些东西一次性讲透。先说适用范围。如果你正在做数据可视化大屏或者想搭一套免费好用的可视化报表平台又或者你的业务数据量已经到了MySQL、PostgreSQL撑不住聚合查询的程度那你适合往下看。就算你目前数据量才几百万行只要学会列式存储的核心思路也能提前把架构设计好省得以后数据一涨就抓瞎。1. 列式存储到底是什么先把底层基本功打牢1.1 行式存储与列式存储的本质差异传统关系型数据库比如MySQL、PostgreSQL、SQL Server默认采用行式存储。行式存储的逻辑特别像一本通史书每一行记录里所有的字段值都连续地存放在一起。你要找某个人买过哪些商品直接翻到对应的那行就行这是典型的交易型场景。但它也有个很麻烦的地方如果你想统计全表十亿行数据的销售额哪怕只需要销售额这一个字段也得把十亿行的所有字段全部读一遍才能拿到那一列的数值。这就像你去图书馆找一个班级所有学生的“手机号”信息。行式存储的做法是把全班五十个学生的档案袋全搬出来然后挨个拆开从每个档案袋里抽出一张写着手机号的纸最后再把档案袋放回去。光是拆装就折腾半天。列式存储的做法完全不一样班里每个学生的手机号早就在同一张表上按顺序排好了你直接把那张表抽出来看一眼就行其余什么身高、体重、家庭住址全部不用动。列式存储的核心思想就是把同一列的数据物理上连续存放。整个表的数据不是按“一行”组织结构而是按“一列”组织。每一列单独存储单独压缩查询的时候只需要读取你真正关心的那几列。1.2 列式存储为什么天然适配可视化查询大数据可视化这个场景对存储的诉求和电商交易完全不是一个路数。大屏上常见的图表无外乎折线图、柱状图、地图、排行榜这些图表背后对应的SQL都是典型的分析型查询。举个例子销售大屏上最常见的查询是SELECT region, SUM(amount), COUNT(*) FROM sales_order WHERE order_date BETWEEN 2024-01-01 AND 2024-12-31 GROUP BY region ORDER BY SUM(amount) DESC这条SQL本质上只关心三样东西区域字段、金额字段、下单日期字段。如果这张sales_order表里有五十个字段行式存储会把每一行的五十个字段全读出来然后才从中抽取这三个字段做聚合。列式存储则只读取region、amount、order_date这三列其余四十七列连碰都不碰。就这一步磁盘IO差距就是十到二十倍。更关键的是压缩率。同一列的数据类型一致而且往往重复值很多比如region字段就那几个省份城市order_date字段在时间维上有天然的连续性。列式存储可以利用这种局部性用各种编码算法把数据压缩到很小。数据压小了从磁盘读上来的字节数就少了网络传输给前端的量也跟着少。IO变少、传输变快可视化自然就快。还有一点很多人忽略列式存储基本都会配合向量化执行或者批处理技术。传统数据库一条条记录处理列式引擎一次处理一批数据而且不需要频繁解析复杂的事务日志。这对大屏上那种高并发、多图表同时请求的场景特别友好。大屏一打开可能就是十个图表一起发请求如果你底层还是那种传统OLTP数据库这种分析型负载很容易把CPU和IO打满。1.3 常见的列式存储方案盘点我平时实际用下来列式存储方案主要分这么几类列式文件格式Parquet、ORC、Arrow这些不是数据库而是存储文件的数据格式。它们可以配合Spark、Trino、DuckDB等引擎去读。大数据平台里最常用。列式数据库ClickHouse、Apache Doris、TiDB的TiFlash列存引擎等。这类数据库直接把列式存储和分析能力收进一个系统里你拿SQL查就行。嵌入式分析引擎DuckDB。它本质上是个进程内数据库底层也是列式存储非常适合本地分析、快速试验配合Parquet文件特别爽。选型的时候不要迷信“谁高大上选谁”要看你数据量和场景复杂度。数据量几百GB以下DuckDB加Parquet完全够用部署成本低到可以忽略数据量到了TB级还想长期跑报表大屏ClickHouse更合适如果你整个数据湖都用HDFS或S3存Parquet那用Trino或SparkSQL统一查询就好。不管前端用ECharts也好用Grafana、Superset也好其实它们都不关心你的底层是什么数据库。前端只要拿到聚合后的JSON数据就能画图。所以核心问题从来就是怎么让后端聚合计算更快。列式存储在这里做的就是把最重的IO和聚合压力扛下来。2. 大数据可视化卡顿的根源对症才知道下什么药2.1 卡顿的主要表现可视化大屏卡顿这一块用户最容易感知到的无非几种情况首次加载大屏时整体白屏时间过长数据一直不出现。点击筛选器、切换时间范围后图表等待时间太久。点某个柱子想下钻到明细结果请求没响应。多个图表同时刷新数据库CPU跑到100%新请求全部超时。这些现象表面看是前端渲染问题但绝大多数根因都在后端。ECharts渲染几万个点通常只需要几十毫秒真正慢的是后端SQL。你可以开一下浏览器的Network面板如果某个接口耗时超过两秒那问题基本可以定性为查询层太慢。2.2 为什么传统SQL在可视化场景下经常“不给力”有人会问我的MySQL配置也不差加了索引之后查询好像挺快为什么一做大屏就拉胯这就要说到OLTP和OLAP的差别了。MySQL是为交易设计的要处理高并发的插入、更新、删除还要保证事务一致性。它内部有事务日志、锁管理、MVCC等一堆机制这些机制在处理点查询时是优势但在处理大范围聚合扫描时全是负担。一个大屏报表SQL比如按天统计全量订单金额动辄要扫描几千万行到几亿行。就算你给日期字段加了索引MySQL也只能用索引定位到某个范围之后还得回表把所有需要的列取出来再做group by。一旦遇到多表join、子查询优化器选错执行计划的概率也不低最后就是长时间全表扫描。我自己踩过最坑的一次一张订单表有六亿行MySQL单表存不下业务方分成了六十张表。大屏每次查询都要union all六十张表再聚合简直噩梦。后来迁移到ClickHouse单表六亿行改了分区和排序键查询从几十秒掉到一秒钟以内。这个差距不是硬件瓶颈而是存储和引擎设计上的代数级差距。2.3 可视化数据库瓶颈的评估维度做可视化性能优化不能只凭感觉。我一般会盯三个指标响应时间RT从发SQL到返回结果的耗时。大屏接口建议控制在500ms以内最差的查询也不应该超过2秒。吞吐量QPS同一时间能承接多少个查询。大屏可能有十多个图表组件每个组件一个接口所以数据库得扛住几十到上百并发。扫描行数执行一次查询到底扫了多少行数据。这个指标特别直观如果扫描行数等于全表行数说明索引、分区、排序键都没用上查询肯定是慢的。列式存储在这三个指标上都有巨大优势。响应时间靠减少扫描量和向量化执行来压吞吐量靠并行与轻量化事务来扛扫描行数则靠分区裁剪、稀疏索引、聚合下推来降。后面第3章我会给出一套能落地的具体配置。3. 实操用列式存储加速一张可视化大屏3.1 选型不同数据量级怎么选引擎我在实际项目里总结了一套比较无脑的选型策略直接按数据量去套数据量级推荐方案适用场景MB~GB级DuckDB Parquet文件个人分析、小型团队、临时大屏、本地方案GB~TB级ClickHouse / Doris企业级在线可视化大屏、实时指标TB级以上Trino/Presto Hive/HDFS Parquet数据湖架构下的大规模分析实时流式场景ClickHouse / Doris Kafka实时监控大屏、日志分析免费可视化大屏方面我最常用的是Apache Superset和Grafana。Superset对ClickHouse支持很完善直接连ClickHouse数据源就能拖拽出图表生成大屏Dashboard也是免费的。Grafana更偏监控类图表但做实时指标大屏也完全够用。再加上ECharts作为前端图表库整个链路都是开源的成本为零。如果你不想部署太重的平台还有个更轻的方案用DuckDB在本地做聚合把结果导出成JSON然后前端ECharts直接读JSON渲染。这种方式适合固定报表、每日更新一次的那种大屏。好处是后端依赖极小一台普通虚拟机就能跑也不需要常驻数据库服务。3.2 ClickHouse建表与压缩调优以ClickHouse为例我把建表的关键点拆开讲一下。假设我们有一个订单事实表字段包括order_id、user_id、product_id、order_date、region、amount、quantity、status等。建表语句如下CREATE TABLE sales_order_local ( order_id String, user_id UInt64, product_id UInt64, order_date Date, region LowCardinality(String), amount Decimal(18,2), quantity UInt32, status LowCardinality(String) ) ENGINE MergeTree PARTITION BY toYYYYMM(order_date) ORDER BY (order_date, region, product_id) SETTINGS index_granularity 8192;这里几个关键点我展开说一下。PARTITION BY按月份做分区这样按时间范围过滤时可以快速跳过无关分区。比如大屏只查2024年3月的数据ClickHouse只要扫描3月份那个分区其他分区直接跳过。分区粒度不要太小如果按天分区数据量不大时会生成太多小分区反而影响性能。一般按月或按周比较合适。ORDER BY这决定了表内数据的物理排序和稀疏索引的构建方式。在这个例子里我把order_date放在最前面是因为大屏查询几乎都会带时间范围。排序键和查询过滤条件匹配得越好扫描的数据量就越少。如果你经常按区域下钻也可以调整排序键把region提前或者建一张明细物化表专门服务区域维度查询。LowCardinality优化像region、status这种取值有限的字段用LowCardinality类型包装一下既能大幅降低存储空间查询时还能减少重复解码的开销。实测几亿行数据下这个操作能明显提升group by场景的速度。索引粒度index_granularity默认8192行一般不用改。这个参数控制稀疏索引的打点距离调太小会增加索引体积调太大又会失去索引价值。除非你有特别明确的需求否则保持默认就好。压缩算法方面ClickHouse支持LZ4和ZSTD默认是LZ4。LZ4压缩和解压速度极快适合高吞吐查询ZSTD压缩率更高但解压开销略大。大屏场景追求响应速度所以我一般习惯用默认LZ4。如果磁盘空间紧张或者查询大宽表且只读极少列可以试试ZSTD。你可以用下面的语句看一下每个表列的压缩情况SELECT name, formatReadableSize(sum(data_compressed_bytes)) AS compressed, formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed, sum(data_compressed_bytes) / sum(data_uncompressed_bytes) AS ratio FROM system.parts_columns WHERE table sales_order_local GROUP BY name;通过这个查询你可以直观看到每一列实际占了多少空间压缩比是多少。如果发现某些列的压缩比特别低就要考虑是不是数据类型不够紧凑或者这个列重复值太少了。3.3 数据导入从Parquet文件快速加载ClickHouse导入数据的方式很多我比较推荐直接读Parquet文件导入速度快且不用关心解析细节。假设你手里有一个data.parquet文件字段定义和表结构一致可以这样写INSERT INTO sales_order_local SELECT order_id, user_id, product_id, order_date, region, amount, quantity, status FROM file(data.parquet)如果你是从MySQL同步数据过来可以用ClickHouse的MaterializedMySQL或者直接写同步脚本。但大厂实践里更常见的还是先把数据落到Parquet或ORC然后用上面这种方式批量导入。因为列式文件本身结构紧凑导入时避免了在MySQL客户端和ClickHouse之间往返逐行传输。导入完记得执行一下优化操作OPTIMIZE TABLE sales_order_local FINAL;这个操作会把表的分区数据重新整理合并让物理存储更紧凑。但要注意OPTIMIZE不能频繁跑它非常消耗资源一般每天一次或者每次批量导入完整数据后跑一次就行。如果你的表是实时写入的那就不用天天OPTIMIZE让后台MERGE进程自己处理就好。3.4 大屏接口SQL的编写技巧有了列式存储SQL写不好照样慢。想彻底发挥列式存储的性能必须把“裁剪”思路贯穿到每条SQL里。第一个原则永远不要select *。大屏接口只需要图表画图要用的字段多一个字段就多一次IO和网络开销。第二个原则过滤条件要能被下推。比如这条SQLSELECT toMonday(order_date) AS week, sum(amount) AS sales_amount FROM sales_order_local WHERE order_date 2024-01-01 AND order_date 2024-04-01 AND region IN (华东, 华南) GROUP BY week ORDER BY week;这个查询能直接用上ORDER BY键里的order_date做范围裁剪再加上分区裁剪实际扫描的数据量可能只有全表的十分之一。如果过滤字段里有一个不在排序键上ClickHouse也能通过二级索引或跳过索引优化但效果肯定不如排序键那么稳。第三个原则尽量用预聚合。可视化大屏很少需要精确到每一行的明细大多数图表只要天级、周级、月级的汇总值。这时最好的做法是建一张预聚合表CREATE TABLE sales_daily_summary ( order_date Date, region LowCardinality(String), sales_amount Decimal(18,2), order_count UInt64 ) ENGINE SummingMergeTree() PARTITION BY toMonth(order_date) ORDER BY (order_date, region);然后通过定时任务把明细表聚合到汇总表大屏直接查汇总表。这样即使明细表有几十亿行查询也可以做到几十毫秒返回。实例中我把一张6亿行的订单明细表用一个物化视图或定时任务按天、按区域、按商品维度做多张预聚合表大屏上所有查询都走预聚合表响应时间不超过300ms。3.5 ECharts大屏前端的加速技巧后端数据再快前端渲染不给力大屏还是会掉帧。ECharts这块我分享几个我在实际项目中很受用的点。数据节流与降采样折线图如果数据点超过一两万个浏览器绘制路径时会有明显卡顿。ECharts的line系列支持sampling配置可以设置成lttb这个算法能在尽量保留波形特征的前提下把数据点降到几千个。实测一万五千个点的折线图开启LTTB后交互流畅度提升非常明显。option { xAxis: { type: category }, yAxis: { type: value }, series: [{ type: line, sampling: lttb, data: chartData }] };避免一次性渲染全量明细很多大屏其实根本不需要展示明细只需要展示汇总结果。如果确实要做明细表格也可以采用虚拟滚动或分页不要一次性塞给DOM。ECharts的dataZoom可以配合slider让用户自由拖拽查看区间但dataZoom本身不减少数据点数它只是可视地显示区间。所以底层还是要靠后端先把数据聚合成合适粒度的序列。合并请求一个大屏往往有十几个图表。如果每个图表都单独发一个HTTP请求浏览器并发连接数会被打满网络往返时间也会拖慢首屏。我一般会做一个聚合接口一个大屏一次请求后端把N个图表的聚合结果一次性返回前端再用一条数据更新全部图表。这样不仅减少了网络开销也让列式存储能够复用一个连接池做批量查询。ClickHouse本身查询轻量单次并发几十个查询没问题但对大屏这种场景合并请求还是更好的实践。开启ECharts渐进渲染当数据点很多时设置progressive配置可以开启渐进式渲染让图表先模糊地出来再逐步细化体感上比白屏等待要舒服很多。4. 常见问题与排查技巧实录4.1 为什么换了列式存储查询还是很慢这是我最常碰到的疑问。很多同学一听说列式存储快就把表导过去结果发现查询还是慢甚至更慢于是得出结论“列式存储也就那样”。其实大概率是下面这些坑之一。坑一没有设置合理的ORDER BY或主键。ClickHouse的MergeTree通过排序键建立稀疏索引查询时如果过滤条件和排序键对不上就等于全表扫描。比如你把排序键设成(order_date, region)但大屏很多查询是where product_id xxx group by order_date那product_id不在排序键前列查询时无法高效定位性能就上不去。解决办法是针对高并发查询路径再建一张以product_id为第一排序键的表或投影。坑二查询里用了select *。列式存储的优势是按列裁剪IO。你一个select *把几十列全捞上来哪怕是列式存储也得把每列都读一遍。这是很多测试不准的根本原因。大屏场景一定要想办法把结果集缩小只取必要字段。坑三分区粒度和查询条件不匹配。如果你按月份分区但大屏查询经常只查最近三天那每个月分区里有大量无用数据被扫描。这时可以考虑按周分区或者在表设计时保留一张天级分区表。反过来如果你数据量不大但分区特别多表碎片会很多查询性能也会下降。坑四group by的基数太高。比如按order_id分组几亿行里几乎每一行都不同这种高基数聚合在列存下也会消耗大量内存和CPU。大屏上很少需要按订单ID这种超高基数维度去统计我还是建议在业务层就做降维把不需要的明细列给省略掉。4.2 内存暴涨和OOM问题列式存储做大聚合时内存消耗也不小。比如超大GROUP BY或者JOIN大表都可能把内存打爆。ClickHouse有几个配置项可以限制。max_memory_usage限制单个查询的最大内存使用。max_result_rows限制返回的最大行数。max_bytes_before_external_group_by在group by时数据量超过阈值就溢写到磁盘而不是一直在内存里撑。max_threads限制并发线程数太高也会导致内存过大。我一般会在大屏专用的查询账号或查询配置文件里设置一个适合自己实例规格的阈值。例如SET max_memory_usage 20000000000; -- 20GB SET max_result_rows 500000; SET max_bytes_before_external_group_by 50000000000; -- 50GB这么做的好处是即使前端误发了一个极端查询数据库也不会被拖垮而是快速返回超过限制的报错。大屏项目最怕的就是一个慢查询拖死整个实例合理的资源配置比单纯追求快更重要。4.3 免费可视化大屏平台接入列式存储如果你想搭一套完全免费的在线大屏我推荐用Apache Superset连ClickHouse流程不复杂在Superset中配置ClickHouse数据库连接。从数据表里选择需要的维度、指标、过滤条件拖拽生成图表。创建Dashboard把多个图表拼成大屏。设置自动刷新让大屏实时更新。Superset对ClickHouse的SQL方言支持还算不错时间过滤、聚合、下钻都能用。如果哪块不满意也完全可以用ECharts自己写前端把ClickHouse的HTTP接口当作后端自己封装JSON结果。反正底层的列式存储已经帮你把最重的活做完了。4.4 快速排查SQL性能的脚本最后分享一个我常用的“体检”查询查看整个ClickHouse实例里当前有哪些慢查询在跑SELECT query, query_duration_ms, read_rows, read_bytes, memory_usage, written_rows, environment FROM system.query_log WHERE event_time now() - INTERVAL 1 HOUR AND type QueryFinish ORDER BY query_duration_ms DESC LIMIT 20;通过这个查询我可以一眼看到哪些大屏接口的SQL在扫描大量行哪些查询耗时最高。结合查询里用到的表大小就能判断是不是索引未命中、过滤条件缺失或者排序键不匹配。这个排障思路不仅适用于ClickHouse也适用于其他支持查询日志的列式引擎比如Doris的审计日志、Trino的query info。做可视化优化别拍脑袋先拿数据说话。4.5 列式存储加速可视化的大实话列式存储不是万能药。如果你的可视化查询都是点查询比如按主键查单条订单那列式存储未必比MySQL有优势甚至因为写入成本高、事务能力弱反而更别扭。可一旦进入“大数据、多指标、多维度聚合”的可视化场景比如大屏、BI报表、实时监控列式存储基本是绕不开的最优解。这一点从我做过的大大小小的项目里已经反复验证过了。我自己在项目里还有一种非常推荐的组合前端ECharts负责渲染后端用一个轻量API网关把ClickHouse返回的数据流式转发给前端中间加一层Redis做缓存热点查询比如“今日销售额”“本月环比”直接打到缓存上只有缓存过期或者时间点切换时才回源到ClickHouse。这套架构在大数据量下稳定跑了一年多大屏从来没有人抱怨过卡顿。最后再分享一个小技巧列式存储的排序键不要按你最想查的维度来设计而要按所有高频查询里公共的过滤维度来设计。比如你所有查询都会带时间范围时间字段一定要排在排序键最前面如果某些查询经常按区域过滤就考虑再加一个区域字段或者建一张以区域为前置排序键的聚合表。这个细节决定了大屏在高峰期能不能扛住几十个查询的并发压力。希望我的这些踩坑经验能帮你少走弯路。工具是死的思路是活的列式存储只是一个加速器怎么把它和你的可视化场景结合好才是真正值得花时间琢磨的地方。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价