当前位置:   article > 正文

转 -- 内存数据库性能评测之SQLite数据库_嵌入式数据库sqlite性能测试=指标

嵌入式数据库sqlite性能测试=指标

原址如下:

http://tech.it168.com/a2012/1016/1408/000001408814_all.shtml

内存数据库性能评测之SQLite数据库

【IT168 专稿】SQLite是一款轻型的数据库,它占用资源非常的低,同时能够跟很多程序语言相结合,但是支持的SQL语句不会逊色于其他开源数据库。它的设计目标是嵌入式的,而且目前已经在很多嵌入式产品中使用了它,它占用资源非常的低,在嵌入式设备中,可能只需要几百K的内存就够了。它能够支持Windows/Linux/Unix等等主流的操作系统,同时能够跟很多程序语言相结合,比如Tcl、PHP、Java 等,还有ODBC接口,同样比起Mysql、PostgreSQL这两款开源世界著名的数据库管理系统来讲,它的处理速度比他们都快。

  SQLite虽然很小巧,但是支持的SQL语句不会逊色于其他开源数据库,它支持的SQL包括:

ATTACH DATABASE
BEGIN TRANSACTION
comment
COMMIT TRANSACTION
COPY
CREATE INDEX
CREATE TABLE
CREATE TRIGGER
CREATE VIEW
DELETE
DETACH DATABASE
DROP INDEX
DROP TABLE
DROP TRIGGER
DROP VIEW
END TRANSACTION
EXPLAIN
expression
INSERT
ON CONFLICT clause
PRAGMA
REPLACE
ROLLBACK TRANSACTION
SELECT
UPDATE

  同时它还支持事务处理功能等等。也有人说它象Microsoft的Access,有时候真的觉得有点象,但是事实上它们区别很大。比如SQLite 支持跨平台,操作简单,能够使用很多语言直接创建数据库,而不象Access一样需要Office的支持。如果你是个很小型的应用,或者你想做嵌入式开发,没有合适的数据库系统,那么现在你可以考虑使用SQLite。它的官方网站是:http://www.SQLite.org或者http://www.SQLite.com.cn,能在上面获得源代码和文档。同时因为数据库结构简单,系统源代码也不是很多,也适合想研究数据库系统开发的专业人士。

  下面是访问SQLite官方网站: http://www.SQLite.org/ 时第一眼看到关于SQLite的特性:

•    ACID事务
•    零配置 – 无需安装和管理配置
•    储存在单一磁盘文件中的一个完整的数据库
•    数据库文件可以在不同字节顺序的机器间自由的共享
•    支持数据库大小至2TB
•    足够小, 大致3万行C代码, 250K
•    比一些流行的数据库在大部分普通数据库操作要快
•    简单, 轻松的API
•    包含TCL绑定, 同时通过Wrapper支持其他语言的绑定
•    良好注释的源代码, 并且有着90%以上的测试覆盖率
•    独立: 没有额外依赖
•    Source完全的Open, 你可以用于任何用途, 包括出售它
•    支持多种开发语言,C, PHP, Perl, Java, ASP.NET,Python

  安装配置

  要使用SQLite,需要从SQLite官网下载到三个文件,分别为SQLite3.a,SQLite3.h,然后再在自己的工程中配置好头文件和库文件,同时将dll文件放到当前目录下,就完成配置可以使用SQLite了。

  使用的过程根据使用的函数大致分为如下几个过程:

•    SQLite3_open()
•    SQLite3_prepare()
•    SQLite3_step()
•    SQLite3_column()
•    SQLite3_finalize()
•    SQLite3_close()

  这几个过程是概念上的说法,而不完全是程序运行的过程,如SQLite3_column()表示的是对查询获得一行里面的数据的列的各个操作统称,实际上在SQLite中并不存在这个函数。

  SQLite3_open():打开数据库

  在操作数据库之前,首先要打开数据库。这个函数打开一个SQLite数据库文件的连接并且返回一个数据库连接对象。这个操作同时程序中的第一个调用的SQLite函数,同时也是其他SQLite api的先决条件。许多的SQLite接口函数都需要一个数据库连接对象的指针作为它们的第一个参数。

  1.函数定义:

