当前位置: 首页 > news >正文

单表五亿数据的查询优化 | Mysql、StarRocks

本次测试所用服务器硬件配置如下,此机器除了 Mysql 和 StartRocks 还部署了其它很多 Docker 服务。

CPU:

AMD Ryzen™ 7 8745H w/ Radeon™ 780M Graphics × 16

内存:

DDR5 5600 MT/S 32G(16G*2)

磁盘性能:

Timing cached reads: 64174 MB in 1.99 seconds = 32256.62 MB/sec Timing buffered disk reads: 3562 MB in 3.00 seconds = 1186.39 MB/sec

初始化数据库环境

Mysql:

CREATE TABLE `users` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `username` varchar(32) NOT NULL, `phone` char(11) NOT NULL, `email` varchar(64) NOT NULL, `gender` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT '0未知 1男 2女', `age` tinyint(3) unsigned NOT NULL DEFAULT 0, `status` tinyint(3) unsigned NOT NULL DEFAULT 1 COMMENT '1正常 2禁用 3注销', `province_id` smallint(5) unsigned NOT NULL DEFAULT 0, `city_id` mediumint(8) unsigned NOT NULL DEFAULT 0, `register_source` tinyint(3) unsigned NOT NULL DEFAULT 1 COMMENT '1web 2ios 3android 4api', `score` int(10) unsigned NOT NULL DEFAULT 0, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `last_login_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_phone` (`phone`), KEY `idx_status_created_id` (`status`,`created_at`,`id`), KEY `idx_created_at_id` (`created_at`,`id`), KEY `idx_email` (`email`), KEY `users_created_at_index` (`created_at`) ) ENGINE=InnoDB AUTO_INCREMENT=935300001 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC

StarRocks:

CREATE TABLE `users` ( `id` bigint NOT NULL COMMENT "", `created_at` datetime NOT NULL COMMENT "", `username` varchar(32) NOT NULL COMMENT "", `phone` varchar(11) NOT NULL COMMENT "", `email` varchar(64) NOT NULL COMMENT "", `gender` tinyint NOT NULL DEFAULT "0" COMMENT "", `age` tinyint NOT NULL DEFAULT "0" COMMENT "", `status` tinyint NOT NULL DEFAULT "1" COMMENT "", `province_id` smallint NOT NULL DEFAULT "0" COMMENT "", `city_id` int NOT NULL DEFAULT "0" COMMENT "", `register_source` tinyint NOT NULL DEFAULT "1" COMMENT "", `score` int NOT NULL DEFAULT "0" COMMENT "", `updated_at` datetime NOT NULL COMMENT "", `last_login_at` datetime NULL COMMENT "" ) ENGINE=OLAP PRIMARY KEY(`id`, `created_at`) PARTITION BY RANGE(`created_at`) ( START ("2020-01-01") END ("2026-12-31") EVERY (INTERVAL 1 MONTH) ) DISTRIBUTED BY HASH(`id`) BUCKETS 8 PROPERTIES ( "replication_num" = "1" );

Python 写入数据的脚本:

