Windows Phone本地数据库性能最佳实践
原文链接:http://windowsphonegeek.com/articles/Performance-Best-Practices-Windows-Phone-Mango-Local-Database
In this article I am going to talk about Windows Phone Mango Local Database Performance Best Practices. I will discuss three different ways in which you can improve the performance of your windows phone app when working with a local database both in terms of speed and memory consumption. I will not only prove that the performance is really improved but will also measure exactly how much it is improved.(本文讨论Mango本地数据库最佳实践。将从三个方面来讨论如何改善wp数据库应用在速度及内存消耗方面的性能。不仅要改善程序的性能而且要量化到具体改善了多少。)
NOTE: "Local Database" comes with the Windows Phone 7.1 Mango update. Here are some of our previous posts you can take a look for reference:
- Windows Phone Mango Local Database: mapping and database operations
- Using SqlMetal to generate Windows Phone Mango Local Database classes
Local Database Performance Best Practices:
The most important techniques that you can use to improve the performance of your app(regarding Local Database) are:
- Defining a Version Column in the Entity classes
- Implementing INotifyPropertyChanging in the Entity classes
- Using Compiled Queries
关于提升应用性能最重要的技术是:
在实体类中定义版本列 ColumnAttribute.IsVersion
在实体类中实现INotifyPropertyChanging
使用编译查询
For reference you can also take a look at the official MSDN documentation.
To begin with lets first create a new Windows Phone 7.1 Mango(in our case we will use the latest Beta version of the tools) application project. We will first create here a test setup that will be the base line for our testing and measurements.
首先,建立一个WP7.1应用。首先创建一个测试设置,这将是我们的测试和测量基准线。
The Baseline
We will use the following class without any improvements in order to test the baseline performance of the Local Database:
我们将使用下面的类,不做任何改善,作为本地数据库的性能测试基准:
[Table]
public
class
PersonSimple : IPerson
{
[Column(IsPrimaryKey = true, IsDbGenerated = true)]
public int ID
{
get;
set;
}
[Column(CanBeNull = false)]
public string FirstName
{
get;
set;
}
[Column(CanBeNull = false)]
public string LastName
{
get;
set;
}
[Column(CanBeNull = false)]
public int Age
{
get;
set;
}
[Column(CanBeNull = true)]
public string Address
{
get;
set;
}
[Column(CanBeNull = true)]
public string Email
{
get;
set;
}
[Column(CanBeNull = true)]
public string WebSite
{
get;
set;
}
}
IPerson is a simple Interface that we will use to test the different versions of the Person class using the same Generic code:
我们使用 IPerson 接口,通过同样的泛型代码来测试不同版本Person类
public interface IPerson
{
string FirstName { get; set; }
string LastName { get; set; }
int Age { get; set; }
string Address { get; set; }
string Email { get; set; }
string WebSite { get; set; }
}
Test Setup Classes
Besides the PersonSimple the test setup also includes:
除 PersonSimple 测试设置还包括:
1. The following two improved versions:(下面两个增强版本)
- PersonVersioned: with added an additional version column(含版本列)
- PersonOptimized: with version column and implemented INotifyPropertyChanging (包含版本列并且实现INotifyPropertyChanging)
2. For each one of the three types above, there is a separate table in the database which is initialized with 10000 rows. 对于上述三类Person,分别对应数据库中三个独立的表,分别被初始化存放10000条记录
3. In order to test the version column and INotifyPropertyChanging techniques, we will change the FirstName and LastName of every 50th row from the given table. Here is how our helper method should look like: (为了测试版本列及INotifyPropertyChanging,分别更改第50行记录的FirstName和LastName。泛型方法如下)
private long UpdatePersonEntities<T>(Func<PersonDataContext, Table<T>> getCollection) where T : class, IPerson, new()
{
long elapsedMilliseconds;
using (PersonDataContext context = new PersonDataContext(ConnectionString))
{
Table<T> collection = getCollection(context);
List<T> personList = collection.ToList();
int count = personList.Count;
for (int i = 0; i < count; i++)
{
if ((i % 50) != 0)
{
continue;
}
IPerson person = personList[i];
person.FirstName = string.Concat(person.FirstName, "changed");
person.LastName = string.Concat(person.LastName, "changed");
//person.Age = person.Age + 1;
//person.Address = string.Concat(person.Address, "changed");
//person.Email = string.Concat(person.Email, "changed");
//person.WebSite = string.Concat(person.WebSite, "changed");
}
Debug.WriteLine("Update {0} entities...", count);
Stopwatch stopwatch = Stopwatch.StartNew();
// save changes to the database
context.SubmitChanges();
stopwatch.Stop();
elapsedMilliseconds = stopwatch.ElapsedMilliseconds;
Debug.WriteLine("Time elapsed: {0} ms", elapsedMilliseconds);
}
return elapsedMilliseconds;
}
In short this method tries to reproduce the conditions which expose the performance improvements achieved using these two optimizations.
总之,这种方法试图重现这两种技术的性能改进的情况。
Defining a Version Column in the Entity classes
This optimization is specific to LINQ to SQL for Windows Phone. For example, in an entity, add the following code.
这种优化是关于Windows Phone 的 LINQ To SQL
[Table]
public
class
PersonVersioned : IPerson
{
[Column(IsVersion =
true
)]
private
Binary version;
// More code here...
}
Implementing this optimization can yield a significant performance improvement for large updates.
本优化可以对大量数据更新产生性能改进。
Implementing INotifyPropertyChanging in the Entity classes
According to the MSDN:
"LINQ to SQL change tracking works by maintaining two copies of each object. One copy of the object remains as it was originally materialized from the database. The other copy is changed by the application. Then, when a change is submitted, LINQ to SQL can determine which properties have been updated and submit only those changes to the database transaction.(LINQ to SQl通过维护一个实例的两个副本跟踪变化。一个副本表示从数据库中取出后的原始状态,另一个表示应用程序的操作更改。当更改被提交到数据库时,LINQ to SQL可以判断哪些已经被更新,并且仅更新哪些发生改变的。)
By default, LINQ to SQL will create the two copies of the object when the objects are materialized. Frequently, however, only a handful of objects in the materialized collection will actually get modified within a specific transaction. In this case, there is no reason to keep a second copy of the object.(默认情况下,LINQ to SQL为属性创建两个副本。然而,通常情况下,只有集合中的少数对象会在特定操作中被修改。因此,就没必要再保存第二个副本了。)
The INotifyPropertyChanging interface allows the application to notify the DataContext when it is modifying a property that will ultimately be submitted as an update to the database. The DataContext can use that notification as a trigger to create the copy. This way, only the items that are actually changing need to be duplicated."(INotifyPropertyChanging可以使程序修改属性时通知DataContext。DataContext可以像触发器一样使用这些通知创建副本。这样,只有发生更改的属性需要第二个副本,这将减少内存占用)
[Table]
public class PersonOptimized : IPerson, INotifyPropertyChanging
{
private int id;
private string firstName;
private string lastName;
private int age;
private string address;
private string email;
private string webSite;
[Column(IsVersion = true)]
private Binary version;
[Column(IsPrimaryKey = true, IsDbGenerated = true)]
public int ID
{
get
{
return this.id;
}
set
{
if (this.id != value)
{
this.OnPropertyChanging("ID");
this.id = value;
}
}
}
//...
//More code...
}
测试结果:
PersonSimple:
更新前内存使用:11560 KB
更新后内存使用:22092 KB
更新使用内存: 10532 KB
更新耗时: 2082 ms
PersonVersioned:
更新前内存使用:12580 KB
更新后内存使用:20244 KB
更新使用内存: 7664 KB
更新耗时: 1278 ms
PersonOptimized:
更新前内存使用:12932 K
更新后内存使用:B16264 KB
更新使用内存: 3332 KB
更新耗时: 326 ms
Compiled Query Tests
What is Compiled Query?
"By default, LINQ to SQL will translate the LINQ expression tree into the corresponding Transact-SQL statement every time the query is executed at run time. For queries that are executed with great frequency (for example, find the record with this ID), the overhead of generating the corresponding Transact-SQL each time is very wasteful. To avoid this inefficiency, you can use compiled queries. Compiled queries generate parameterized Transact-SQL statements ahead of time, which can then be reused with different values."(默认情况下,查询执行时LINQ to SQL将LINQ表达式树翻译为对应的T-SQL语句,对于经常执行的查询(例如,根据ID查找相应记录),每次生成相应的T-SQL的开销是非常浪费的。为了避免这种低效,可以使用编译查询。编译查询提前生成带参数的T-SQL语句,然后可以带入不同的参数值重用已生成的T-SQL语句。)
Here is how our Compiled Query Test method looks like. The method executes 100 times two versions of the same query- compiled and not compiled.
private
void
btnTestCompiledQuery_Click(
object
sender, RoutedEventArgs e)
{
Func<PersonDataContext, string, int, IQueryable<PersonSimple>> getPeopleByAddress =
CompiledQuery.Compile((PersonDataContext context, string addressQuery, int age) =>
from p in context.PeopleSimple where p.Address.Contains(addressQuery) && p.Age > age select p);
int count = 100;
long millisecondsCompiled;
long millisecondsQuery;
using (PersonDataContext context = new PersonDataContext(ConnectionString))
{
Debug.WriteLine("Start compiled query test...");
Stopwatch stopwatch = Stopwatch.StartNew();
for (int i = 0; i < count; i++)
{
var peopleByAddressQuery = getPeopleByAddress(context, i.ToString(), i);
//execute query
PersonSimple personByAddress = peopleByAddressQuery.FirstOrDefault();
}
stopwatch.Stop();
millisecondsCompiled = stopwatch.ElapsedMilliseconds;
Debug.WriteLine("Time elapsed: {0} ms", millisecondsCompiled);
Debug.WriteLine("Start normal query test...");
stopwatch = Stopwatch.StartNew();
for (int i = 0; i < count; i++)
{
var peopleByAddressQuery = from p in context.PeopleSimple where p.Address.Contains(i.ToString()) && p.Age > i select p;
//execute query
PersonSimple personByAddress = peopleByAddressQuery.FirstOrDefault();
}
stopwatch.Stop();
millisecondsQuery = stopwatch.ElapsedMilliseconds;
Debug.WriteLine("Time elapsed: {0} ms", millisecondsQuery);
}
StringBuilder messageBuilder = new StringBuilder();
messageBuilder.AppendFormat("Get people by age normal: {0} ms", millisecondsQuery).AppendLine();
messageBuilder.AppendFormat("Get people by age compiled: {0} ms", millisecondsCompiled).AppendLine();
MessageBox.Show(messageBuilder.ToString());
}
stopwatch.Stop();
millisecondsQuery = stopwatch.ElapsedMilliseconds;
Debug.WriteLine(
"Time elapsed: {0} ms"
, millisecondsQuery);
}
StringBuilder messageBuilder =
new
StringBuilder();
messageBuilder.AppendFormat(
"Get people by age normal: {0} ms"
, millisecondsQuery).AppendLine();
messageBuilder.AppendFormat(
"Get people by age compiled: {0} ms"
, millisecondsCompiled).AppendLine();
MessageBox.Show(messageBuilder.ToString());
}
Test Results
非编译查询耗时: 701 ms
编译查询耗时: 501 ms
Conclusion
In conclusion we have proved that all three methods improve the performance significantly. We were even pleasantly surprised that by just adding a version column there was around 30% Memory usage improvement, besides the 40% improvements in speed.(总之,我们已经证明,所有这三种方法显着提高性能。我们甚至惊喜,只需添加一个版本列就有30%左右的内存使用率改善以及40%的速度提升 。)
Here are the results for the version column and INotifyPropertyChanging techniques combined in a chart:
Finally, using Compiled Queries can also lead to significant time savings (in our test about 29%)
That was all about Windows Phone Mango Local Database Performance Best Practices. Here is the full source code of the test project:
WPMangoLocalDatabasePerformance
I hope that the article was helpful.