int SQLite3_open(
  
const char * filename,   /* Database filename (UTF - 8 ) */
  SQLite3
** ppDb           /* OUT: SQLite db handle */
);
int SQLite3_open16(
  
const void * filename,   /* Database filename (UTF - 16 ) */
  SQLite3
** ppDb           /* OUT: SQLite db handle */
);
int SQLite3_open_v2(
  
const char * filename,   /* Database filename (UTF - 8 ) */
  SQLite3
** ppDb,         /* OUT: SQLite db handle */
  
int flags,               /* Flags */
  
const char * zVfs         /* Name of VFS module to use */
);

  2.说明:

  假如这个要被打开的数据文件不存在,则一个同名的数据库文件将被创建。如果使用SQLite3_open和SQLite3_open_v2的话,数据库将采用UTF-8的编码方式,SQLite3_open16采用UTF-16的编码方式

  3.返回值:

  如果SQLite数据库被成功打开(或创建),将会返回SQLITE_OK,否则将会返回错误码。SQLite3_errmsg()或者SQLite3_errmsg16可以用于获得数据库打开错误码的英文描述,这两个函数定义为:

const char * SQLite3_errmsg(SQLite3 * );
const void * SQLite3_errmsg16(SQLite3 * );

  4.参数说明:

  · filename:需要被打开的数据库文件的文件名,在SQLite3_open和SQLite3_open_v2中这个参数采用UTF-8编码,而在SQLite3_open16中则采用UTF-16编码

  · ppDb:一个数据库连接句柄被返回到这个参数,即使发生错误。唯一的一场是如果SQLite不能分配内存来存放SQLite对象,ppDb将会被返回一个NULL值。

  · flags:作为数据库连接的额外控制的参数,可以是SQLITE_OPEN_READONLY,SQLITE_OPEN_READWRITE和SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE中的一个,用于控制数据库的打开方式,可以和SQLITE_OPEN_NOMUTEX,SQLITE_OPEN_FULLMUTEX, SQLITE_OPEN_SHAREDCACHE,以及SQLITE_OPEN_PRIVATECACHE结合使用,具体的详细情况可以查阅文档。


 

  SQLite3_prepare()

  这个函数将sql文本转换成一个准备语句(prepared statement)对象,同时返回这个对象的指针。这个接口需要一个数据库连接指针以及一个要准备的包含SQL语句的文本。它实际上并不执行(evaluate)这个SQL语句,它仅仅为执行准备这个sql语句

  1.函数定义(仅列出UTF-8的):

int SQLite3_prepare(
  SQLite3
* db,             /* Database handle */
  
const char * zSql,       /* SQL statement, UTF - 8 encoded */
  
int nByte,               /* Maximum length of zSql in bytes. */
  SQLite3_stmt
** ppStmt,   /* OUT: Statement handle */
  
const char ** pzTail     /* OUT: Pointer to unused portion of zSql */
);
int SQLite3_prepare_v2(
  SQLite3
* db,             /* Database handle */
  
const char * zSql,       /* SQL statement, UTF - 8 encoded */
  
int nByte,               /* Maximum length of zSql in bytes. */
  SQLite3_stmt
** ppStmt,   /* OUT: Statement handle */
  
const char ** pzTail     /* OUT: Pointer to unused portion of zSql */
);

  2.参数:

  db:数据指针

  zSql:sql语句,使用UTF-8编码

  nByte:如果nByte小于0,则函数取出zSql中从开始到第一个0终止符的内容;如果nByte不是负的,那么它就是这个函数能从zSql中读取的字节数的最大值。如果nBytes非负,zSql在第一次遇见’/000/或’u000’的时候终止

  pzTail:上面提到zSql在遇见终止符或者是达到设定的nByte之后结束,假如zSql还有剩余的内容,那么这些剩余的内容被存放到pZTail中,不包括终止符

  ppStmt:能够使用SQLite3_step()执行的编译好的准备语句的指针,如果错误发生,它被置为NULL,如假如输入的文本不包括sql语句。调用过程必须负责在编译好的sql语句完成使用后使用SQLite3_finalize()删除它。

  3.说明:

  如果执行成功,则返回SQLITE_OK,否则返回一个错误码。推荐在现在任何的程序中都使用SQLite3_prepare_v2这个函数,SQLite3_prepare只是用于前向兼容

  4.备注:

  <1>准备语句(prepared statement)对象:

