Qt+SQLite千万级数据游标分页实战与性能优化
这次我们直接看一个 Qt 项目里非常实际的性能问题SQLite 单表数据量到了千万级之后原来的列表查询、CRUD 和界面刷新会越来越卡。很多项目其实不是死在 SQLite 本身而是死在“拿处理万级数据量的分页方式去处理千万级数据”。本文把 Qt SQLite 的游标分页实战完整拆开给出表设计、索引策略、CRUD 代码、UI 模型和异步刷新方案目标是让千万级数据列表在 QTableView 上保持可交互的流畅度。先说结论。传统 LIMIT OFFSET 分页在 OFFSET 到几十万、上百万之后SQLite 每次都要扫描并丢弃前面的行耗时随页码上涨游标分页Keyset Pagination用索引直接定位“上一页最后一条记录”下一页从它后面继续取查询耗时与总数据量基本解耦。这是 Qt 客户端做本地大数据量列表最值得先尝试的方案之一。本文面向 Qt 开发者和正在做上位机、数据采集、工业组态、本地历史数据查询的同学。下面包含核心能力速览、适用场景、环境准备、表设计与索引、游标分页实现、千万级 CRUD 实战、UI 分页模型与异步加载、性能观察方法、问题排查和最佳实践。文中代码基于 Qt 5.15 / Qt 6 的 QSql 模块可以直接复制后用 CMake 工程验证。1. 核心能力速览能力项说明技术栈Qt 5.15 / Qt 6QSqlDatabase QSqlQuery QAbstractTableModel数据量目标单表千万级核心方案游标分页Keyset Pagination替代 LIMIT OFFSETUI 优化分页加载、后台线程查询、只渲染当前页数据写入优化事务 预编译语句批量写入更新策略UPSERTON CONFLICT支持平台Windows / Linux / macOS接口形态本地数据库访问可封装为 Qt 内部服务接口批量任务支持批量导入、批处理更新和删除硬件建议SSD 8G 内存以上具体耗时以本机为准这套方案有一个前提分页是基于排序键的连续翻页而不是任意跳页。如果产品要求“直接跳到第 100 万条”游标分页并不合适需要另行设计索引、缓存或离线统计方案。先认清这一点再决定是否采用。2. 适用场景与使用边界游标分页最适合的场景是数据持续增长、用户按顺序翻页查看、列表需要保持稳定响应。典型例子包括设备上报数据、温度采样曲线、日志检索、订单流水、历史告警记录。这些场景的共同特征是数据量大、写入以追加为主、读取是顺序翻页。使用边界也很明确。第一SQLite 是单文件单写者数据库不适合多进程高并发写入场景。第二游标分页擅长顺序翻页不支持随机跳页如果 UI 提供“跳转到第 N 页”的输入框那还需要结合总数估算或改用其他方案。第三千万级数据仍然不适合一次性加载到界面上即使改用了游标分页UI 模型也必须按页装载数据不能把整表塞进 QTableView。数据合规方面需要单独说明演示和开发时建议使用模拟数据或脱敏数据如果使用真实采集数据需要确认数据来源合法、不包含未授权的个人信息或敏感内容。尤其是设备编号、工号、位置信息、鉴权字段发布示例代码前要清理干净。测试环境用模拟数据验证逻辑生产库的 Schema 变更和批量操作要先备份再执行。3. 环境准备与 Qt 项目搭建写 Qt 工程前先确认环境里安装了 Qt 的 SQL 模块。Qt 官方安装包默认包含 QSQLITE 驱动不需要额外编译 SQLite。如果发现驱动加载失败查看是否缺少 Qt SQL 相关模块以及插件目录路径是否正确。Windows 下常见问题是 release 程序加载 debug 插件或缺少插件 DLL这个在常见问题章节统一排查。建议工程用 CMake 管理。确认以下依赖已经通过 find_package 引入cmake_minimum_required(VERSION 3.16) project(QtSqliteCursorPaging) set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) find_package(Qt6 COMPONENTS Core Gui Widgets Sql REQUIRED) qt_add_executable(QtSqliteCursorPaging main.cpp MainWindow.cpp MainWindow.h MeasureDataModel.cpp MeasureDataModel.h MeasureQueryWorker.cpp MeasureQueryWorker.h ) target_link_libraries(QtSqliteCursorPaging PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Sql )如果还在用 Qt 5把 Qt6 换成 Qt5CMake 用法一致。工程目录建议按 db / model / worker / ui 分层避免把 SQL 散落在界面代码里。下面是一套简单分层src/ db/ SQLiteHelper.cpp SQLiteHelper.h model/ MeasureDataModel.cpp MeasureDataModel.h worker/ MeasureQueryWorker.cpp MeasureQueryWorker.h ui/ MainWindow.cpp MainWindow.h打开数据库时连接名不要用默认的qt_sql_default_connection到处混用。多线程场景下建议为每个线程显式创建连接并带上不同 connectionName。下面是 SQLiteHelper 的打开逻辑示例#include QSqlDatabase #include QSqlQuery #include QSqlError #include QSqlRecord #include QVariant #include QDebug bool openDatabase(const QString dbPath, const QString connectionName, QSqlDatabase db) { db QSqlDatabase::addDatabase(QSQLITE, connectionName); db.setDatabaseName(dbPath); if (!db.open()) { qCritical() open database failed db.lastError().text(); return false; } QSqlQuery query(db); query.exec(PRAGMA journal_mode WAL); query.exec(PRAGMA synchronous NORMAL); query.exec(PRAGMA busy_timeout 5000); query.exec(PRAGMA cache_size -20000); // 约 20MB page cache return true; }打开连接后建议先执行一遍 WAL、busy_timeout 和 cache_size 设置。WAL 模式能明显改善读写并发时的database is locked问题busy_timeout 让 SQLite 等待锁而不是立即报错。cache_size 是示例值具体大小要结合机器内存调整不是越大越好。4. 表设计与索引策略千万级数据能不能跑起来首先看表结构。下面是一张设备采集表字段尽量贴近生产场景CREATE TABLE IF NOT EXISTS measure_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_no TEXT NOT NULL, channel INTEGER NOT NULL DEFAULT 0, value REAL NOT NULL, ts TEXT NOT NULL, remark TEXT ); CREATE INDEX IF NOT EXISTS idx_measure_data_ts ON measure_data(ts DESC); CREATE INDEX IF NOT EXISTS idx_measure_data_device_ts ON measure_data(device_no, ts DESC);id 作为 INTEGER PRIMARY KEY在 SQLite 中会作为 ROWID 别名天然适合做游标分页的排序键。ts 是业务时间device_no ts 的组合索引用来支撑“按设备查询并按时间倒序翻页”的常见场景。索引不是越多越好。千万级表上每次写入都要同步维护索引索引过多会拖慢插入和更新。先根据实际查询条件设计两三个索引再通过 EXPLAIN QUERY PLAN 验证是否命中不要盲目给每一个字段都建索引。造数阶段建议用脚本或 Qt 程序批量生成模拟数据。千万级 CSV 手工准备很慢更推荐直接本地生成。生成时保持字段分布接近真实设备编号几十台、通道几个、时间连续、数值随机。注意模拟数据不要包含真实设备编号、真实地址和真实个人信息。5. 游标分页原理与 SQL 对照先看传统分页的痛点。假设每页 100 条要取第 9000 页的数据SELECT id, device_no, channel, value, ts, remark FROM measure_data ORDER BY id ASC LIMIT 100 OFFSET 899900;这条 SQL 在数据量小的时候没问题。但当表里有 1000 万条记录时SQLite 为了算出 OFFSET 899900需要扫描并丢弃前 899900 行查询耗时会随 OFFSET 越来越大。这是列表越翻越卡的直接原因。游标分页的思路是记住当前页最后一条记录的 id下一页查询用 WHERE id :last_id然后 LIMIT 100。SQL 如下-- 第一页 SELECT id, device_no, channel, value, ts, remark FROM measure_data ORDER BY id ASC LIMIT 100; -- 下一页last_id 是上一页最后一条记录的 id SELECT id, device_no, channel, value, ts, remark FROM measure_data WHERE id :last_id ORDER BY id ASC LIMIT 100;因为 id 是主键SQLite 利用索引可以直接跳到最后一条位置不需要扫描前面的大量数据。数据量从 100 万涨到 1000 万后只要翻页位置接近最新数据查询耗时的变化会远小于 OFFSET 分页。多字段排序也可以做游标分页。比如按 device_no 过滤然后按 ts DESC, id DESC 排序。SQLite 3.15 及以上支持行值比较SELECT id, device_no, channel, value, ts, remark FROM measure_data WHERE device_no DEV-001 AND (ts, id) (:last_ts, :last_id) ORDER BY ts DESC, id DESC LIMIT 100;使用 (ts, id) 行值语法时需要确保排序列和过滤列能被索引覆盖。以上这条 SQL 对应的索引是 idx_measure_data_device_ts。如果只写 ts 条件而索引顺序不匹配SQLite 可能仍然走全表扫描性能就回不到预期。下面是把游标分页封装成 Qt 函数的完整实现struct MeasureRecord { qlonglong id 0; QString deviceNo; int channel 0; double value 0.0; QString ts; QString remark; }; QVectorMeasureRecord queryPage( qlonglong cursorId, int pageSize, QSqlDatabase db, QString *errorText nullptr) { QVectorMeasureRecord records; // pageSize 来自调用方常量或整数参数拼入前做范围校验 if (pageSize 0 || pageSize 5000) { pageSize 100; } QString sql QStringLiteral( SELECT id, device_no, channel, value, ts, remark FROM measure_data WHERE id %1 ORDER BY id ASC LIMIT %2) .arg(cursorId) .arg(pageSize); QSqlQuery query(db); query.setForwardOnly(true); if (!query.exec(sql)) { if (errorText) { *errorText query.lastError().text(); } return records; } while (query.next()) { MeasureRecord rec; rec.id query.value(0).toLongLong(); rec.deviceNo query.value(1).toString(); rec.channel query.value(2).toInt(); rec.value query.value(3).toDouble(); rec.ts query.value(4).toString(); rec.remark query.value(5).toString(); records.append(rec); } return records; }这里 cursorId 是从上一页最后一条记录中取出的整数pageSize 也做了

