最近在完成矢量数据持久化到Spatialite数据库的功能,主要的应用场景,是为了通过Spatialite提供的基于RTree的空间索引能力,完成实时从db内请求某个范围内的矢量数据,获取其信息(点集、样式、字段),呈现在地图上,或者进行矢量信息处理相关的业务。
为什么选择Spatialite:
本章的重点,将主要放在N条矢量数据的插入效率的优化上面,本质就是Sqlite3插入效率的优化(任何关于矢量和Spatialite方面的都只是应用环境支撑,可以暂时忽略)。
(结合一个简单的应用环境验证,因为是结合GIS领域的开发,所以表设计和矢量信息表达有关)
提供一张名为【shape】的表,字段设计如下:
也就是说,后面的一条条数据将对应以上字段的结构插入。
(因为数据是从*.shp文件中获取出来的,所以用到了GDAL-OGR的方法解析矢量数据,不过不是本章的重点,仅仅是为了说明数据来源)
导入文件:【街区.shp】-44.4MB 矢量个数:266311个,即插入条数:266311条。
(OGR读取,点集转Blob数据,测试后不消耗时间,可以忽略)
Load -> TravelShpFile -> Unload
sqlite3提供了以下2种操作插入sql的方式:
关于Sqlite3插入优化的对比实践,将分为以下3种情况对比:
因为表字段有blob字段,所以没有直接使用sqlite3_exec()调用来进行分析;
以上的操作,可以理解为该函数封装成sqlite3_exec();
批量操作时,不推荐的一种调用方式,这里为了测试数据而封装。
bool CShapeGeometryDB::InsertShape(const StShapeInfo& stShapeInfo, unsigned char* pBlob, int nBlobSize)
{
std::string strSql = "INSERT INTO 'main'.'shape'('uuid', 'puuid', 'type', 'name', 'createtime', 'modifytime', 'geom') VALUES(?, ?, ?, ?, ?, ?, ?)";
if (NULL == m_pDB)
{
return false;
}
sqlite3_stmt* stmt;
int ret = sqlite3_prepare_v2(m_pDB, strSql.data(), strSql.size(), &stmt, NULL);
if (ret != SQLITE_OK)
{
std::cout << "INSERT prepare Error! " << sqlite3_errmsg(m_pDB) << std::endl;
return false;
}
sqlite3_bind_text(stmt, 1, stShapeInfo.m_strUUID.data(), stShapeInfo.m_strUUID.size(), NULL);
sqlite3_bind_text(stmt, 2, stShapeInfo.m_strPUUID.data(), stShapeInfo.m_strPUUID.size(), NULL);
sqlite3_bind_int(stmt, 3, stShapeInfo.m_nType);
sqlite3_bind_text(stmt, 4, stShapeInfo.m_strName.data(), stShapeInfo.m_strName.size(), NULL);
sqlite3_bind_text(stmt, 5, NULL, 0, NULL);
sqlite3_bind_text(stmt, 6, NULL, 0, NULL);
sqlite3_bind_blob(stmt, 7, pBlob, nBlobSize, free);
bool bOk = true;
ret = sqlite3_step(stmt);
if (ret != SQLITE_DONE && ret != SQLITE_ROW)
{
std::cout << "InsertShape sqlite3_step Error! " << sqlite3_errmsg(m_pDB) << std::endl;
bOk = false;
}
sqlite3_finalize(stmt);
return bOk;
}
bool CShapeGeometryDB::Transaction()
{
if (NULL == m_pDB)
{
return false;
}
char* err_msg = NULL;
std::string strSql = "BEGIN"; // 开启事务
int ret = sqlite3_exec(m_pDB, strSql.data(), NULL, NULL, &err_msg);
if (ret != SQLITE_OK)
{
if (NULL != err_msg)
{
std::cout << "BEGIN Error! " << err_msg << std::endl;
sqlite3_free(err_msg);
}
return false;
}
return true;
}
bool CShapeGeometryDB::Commit()
{
if (NULL == m_pDB)
{
return false;
}
char* err_msg = NULL;
std::string strSql = "COMMIT"; // 提交事务
int ret = sqlite3_exec(m_pDB, strSql.data(), NULL, NULL, &err_msg);
if (ret != SQLITE_OK)
{
if (NULL != err_msg)
{
std::cout << "COMMIT Error! " << err_msg << std::endl;
sqlite3_free(err_msg);
}
return false;
}
return true;
}
bool CShapeGeometryDB::RollBack()
{
if (NULL == m_pDB)
{
return false;
}
char* err_msg = NULL;
std::string strSql = "ROLLBACK"; // 回滚事务
int ret = sqlite3_exec(m_pDB, strSql.data(), NULL, NULL, &err_msg);
if (ret != SQLITE_OK)
{
if (NULL != err_msg)
{
std::cout << "COMMIT Error! " << err_msg << std::endl;
sqlite3_free(err_msg);
}
return false;
}
return true;
}
使用 sqlite3_stmt* 的成员变量来串起这次批量操作的执行;
Begin和End标志这次批量的开始和结束,也是sqlite3提供的“执行准备”,一次完整的闭环。
成员变量,初始化列表:NULL
sqlite3_stmt* m_stmt;
bool CShapeGeometryDB::BeginForBatch()
{
Transaction();
return true;
}
bool CShapeGeometryDB::InsertForBatch(const StShapeInfo& stShapeInfo, unsigned char* pBlob, int nBlobSize)
{
// reset
if (NULL != m_stmt)
{
sqlite3_reset(m_stmt);
sqlite3_clear_bindings(m_stmt);
}
// prepare
if (NULL == m_stmt)
{
std::string strSql = "INSERT INTO 'main'.'shape'('uuid', 'puuid', 'type', 'name', 'createtime', 'modifytime', 'geom') VALUES(?, ?, ?, ?, ?, ?, ?)";
int ret = sqlite3_prepare_v2(m_pDB, strSql.data(), strSql.size(), &m_stmt, NULL);
if (ret != SQLITE_OK)
{
std::cout << "INSERT prepare Error! " << sqlite3_errmsg(m_pDB) << std::endl;
return false;
}
}
// bind
sqlite3_bind_text(m_stmt, 1, stShapeInfo.m_strUUID.data(), stShapeInfo.m_strUUID.size(), NULL);
sqlite3_bind_text(m_stmt, 2, stShapeInfo.m_strPUUID.data(), stShapeInfo.m_strPUUID.size(), NULL);
sqlite3_bind_int(m_stmt, 3, stShapeInfo.m_nType);
sqlite3_bind_text(m_stmt, 4, stShapeInfo.m_strName.data(), stShapeInfo.m_strName.size(), NULL);
sqlite3_bind_text(m_stmt, 5, NULL, 0, NULL);
sqlite3_bind_text(m_stmt, 6, NULL, 0, NULL);
sqlite3_bind_blob(m_stmt, 7, pBlob, nBlobSize, free);
// step
bool bOk = true;
int ret = sqlite3_step(m_stmt);
if (ret != SQLITE_DONE && ret != SQLITE_ROW)
{
std::cout << "InsertShape sqlite3_step Error! " << sqlite3_errmsg(m_pDB) << std::endl;
bOk = false;
}
return bOk;
}
bool CShapeGeometryDB::EndForBatch()
{
if (NULL == m_stmt)
{
return false;
}
// finalize
sqlite3_finalize(m_stmt);
m_stmt = NULL;
Commit();
return true;
}
方法一:不开启事务,一个函数内封装[preparer -> reset -> bind -> step -> finalize] ,n次调用。
方法二:开启事务后,执行同上函数。
方法三:开启事务后,使用“执行准备”的方式。
综上所述:开启事务和不开启事务的区别是相当大的,毕竟频繁开启和关闭I/O的操作是最不推荐的,而开启了事务,相当于一次开启后,批量写入,然后一次关闭。
“执行准备”也相较普通的exec()操作快了一点,因为这是sqlite3提供的更细分的API来支持“准备 -> 执行 x N -> 编译”。
用方法三,导入1.02GB的shp文件,即6994448条数据插入:
共耗时:
1583000ms (26.38分钟)
每50000条插入统计:10125ms-15265ms范围浮动,没有逐步递增的规律
关于Sqlite的插入优化,还有其它的从para等配置的方式,改变一些机制来支持,比如:
Sqlite3的synchronous的模式选择
bool BuildParamaSynchronous()
{
std::string strSql = "PRAGMA synchronous = OFF";
return SQLITE_OK == sqlite3_exec(m_pHandle, strSql.data(), NULL, NULL, NULL);
}
有三种模式:
PRAGMA synchronous = FULL; (2)
PRAGMA synchronous = NORMAL; (1)
PRAGMA synchronous = OFF; (0)
可参考:https://blog.csdn.net/chinaclock/article/details/48622243