65.9K
CodeProject 正在变化。 阅读更多。
Home

对 .NET 嵌入式数据库性能的基准测试:SQL CE 4.0 vs SQLite

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.87/5 (10投票s)

2011年7月4日

CPOL

11分钟阅读

viewsIcon

162127

downloadIcon

1520

提供了一个简单的测试基准应用程序的示例代码,并比较了两种最常见的 .NET 嵌入式数据库:Microsoft SQL CE 4.0 和 SQLite 的性能。

benchmark-console-output

引言

随着应用程序的复杂性和需要处理的数据量的增加,对应用程序内部灵活而强大的数据存储的需求也在快速增长。存储数据的常见方法,例如写入自定义二进制或文本文件、使用 XML 文件或外部数据库,都存在一定的缺点。

纯文本文件以及自定义二进制格式都难以导航,它们不支持基本的 CRUD(创建、读取、更新、删除)操作,并且需要大量自定义编写和维护的代码才能正常运行。

XML 文件可能很有帮助,特别是考虑到 LINQ-to-XML 对 XML 的 CRUD 操作提供了良好的支持。无论如何,读写 XML 文件都需要自定义代码,而且在没有死锁风险的情况下,操作不容易并行化。

外部数据库提供了最佳的数据存储和检索支持,但它们在应用程序上下文之外运行,并且必须在应用程序安装之前进行安装。这个先决条件并不总是容易满足,特别是对于那些需要在世界各地众多桌面计算机上运行的应用程序。我们不是指具有一个共享中央数据库的客户端应用程序,而是指需要每个小型独立数据库来存储其数据的应用程序。

因此,对小型但灵活且功能丰富(如支持 SQL 以实现统一数据访问)的数据库的需求,导致了嵌入式数据库的出现。

嵌入式数据库引擎

嵌入式与服务器端数据库引擎的快速比较

与传统的 RDBMS 相比,嵌入式数据库引擎(如 MS SQL Compact EditionSQLiteOracle Berkeley DB)直接“嵌入”到应用程序中,这意味着完整的数据库引擎代码被打包成一个(或最多两个)DLL,与您的应用程序一起分发。因此,它们直接在应用程序的上下文中运行,数据访问的资源与其他应用程序部分共享:可用内存、CPU 时间、磁盘吞吐量等。

通用服务器端关系数据库(如 MS SQLOracleMySQLPostgreSQLFirebird 等)则作为独立服务安装,并在客户端应用程序范围之外运行。由于其固有的对多核和多 CPU 架构的支持,它们可以获得更好的性能,利用操作系统功能(如预缓存、VSS 等)来提高密集数据库操作的吞吐量,并且可以声明尽可能多的内存,只要操作系统能为单个服务/应用程序提供。

由于应用领域不同,服务器端和嵌入式数据库具有不同的选项集:服务器端数据库提供广泛的用户和权限管理、对视图和存储过程的支持,而嵌入式数据库通常缺乏任何用户和权限管理支持,并且对视图和存储过程的支持有限(后者失去了在服务器端运行的大部分优势)。数据吞吐量是 RDBMS 的常见瓶颈,服务器版本通常安装在条带 RAID 卷上,而嵌入式数据库通常面向内存(尝试将所有实际数据保留在内存中)并最大限度地减少数据存储访问操作。

在本文中,我们仅专注于传统的 ACID 数据库,并故意不考虑其他类型的数据库。值得一提的是,最近 NoSQL 数据库尤其受欢迎。今天,它们可以被视为传统关系数据库存储的有力竞争对手,因此值得单独撰写一篇文章。

关于“嵌入式”一词