相关新闻

ipatool 新手入门,三步搞定 iOS 应用 IPA 下载

ipatool 新手入门,三步搞定 iOS 应用 IPA 下载

为什么 iOS 开发者需要 ipatool? 在 iOS 开发与测试的日常工作中,获取应用的安装包(IPA 文件)往往是一个绕不开的环节。传统模式下,我们通常依赖 Xcode 进行归档导出,或者需要在 macOS 环境下通过 App Stor…

2026/10/23 19:55:24 阅读更多 →
FPGA实战:BPSK调制解调与卷积编解码完整链路设计

FPGA实战:BPSK调制解调与卷积编解码完整链路设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/25 2:49:30 阅读更多 →
去除AI写作痕迹的软件有哪些?免费降AI率、论文AI降重工具怎么选?

去除AI写作痕迹的软件有哪些?免费降AI率、论文AI降重工具怎么选?

去除AI写作痕迹的软件有哪些?免费降AI率、论文AI降重工具怎么选? 论文读起来很像模板,搜索去AI痕迹免费的软件有哪些,却发现每个页面都写免费。你真正需要知道的是:免费得到的是修改建议、处理后的文本,还…

2026/10/24 11:57:44 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/10/24 11:09:31 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/10/24 17:47:00 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/24 18:34:36 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/10/24 9:46:55 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/10/25 3:12:30 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/10/24 21:09:59 阅读更多 →

日新闻

周新闻

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/24 9:15:56 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/24 11:09:35 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/25 7:47:32 阅读更多 →