news 2026/9/2 19:44:26

Qt+SQLite千万级数据性能优化:游标分页与模型增量加载实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt+SQLite千万级数据性能优化:游标分页与模型增量加载实践

如果你的 Qt 程序里 QTableView 加载几十万行数据时,界面已经卡得拖不动,滚动一下要等两三秒,那这篇文章就是给你准备的。

这次我们来看一个非常务实的组合:Qt + SQLite。SQLite 常被误认为只能做小工具、小配置存储,但实际上只要正确使用索引、事务和分页策略,它完全可以支撑千万级数据量的桌面应用。核心问题只有一个:数据量上来后,直接把全部结果集塞给 UI 控件的写法,会让 UI 线程被查询、内存分配和控件刷新拖垮。

这篇文章会先给出一套完整的 Qt + SQLite 千万级数据 CRUD 方案,重点讲游标分页(Keyset Paging)怎么在 UI 上落地,并附上基础 CRUD、批量写入、事务优化、UI 模型刷新、性能观察和排查方法。读完你可以直接套用,不需要改动现有项目结构。

1. 核心能力速览

能力项说明
项目类型Qt 桌面应用 + SQLite 数据库的工程实践
核心功能千万级数据量的增删改查、批量写入、游标分页查询
分页方案Keyset 分页(基于排序字段游标),替代 OFFSET 大偏移分页
UI 刷新方式QAbstractTableModel 模型增量刷新 + 滚动加载,避免一次性加载全量数据
数据库文件单文件 SQLite,可随应用分发,支持 WAL 模式提升并发读写
适合场景本地数据管理工具、工业组态历史数据查询、桌面报表、日志分析工具
不适合场景高并发服务端写入、多进程大规模同时写入、超复杂 SQL 分析

先说明一点:本文讲的是工程方案,不是某个开源整合包。你不需要额外安装大型运行时,只需要 Qt 开发环境和一个能跑 SQLite 的编译器,就能把这个架构应用到自己项目里。

2. 适用场景与使用边界

2.1 适合谁

  • Qt 桌面应用开发者,尤其是 QTableView、QListView、QTreeView 遇到大数据量卡顿的人。
  • 需要做本地数据管理系统、历史数据查询工具、组态软件数据记录模块的人。
  • 希望不引入 MySQL、PostgreSQL 等独立数据库服务,直接用 SQLite 扛住大数据量单机场景的人。

2.2 能解决什么问题

用游标分页后,UI 不再需要一次拿到几十万行数据。每次只查询当前屏幕附近的一小段数据,比如 50 行或 100 行,滚动到接近底部时再取下一批。这样内存占用、查询耗时、UI 刷新成本都控制在很小范围内,用户感知就是“滑动流畅、不卡顿”。

2.3 不适合什么场景

  • 需要随机跳到第 800000 行,并且要求瞬时完成,这种场景不适合游标分页,更适合数据库端聚合统计或数据仓库方案。
  • 多个进程同时高频写同一个 SQLite 文件,锁竞争会非常明显。SQLite 适合单进程多线程或少量进程低频写入。
  • 数据本身在服务端,不需要本地缓存,就没必要往客户端塞 SQLite。

2.4 使用边界与合规提醒

SQLite 本身是开源且可靠的嵌入式数据库,但如果你把用户数据、客户资料、隐私信息放入本地数据库,仍然要注意:

  • 对敏感字段做加密或采用系统级加密方案。
  • 导出、分享数据前确认授权。
  • 删除数据时要考虑“物理删除”和“逻辑删除”的差别,避免隐私残留。
  • 涉及版权素材、人脸、声音、个人隐私数据时,必须获得明确授权。

3. 为什么 SQLite 在 Qt 里会卡 UI

很多人把卡顿归咎于 SQLite 性能差,实际更常见的原因是使用方式不对。

3.1 主线程同步执行长查询

如果直接在 UI 线程执行SELECT * FROM table,当数据量到几十万行时,SQLite 解析和返回数据的时间会阻塞事件循环。对于 Qt 应用,QTableWidget 大量 setItem 也会让界面长时间无响应。

3.2 一次加载全量数据到 Item 控件

