SQLite R*Tree 模塊測試
相關參考:
我另外做了GEOS STRtree/Quadtree 空間檢索的性能,測試代碼和數據可見Spatial_Index_Test
1、SQLite R*Tree 模塊特性簡介
關於SQLite的空間索引相關介紹可以查看官方文檔 The SQLite R*Tree Module ,這里只做簡單的介紹。
1、SQLite R *Tree模塊實現部分在其源代碼內(源碼下載頁面),無需另外合並。但是默認是沒有啟用的,啟用需要定義SQLITE_ENABLE_RTREE=1
宏再編譯。
2、SQLite R *Tree模塊采用虛擬表實現,每個R *Tree索引都是一個虛擬表。對於這個表,其第一列必須是64位有符號整數類型,作為主鍵。其它的列(2-12列)根據空間維度確定,每個維度包含一對(兩列),分別是該維度的最小和最大值。例如:一維R *Tree索引虛擬表包含3列,分別是Int64主鍵| 最小值| 最大值;二維R*Tree索引虛擬表包含5列,分別是Int64主鍵| 第一維最小值| 第一維最大值| 第二維最小值| 第二維最大值;3、4、5維R*Tree索引虛擬表列數情況的以此論推,SQLite R *Tree實現不支持寬度超過5維的R *樹。
3、對於各個維度的最大最小值列,SQLite中可以使用int32
或者float32
類型進行數據存儲。與其它常規表中的列不同,這里存儲就是二進制類型的值,而不是轉換為字符串。如果在插入數據的時候,使用了這兩者之外的類型,則會進行隱式轉換。
-- 創建整型坐標rtree索引虛擬表
CREATE VIRTUAL TABLE intrtree USING rtree_i32(id,x0,x1,y0,y1,z0,z1);
-- 創建浮點型坐標rtree索引虛擬表
CREATE VIRTUAL TABLE floatrtree USING rtree(id,x0,x1,y0,y1,z0,z1);
4、SQLite R *Tree中查詢並不限制查詢的維度一定要與所查詢的表中的維度一致,可以僅查詢其中的某幾個維度(如3維空間僅查詢2個維度)。一般來說,約束(維度)越多,查詢的范圍框越小,速度越快。
5、默認情況下使用float32
存儲坐標值,當無法精確表示傳入值時,下限坐標向下舍入,上限坐標向上舍入,因此邊界框可能略大於指定,但永遠不會變小。這在查詢某個范圍之外的數據時,可能會有極小的誤差。
6、對於3.24.0之前的版本,SQLite R *Tree索引虛擬表僅能存儲整數主鍵和坐標值列,其它的信息需要另存於其它表中(通過主鍵進行關聯)。從3.24.0版本開始,SQLite R *Tree索引虛擬表可以存儲任意類型數據的輔助列,輔助列必須以+
開頭,最多可以存儲100個輔助列。
CREATE VIRTUAL TABLE demo_index2 USING rtree(
id, -- 64位整型主鍵
minX, maxX, -- X方向最小最大值
minY, maxY, -- Y方向最小最大值
+objname TEXT, -- 輔助列 文本類型
+objtype TEXT, -- 輔助列 文本類型
+boundary BLOB -- 輔助列 二進制數據
);
7、可以自定義R-Tree查詢,以便實現非矩形框碰撞。這需要通過sqlite3_rtree_query_callback
(新,3.8.5開始提供)或sqlite3_rtree_geometry_callback
(舊)注冊查詢SQL語句和匹配檢測回調。相關信息在SQLite網站上有詳細介紹。
8、一個SQLite R *Tree會附帶三個影子表,用於存儲數據,分別是虛擬表名_node
(存儲節點) 虛擬表名_parent
(存儲父節點) 虛擬表名_rowid
(存儲節點的rowid)。
9、可以使用SELECT rtreecheck('虛擬表名')
來對R-Tree索引進行完整性和正確性檢查。
2、SQLite R*Tree 模塊簡單測試代碼
寫了一個簡單的測試程序來測試一下R *Tree樹的速度,結果還是可以的。(可用,並不是最佳)
我的機器環境是:
Windows 10 1903 x64專業版
AMD 銳龍 2600X
DDR4 2400 8G
編譯器:VS2017 Native x64
使用本地文件的時候,十萬條數據插入時間大概在2秒以內,查詢一個5x5度大小的范圍,時間基本在0.07秒以內;使用內存模式時,插入時間大概在1.8秒以內,查詢一個5x5度大小的范圍,時間基本在0.04秒以內。
注意:編譯SQLite的時候要定義SQLITE_ENABLE_RTREE
宏,開啟RTree索引支持。
#include "sqlite/sqlite3.h"
#include<time.h>
#include <stdlib.h>
#include <stdio.h>
// 因為僅僅是進行一下試用測試,所以有些地方就沒有處理,包括close
int main()
{
sqlite3* db = NULL;
int rc = sqlite3_open(":memory:", &db);
// int rc = sqlite3_open("D:/sqlite_rtree/test.db", &db);
if (rc != SQLITE_OK)
{
return -1;
}
char* errmsg;
// 創建RTree索引虛擬表
rc = sqlite3_exec(db,
"CREATE VIRTUAL TABLE demo_index USING rtree(id,minX, maxX,minY, maxY,+axucol INTEGER NOT NULL)",
NULL, NULL, &errmsg);
if (rc != SQLITE_OK)
{
printf("%4d Error:%sn", __LINE__, errmsg);
return -2;
}
// 開始計時
clock_t start = clock();
// 開啟事物
if (sqlite3_exec(db, "begin", NULL, NULL, &errmsg) != SQLITE_OK) {
printf("%4d Error:%sn", __LINE__, errmsg);
return -2;
}
// 生成十萬個大小在 邊長在[0.002,0.202]度大小以內的數據(0.2~22.5公里左右)
srand(time(NULL)); // 初始化隨機數種子
sqlite3_stmt *pStmt = NULL;
// 預處理SQL語句
if(sqlite3_prepare_v2(db,
"INSERT INTO demo_index VALUES(?,?,?,?,?,?)",
-1, &pStmt, NULL) != SQLITE_OK) {
printf("%4d Error:%sn", __LINE__, errmsg);
return -3;
}
// 逐個插入
for (int i = 0; i < 100000; ++i) {
// 生成在經緯度范圍內的x,y
double x0 = ((double)rand() / (double)RAND_MAX) * 360 - 180;
double y0 = ((double)rand() / (double)RAND_MAX) * 180 - 90;
double x1 = x0 + 0.002 + ((double)rand() / (double)RAND_MAX)*0.2;
double y1 = y0 + 0.002 + ((double)rand() / (double)RAND_MAX)*0.2;
// 綁定數據
sqlite3_bind_int64(pStmt, 1, i);
sqlite3_bind_double(pStmt, 2, x0);
sqlite3_bind_double(pStmt, 3, x1);
sqlite3_bind_double(pStmt, 4, y0);
sqlite3_bind_double(pStmt, 5, y1);
sqlite3_bind_int(pStmt, 6, rand()%3);
// 執行
sqlite3_step(pStmt);
// 重置
sqlite3_reset(pStmt);
}
sqlite3_finalize(pStmt); //結束語句,釋放語句句柄
// 結束事物
if (sqlite3_exec(db, "commit", NULL, NULL, &errmsg) != SQLITE_OK){
printf("%4d Error:%sn", __LINE__, errmsg);
return -2;
}
// 結束計時
clock_t end = clock();
double hs = (double)(end - start) * 1000 / CLOCKS_PER_SEC;
printf("插入總耗時: %lf msn", hs);
// 查詢
// select * from test where NOT(maxX<74.254915 OR minX>79.765758 OR maxY< 24.214285 OR minY>29.725129) AND auxcol==2 ORDER BY id;
// 預處理SQL語句
pStmt = NULL;
if (sqlite3_prepare_v2(db,
"SELECT id,minX,minY,auxcol FROM demo_index WHERE NOT(maxX<? OR minX>? OR maxY<? OR minY>?) AND auxcol==1;",
-1, &pStmt, NULL) != SQLITE_OK) {
printf("%4d Error:%sn", __LINE__, errmsg);
return -4;
}
//-------------------------------------------------------------------------
// 輸入查詢的范圍框數據
puts("Input x0,x1,y0,y1:");
double x0, x1, y0, y1;
scanf("%lf,%lf,%lf,%lf", &x0, &x1, &y0, &y1);
printf("-----------[%lf,%lf,%lf,%lf]-------------n", x0, x1, y0, y1);
// 開始計時
start = clock();
// 綁定查詢范圍數據
sqlite3_bind_double(pStmt, 1, x0);
sqlite3_bind_double(pStmt, 2, x1);
sqlite3_bind_double(pStmt, 3, y0);
sqlite3_bind_double(pStmt, 4, y1);
while (sqlite3_step(pStmt) == SQLITE_ROW) {
int id = sqlite3_column_int(pStmt, 0);
double x = sqlite3_column_double(pStmt, 1);
double y = sqlite3_column_double(pStmt, 2);
int auxcol = sqlite3_column_int(pStmt, 3);
// 可以把輸出去掉,減少對時間統計的影響
printf("%dt %lf,%lfn", id, x, y);
}
sqlite3_reset(pStmt); // 這里只查詢一次可以沒有,如果需要多次使用這個查詢語句,則必須有,不然查出數據不對
sqlite3_finalize(pStmt); //結束語句,釋放語句句柄
// 結束計時
end = clock();
hs = (double)(end - start) * 1000 / CLOCKS_PER_SEC;
printf("本次查詢總耗時: %lf msn", hs);
sqlite3_close(db);
system("pause");
return 0;
}