from concurrent.futures import ThreadPoolExecutor, as_completed from datetime import datetime, timedelta import pymysql HOST = "192.168.1.1" PORT = 3306 // 或 9030 USER = "root" PASSWORD = "123456" DATABASE = "testdata" START_ID = 1 TOTAL_ROWS = 500_000_000 WORKERS = 8 BATCH_SIZE = 10_000 BASE_TIME = datetime(2024, 1, 1, 0, 0, 0) INSERT_SQL = """ INSERT INTO users ( id, username, phone, email, gender, age, status, province_id, city_id, register_source, score, created_at, updated_at, last_login_at ) VALUES ( %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s ) """ def make_conn(): return pymysql.connect( host=HOST, port=PORT, user=USER, password=PASSWORD, database=DATABASE, charset="utf8mb4", autocommit=False, read_timeout=300, write_timeout=300, connect_timeout=30, ) def build_rows(start_id: int, end_id: int): rows = [] for n in range(start_id, end_id): created_at = BASE_TIME + timedelta(seconds=n % 31_536_000) updated_at = created_at last_login_at = created_at + timedelta(days=n % 30) rows.append(( n, f"user_{n}", f"1{n:010d}", f"user_{n}@test.local", n % 3, 18 + (n % 43), 2 if n % 20 == 0 else 1, (n % 34) + 1, (n % 340) + 1, (n % 4) + 1, n % 100000, created_at.strftime("%Y-%m-%d %H:%M:%S"), updated_at.strftime("%Y-%m-%d %H:%M:%S"), last_login_at.strftime("%Y-%m-%d %H:%M:%S"), )) return rows def worker(worker_no: int, start_id: int, end_id: int): conn = make_conn() inserted = 0 try: with conn.cursor() as cur: current = start_id while current <= end_id: next_id = min(current + BATCH_SIZE, end_id + 1) rows = build_rows(current, next_id) cur.executemany(INSERT_SQL, rows) conn.commit() inserted += len(rows) current = next_id if inserted % 100000 == 0 or current > end_id: print(f"worker={worker_no} inserted={inserted} range={start_id}-{end_id}") finally: conn.close() def split_ranges(start_id: int, total_rows: int, workers: int): base = total_rows // workers remain = total_rows % workers current = start_id result = [] for i in range(workers): size = base + (1 if i < remain else 0) s = current e = current + size - 1 result.append((i + 1, s, e)) current = e + 1 return result def main(): ranges = split_ranges(START_ID, TOTAL_ROWS, WORKERS) print("ranges:", ranges) with ThreadPoolExecutor(max_workers=WORKERS) as pool: futures = [pool.submit(worker, worker_no, s, e) for worker_no, s, e in ranges] for future in as_completed(futures): future.result() print("done") if __name__ == "__main__": main()

5亿条数据到底占多大空间

本次测试中,MySQL实际写入数据量为4.6亿条(因测试过程中未完成5亿条写入),StarRocks按计划写入5亿条数据,以下为两者的存储占用详情。

对于 Mysql:

8.0K ./testdata/users.frm 136G ./testdata/users.ibd 4.0K ./testdata/db.opt 136G ./testdata

StarRocks 是 256G。

Mysql 表里面创建了比较多的索引,通过以下 SQL 可以获取表的索引以及索引数据占用的空间:

SELECT TABLE_NAME AS 表名, CONCAT(ROUND((INDEX_LENGTH / 1024 / 1024), 2), ' MB') AS 索引大小, CONCAT(ROUND((DATA_LENGTH / 1024 / 1024), 2), ' MB') AS 数据大小, CONCAT(ROUND(((INDEX_LENGTH + DATA_LENGTH) / 1024 / 1024), 2), ' MB') AS 总大小 FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'testdata' AND TABLE_NAME = 'users';
指标数值换算
索引大小79943.56 MB78.07 GB
数据大小51887.00 MB50.67 GB
总大小131830.56 MB128.74 GB

笔者这台机器部署了很多服务,所以实际上日常运行 Mysql、StarRocks 两个数据库应该不需要 10G 内存。

统计数据量

大数据量下,全表数据量统计(select count(*))是业务中常见场景,以下为MySQL与StarRocks的性能对比,每组测试重复3次,取平均值以减少偶然误差。

select count(*) from users;

在 Mysql 里面,统计数据量是大坑,需要 1-2 分钟。

[2026-04-11 14:21:22] 在 1 m 11 s 165 ms (execution: 1 m 11 s 153 ms, fetching: 12 ms) 内检索到从 1 开始的 1 行

StarRocks 只需要 260ms。

[2026-04-11 14:20:29] 在 261 ms (execution: 254 ms, fetching: 7 ms) 内检索到从 1 开始的 1 行

所以对在业务系统中统计数据量是非常麻烦的一个事情,如果只是需要知道全表数据量,有很多方法可以做,例如单独设计数据量统计表、Redis 记录数据量等,但是往往页面显示数据量时需要分页、搜索、筛选,会导致在大数据量下耗时非常长。

因此在大数量时读写分离很有必要,统计数据量、分页查询大小、join 条件等通过 StarRocks 来操作。

有索引会多块

数据库本身已有以下索引:

一开始 AI 给我生成表的时候,我在想为什么有的索引只包含字段,有的列把id也包进去了,查了资料发现 InnoDB 二级索引默认含主键,所以实际上(created_at,id)(created_at)是一样的,但是在排序方面有区别,因为索引的数据是会排序的。

以下场景显式定义(col, id)有明显收益:

  • 查询包含ORDER BY col, id
  • 查询需要按col分页(如WHERE col > x ORDER BY col, id LIMIT n),id可用于稳定分页顺序;
  • col的区分度很低(如大量重复值),加上id可提高索引的 “区分度”,优化索引查找效率。

回归正题,在使用主键的情况下,Mysql 读取 1000 条数据:

select * from users where id in (...);
[2026-04-12 08:52:56] 在 113 ms (execution: 79 ms, fetching: 34 ms) 内检索到从 1 开始的 64 行

所以在表数据量非常大时,完全可以在 StarRocks 执行一些查询操作,最终获取一份数据 id 后在 Mysql 业务数据库查询数据做业务处理。

对于手机号这种字符串字段,如果做了索引,其实各种查询操作也不会慢的。

select * from users where phone like '1000%' order by phone desc limit 100 offset 10; select * from users where phone like '%1000%' order by phone desc limit 100 offset 10;

Mysql:

[2026-04-11 14:23:05] 在 48 ms (execution: 13 ms, fetching: 35 ms) 内检索到从 1 开始的 100 行 [2026-04-12 09:11:44] 在 375 ms (execution: 347 ms, fetching: 28 ms) 内检索到从 1 开始的 100 行

StarRocks:

[2026-04-12 09:08:09] 在 576 ms (execution: 546 ms, fetching: 30 ms) 内检索到从 1 开始的 100 行 [2026-04-12 09:11:36] 在 822 ms (execution: 789 ms, fetching: 33 ms) 内检索到从 1 开始的 100 行

上面的测验可以说明几个问题。

对于字符串,走前缀区配时,like 'xxx%'性能性能也会非常好,4.6 亿数据执行时间只需要 13ms。

只有前缀匹配like 'xxx%',才能真正利用索引做范围扫描(range)B + 树可以直接定位到前缀匹配的起始位置,只扫描符合范围的索引节点,所以%xxx%这种走不了索引会导致全盘扫描,导致 MySQL 执行耗时从 13ms 暴涨到 347ms,性能下降了 26 倍。

不过对于 StarRocks,StarRocks 是 OLAP 引擎,默认的前缀索引对like '%x%'完全无效,所以查询都会比 Mysql 慢。

对于字符串等场景,如果设计的查询方案可以走索引,那么即使数据量很大,其实也不需要担心查询时间。

优化筛选查询

单是用户表,在业务需求中往往需要对手机号、用户名、邮箱等进行模糊查询,like %xxx%这种情况必然会出现,我们不可能让产品经理改需求,但是无论在 Mysql 还是 StarRocks 使用like %xxx%在大数据量时耗时都会变大,所以我们需要找到一种方式,既可以满足产品对于订单、用户表等多个动态字段模糊搜索,又要让查询速度变快。

select * from users where phone like '%1000%' or email like '%user_10%' or username like '%user_11%' order by phone desc limit 100 offset 100;

Mysql:

[2026-04-12 09:31:37] 在 912 ms (execution: 880 ms, fetching: 32 ms) 内检索到从 1 开始的 100 行

StarRocks:

[2026-04-12 09:32:24] 在 1 s 588 ms (execution: 1 s 557 ms, fetching: 31 ms) 内检索到从 1 开始的 100 行

StarRocks/Doris 支持ngram 分词倒排索引,原理和 ES 类似,但直接集成在数仓引擎中,避免了数据同步的麻烦,适合分析场景。但是经过笔者测试,where phone like '%1000%' or email like '%user_10%' or username like '%user_11%'走不了索引,查询速度也好慢。

无论是 Mysql 还是 StarRocks 在多条件模糊查询时,由于索引机制,都会导致查询速度缓慢,最后只能使用 ElasticSearch 做模糊查询,ElasticSearch 这方面非常强。