阅读有关嵌入式数据库的内容时,不应错误地认为这些数据库仅用于面向嵌入式系统的应用程序,例如运行 Windows CE 或 Embedded ARM/Tiny OS 的便携式设备。嵌入式数据库在各种桌面甚至某些类型的服务器应用程序中都很常见:SQLite 和 SQL CE 4.0 都支持并发性,这使得它们成为中小型 Web 应用程序的流行数据库选择。Microsoft 推出的一个相对较新的 Web 开发 IDE WebMatrix 将 SQL CE 4.0 用作其所有项目和模板的默认数据库。

同时,其紧凑性和低性能要求也使嵌入式数据库成为移动设备数据存储的有吸引力的解决方案:即将发布的 Windows Phone OS“Mango”版本将 SQL CE 作为本地数据库。对于 Windows Phone 的前一个版本,有改编版的 SQLite 可用

对 .NET 的支持

快速查看维基百科上关于嵌入式数据库的页面,您会发现许多本文未提及的名称。这是因为许多数据库引擎仍然缺乏对 .NET 托管代码的良好支持,或者在 .NET 世界中的使用非常有限[1]

SQLite 也存在缺乏对 .NET 直接支持的情况。幸运的是,由于该库在非托管世界中的高人气,存在几种 ADO.NET 绑定实现。我们在本文中随意选择了 System.Data.SQLite 数据提供程序,它实现了 ADO.NET 的DbConnection 类,使我们能够以数据引擎抽象的方式提供所有基准测试操作。同时,我们意识到使用此数据提供程序获得的测试结果不能直接归咎于(或赞扬)数据库引擎本身,而应明确包括数据提供程序。

数据库引擎性能基准测试

要进行基准测试的关键操作

性能基准测试是数据库领域的一项常见任务。为此已经开发了几个独立的排名系统:TPC、SPEC、PolePosition 等。您会在 Oracle 的这个维基页面上找到一个更长的列表和一些评论。这些基准测试主要针对服务器端数据库,并包含大型处理系统常见的场景:大型购物中心、证券和商品交易所等的 POS 系统。这些场景对于嵌入式数据库来说非常不典型,难以用于对它们进行基准测试。

对于那些不直接以数据库为中心的应用,例如,它们并非仅仅是为了存储数据而设计的厚客户端,而是有其他主要目标,仅使用数据库来存储应用程序状态和用户特定数据[2],以下将是最常见的数据库操作:

  1. 将单个数据条目插入表中
  2. 对表行的随机读取访问
  3. 对表行的随机写入(更新)访问
  4. 随机删除行

我们将每种访问类型实现为 Action<dbconnection> 通用委托,以抽象化具体的数据库连接。请参阅列表

性能计数器

对于作为独立实例运行的服务器端数据库,性能指标或多或少独立于客户端应用程序,但很大程度上取决于您的硬件。嵌入式引擎与主机应用程序共享所有资源,并且计算密集型线程与数据访问线程并行运行可能会导致数据库性能急剧下降。

然而,对于桌面应用程序,主要目标通常是提供用户交互性。这意味着应用程序应在一定时间内响应用户输入,并且此处的最大可能延迟通常是最关键的点。尽管现代用户应用程序架构要求数据操作在工作线程中执行,但这些类型线程的长时间执行会阻碍用户继续其操作,尽管 GUI 看起来响应迅速。

因此,每次数据库操作和一系列操作所需的时间是嵌入式数据库系统最重要和最关键的参数。在我们简单的处理方法中,这是我们将使用的唯一指标[3]

测试基准应用程序

测试基准应用程序是用 C# 编写的(项目类型:控制台应用程序),并提供了对用于基准测试的数据库引擎的某种抽象。所有数据库操作都实现为委托,以 DbConnection 对象作为输入参数。该对象以最抽象的方式实现了数据库连接。

由于 SQL CE 和 SQLite 之间存在细微的语法差异,我们不得不使用两个不同的 SQL 字符串来创建表。样本量——在每个步骤中针对数据库运行的查询数量——是一个固定的常量值 NUMBER_OF_TESTSGuid 用于生成插入或更新的伪随机字符串数据。