typedef struct SQLite3_stmt SQLite3_stmt;

  准备语句(prepared statement)对象一个代表一个简单SQL语句对象的实例,这个对象通常被称为“准备语句”或者“编译好的SQL语句”或者就直接称为“语句”。

  语句对象的生命周期经历这样的过程:

•    使用SQLite3_prepare_v2或相关的函数创建这个对象
•    使用SQLite3_bind_
* ()给宿主参数(host parameters)绑定值
•    通过调用SQLite3_step一次或多次来执行这个sql
•    使用SQLite3——reset()重置这个语句,然后回到第2步,这个过程做0次或多次
•    使用SQLite3_finalize()销毁这个对象

  在SQLite中并没有定义SQLite3_stmt这个结构的具体内容,它只是一个抽象类型,在使用过程中一般以它的指针进行操作,而SQLite3_stmt类型的指针在实际上是一个指向Vdbe的结构体得指针

  <2>宿主参数(host parameters)

  在传给SQLite3_prepare_v2()的sql的语句文本或者它的变量中,满足如下模板的文字将被替换成一个参数:

•    ?  ?
•    ?  ?NNN,NNN代表数字
•    ?  :VVV,VVV代表字符
•    ?  @VVV
•    ?  $VVV

  在上面这些模板中,NNN代表一个数字,VVV代表一个字母数字标记符(例如:222表示名称为222的标记符),sql语句中的参数(变量)通过上面的几个模板来指定,如

  “select ? from ? “这个语句中指定了两个参数,SQLite语句中的第一个参数的索引值是1,这就知道这个语句中的两个参数的索引分别为1和2,使用”?”的话会被自动给予索引值,而使用”?NNN”则可以自己指定参数的索引值,它表示这个参数的索引值为NNN。”:VVV”表示一个名为”VVV”的参数,它也有一个索引值,被自动指定。

  可以使用SQLite3_bind_*()来给这些参数绑定值。


 

  SQLite3_setp()

  这个过程用于执行有前面SQLite3_prepare创建的准备语句。这个语句执行到结果的第一行可用的位置。继续前进到结果的第二行的话,只需再次调用SQLite3_setp()。继续调用SQLite3_setp()知道这个语句完成,那些不返回结果的语句(如:INSERT,UPDATE,或DELETE),SQLite3_step()只执行一次就返回

  1.函数定义

int SQLite3_step(SQLite3_stmt * );

  2.返回值

  函数的返回值基于创建SQLite3_stmt参数所使用的函数,假如是使用老版本的接口SQLite3_prepare()和SQLite3_prepare16(),返回值会是 SQLITE_BUSY, SQLITE_DONE, SQLITE_ROW, SQLITE_ERROR 或 SQLITE_MISUSE,而v2版本的接口SQLite3_prepare_v2()和SQLite3_prepare16_v2()则会同时返回这些结果码和扩展结果码。

  对所有V3.6.23.1以及其前面的所有版本,需要在SQLite3_step()之后调用SQLite3_reset(),在后续的SQLite3_ step之前。如果调用SQLite3_reset重置准备语句失败,将会导致SQLite3_ step返回SQLITE_MISUSE,但是在V3. 6.23.1以后,SQLite3_step()将会自动调用SQLite3_reset。

int SQLite3_reset(SQLite3_stmt * pStmt);

  SQLite3_reset用于重置一个准备语句对象到它的初始状态,然后准备被重新执行。所有sql语句变量使用SQLite3_bind*绑定值,使用SQLite3_clear_bindings重设这些绑定。SQLite3_reset接口重置准备语句到它代码开始的时候。SQLite3_reset并不改变在准备语句上的任何绑定值,那么这里猜测,可能是语句在被执行的过程中发生了其他的改变,然后这个语句将它重置到绑定值的时候的那个状态。

  SQLite3_column()

  这个过程从执行SQLite3_step()执行一个准备语句得到的结果集的当前行中返回一个列。每次SQLite3_step得到一个结果集的列停下后,这个过程就可以被多次调用去查询这个行的各列的值。对列操作是有多个函数,均以SQLite3_column为前缀:

const void * SQLite3_column_blob(SQLite3_stmt * , int iCol);
int SQLite3_column_bytes(SQLite3_stmt * , int iCol);
int SQLite3_column_bytes16(SQLite3_stmt * , int iCol);
double SQLite3_column_double(SQLite3_stmt * , int iCol);
int SQLite3_column_int(SQLite3_stmt * , int iCol);
SQLite3_int64 SQLite3_column_int64(SQLite3_stmt
* , int iCol);
const unsigned char * SQLite3_column_text(SQLite3_stmt * , int iCol);
const void * SQLite3_column_text16(SQLite3_stmt * , int iCol);
int SQLite3_column_type(SQLite3_stmt * , int iCol);
SQLite3_value
* SQLite3_column_value(SQLite3_stmt * , int iCol);

  1.说明

  第一个参数为从SQLite3_prepare返回来的prepared statement对象的指针,第二参数指定这一行中的想要被返回的列的索引。最左边的一列的索引号是0,行的列数可以使用SQLite3_colum_count()获得。

  这些过程会根据情况去转换数值的类型,SQLite内部使用SQLite3_snprintf()去自动进行这个转换,下面是关于转换的细节表:

 

内部类型

请求的类型

转换

NULL

INTEGER

结果是0

NULL

FLOAT

结果是0.0

NULL

TEXT

结果是NULL

NULL

BLOB

结果是NULL

INTEGER

FLOAT

从整形转换到浮点型

INTEGER

TEXT

整形的ASCII码显示

INTEGER

BLOB

同上

FLOAT

INTEGER

浮点型转换到整形

FLOAT

TEXT

浮点型的ASCII显示

FLOAT

BLOB

同上

TEXT

INTEGER

使用atoi()

TEXT

FLOAT

使用atof()

TEXT

BLOB

没有转换

BLOB

INTEGER

先到TEXT,然后使用atoi

BLOB

FLOAT

先到TEXT,然后使用atof

BLOB

TEXT

如果需要的话添加0终止符

  注:BLOB数据类型是指二进制的数据块,比如要在数据库中存放一张图片,这张图片就会以二进制形式存放,在SQLite中对应的数据类型就是BLOB

  int SQLite3_column_bytes(SQLite3_stmt*, int iCol),int SQLite3_column_bytes16(SQLite3_stmt*, int iCol)两个函数返回对应列的内容的字节数,这个字节数不包括后面类型转换过程中加上的0终止符。

  下面是几个最安全和最简单的使用策略

•    先SQLite3_column_text() ,然后 SQLite3_column_bytes()
•    先SQLite3_column_blob(),然后SQLite3_column_bytes()
•    先SQLite3_column_text16(),然后SQLite3_column_bytes16()

  SQLite3_finalize

int SQLite3_finalize(SQLite3_stmt * pStmt);

  这个过程销毁前面被SQLite3_prepare创建的准备语句,每个准备语句都必须使用这个函数去销毁以防止内存泄露。

  在空指针上调用这个函数没有什么影响,同时可以准备语句的生命周期的任一时刻调用这个函数:在语句被执行前,一次或多次调用SQLite_reset之后,或者在SQLite3_step任何调用之后不管语句是否完成执行

  SQLite3_close

  这个过程关闭前面使用SQLite3_open打开的数据库连接,任何与这个连接相关的准备语句必须在调用这个关闭函数之前被释放。


 

  Benchmark测试

  1.测试环境

  本次测试使用的软硬件环境如下:

  硬件配置:Intel(R) Core(TM) i7 CPU 860 @ 2.80GHz,4核8线程, 内存8GB。

  操作系统: Redhat Enterprise Linux 6.0 X64。

  2.测试假定

  本次测试为充分展示内存数据库的性能,使用SQLite的内存方式,以嵌入方式来完成测试。

  3.数据结构

create table Record (id int , i1 int ,i2 int ,d1 double ,d2 double , s1 VARCHAR( 30 ), s2 VARCHAR( 30 ))

  插入测试

  1.单线程

  首先进行单线程的插入测试,向数据库中插入10000000条记录,性能如下:

 

线程ID

记录数

耗时(毫秒)

1

10000000

65631

每条记录所花费的时间(微秒)

6.56

