小。快。灵活。
选择所有三个。
许多开发者考虑在他们的项目中使用数据库,目的是进行数据存储,但他们还在对此犹豫,知道安装 SQL 服务器需要多少额外时间。然而对程序员来讲这不是很难 (如果数据库管理系统 (DBMS) 已经因其它目的而安装), 它对一般用户确实是个问题,以至最终对此安装灰心。
许多开发者认识到这样做会导致他们的工作仅有少数人使用,所以他们选择不处理 DBMS。结果, 他们转而使用文件 (经常会处理多于一个文件, 使用多种数据变种): CSV, 较少的 XML 或 JSON, 或限定结构的尺寸的二进制文件, 等等。
然而,事实证明,有一个极好的 SQL 服务器替代者!而你甚至不需要安装额外的软件,因为您的项目一切都在本地完成,同时还允许您使用 SQL 的强大功能。我们来讨论 SQLite。
本文目的是令您快速开始应用 SQLite。因此我不再罗嗦参数集和函数标志等细微之处,而是创建一个轻量连接包装来执行 SQL 命令,并演示它的使用。
继续阅读本文,您需要:
目录
1. SQLite 原理
2. SQLite3 API
2.1. 打开和关闭数据库
2.2. 执行 SQL 查询
2.3. 从表中获取数据
2.4. 通过绑定写参数数据
2.5. 事务 / 多行插入 (创建交易账户成交表例子)
3. 编译 64-位版本 (sqlite3_64.dll)
SQLite 是一款关系型数据库,其关键特征是无需安装本地 SQL 服务器。您的应用看上去像服务器。与 SQLite 数据库集成,基本上就是与一个文件打交道 (在磁盘上或 在内存中)。所有数据可以被存档或是移动到另外的电脑上而无需以任何特别方式安装它们。
协同 SQLite,开发者和用户可以从若干难以否认的优势中获益:
有三种方式与 SQLite 工作:
在本文中, 我将描述第一个选项, 用最习惯的 MQL5。
此连接器操作需要使用以下 SQLite 函数:
//--- general functions sqlite3_open sqlite3_prepare sqlite3_step sqlite3_finalize sqlite3_close sqlite3_exec //--- functions for getting error descriptions sqlite3_errcode sqlite3_extended_errcode sqlite3_errmsg //--- functions for saving data sqlite3_bind_null sqlite3_bind_int sqlite3_bind_int64 sqlite3_bind_double sqlite3_bind_text sqlite3_bind_blob //--- functions for getting data sqlite3_column_count sqlite3_column_name sqlite3_column_type sqlite3_column_bytes sqlite3_column_int sqlite3_column_int64 sqlite3_column_double sqlite3_column_text sqlite3_column_blob
您还需要底层 msvcrt.dll 函数来与指针工作:
strlen strcpy memcpy
因为我正在创建的连接器需要支持 32 位和64 位的客户端,考虑传递到 API 函数的指针尺寸就十分重要。让我们分离它们的名称:
// for a 32 bit terminal #define PTR32 int #define sqlite3_stmt_p32 PTR32 #define sqlite3_p32 PTR32 #define PTRPTR32 PTR32 // for a 64 bit terminal #define PTR64 long #define sqlite3_stmt_p64 PTR64 #define sqlite3_p64 PTR64 #define PTRPTR64 PTR64
如果有必要, 所有 API 函数将被针对 32 和 64 位重载指针。请注意所有连接器指针将是 64 位的。它们将在重载 API 函数中被直接转换到 32 位。此 API 函数导入的源代码在 SQLite3Import.mqh 中提供。
SQLite 数据类型
在 SQLite 版本 3 中有五种数据类型
类型 |
描述 |
---|---|
NULL | 空值。 |
INTEGER | 整数值 1, 2, 3, 4, 6, 或 8 字节 (根据存储值的大小)。 |
REAL | 8-字节实数。 |
TEXT | 文本字符串。结尾字符为 \0 使用 UTF-8 或 UTF-16 编码存储。 |
BLOB | 任意的二进制数据 |
当使用 SQL 查询创建一个表时,字段的数据类型您还可以使用其它类型名称指定, 如在其它多种 DBMS 中接受的 BIGINT 或 INT。在此情况下, SQLite 将转换它们至内在的类型,如 INTEGER。有关数据类型及其相互关系的进一步信息,请阅读文档 http://www.sqlite.org/datatype3.html
2.1. 打开与关闭数据库
正如您已经知道的, 在 SQLite3 中一个数据库就是一个正常文件。所以打开一个数据库事实上等同于打开一个文件并获取它的句柄。
它可以使用 sqlite3_open 函数完成:
int sqlite3_open(const uchar &filename[], sqlite3_p64 &ppDb); filename [in] - 路径,或当前位置打开的文件名。ppDb [out] -保存文件句柄的变量。若成功函数返回 SQLITE_OK,或一个错误代码。
关闭一个数据库文件使用 sqlite3_close 函数:
int sqlite3_close(sqlite3_p64 ppDb); ppDb [in] - 文件句柄 若成功函数返回 SQLITE_OK,或一个错误代码。
让我们在当前连接器中创建数据库打开和关闭函数。
//+------------------------------------------------------------------+ //| CSQLite3Base class | //+------------------------------------------------------------------+ class CSQLite3Base { sqlite3_p64 m_db; // pointer to database file bool m_bopened; // flag "Is m_db handle valid" string m_dbfile; // path to database file public: CSQLite3Base(); // constructor virtual ~CSQLite3Base(); // destructor public: //--- connection to database bool IsConnected(); int Connect(string dbfile); void Disconnect(); int Reconnect(); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CSQLite3Base::CSQLite3Base() { m_db=NULL; m_bopened=false; } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CSQLite3Base::~CSQLite3Base() { Disconnect(); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool CSQLite3Base::IsConnected() { return(m_bopened && m_db); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ int CSQLite3Base::Connect(string dbfile) { if(IsConnected()) return(SQLITE_OK); m_dbfile=dbfile; return(Reconnect()); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CSQLite3Base::Disconnect() { if(IsConnected()) ::sqlite3_close(m_db); m_db=NULL; m_bopened=false; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ int CSQLite3Base::Reconnect() { Disconnect(); uchar file[]; StringToCharArray(m_dbfile,file); int res=::sqlite3_open(file,m_db); m_bopened=(res==SQLITE_OK && m_db); return(res); }
此连接器现在可以打开和关闭一个数据库。现在通过一个简单脚本来检查它的性能:
#include <MQH\Lib\SQLite3\SQLite3Base.mqh> CSQLite3Base sql3; // database connector //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnStart() { //--- open database connection if(sql3.Connect("SQLite3Test.db3")!=SQLITE_OK) return; //--- close connection sql3.Disconnect(); }
在调试模式中运行此脚本, 做一次深呼吸并检查每个字符串的操作结果。作为结果, 一个数据库文件将出现在 MetaTrader 5 客户端安装文件夹中。祝贺您自己的成功并进入下一阶段。
2.2. 执行 SQL 查询
任何在 SQLite3 中的 SQL 查询都要经过三个阶段:
这种结构主要适用于创建或删除表,以及写非二进制数据,亦即,在那些 SQL 查询除了执行状态,并不返回其它数据时。
如果查询涉及接收数据或写二进制数据, sqlite3_column_хх 或 sqlite3_bind_хх 函数分别用在第二个阶段。这些函数的详细描述在下一段。
让我们编写 CSQLite3Base::Query 方法来执行一个简单的 SQL 查询:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ int CSQLite3Base::Query(string query) { //--- check connection if(!IsConnected()) if(!Reconnect()) return(SQLITE_ERROR); //--- check query string if(StringLen(query)<=0) return(SQLITE_DONE); sqlite3_stmt_p64 stmt=0; // variable for pointer //--- get pointer PTR64 pstmt=::memcpy(stmt,stmt,0); uchar str[]; StringToCharArray(query,str); //--- prepare statement and check result int res=::sqlite3_prepare(m_db,str,-1,pstmt,NULL); if(res!=SQLITE_OK) return(res); //--- execute res=::sqlite3_step(pstmt); //--- clean ::sqlite3_finalize(pstmt); //--- return result return(res); }
如您所见, sqlite3_prepare, sqlite3_step 和 sqlite3_finalize 函数是一个接一个的执行。
考虑在 SQLite 中对数据表执行 CSQLite3Base::Query :
// Create the table (CREATE TABLE) sql3.Query("CREATE TABLE IF NOT EXISTS `TestQuery` (`ticket` INTEGER, `open_price` DOUBLE, `comment` TEXT)");
在命令执行后, 数据表出现在数据库:
// Rename the table (ALTER TABLE RENAME) sql3.Query("ALTER TABLE `TestQuery` RENAME TO `Trades`"); // Add the column (ALTER TABLE ADD COLUMN) sql3.Query("ALTER TABLE `Trades` ADD COLUMN `profit`");
在执行这些命令后, 我们收到数据表的名称以及一个附加字段:
// Add the row (INSERT INTO) sql3.Query("INSERT INTO `Trades` VALUES(3, 1.212, 'info', 1)"); // Update the row (UPDATE) sql3.Query("UPDATE `Trades` SET `open_price`=5.555, `comment`='New price' WHERE(`ticket`=3)")
以下条目会在新数据行加入和修改后出现在数据表中:
最后, 以下命令将会在其它命令执行后清理数据库。
// Delete all rows from the table (DELETE FROM) sql3.Query("DELETE FROM `Trades`") // Delete the table (DROP TABLE) sql3.Query("DROP TABLE IF EXISTS `Trades`"); // Compact database (VACUUM) sql3.Query("VACUUM");
在移步到下一段之前, 我们需要接受错误描述的方法。从我自己的经验我可以说,在 SQL 查询中提供的错误码可以提供很丰富的信息,但错误描述对于检测和修复则过于简单。
const PTR64 sqlite3_errmsg(sqlite3_p64 db); db [in] - handle received by function sqlite3_open 指针返回的字符串包含错误描述。
在连接器中, 我们应该加入从指针接收字符串的方法,这要使用 strcpy 和 strlen。
//+------------------------------------------------------------------+ //| Error message | //+------------------------------------------------------------------+ string CSQLite3Base::ErrorMsg() { PTR64 pstr=::sqlite3_errmsg(m_db); // get message string int len=::strlen(pstr); // length of string uchar str[]; ArrayResize(str,len+1); // prepare buffer ::strcpy(str,pstr); // read string to buffer return(CharArrayToString(str)); // return string }
2.3. 从表中获取数据
正如我已经提到的,在 2.2 段的开始, 数据读取要执行 sqlite3_column_хх 函数。示意如下:
由于我们正在接近广泛的有关数据读写部分,这是一个很好的时机来描述整个数据交换中使用的三个容器类。必要的数据模型依赖于如何将数据存储在数据库中:
数据库
|
表 是数据行数组。
|
行 是单元数组。
|
单元 是一个任意长度的字节缓冲区。
//+------------------------------------------------------------------+ //| CSQLite3Table class | //+------------------------------------------------------------------+ class CSQLite3Table { public: string m_colname[]; // column name CSQLite3Row m_data[]; // database rows //... };
//+------------------------------------------------------------------+ //| CSQLite3Row class | //+------------------------------------------------------------------+ class CSQLite3Row { public: CSQLite3Cell m_data[]; //... };
//+------------------------------------------------------------------+ //| CSQLite3Cell class | //+------------------------------------------------------------------+ class CSQLite3Cell { public: enCellType type; CByteImg buf; //... };
如您所见, CSQLite3Row 和 CSQLite3Table 连接是元数据 - 这些都是传统的数据数组。CSQLite3Cell 单元类也有 uchar 数组 + 数据类型域。字节数组在 CByteImage 类中实现 (类似著名的 CFastFile)。
我已经创建了以下枚举,以方便连接器的操作和管理单元中的数据类型:
enum enCellType
{
CT_UNDEF,
CT_NULL,
CT_INT,
CT_INT64,
CT_DBL,
CT_TEXT,
CT_BLOB,
CT_LAST
};
注意这个 CT_UNDEF 类型已经被加到五个基础 SQLite3 类型中,来标识出示单元状态。完整的 INTEGER 分为 CT_INT 和 CT_INT64,同理划分 sqlite3_bind_intXX 和 sqlite3_column_intXX 函数。
获取数据
为了从单元中得到数据, 我们将创建方法生成 sqlite3_column_хх 类型函数。它将检查数据类型和尺寸并将之写入 CSQLite3Cell。
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool CSQLite3Base::ReadStatement(sqlite3_stmt_p64 stmt,int column,CSQLite3Cell &cell) { cell.Clear(); if(!stmt || column<0) return(false); int bytes=::sqlite3_column_bytes(stmt,column); int type=::sqlite3_column_type(stmt,column); //--- if(type==SQLITE_NULL) cell.type=CT_NULL; else if(type==SQLITE_INTEGER) { if(bytes<5) cell.Set(::sqlite3_column_int(stmt,column)); else cell.Set(::sqlite3_column_int64(stmt,column)); } else if(type==SQLITE_FLOAT) cell.Set(::sqlite3_column_double(stmt,column)); else if(type==SQLITE_TEXT || type==SQLITE_BLOB) { uchar dst[]; ArrayResize(dst,bytes); PTR64 ptr=0; if(type==SQLITE_TEXT) ptr=::sqlite3_column_text(stmt,column); else ptr=::sqlite3_column_blob(stmt,column); ::memcpy(dst,ptr,bytes); if(type==SQLITE_TEXT) cell.Set(CharArrayToString(dst)); else cell.Set(dst); } return(true); }
这个函数十分巨大, 但它仅从当前语句中读数据并保存在一个单元中。
我们也重载 CSQLite3Base::Query 函数,添加 CSQLite3Table 容器表来接收数据作为第一个参数。
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ int CSQLite3Base::Query(CSQLite3Table &tbl,string query) { tbl.Clear(); //--- check connection if(!IsConnected()) if(!Reconnect()) return(SQLITE_ERROR); //--- check query string if(StringLen(query)<=0) return(SQLITE_DONE); //--- sqlite3_stmt_p64 stmt=NULL; PTR64 pstmt=::memcpy(stmt,stmt,0); uchar str[]; StringToCharArray(query,str); int res=::sqlite3_prepare(m_db, str, -1, pstmt, NULL); if(res!=SQLITE_OK) return(res); int cols=::sqlite3_column_count(pstmt); // get column count bool b=true; while(::sqlite3_step(pstmt)==SQLITE_ROW) // in loop get row data { CSQLite3Row row; // row for table for(int i=0; i<cols; i++) // add cells to row { CSQLite3Cell cell; if(ReadStatement(pstmt,i,cell)) row.Add(cell); else { b=false; break; } } tbl.Add(row); // add row to table if(!b) break; // if error enabled } // get column name for(int i=0; i<cols; i++) { PTR64 pstr=::sqlite3_column_name(pstmt,i); if(!pstr) { tbl.ColumnName(i,""); continue; } int len=::strlen(pstr); ArrayResize(str,len+1); ::strcpy(str,pstr); tbl.ColumnName(i,CharArrayToString(str)); } ::sqlite3_finalize(stmt); // clean return(b?SQLITE_DONE:res); // return result code }
我们已经有了全部接收数据所必需的函数。让我们通过例子:
// Read data (SELECT) CSQLite3Table tbl; sql3.Query(tbl, "SELECT * FROM `Trades`")
在客户端上打印出查询结果,使用以下命令 Print(TablePrint(tbl))。我们将在日志中看到以下条目 (顺序是从下到上):
// Sample calculation of stat. data from the tables (COUNT, MAX, AVG ...) sql3.Query(tbl, "SELECT COUNT(*) FROM `Trades` WHERE(`profit`>0)") sql3.Query(tbl, "SELECT MAX(`ticket`) FROM `Trades`") sql3.Query(tbl, "SELECT SUM(`profit`) AS `sumprof`, AVG(`profit`) AS `avgprof` FROM `Trades`")
// Get the names of all tables in the base sql3.Query(tbl, "SELECT `name` FROM `sqlite_master` WHERE `type`='table' ORDER BY `name`;");
查询结果的打印输出采用相同方式 Print(TablePrint(tbl))。我们可以看到存在的数据表:
正如从例子中看到的那样, 查询执行结果放在 tbl 变量中。在这之后, 您可以容易地获取并处理它们。
2.4. 写参数数据与绑定
另一个对初学者很重要的话题就是写 "不方便的" 格式的数据到数据库。当然, 此处我们的意思是指二进制数据。它不能被直接传递到通用的 INSERT 或 UPDATE 文本语句中, 当遇到第一个零时,它会被整个认作字符串。相同的问题发生在字符串里包含单引号的情况 '。
后期绑定也许在有些情况下有用, 特别是当表很宽。将所有字段写入单行是困难的,并且不可靠,因为您会很容易的丢失一些东西。函数 sqlite3_bind_хх 系列对于绑定操作是必需的。
为了应用绑定, 插入模板来替代传递数据。我将考虑一种情况 - "?" 符号。换句话说, UPDATE 查询将看上去如下:
之后, sqlite3_bind_double 和 sqlite3_bind_text 函数一接一个执行来放置数据至 open_price 和 comment。通常, 与 bind 函数工作可以用以下方式代表:
所有的类型数量 sqlite3_bind_xx ,都要完整重复以上描述的阅读功能。因此,您可以轻松地将它们结合在连接器 CSQLite3Base::BindStatement:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool CSQLite3Base::BindStatement(sqlite3_stmt_p64 stmt,int column,CSQLite3Cell &cell) { if(!stmt || column<0) return(false); int bytes=cell.buf.Len(); enCellType type=cell.type; //--- if(type==CT_INT) return(::sqlite3_bind_int(stmt, column+1, cell.buf.ViewInt())==SQLITE_OK); else if(type==CT_INT64) return(::sqlite3_bind_int64(stmt, column+1, cell.buf.ViewInt64())==SQLITE_OK); else if(type==CT_DBL) return(::sqlite3_bind_double(stmt, column+1, cell.buf.ViewDouble())==SQLITE_OK); else if(type==CT_TEXT) return(::sqlite3_bind_text(stmt, column+1, cell.buf.m_data, cell.buf.Len(), SQLITE_STATIC)==SQLITE_OK); else if(type==CT_BLOB) return(::sqlite3_bind_blob(stmt, column+1, cell.buf.m_data, cell.buf.Len(), SQLITE_STATIC)==SQLITE_OK); else if(type==CT_NULL) return(::sqlite3_bind_null(stmt, column+1)==SQLITE_OK); else return(::sqlite3_bind_null(stmt, column+1)==SQLITE_OK); }
这个方法仅有的目的就是写传递单元缓存区至语句。
让我们用简单的方式加入 CQLite3Table::QueryBind 方法。它的第一个参数是写入的数据字符串:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ int CSQLite3Base::QueryBind(CSQLite3Row &row,string query) // UPDATE <table> SET <row>=?, <row2>=? WHERE (cond) { if(!IsConnected()) if(!Reconnect()) return(SQLITE_ERROR); //--- if(StringLen(query)<=0 || ArraySize(row.m_data)<=0) return(SQLITE_DONE); //--- sqlite3_stmt_p64 stmt=NULL; PTR64 pstmt=::memcpy(stmt,stmt,0); uchar str[]; StringToCharArray(query,str); int res=::sqlite3_prepare(m_db, str, -1, pstmt, NULL); if(res!=SQLITE_OK) return(res); //--- bool b=true; for(int i=0; i<ArraySize(row.m_data); i++) { if(!BindStatement(pstmt,i,row.m_data[i])) { b=false; break; } } if(b) res=::sqlite3_step(pstmt); // executed ::sqlite3_finalize(pstmt); // clean return(b?res:SQLITE_ERROR); // result }
它的目的是写字符串至适当的参数。
2.5. 事务 / 多行插入
在进入本话题之前, 您需要了解一些更多的 SQLite API 函数。在之前的段落, 我已经描述了三阶段请求处理: 准备+执行+完成。然而,还存在另一种 (在某些情况下,简单的或更严重的) 方案 – sqlite3_exec 函数:
int sqlite3_exec(sqlite3_p64 ppDb, const char &sql[], PTR64 callback, PTR64 pvoid, PTRPTR64 errmsg); ppDb [in] - database handle sql [in] - SQL query The remaining three parameters are not considered yet in relation to MQL5. 当成功时,它返回 SQLITE_OK,否则是一个错误代码。
它的主要目的是在单个单元中执行查询而无需创建三阶段构造。
让我们在连接器中加入它的调用:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ int CSQLite3Base::Exec(string query) { if(!IsConnected()) if(!Reconnect()) return(SQLITE_ERROR); if(StringLen(query)<=0) return(SQLITE_DONE); uchar str[]; StringToCharArray(query,str); int res=::sqlite3_exec(m_db,str,NULL,NULL,NULL); return(res); }
结果方法很容易使用。例如, 您可以执行表删除 (DROP TABLE) 或数据库压缩 (VACUUM) 命令,如下方式:
sql3.Exec("DROP TABLE `Trades`"); sql3.Exec("VACUUM");
事务
现在, 假设我们不得不加入几千行数据到表中。如果我们在循环中插入所有这些:
for (int i=0; i<N; i++) sql3.Query("INSERT INTO `Table` VALUES(1, 2, 'text')");
执行将会很慢 (超过 10(!) 秒)。所以, 如此实现在 SQLite 中是 不建议的。在此,最合适的方案是使用 事务: 所有 SQL 语句的输入进入到通用列表,并传递到单一查询语句中。
以下 SQL 语句用于写事务开始和结束:
BEGIN ... COMMIT
所有内容在最后的 COMMIT 语句执行。ROLLBACK 语句用于中断循环或停止已经加入的语句执行。
举例如下, 所有账户成交加入表。
#include <MQH\Lib\SQLite3\SQLite3Base.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnStart() { CSQLite3Base sql3; //--- open database connection if(sql3.Connect("Deals.db3")!=SQLITE_OK) return; //--- if(sql3.Query("CREATE TABLE IF NOT EXISTS `Deals` (`ticket` INTEGER PRIMARY KEY, `open_price` DOUBLE, `profit` DOUBLE, `comment` TEXT)")!=SQLITE_DONE) { Print(sql3.ErrorMsg()); return; } //--- create transaction if(sql3.Exec("BEGIN")!=SQLITE_OK) { Print(sql3.ErrorMsg()); return; } HistorySelect(0,TimeCurrent()); //--- dump all deals from terminal to table for(int i=0; i<HistoryDealsTotal(); i++) { CSQLite3Row row; long ticket=(long)HistoryDealGetTicket(i); row.Add(ticket); row.Add(HistoryDealGetDouble(ticket, DEAL_PRICE)); row.Add(HistoryDealGetDouble(ticket, DEAL_PROFIT)); row.Add(HistoryDealGetString(ticket, DEAL_COMMENT)); if(sql3.QueryBind(row,"REPLACE INTO `Deals` VALUES("+row.BindStr()+")")!=SQLITE_DONE) { sql3.Exec("ROLLBACK"); Print(sql3.ErrorMsg()); return; } } //--- end transaction if(sql3.Exec("COMMIT")!=SQLITE_OK) return; //--- get statistical information from table CSQLite3Table tbl; CSQLite3Cell cell; if(sql3.Query(tbl,"SELECT COUNT(*) FROM `Deals` WHERE(`profit`>0)")!=SQLITE_DONE) { Print(sql3.ErrorMsg()); return; } tbl.Cell(0,0,cell); Print("Count(*)=",cell.GetInt64()); //--- if(sql3.Query(tbl,"SELECT SUM(`profit`) AS `sumprof`, AVG(`profit`) AS `avgprof` FROM `Deals`")!=SQLITE_DONE) { Print(sql3.ErrorMsg()); return; } tbl.Cell(0,0,cell); Print("SUM(`profit`)=",cell.GetDouble()); tbl.Cell(0,1,cell); Print("AVG(`profit`)=",cell.GetDouble()); }
当脚本在账户应用后, 它立即在表中插入账户成交记录。
在客户端日志中显示统计
您可以随意试验此脚本: 注释掉包含 BEGIN, ROLLBACK 和 COMMIT 的语句。如果您的账户里有超过数百的成交记录, 您将立即看到差异。顺便说, 根据 一些 测试, SQLite 事务的工作速度比 MySQL 或 PostgreSQL 还快。
我希望本文能成为您掌握 SQLite 的不可缺少的指南。也许, 您会在将来的项目中用到它。这个简短的概述提供了一些对于 SQLite 功能的洞察,可作为完美并可靠的解决方案。
在本文中,我已经描述了所有当您处理交易数据时可能面对的情况。作为一门功课,我建议您开发一个简单的即时价格收集器,插入每个品种的即时价格到表中。您可以在下面的附件中找到类库源码以及测试脚本。
我希望您有好运,并且获利丰厚!
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...
移动端课程