QTableWidget 是 Qt 里最“重”的表格控件,因为每一个单元格都是一个 QTableWidgetItem 对象。50 万行乘 10 列就是 500 万个对象,光构造和释放就把内存吃满了。

3.3 分页用了 OFFSET

传统分页写法:

SELECT * FROM record ORDER BY id LIMIT 50 OFFSET 800000;

这个 SQL 在 SQLite 中的开销很大,因为数据库必须先扫描并丢弃前 800000 行,才能返回目标 50 行。数据量越大,越往后翻越慢。

3.4 缺少索引或排序字段不稳定

ORDER BY的字段如果没有索引,每次分页查询都要做一次全表排序,耗时呈指数级上升。

4. 环境准备与项目基线

4.1 开发环境

本文代码基于 Qt 6 + Qt SQL 模块编写,但思路完全适用于 Qt 5.15 及更高版本。

依赖项说明
操作系统Windows / Linux / macOS 均可
Qt 版本Qt 5.15 或 Qt 6.x
Qt 模块Qt Core、Qt GUI、Qt Widgets、Qt SQL
编译器MSVC / MinGW / GCC / Clang
数据库文件SQLite 3.x,Qt 自带驱动
可视化调试工具可选:DB Browser for SQLite

新建数据库文件时,建议用QFileDialog::getSaveFileName让用户选择保存路径,避免把数据库文件藏在程序安装目录导致权限问题。

4.2 准备一个测试数据表

先建立表结构,并给排序字段加索引。这个表模拟“设备上报记录”场景,包含自增主键、设备编号、时间戳、数值和备注。

CREATE TABLE IF NOT EXISTS device_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_id TEXT NOT NULL, record_time DATETIME NOT NULL, value REAL NOT NULL, remark TEXT ); CREATE INDEX IF NOT EXISTS idx_device_record_time ON device_record(record_time); CREATE INDEX IF NOT EXISTS idx_device_record_device_id ON device_record(device_id);

从 CRUD 和分页的角度看,主键id天然带索引,record_time是高频率排序字段,必须手动建索引。

5. 基础 CRUD 实现

5.1 数据库连接管理

建议封装一个数据库管理类,每个线程使用独立连接名,避免跨线程共享 QSqlDatabase 实例。

// DatabaseManager.h #pragma once #include <QSqlDatabase> #include <QString> class DatabaseManager { public: static DatabaseManager &instance(); bool open(const QString &filePath); QSqlDatabase connection(const QString &connectionName = QStringLiteral("qt_sqlite_connection")); private: DatabaseManager() = default; QString m_filePath; };
// DatabaseManager.cpp #include "DatabaseManager.h" #include <QSqlQuery> #include <QVariant> #include <QDebug> DatabaseManager &DatabaseManager::instance() { static DatabaseManager manager; return manager; } bool DatabaseManager::open(const QString &filePath) { m_filePath = filePath; auto db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), QStringLiteral("qt_sqlite_connection")); db.setDatabaseName(filePath); if (!db.open()) { qWarning() << "Failed to open database:" << db.lastError().text(); return false; } QSqlQuery query(db); query.exec(QStringLiteral("PRAGMA journal_mode=WAL;")); query.exec(QStringLiteral("PRAGMA synchronous=NORMAL;")); query.exec(QStringLiteral("PRAGMA cache_size=-16000;")); return true; } QSqlDatabase DatabaseManager::connection(const QString &connectionName) { return QSqlDatabase::database(connectionName); }

连接初始化时打开 WAL 模式,可以让读操作不阻塞写操作,对 UI 快速查询和后台写入同时进行很有帮助。

5.2 插入记录

QSqlQuery::prepare绑定参数,避免字符串拼接带来的注入风险和 SQL 解析开销。

bool insertRecord(const QString &deviceId, const QDateTime &time, double value, const QString &remark) { auto db = DatabaseManager::instance().connection(); QSqlQuery query(db); query.prepare(QStringLiteral( "INSERT INTO device_record (device_id, record_time, value, remark) " "VALUES (?, ?, ?, ?)")); query.addBindValue(deviceId); query.addBindValue(time.toString(Qt::ISODate)); query.addBindValue(value); query.addBindValue(remark); return query.exec(); }