每秒吞吐率(object/s

152439

  单线程插入10000000条记录的耗时为65.6秒,每条记录的花费时间为6.56微秒,每秒处理的记录数为15.2万。

  2.四线程

  之后我们增加线程数为4.四个线程同时插入10000000条记录,性能如下:

 

线程ID

记录数

耗时(毫秒)

1

2500000

123733

2

2500000

124221

3

2500000

126657

4

2500000

126677

插入10000000条记录所花费的总时间(秒)

125.3

每条记录所花费的时间(微秒)

12.5322

每秒吞吐率(object/s

79794.4495

  四个线程插入10000000条记录的总耗时为125.3秒,平均每条记录耗时12.5微秒,每秒处理8万条数据。

  3.八线程

  最后将线程数增加到八个线程,向数据库中添加10000000条记录,每个线程的性能和总体性能如下:

 

线程ID

记录数

耗时(毫秒)

1

1250000

147201

2

1250000

149193

3

1250000

150958

4

1250000

153391

5

1250000

153699

6

1250000

154038

7

1250000

155079

8

1250000

155119

插入10000000条记录所花费的总时间(秒)

152.33475

每条记录所花费的时间(微秒)

15.233475

每秒吞吐率(object/s

65644

  可以看到8个并发写入10000000条记录所花费的时间大概为152秒,平均每秒可以添加65644条记录。

  4.总结

  插入操作的总体吞吐率:

内存数据库SQLite评测:Benchmark测试

  可以看到,插入操作的性能,单个线程并发操作时,吞吐率最大。这是由于SQLite在内存模式时,对于多线程的支持很弱。


 

  更新测试

  1.单线程

  首先进行单线程的更新测试,在数据库中进行10000000次更新,每次更新一条记录的所有字段,性能如下:

 

线程ID

记录数

耗时(毫秒)

1

10000000

67519

每条记录所花费的时间(微秒)

6.75

每秒吞吐率(object/s

148106

  单线程更新10000000条记录的耗时为67秒,每条记录的更新花费时间为6.75微秒,每秒处理的记录数为14.8万。

  2.四线程

  之后我们增加线程数为4.

  四个线程同时更新10000000条记录,性能如下:

 

线程ID

记录数

耗时(毫秒)

1

2500000

121080

2

2500000

124996

3

2500000

125923

4

2500000

125936

插入10000000条记录所花费的总时间(秒)

124.5

每条记录所花费的时间(微秒)

12.44838

每秒吞吐率(object/s

80331.77

  四个线程更新10000000条记录的总耗时为124.5秒,平均每条记录耗时12.4微秒,每秒处理8万条数据。

  3.八线程

  更新测试是通过八个线程,同时更新数据库中记录,共10000000次操作,每个线程的性能和总体性能如下:

 

线程ID

记录数

耗时(毫秒)

1

1250000

130750

2

1250000

132481

3

1250000

137324

4

1250000

137781

5

1250000

139837

6

1250000

141223

7

1250000

141283

8

1250000

141357

更新10000000条记录的耗时(秒)

137.7545

更新每条记录所的耗时(微秒)

13.77545

每秒吞吐率(object/s

72593

  可以看到8个并发同时更新10000000条记录所花费的时间大概为138秒,平均每秒可以更新72593万条记录。此处的更新为涉及到了每条记录的每个字段。

  4.总结

  更新操作的总体吞吐率:

内存数据库SQLite性能评测:更新测试

  可以看到,更新操作的性能,同样也是单个线程并发操作时,吞吐率最大。


 

  查询测试

  1.单线程

  首先进行单线程的查询测试,在数据库中进行10000000次查找,性能如下:

 

线程ID

记录数

耗时(毫秒)

1

10000000

36248

每条记录所花费的时间(微秒)

3.6248

每秒吞吐率(object/s

275877

  单线程进行10000000次查询的耗时为36.2秒,每次查询花费时间为3.6微秒,每秒处理的操作数为27.6万。

  2.四线程

  之后我们增加线程数为4.

  四个线程进行10000000次查找操作,性能如下:

 

线程ID

记录数

耗时(毫秒)

1

2500000

71445

2

2500000

77419

3

2500000

79154

4

2500000

79806

插入10000000条记录所花费的总时间(秒)

76.956

每条记录所花费的时间(微秒)

7.6956

每秒吞吐率(object/s

129944.3838

  四个线程进行10000000次查找操作的总耗时为79.9秒,平均每条记录耗时7.7微秒,每秒处理13万次查询操作。

  3.八线程

  查询测试是通过八个线程,同时查询数据库中记录,共10000000次查询,每个线程的性能和总体性能如下:

 

线程ID

记录数

耗时(毫秒)

1

1250000

77051

2

1250000

79457

3

1250000

81595

4

1250000

83005

5

1250000

83341

6

1250000

83752

7

1250000

83962

8

1250000

84063

查询10000000次的耗时(秒)

82.02825

每次查询的耗时(微秒)

8.202825

每秒吞吐率(object/s

121909

  可以看到8个并发同时查询10000000条记录所花费的时间大概为82秒,平均每秒可以进行121909次查询。

  4.总结

  查询操作的总体吞吐率:

内存数据库SQLite性能评测:查询测试

  可以看到,查询操作的性能,同样也是4个线程并发操作时,吞吐率最大。


 

  1:1读写测试

  1.四线程

  首先进行四线程的读写测试,其中2个线程做更新操作,另外两个线程做查询操作,持续运行10秒钟,每个线程的性能和总体性能如下:

 

线程ID

操作

操作次数

1

查询

331028

2

查询

347203

3

更新

197998

4

更新

175434

每秒查询吞吐率(完成次数/s)

67823.1

每秒更新吞吐率(完成次数/s)

37343.2

总吞吐率(完成次数/s

105166.3

  在四个线程进行读写测试时,平均每秒可以进行6.7万次查询,3.7万次更新,总体吞吐率为10.5万。

  2.八线程

  读写测试是通过八个线程,其中四个线程持续做更新操作,另外四个线程做查询操作,持续运行10秒中,每个线程的性能和总体性能如下:

 

线程ID

操作

操作次数

1

查询

203945

2

查询

206567

3

查询

167354

4

查询

229630

5

更新

60105

6

更新

49404

7

更新

49996

8

更新

55091

每秒查询吞吐率(完成次数/s)

80749.6

每秒更新吞吐率(完成次数/s)

21459.6

总吞吐率(完成次数/s

102209.2

  可以看到同时进行读写测试时,平均每秒可以进行8万次查询,2万次更新,总体吞吐率为10万。

  3.总结

  1:1读写操作的总体吞吐率:

内存数据库SQLite评测:1:1读写测试

  可以看到,1:1读写操作的性能,4线程与8线程总体吞吐率相近,而更新吞吐率四线程比8线程高,查询吞吐率则正好相反。


 

  删除测试

  1.单线程

  首先进行单线程的删除测试,在数据库中进行10000000次删除,每次删除一条记录,性能如下:

 

线程ID

记录数

耗时(毫秒)

1

10000000

68516

每条记录所花费的时间(微秒)

6.8516

每秒吞吐率(object/s

145951.3

  单线程进行10000000次删除操作的耗时为68.5秒,每次查询花费时间为6.8微秒,每秒处理的操作数为14.6万。

  2.四线程

  之后我们增加线程数为4.

  四个线程进行10000000次删除操作,性能如下:

 

线程ID

记录数

耗时(毫秒)

1

2500000

117681

2

2500000

118419

3

2500000

119594

4

2500000

120184

插入10000000条记录所花费的总时间(秒)

118.9695

每条记录所花费的时间(微秒)

11.89695

每秒吞吐率(object/s

84055.16

  四个线程进行10000000次删除操作的总耗时为119秒,平均删除每条记录耗时11.9微秒,每秒处理8.4万次删除操作。

  3.八线程

  删除测试是通过八个线程,按照记录ID,同时删除数据库中记录,共10000000个对象,每个线程的性能和总体性能如下:

 

线程ID

记录数

耗时(毫秒)

1

1250000

119953

2

1250000

122847

3

1250000

124368

4

1250000

124598

5

1250000

126361

6

1250000

126591

7

1250000

126983

8

1250000

127586

查询10000000次的耗时(秒)

124.9109

每次查询的耗时(微秒)

12.49109

每秒吞吐率(object/s

80057.08

  可以看到8个并发同时删除10000000条记录所花费的时间大概为124秒,平均每秒可以进行8万次删除。

  4.总结

  查询操作的总体吞吐率:

内存数据库SQLite性能评测:删除测试

  可以看到,删除操作的性能,同样也是单个线程并发操作时,吞吐率最大。

  以上测试都是每次操作都为一个事务,每次操作只涉及一条记录。

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/681587
推荐阅读
相关标签
  

闽ICP备14008679号