由于 SQLite.DLL 是一个混合 DLL,其中包含用 C 编写的原始 SQLite.dll 库的非托管二进制代码以及该库的托管绑定,因此我们必须在 .NET 4.0 的应用程序域中加载此程序集之前启用对混合程序集的支持。为此,只需向控制台应用程序添加一个应用程序配置文件,并在 configuration 部分添加一些选项。App.config 文件的完整内容如下所示。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup useLegacyV2RuntimeActivationPolicy="true">
    <supportedRuntime version="v4.0"/>
  </startup>
</configuration>

这是应用程序的源代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SQLite;
using System.Data.SqlServerCe;
using System.Data.Common;

namespace TestSQL
{
    class Program
    {
        const int NUMBER_OF_TESTS = 200;

        private static string create_table;

        private static string create_table_sqlce =  "CREATE TABLE Test 
	( id integer not null identity primary key, textdata nvarchar(500));";
        private static string create_table_sqlite = "CREATE TABLE Test 
	( id integer not null primary key, textdata nvarchar(500));";

        private static string drop_table = "DROP TABLE Test";
        private static string insert_data = "INSERT INTO Test 
					(textdata) VALUES ('{0}');";
        private static string read_data = "SELECT textdata FROM Test WHERE id = {0}";
        private static string update_data = "UPDATE Test SET textdata = 
						'{1}' WHERE id = {0}";
        private static string delete_data = "DELETE FROM Test WHERE id = {0}";

        static Action<dbconnection> ACreateTable = (a) => CreateTable(a);
        static Action<dbconnection> ATestWrite = (a) => TestWrite(a, NUMBER_OF_TESTS);
        static Action<dbconnection> ATestRead = (a) => TestRead(a, NUMBER_OF_TESTS);
        static Action<dbconnection> ATestUpdate = (a) => TestUpdate(a, NUMBER_OF_TESTS);
        static Action<dbconnection> ATestDelete = (a) => TestDelete(a, NUMBER_OF_TESTS);
        static Action<dbconnection> ADropTable = (a) => DropTable(a);

        static Func<action<dbconnection>,DbConnection, TimeSpan> 
		MeasureExecTime = (a,b) => { var start = DateTime.Now; a(b); 
		var finish = DateTime.Now; return finish - start; };

        static Action<string,> AMeasureAndOutput = (a, b) => 
			Console.WriteLine(a, b.TotalMilliseconds);

        static void Main(string[] args)
        {
            // opening databases
            SQLiteConnection.CreateFile("sqlite.db");
            SQLiteConnection sqliteconnect = 
			new SQLiteConnection("Data Source=sqlite.db");
            SqlCeConnection sqlceconnect = 
			new SqlCeConnection("Data Source=sqlce.sdf");

            sqlceconnect.Open();
            sqliteconnect.Open();

            Console.WriteLine("=Testing CRUD performance of embedded DBs=");
            Console.WriteLine(" => Samplesize: {0}", NUMBER_OF_TESTS);

            create_table = create_table_sqlite;
            Console.WriteLine("==Testing SQLite==");
            DoMeasures(sqliteconnect);

            create_table = create_table_sqlce;
            Console.WriteLine("==Testing SQL CE 4.0==");
            DoMeasures(sqlceconnect);

            sqlceconnect.Close();
            sqliteconnect.Close();

            Console.ReadKey();
        }

        static void DoMeasures(DbConnection con)
        {
            AMeasureAndOutput("Creating table: {0} ms", 
			MeasureExecTime(ACreateTable, con));
            AMeasureAndOutput("Writing data: {0} ms", MeasureExecTime(ATestWrite, con));
            AMeasureAndOutput("Updating data: {0} ms", 
			MeasureExecTime(ATestUpdate, con));
            AMeasureAndOutput("Reading data: {0} ms", MeasureExecTime(ATestRead, con));
            AMeasureAndOutput("Deleting data: {0} ms", 
			MeasureExecTime(ATestDelete, con));
            AMeasureAndOutput("Dropping table: {0} ms", 
			MeasureExecTime(ADropTable, con));
        }