5.3 更新记录

bool updateRecord(qint64 id, double newValue) { auto db = DatabaseManager::instance().connection(); QSqlQuery query(db); query.prepare(QStringLiteral( "UPDATE device_record SET value = ? WHERE id = ?")); query.addBindValue(newValue); query.addBindValue(id); return query.exec(); }

5.4 删除记录

bool deleteRecord(qint64 id) { auto db = DatabaseManager::instance().connection(); QSqlQuery query(db); query.prepare(QStringLiteral("DELETE FROM device_record WHERE id = ?")); query.addBindValue(id); return query.exec(); }

5.5 查询判断标准

每次 CRUD 操作后,都应检查query.lastError()和影响行数:

if (!query.exec()) { qWarning() << "SQL error:" << query.lastError().text(); return false; }

在测试阶段,你可以用 DB Browser for SQLite 直接打开数据库文件,实时查看插入、更新和删除是否生效。

6. 游标分页实战(核心章节)

6.1 什么是游标分页

游标分页的关键思想是:不再告诉数据库“跳过多少行”,而是告诉数据库“上一次拿到哪一行”以及“往下取多少行”。因为查询条件里直接带上了排序字段的边界值,SQLite 可以通过索引快速定位起点,不需要扫描大量中间行。

常见的术语叫 Keyset Pagination,也叫 Seek Pagination,中文叫“游标分页”或“基于键集的分页”。

6.2 方案 A:基于主键的正向游标

适合按主键id升序往下翻的场景。

SELECT id, device_id, record_time, value, remark FROM device_record WHERE id > :lastId ORDER BY id ASC LIMIT :pageSize;

对应 Qt 代码:

QVector<DeviceRecord> fetchNextPage(qint64 lastId, int pageSize) { QVector<DeviceRecord> records; auto db = DatabaseManager::instance().connection(); QSqlQuery query(db); query.prepare(QStringLiteral( "SELECT id, device_id, record_time, value, remark " "FROM device_record " "WHERE id > ? " "ORDER BY id ASC " "LIMIT ?")); query.addBindValue(lastId); query.addBindValue(pageSize); if (!query.exec()) { qWarning() << "fetch next page failed:" << query.lastError().text(); return records; } while (query.next()) { DeviceRecord rec; rec.id = query.value(0).toLongLong(); rec.deviceId = query.value(1).toString(); rec.recordTime = QDateTime::fromString(query.value(2).toString(), Qt::ISODate); rec.value = query.value(3).toDouble(); rec.remark = query.value(4).toString(); records.append(rec); } return records; }

首页查询时lastId = 0,之后每次取records.last().id作为下一轮的lastId。这个方案在千万级数据下表现很好,因为id是主键,索引定位非常快。

6.3 方案 B:支持双向翻页的时间游标

按时间排序更符合业务习惯。用record_time作为排序键时,查询条件里同时带上record_timeid,确保排序稳定。因为record_time可能重复,光靠它无法唯一确定上一行。

SELECT id, device_id, record_time, value, remark FROM device_record WHERE (record_time < :lastTime) OR (record_time = :lastTime AND id < :lastId) ORDER BY record_time DESC, id DESC LIMIT :pageSize;

这段 SQL 表示:取晚于当前页面最后一条记录的数据,同一时间点则按主键id继续往回取。这种方式也天然规避了 OFFSET 大偏移问题。

对应的 Qt 封装:

QVector<DeviceRecord> fetchPrevPage(const QDateTime &lastTime, qint64 lastId, int pageSize) { QVector<DeviceRecord> records; auto db = DatabaseManager::instance().connection(); QSqlQuery query(db); query.prepare(QStringLiteral( "SELECT id, device_id, record_time, value, remark " "FROM device_record " "WHERE (record_time < ?) " " OR (record_time = ? AND id < ?) " "ORDER BY record_time DESC, id DESC " "LIMIT ?")); query.addBindValue(lastTime.toString(Qt::ISODate)); query.addBindValue(lastTime.toString(Qt::ISODate)); query.addBindValue(lastId); query.addBindValue(pageSize); if (!query.exec()) { qWarning() << "fetch prev page failed:" << query.lastError().text(); return records; } while (query.next()) { DeviceRecord rec; rec.id = query.value(0).toLongLong(); rec.deviceId = query.value(1).toString(); rec.recordTime = QDateTime::fromString(query.value(2).toString(), Qt::ISODate); rec.value = query.value(3).toDouble(); rec.remark = query.value(4).toString(); records.append(rec); } return records; }