POST /users/_search { "from": 0, "size": 10, "query": { "bool": { "should": [ { "match": { "phone": "1000" }}, { "match": { "email": "user_10" }}, { "match": { "username": "user_11" }} ], "minimum_should_match": 1 } } }

一个查询只能使用一个索引

在 Mysql 的 users 表中,我们给很多字段设置了索引,包括创建时间idx_created_at_id

PRIMARY KEY (`id`), UNIQUE KEY `uk_phone` (`phone`), KEY `idx_status_created_id` (`status`,`created_at`,`id`), KEY `idx_created_at_id` (`created_at`,`id`), KEY `idx_email` (`email`)

如果我们按时间来排查范围,有idx_created_at_id的加持,下面的 SQL 执行速度会不会非常快?

select * from users where phone like '1000%' and created_at > '2024-04-25 17:46:20' order by phone desc limit 100 offset 10;

然而实际测试,Mysql:

[2026-04-11 11:55:19] 在 24 s 86 ms (execution: 24 s 66 ms, fetching: 20 ms) 内检索到从 1 开始的 9 行

StarRocks:

在 19 s 578 ms (execution: 19 s 561 ms, fetching: 17 ms) 内检索到 0 行

但是我不是给 phone、create_at 都创建索引了嘛,为什么还会这么慢?先看执行计划。

确实有两个索引:

  1. uk_phone(phone)
  2. idx_created_at_id(created_at, id)

但问题是 MySQL 通常只能选一个 “最有用” 的索引,然后回表过滤其他条件。所以

http://www.jsqmd.com/news/1071557/

相关文章:

  • 最新mpay码支付系统源码+监听app完美可用改版
  • openclaw不存在?Ubuntu 22.04下安全替代方案指南
  • Etsy 把 1000 个 MySQL 分片迁进 Vitess:425TB 数据背后的真正问题不是性能,而是运维规模
  • VB6.0下载安装教程(附安装包)2026最新版(Visual Basic 6.0中文企业版)
  • 区间预测 | Matlab实现OOA-BP-KDE核密度估计多置信区间多变量回归区间预测
  • 【免费数据】2012和2020年中国1km分辨率POI密度栅格数据
  • 人工智能和大数据专业,填报时怎么区分取舍
  • 前端状态持久化
  • 按照这个方法真的领到了8元,超简单,实打实的,可点奶茶外卖.千问无门槛优惠券 大数据推给有需要的人,下载千问,输入口令:千问新用户专属876028,就可以领取啦
  • MATLAB稳健性设计:从不确定性量化到可变性优化实战
  • 大数据、计算机科学、软件工程三者该如何择校
  • 公考时政常识|公务员备考时政|每月时政热点汇总
  • 列车-轨道-桥梁交互仿真研究附Matlab代码
  • 华为MetaERP SAP FI-AA vs Oracle EBS FA:资产期间关闭可逆 / 不可逆 —— 设计哲学 + 底层实现逻辑完整对比一、核心现象先明确边界SAP FI-AA(资产会计)
  • 安全的即时通讯软件原理与设计的调研报告
  • 基于多目标鲸鱼优化算法(NSWOA)求解地铁隧道竖向位移和成本的双目标求解(以铁道科学报与工程文章为例)研究附Matlab代码
  • Rust的匹配中的编译器技术
  • VSCode 插件推荐:让你编码效率翻倍
  • Agent常见面试题目
  • 软件模板方法管理化的算法骨架定义
  • Django毕设选题推荐:基于 Django 框架的智能文件加密解密系统的设计与实现 基于轻量化 AES 加密的文件安全管理系统的设计与实现【附源码、mysql、文档、调试+代码讲解+全bao等】
  • K230+CanMV+OpenCV的实际画面效果展示
  • web第五次作业
  • FastAPI-路由机制和依赖注入
  • OpenSpec OPSX:用语义规范驱动可执行工作流
  • 量子力学:在绝对局限中逼近真相的唯一方法
  • 技术部署的标准化流程与环境管理
  • Docker Docs
  • 个性化服务化技术用户画像构建与实时更新策略
  • AI 对话为什么还在用 Markdown:流式富 UI 才是