        static void CreateTable(DbConnection con)
        {
            var sqlcmd = con.CreateCommand();
            sqlcmd.CommandText = create_table;
            sqlcmd.ExecuteNonQuery();
        }

        static void TestWrite(DbConnection con, int num)
        {
            for (; num-- > 0; )
            {
                var sqlcmd = con.CreateCommand();
                sqlcmd.CommandText = 
		string.Format(insert_data,Guid.NewGuid().ToString());
                sqlcmd.ExecuteNonQuery();
            }

        }

        static void TestRead(DbConnection con, int num)
        {
            Random rnd = new Random(DateTime.Now.Millisecond);
            for (var max = num; max-- > 0; )
            {
                var sqlcmd = con.CreateCommand();
                sqlcmd.CommandText = string.Format(read_data, rnd.Next(1,num-1));
                sqlcmd.ExecuteNonQuery();
            }
        }

        static void TestUpdate(DbConnection con, int num)
        {
            Random rnd = new Random(DateTime.Now.Millisecond);
            for (var max = num; max-- > 0; )
            {
                var sqlcmd = con.CreateCommand();
                sqlcmd.CommandText = string.Format
		(update_data, rnd.Next(1, num - 1), Guid.NewGuid().ToString());
                sqlcmd.ExecuteNonQuery();
            }
        }

        static void TestDelete(DbConnection con, int num)
        {
            Random rnd = new Random(DateTime.Now.Millisecond);
            var order = Enumerable.Range(1, num).ToArray<int>();
            Action<int[],> swap = (arr, a, b) => { int c = arr[a]; 
				arr[a] = arr[b]; arr[b] = c; };
            
            // shuffling the array
            for (var max=num; max-- > 0; ) swap(order, rnd.Next(0, num - 1), 
			rnd.Next(0, num - 1));
            

            foreach(int index in order)
            {
                var sqlcmd = con.CreateCommand();
                sqlcmd.CommandText = string.Format(delete_data, index);
                sqlcmd.ExecuteNonQuery();
            }
        }

        static void DropTable(DbConnection con)
        {
            var sqlcmd = con.CreateCommand();
            sqlcmd.CommandText = drop_table;
            sqlcmd.ExecuteNonQuery();
        }
    }
}

执行基准测试

有关测试的一些信息

为了获得时间值的参考,以下是有关基准测试环境的一些信息。

  • 测试使用了以下硬件:配备 2 个 Intel Xeon E5420 CPU 和 8GB RAM 的 Dell Precision WorkStation T7400,运行 64 位 Win7 Enterprise
  • 两个引擎都使用了其默认设置,连接字符串看起来像是 “Data Source=database_file_name”
  • 使用了 SQL CE 4.0 和 SQLite/System.Data.SQLite(截至 2011 年 6 月 3 日)的最新版本。

以下是两个不同样本的结果

=Testing CRUD performance of embedded DBs=  
=> Samplesize: 200
==Testing SQLite== 
Creating table: 396.0396 ms 
Writing data: 22189.2187 ms 
Updating data: 23591.3589 ms
Reading data: 21.0021 ms 
Deleting data: 20963.0961 ms 
Dropping table: 85.0085 ms

==Testing SQL CE 4.0== 
Creating table: 16.0016 ms 
Writing data: 25.0025 ms 
Updating data: 56.0056 ms 
Reading data: 28.0028 ms 
Deleting data: 53.0053 ms 
Dropping table: 11.0011 ms

……和一个更大的样本

=Testing CRUD performance of embedded DBs=
=> Samplesize: 1000
==Testing SQLite==
Creating table: 93.0093 ms
Writing data: 116632.6621 ms
Updating data: 104967.4957 ms
Reading data: 134.0134 ms
Deleting data: 107666.7656 ms
Dropping table: 83.0083 ms