注意,这里往上一页翻时,返回的结果顺序是“倒序”的,实际展示到 UI 后需要反转,保持用户视觉上的顺序一致。判断是否还有上一页,可以多取一条记录,比如LIMIT pageSize + 1,如果返回条数大于 pageSize,说明还有更多数据。

6.4 方案 C:滚动游标类

如果多张表、多个排序字段都要复用,可以封装一个轻量的 Cursor 类。

class Cursor { public: explicit Cursor(QString table, QString orderColumn, QString idColumn = QStringLiteral("id")); void setPageSize(int size) { m_pageSize = size; } QVector<QSqlRecord> nextPage(); QVector<QSqlRecord> prevPage(); bool hasMore() const { return m_hasMore; } private: QString m_table; QString m_orderColumn; QString m_idColumn; int m_pageSize = 100; QVariant m_lastValue; QVariant m_lastId; bool m_hasMore = true; };

这个类内部记录m_lastValuem_lastIdnextPage()prevPage()生成不同的 SQL 语句,并把查询结果转换成QSqlRecord列表。这样业务层只需要关心数据模型,不需要关心 SQL 边界条件。

6.5 UI 层如何配合

分页方案确定后,UI 层建议使用 QAbstractTableModel 而不是 QTableWidget。QAbstractTableModel 可以做到“只加载可见区域附近的数据”,内存占用和刷新效率都远高于 QTableWidget。

以一个简单实现为例,模型内部维护当前已加载的记录列表:

class DeviceRecordModel : public QAbstractTableModel { Q_OBJECT public: explicit DeviceRecordModel(QObject *parent = nullptr); int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; void appendRecords(const QVector<DeviceRecord> &records); void clear(); bool canFetchMore(const QModelIndex &parent) const override; void fetchMore(const QModelIndex &parent) override; private: QVector<DeviceRecord> m_records; Cursor m_cursor; bool m_fetching = false; };

canFetchMore返回!m_fetching && m_cursor.hasMore(),当用户滚动到表格底部时,Qt 的视图会自动调用fetchMorefetchMore内部开启异步查询或者直接同步查询,拿到结果后调用beginInsertRowsendInsertRows刷新模型。

这里有一个关键点:如果你的查询很快,比如几十毫秒,可以直接在fetchMore里同步执行;如果查询比较慢,建议丢到子线程,完成后通过信号槽回到主线程更新模型。

6.6 随机跳转页与游标分页的取舍

游标分页的核心优势是“向后翻页快”,但无法快速跳到第 800000 行。如果你的业务逻辑必须支持任意跳转页,可以做一个折中:

  • 使用游标分页处理用户连续向下翻页。
  • 在界面单独提供一个“跳转到指定行”的入口,跳转时先用COUNT(*)或快速定位方式找到该行对应的排序值,再转换为游标参数。

这个方案比直接OFFSET 800000高效得多,又能兼容随机跳转需求。

7. 千万级数据写入与更新优化

7.1 用事务批量写入

逐行插入时,SQLite 每执行一次 INSERT 就是一次事务提交,磁盘同步开销非常大。千万级数据写入时,应手动开启事务。

bool batchInsertRecords(const QVector<DeviceRecord> &records) { auto db = DatabaseManager::instance().connection(); if (!db.transaction()) { qWarning() << "begin transaction failed"; return false; } QSqlQuery query(db); query.prepare(QStringLiteral( "INSERT INTO device_record (device_id, record_time, value, remark) " "VALUES (?, ?, ?, ?)")); for (const auto &rec : records) { query.addBindValue(rec.deviceId); query.addBindValue(rec.recordTime.toString(Qt::ISODate)); query.addBindValue(rec.value); query.addBindValue(rec.remark); if (!query.exec()) { qWarning() << "insert failed:" << query.lastError().text(); db.rollback(); return false; } } return db.commit(); }

实际测试时建议每次事务控制在 5000 到 10000 条,事务太大反而会导致回滚耗时过长和内存压力增加。

7.2 复用同一条 QSqlQuery

循环插入时,同一条QSqlQuery反复调用prepareaddBindValue即可,不需要每次循环都新建对象。SQLite 的语句准备(prepare)成本虽低,但在千万级循环中也要尽量避免浪费。

7.3 更新大量数据时只用必要的列

UPDATE语句尽量只更新变化字段,不要每次把一个对象的所有字段都写回数据库。写入字段越多,日志和页复制成本越高。

7.4 定时清理或归档

千万级数据量的应用,通常会遇到“数据会持续增长”的问题。合理的做法是:

  • 保留热数据在业务表。
  • 历史数据按月或按年归档到独立表。
  • 删除数据使用DELETE FROM device_record WHERE id BETWEEN ? AND ?,并放在事务中执行。

8. 数据访问层接口与后续扩展

8.1 为什么建议封装接口层

在 Qt 项目中,把 SQL 直接写在窗口类里会让后续维护非常痛苦。建议按“界面 -> 控制器 -> 数据访问层”的方式拆分。数据访问层对外提供干净的接口:

class DeviceRecordRepository { public: QVector<DeviceRecord> fetchPage(qint64 lastId, int pageSize); bool insert(const DeviceRecord &record); bool batchInsert(const QVector<DeviceRecord> &records); bool updateValue(qint64 id, double value); bool remove(qint64 id); qint64 count(); };

这样做的收益是:

  • UI 代码只关心数据和信号,不关心 SQL。
  • 后续如果换成 MySQL、PostgreSQL,只需要替换 Repository 实现。
  • 数据层可以单独做单元测试。

8.2 后续扩展为本地服务接口

如果桌面应用需要给其他端提供数据查询能力,可以在数据访问层之上再封装一个服务层。你可以使用 Qt 自带的QTcpServerQHttpServer提供本地 HTTP 接口。

此时要注意:

  • 服务默认只绑定127.0.0.1,不要直接暴露到公网。
  • 接口需要做鉴权和访问频率限制,防止其他本机进程非法读取数据。
  • 所有 SQL 参数必须绑定,不要拼接字符串。

典型调用示例(伪代码):