==Testing SQL CE 4.0==
Creating table: 16.0016 ms
Writing data: 128.0128 ms
Updating data: 307.0307 ms
Reading data: 164.0164 ms
Deleting data: 306.0306 ms
Dropping table: 13.0013 ms

结果

运行基准测试应用程序 4 次后,我们更改了数据库的顺序,又运行了 4 次。累积统计数据如下表所示。

Databases      		SQLite 3.6.23.1			SQL CE 4.0	
Values (ms)     	Mean	Stderr			Mean	Stderr
Creating table		109.4	9.579144012		18.4	0.92736185
Writing data		22549.8	218.5306386		25.8	0.583095189
Updating data		21290.8	638.9963537		56.8	0.489897949
Reading data		20.6	0.244948974		28.4	0.678232998
Deleting data		20032.8	323.019566		52.8	0.860232527
Dropping table		127.4	52.21168452		11.0	0.316227766

现在我们可以使用 Excel 为我们的数据生成一个漂亮的柱状图。请注意,我们必须使用对数刻度来表示在单个图表中跨越几个数量级的数据。 

benchmark-results
单击此处查看完整尺寸图像。

讨论

因此,正如您所见,与 SQLCE 相比,SQLite 中的几乎所有写入操作(createupdatedelete)都需要多出近 1000 倍的时间,而数据读取对于两种嵌入式数据库来说似乎处于相同的性能水平。

这种巨大的差异并不难解释,并且不一定反映 SQLite 作为数据库引擎的整体性能不佳。应该考虑以下可能影响基准测试的细节:

  • 我们用于 SQLite 的数据提供程序是 System.Data.SQLite,它是一个混合程序集,包含托管和非托管代码。SQLite 最初完全用 C 编写,DLL 只提供绑定。这意味着它必须进行平台调用以调用原生 SQLite.DLL 中的相应过程,并且在此处以及返回时进行数据转换。这些 P/Invoke 调用和数据封送可能占用了操作时间的一大部分。
  • 两个数据引擎都使用了它们的默认设置!很可能,SQLCE 4.0 默认将所有数据缓存在内存中,而 SQLite 每次更改时都会将大多数数据更改直接刷新到磁盘存储。可以通过连接字符串为两个数据库提供数百个参数并进行适当调整,并强制数据引擎以可能提供更好结果的方式运行[4]
  • 我们使用了一系列单独的查询来测试数据库。至少 SQLCE 通过特殊的 .NET 类支持批量操作,这些类在这里更适合。如果 SQLite 也支持它们,那么进行比较将很有用。

总的来说,我们观察到在 x64 计算机上使用 SQLite/System.Data.SQLite 捆绑包时存在某些问题(使用相同的 .NET 适配器):从连接意外关闭到数据库文件损坏。我们推测数据适配器或库本身存在稳定性问题。

结论

如果您需要一个高性能的嵌入式数据库引擎,它在默认设置下性能优异,提供并发访问,并且在 x86 和 x64 平台上都能在 .NET 应用程序中“开箱即用”,那么您可能应该看看 Microsoft SQL CE 4.0。

结尾注

[1] 如果您不同意并认为某个数据库引擎值得包含在本文中,请随时与我联系并提出您的建议,作者将很高兴为本文增加更多多样性!

[2] 例如,非数据中心应用程序可以考虑 Microsoft Word。作为一个文本编辑器,它需要一定的存储能力,但它的主要目标不仅仅是编辑存储的数据,而是为您提供文档编辑的专用工具。

[3] 在本文的未来版本中,我们可能会考虑使用内存消耗作为另一个非常重要的指标。为了简单起见,目前已跳过此指标。

[4] 作者很乐意接受有关引擎调优的任何建议,并承诺如果结果差异很大,将更新本文。

历史

  • 2011 年 7 月 3 日 -- 初始版本
© . All rights reserved.