httpServer.route("/api/records", [](const QHttpServerRequest &request) { QString lastId = request.query().queryItemValue("lastId"); QString pageSize = request.query().queryItemValue("pageSize"); // 调用 Repository 查询 // 返回 JSON });

这个模式可以让桌面应用的数据能力被脚本、小程序或另一台电脑上的程序复用,但边界是必须确认使用方有合法权限访问这些数据。

9. 资源占用与性能观察

9.1 如何观察 SQLite 查询耗时

最简单的办法是使用QElapsedTimer

QElapsedTimer timer; timer.start(); auto records = repository.fetchPage(m_lastId, 100); qInfo() << "fetch page cost:" << timer.elapsed() << "ms";

分页查询应控制在使用户无感知的范围内,建议单页耗时小于 100ms。如果发现明显变慢,优先检查排序字段是否命中索引。

9.2 如何观察内存占用

Qt Creator 自带内存分析工具,也可以在任务管理器或系统资源监视器观察进程内存。QTableWidget 一旦加载大量 Item,内存会明显上升,所以建议运行项目后用测试数据点击“加载全部”和“游标分页”两种模式对比,这样能直观看到差距。

9.3 影响性能的关键因素

因素影响程度优化方向
排序字段是否有索引对所有 ORDER BY 字段建索引
一次返回行数页面大小控制在 50 到 200 行之间
WAL 模式开启PRAGMA journal_mode=WAL提升并发读
同步频率批量写入时使用PRAGMA synchronous=NORMAL
UI 控件类型用 QAbstractTableModel 代替 QTableWidget
是否在主线程执行大查询耗时查询放到子线程

9.4 降低资源占用的策略

  • 单页数据量控制在 100 行以内。
  • 模型只缓存已加载页的数据,窗口销毁或切换表时调用clear()释放。
  • 禁止SELECT *,只查询需要的列。
  • 图片或 BLOB 字段不要存在业务主表里,单独建关联表存储文件路径。

10. 常见问题与排查方法

问题现象可能原因排查方式解决方案
滚动表格卡顿主线程执行了全量查询用 QElapsedTimer 统计查询耗时改为游标分页,查询放入子线程或模型 fetchMore
翻页越来越慢使用了 OFFSET 大偏移分页查看 SQL 日志改为 Keyset 分页
插入 10 万条数据耗时过长每一条都自动提交事务检查事务开启情况手动开启事务批量提交
查询结果排序不稳定ORDER BY 字段有重复值观察连续翻页是否出现重复或遗漏排序字段加上主键 id 作为第二排序条件
数据库文件被锁定多线程共用同一个连接查看错误日志 “database is locked”每个线程使用独立连接名
界面显示数据后修改不生效模型没有触发 dataChanged 信号检查 model 是否调用 emit dataChanged更新后手动调用 dataChanged
删除大量数据后文件体积不变SQLite 默认不回收空闲页查看文件大小执行VACUUM或定期重构数据库
程序退出后数据丢失未执行事务提交或未关闭连接查看代码中是否调用 commit确保事务提交后再退出

10.1 多线程访问 SQLite 的注意事项

Qt 中每个线程创建数据库连接时,连接名必须不同。

void workerThread() { QString connectionName = QStringLiteral("worker_%1").arg(reinterpret_cast<quintptr>(QThread::currentThreadId())); QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), connectionName); db.setDatabaseName(filePath); db.open(); // 查询逻辑 db.close(); QSqlDatabase::removeDatabase(connectionName); }

只建议一个线程负责写操作,其他线程读。写线程和读线程之间的锁冲突由 SQLite 自己处理,但在高并发写入时仍可能出现SQLITE_BUSY。可以设置 busy timeout:

query.exec("PRAGMA busy_timeout = 5000;");

10.2 游标分页边界值处理

lastId为 0 时,WHERE id > 0相当于取第一页。当数据库为空或已没有更多数据时,返回结果小于 pageSize,此时应把hasMore置为 false。

可以在查询时多取一条记录来判断是否还有下一页:

int queryLimit = pageSize + 1; // 执行查询 bool hasMore = records.size() > pageSize; if (hasMore) { records.removeLast(); }

这种方式简单可靠,避免每次额外执行一次 COUNT 查询。

11. 最佳实践与使用建议

11.1 第一次先小参数测试

把一个 10 行数据的模型接入游标分页,确认翻页方向、顺序、边界正常后,再灌入百万级测试数据。不要在百万级数据上直接调试分页逻辑,会浪费大量时间。

11.2 保留一套最小可运行配置

项目里保留一个独立的TestDatabase测试类,数据库文件可以放在临时目录。每次改动数据层代码后,先跑测试类,确认 CRUD 和分页正常,再做界面联调。

11.3 合理划分目录

  • database/:数据库连接和脚本。
  • repository/:数据访问接口。
  • model/:Qt Model 层。
  • ui/:界面层。
  • data/test.sqlite:测试数据库。
  • output/:导出结果目录。

模型文件、输入素材、输出结果分目录管理,可以避免后续维护时到处找文件。

11.4 批量任务要加日志和失败重试

如果你的应用支持批量导入、批量导出,建议:

  • 每处理一条记录记录日志。
  • 失败记录单独保存到failed_records表。
  • 支持断点续跑,即批量导入中间失败后,下次从失败记录继续。

11.5 涉及数据库文件分发时的安全边界

  • 数据库文件不要放在程序安装目录,推荐放在用户数据目录,例如QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)
  • 如果包含敏感数据,使用 SQLCipher 或对关键字段做加密。
  • 对用户数据做备份机制,建议在应用启动时自动备份最近一个版本的数据库。

12. 总结与下一步

这篇文章的核心结论是:Qt + SQLite 完全可以承载千万级数据的 CRUD,前提是别再用“一次查询全量数据 + QTableWidget 全量加载”的老路子。游标分页可以让翻页操作稳定在极短耗时的范围内,配合 QAbstractTableModel 的增行机制,UI 流畅度会有非常明显的改善。

可以先从下面几步验证:

  1. 建一张百万行测试表,确认索引和 WAL 模式开启。
  2. 在项目里实现fetchNextPage,用日志打印每页查询耗时。
  3. 把表头控件换为 QAbstractTableModel 子类,接入canFetchMorefetchMore
  4. 测试往下翻 100 页、200 页,观察内存是否稳定。

最容易踩的坑有三个:排序字段忘加索引、分页时只用单字段排序、把 QSqlDatabase 连接跨线程共享。这些在代码评审时可以重点检查。

后续可以继续扩展的方向:把 Repository 层升级为可插拔实现,支持 MySQL 或 PostgreSQL;在数据访问层之上开放本地 HTTP 服务接口;为数据库文件增加加密和自动备份;把游标分页封装成通用 Qt 控件,供多个项目复用。

建议收藏备用,下次遇到 Qt 大数据量卡顿可以直接翻到“游标分页实战”这一节对照实现。

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

Substance Painter卡通毛发材质球制作全流程解析

在制作卡通动物角色的流程里&#xff0c;毛发往往是决定成品“像不像、柔不柔、萌不萌”的关键一环。之前我在做宠物类游戏项目时&#xff0c;反复被猫狗毛发的材质表现卡住&#xff1a;直接用写实毛发思路会显得脏乱&#xff0c;普通笔刷一刷又像贴纸条&#xff0c;找遍全网大…

作者头像 李华
网站建设 2026/9/2 19:40:03

C#实战开发学生成绩管理系统:从数据库设计到部署的完整指南

简介&#xff1a;一份面向C#初学者的学生成绩管理系统项目源码&#xff0c;适合课程设计、毕业设计或自学练手。系统围绕成绩录入、查询、统计、分析等核心功能展开&#xff0c;完整演示了三层架构下的表示层、业务逻辑层与数据访问层分工&#xff0c;覆盖SQL Server/MySQL数据…

作者头像 李华
网站建设 2026/9/2 19:38:35

LabVIEW虚拟仪器测控应用130例:从入门到实战的源程序拆解

简介&#xff1a;面向 LabVIEW 初学者的入门到测控应用实例合集&#xff0c;涵盖 130 例可直接运行的 VI 源程序&#xff0c;帮助零基础读者快速上手图形化编程&#xff0c;理解虚拟仪器的设计思路&#xff0c;并降低测控系统开发的入门门槛。压缩包采用 zip 格式&#xff0c;大…

作者头像 李华
网站建设 2026/9/2 19:36:34

WPE三件套实战:抓包、过滤与封包修改全攻略

简介&#xff1a;一份整合了游戏加速、代理共享与封包编辑的“三件套”工具包&#xff0c;主要面向游戏优化爱好者、局域网管理员以及计算机网络协议的入门学习者。工具内置三个核心组件&#xff1a;游戏加速组件负责网络调优&#xff0c;可有效缓解游戏中的高延迟与不稳定&…

作者头像 李华
网站建设 2026/9/2 19:35:55

AD2S1210驱动代码深度解析:五个例程的移植经验与调试避坑指南

简介&#xff1a;这是一套关于AD2S1210旋转变压器至数字转换器的驱动代码资源&#xff0c;面向伺服控制、电机控制及角度测量类项目的嵌入式开发者。资源基于官方例程整理&#xff0c;共包含五个独立示例&#xff0c;对应不同应用场景的初始化、配置、数据读取与状态监测流程。…

作者头像 李华
网站建设 2026/9/2 19:31:54

Claude+Seedance+剪映:从零搭建可复用的短视频动效工作流

上周末我给一条情绪向的慢动作视频做片头&#xff0c;折腾了两个小时&#xff0c;最后说服自己“就这样吧”。问题不在剪辑技术&#xff0c;而在素材。剪映的转场和特效很多&#xff0c;可一旦你想做出不那么模板化的效果&#xff0c;就会撞上同一堵墙&#xff1a;你找不到一段…

作者头像 李华