1.1.1 摘要html
在开发过程当中,咱们不时会遇到系统性能瓶颈问题,而引发这一问题缘由能够不少,有多是代码不够高效、有多是硬件或网络问题,也有多是数据库设计的问题。mysql
本篇博文将针对一些经常使用的数据库性能调休方法进行介绍,并且,为了编写高效的SQL代码,咱们须要掌握一些基本代码优化的技巧,因此,咱们将从一些基本优化技巧进行介绍。sql
假设,咱们要设计一个博客系统,其中包含一个用户表(User),它用来存储用户的帐户名、密码、显示名称和注册日期等信息。数据库
因为时间的关系,咱们已经把User表设计好了,它包括帐户名、密码(注意:这里没有考虑隐私信息的加密存储)、显示名称和注册日期等,具体设计以下:api
-- =============================================
-- Author: JKhuang
-- Create date: 7/8/2012
-- Description: A table stores the user information.
-- =============================================
CREATE TABLE [dbo].[jk_users]( -- This is the reference to Users table, it is primary key. [ID] [bigint] IDENTITY(1,1) NOT NULL, [user_login] [varchar](60) NOT NULL, [user_pass] [varchar](64) NOT NULL, [user_nicename] [varchar](50) NOT NULL, [user_email] [varchar](100) NOT NULL, [user_url] [varchar](100) NOT NULL, -- This field get the default from function GETDATE(). [user_registered] [datetime] NOT NULL CONSTRAINT [DF_jk_users_user_registered] DEFAULT (getdate()), [user_activation_key] [varchar](60) NOT NULL, [user_status] [int] NOT NULL CONSTRAINT [DF_jk_users_user_status] DEFAULT ((0)), [display_name] [varchar](250) NOT NULL )
上面,咱们定义了Users表,它包含帐户名、密码、显示名称和注册日期等10个字段,其中,ID是一个自增的主键,user_resistered用来记录用户的注册时间,它设置了默认值GETDATE()。服务器
接下来,咱们将经过客户端代码实现数据存储到Users表中,具体的代码以下:网络
//// Creates a database connection.
var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN1"].ToString()); conn.Open(); //// This is a massive SQL injection vulnerability, //// don't ever write your own SQL statements with string formatting! string sql = String.Format( @"INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, user_status,display_name, user_url, user_activation_key) VALUES ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}')", userLogin, userPass, userNicename, userEmail, userStatus, displayName, userUrl, userActivationKey); var cmd = new SqlCommand(sql, conn); cmd.ExecuteNonQuery(); //// Because this call to Close() is not wrapped in a try/catch/finally clause, //// it could be missed if an exception occurs above. Don't do this! conn.Close();
上面,咱们使用再普通不过的ADO.NET方式实现数据写入功能,但你们是否发现代码存在问题或能够改进的地方呢?架构
首先,咱们在客户端代码中,建立一个数据库链接,它须要占用必定的系统资源,当操做完毕以后咱们须要释放占用的系统资源,固然,咱们能够手动释放资源,具体实现以下:app
//// Creates a database connection.
var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN1"].ToString()); conn.Open(); //// This is a massive SQL injection vulnerability, //// don't ever write your own SQL statements with string formatting! string sql = String.Format( @"INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, user_status,display_name, user_url, user_activation_key) VALUES ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}')", userLogin, userPass, userNicename, userEmail, userStatus, displayName, userUrl, userActivationKey); var cmd = new SqlCommand(sql, conn); cmd.ExecuteNonQuery(); //// If throws an exception on cmd dispose. cmd.Dispose(); //// conn can't be disposed. conn.Close(); conn.Dispose();
假如,在释放SqlCommand资源时抛出异常,那么在它后面的资源SqlConnection将得不到释放。咱们仔细想一想当发生异常时,能够经过try/catch捕获异常,因此不管是否发生异常均可以使用finally检查资源是否已经释放了,具体实现以下:
SqlCommand cmd = null; SqlConnection conn = null; try { //// Creates a database connection. conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN1"].ToString()); conn.Open(); //// This is a massive SQL injection vulnerability, //// don't ever write your own SQL statements with string formatting! string sql = String.Format( @"INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, user_status,display_name, user_url, user_activation_key) VALUES ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}')", userLogin, userPass, userNicename, userEmail, userStatus, displayName, userUrl, userActivationKey); cmd = new SqlCommand(sql, conn); cmd.ExecuteNonQuery(); } finally { //// Regardless of whether there is an exception, //// we will dispose the resource. if (cmd != null) cmd.Dispose(); if (conn != null) conn.Dispose(); }
经过上面的finally方式处理了异常状况是很广泛的,但为了更安全释放资源,使得咱们增长了finally和if语句,那么是否有更简洁的方法实现资源的安全释放呢?
其实,咱们可使用using语句实现资源的释放,具体实现以下:
using语句:定义一个范围,将在此范围以外释放一个或多个对象。
string sql = String.Format( @"INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, user_status,display_name, user_url, user_activation_key) VALUES ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}')", userLogin, userPass, userNicename, userEmail, userStatus, displayName, userUrl, userActivationKey); //// Creates a database connection. using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN1"].ToString())) using (var cmd = new SqlCommand(sql, conn)) { //// Your code here. }
上面的代码使用了using语句实现资源的释放,那么是否全部对象均可以使用using语句实现释放呢?
只有类型实现了IDisposable接口而且重写Dispose()方法可使用using语句实现资源释放,因为SqlConnection和SqlCommand实现了IDisposable接口,那么咱们可使用using语句实现资源释放和异常处理。
在客户端代码中,咱们使用拼接SQL语句方式实现数据写入,因为SQL语句是动态执行的,因此恶意用户能够经过拼接SQL的方式实施SQL注入攻击。
对于SQL注入攻击,咱们能够经过如下方式防护:
接下来,咱们将经过参数化SQL语句防护SQL注入攻击,你们也可使用其余的方法防护SQL注入攻击,具体实现代码以下:
//// Creates a database connection.
using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN1"].ToString())) { conn.Open(); string sql = string.Format( @"INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, user_status,display_name, user_url, user_activation_key)"); using (var cmd = new SqlCommand(sql, conn)) { //// Parameterized SQL to defense injection attacks cmd.Parameters.Add("@user_login", userLogin); cmd.Parameters.Add("@user_pass", userPass); cmd.Parameters.Add("@user_nicename", userNicename); cmd.Parameters.Add("@user_email", userEmail); cmd.Parameters.Add("@user_status", userStatus); cmd.Parameters.Add("@display_name", displayName); cmd.Parameters.Add("@user_url", userUrl); cmd.Parameters.Add("@user_activation_key", userActivationKey); cmd.ExecuteNonQuery(); } }
上面经过参数化SQL语句和using语句对代码进行改进,如今代码的可读性更强了,并且也避免了SQL注入攻击和资源释放等问题。
接下来,让咱们简单的测试一下代码执行时间,首先咱们在代码中添加方法Stopwatch.StartNew()和Stopwatch.Stop()来计算写入代码的执行时间,具体代码以下:
//// calc insert 10000 records consume time.
var sw = Stopwatch.StartNew(); //// Creates a database connection. using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN2"].ToString())) { conn.Open(); int cnt = 0; while (cnt++ < 10000) { string sql = string.Format(@"INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, user_status,display_name, user_url, user_activation_key) VALUES (@user_login, @user_pass, @user_nicename, @user_email, @user_status, @display_name, @user_url, @user_activation_key)"); using (var cmd = new SqlCommand(sql, conn)) { //// Parameterized SQL to defense injection attacks cmd.Parameters.Add("@user_login", userLogin); cmd.Parameters.Add("@user_pass", userPass); cmd.Parameters.Add("@user_nicename", userNicename); cmd.Parameters.Add("@user_email", userEmail); cmd.Parameters.Add("@user_status", userStatus); cmd.Parameters.Add("@display_name", displayName); cmd.Parameters.Add("@user_url", userUrl); cmd.Parameters.Add("@user_activation_key", userActivationKey); cmd.ExecuteNonQuery(); } } } sw.Stop(); }
上面,咱们往数据库中写入了10000条数据,执行时间为 7.136秒(个人机器很破了),这样系统性能仍是能够知足许多公司的需求了。
假如,用户请求量增大了,咱们还能保证系统能知足需求吗?事实上,咱们不该该知足于现有的系统性能,由于咱们知道代码的执行效率还有很大的提高空间。
接下来,将进一步介绍代码改善的方法。
为了使数据库得到更快的写入速度,咱们必须了解数据库在进行写入操做时的主要耗时。
当咱们执行conn.Open()时,首先,必须创建物理通道(例如套接字或命名管道),必须与服务器进行初次握手,必须分析链接字符串信息,必须由服务器对链接进行身份验证,必须运行检查以便在当前事务中登记,等等
这一系列操做可能须要一两秒钟时间,若是咱们每次执行conn.Open()都有进行这一系列操做是很耗费时间的,为了使打开的链接成本最低,ADO.Net使用称为链接池的优化方法。
链接池:减小新链接须要打开的次数,只要用户在链接上调用Open()方法,池进程就会检查池中是否有可用的链接,若是某个池链接可用,那么将该链接返回给调用者,而不是建立新链接;应用程序在该链接上调用Close()或Dispose()时,池进程会将链接返回到活动链接池集中,而不是真正关闭链接,链接返回到池中以后,便可在下一个Open调用中重复使用。
当咱们向SQL Server传递SQL语句INSERT INTO …时,它须要对SQL语句进行解析,因为SQL Server解析器执行速度很快,因此解析时间每每是能够忽略不计,但咱们仍然能够经过使用存储过程,而不是直SQL语句来减小解析器的开销。
为了提供ACID(事务的四个特性),SQL Server必须确保全部的数据库更改是有序的。它是经过使用锁来确保该数据库插入、删除或更新操做之间不会相互冲突(关于数据库的锁请参考这里)。
因为,大多数数据库都是面向多用户的环境,当咱们对User表进行插入操做时,也许有成千上百的用户也在对User表进行操做,因此说,SQL Server必须确保这些操做是有序进行的。
那么,当SQL Server正在作全部这些事情时,它会产生锁,以确保用户得到有意义的结果。SQL Server保证每条语句执行时,数据库是彻底可预测的(例如:预测SQL执行方式)和管理锁都须要耗费必定的时间。
在插入数据时,每一个约束(如:外键、默认值、SQL CHECK等)须要额外的时间来检测数据是否符合约束;因为SQL Server为了保证每一个插入、更新或删除的记录都符合约束条件,因此,咱们须要考虑是否应该在数据量大的表中增长约束条件。
VARCHAR是数据库经常使用的类型,但它也可能致使意想不到的性能开销;每次咱们存储可变长度的列,那么SQL Server必须作更多的内存管理;字符串能够很容易地消耗数百字节的内存的,若是咱们在一个VARCHAR列中设置索引,那么SQL Server执行B-树搜索时,就须要进行O(字符串长度)次比较,然而,整数字段比较次数只受限于内存延迟和CPU频率。
SQL Server最终会将数据写入到磁盘中,首先,SQL Server把数据写入到事务日志中,当执行备份时,事务日志会合并到永久的数据库文件中;这一系列操做由后台完成,它不会影响到数据查询的速度,但每一个事物都必须拥有属于本身的磁盘空间,因此咱们能够经过给事务日志和主数据文件分配独立的磁盘空间减小IO开销,固然,最好解决办法是尽量减小事务的数量。
正如你们所看到的,咱们经过优化联接时间、 解析器的开销、 数据库联接、约束处理,、Varchar和磁盘IO等方法来优化数据库,接下来,咱们将对前面的例子进行进一步的优化。
前面例子中,咱们把SQL代码直接Hardcode在客户端代码中,那么,数据库就须要使用解析器解析客户端中SQL语句,因此咱们能够改用使用存储过程,从而,减小解析器的时间开销;更重要的一点是,因为SQL是动态执行的,因此咱们修改存储过程当中的SQL语句也无需从新编译和发布程序。
User表中的字段user_registered设置了默认值(GETDATE()),那么咱们经过消除表默认值约束来提升系统的性能,简而言之,咱们须要提供字段user_registered的值。
接下来,让咱们省去User表中的默认值约束和增长存储过程,具体代码以下:
-- =============================================
-- Author: JKhuang
-- Create date: 08/16/2012
-- Description: Creates stored procedure to insert
-- data into table jk_users.
-- =============================================
ALTER PROCEDURE [dbo].[SP_Insert_jk_users] @user_login varchar(60), @user_pass varchar(64), @user_nicename varchar(50), @user_email varchar(100), @user_url varchar(100), @user_activation_key varchar(60), @user_status int, @display_name varchar(250) AS BEGIN SET NOCOUNT ON; -- The stored procedure allows SQL server to avoid virtually all parser work INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, user_status,display_name, user_url, user_activation_key, user_registered) VALUES (@user_login, @user_pass, @user_nicename, @user_email, @user_status, @display_name, @user_url, @user_activation_key, GETDATE()); END
上面咱们定义了存储过程SP_Insert_jk_users向表中插入数据,当咱们从新执行代码时,发现数据插入的时间缩短为6.7401秒。
想一想数据是否能够延长写入到数据库中,是否能够批量地写入呢?若是容许延迟一段时间才写入到数据库中,那么咱们可使用Transaction来延迟数据写入。
数据库事务是数据库管理系统执行过程当中的一个逻辑单位,由一个有限的数据库操做序列构成。 SQL Server确保事务执行成功后,数据写入到数据库中,反之,事务将回滚。
若是咱们对数据库进行十次独立的操做,那么SQL Server就须要分配十次锁开销,但若是把这些操做都封装在一个事务中,那么SQL Server只须要分配一次锁开销。
//// calc insert 10000 records consume time.
var sw = Stopwatch.StartNew(); //// Creates a database connection. using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN2"].ToString())) { conn.Open(); int cnt = 0; SqlTransaction trans = conn.BeginTransaction(); while (cnt++ < 10000) { using (var cmd = new SqlCommand("SP_Insert_jk_users", conn)) { //// Parameterized SQL to defense injection attacks cmd.CommandType = CommandType.StoredProcedure; //// Uses transcation to batch insert data. //// To avoid lock and connection overhead. cmd.Transaction = trans; cmd.Parameters.Add("@user_login", userLogin); cmd.Parameters.Add("@user_pass", userPass); cmd.Parameters.Add("@user_nicename", userNicename); cmd.Parameters.Add("@user_email", userEmail); cmd.Parameters.Add("@user_status", userStatus); cmd.Parameters.Add("@display_name", displayName); cmd.Parameters.Add("@user_url", userUrl); cmd.Parameters.Add("@user_activation_key", userActivationKey); cmd.ExecuteNonQuery(); } } //// If no exception, commit transcation. trans.Commit(); } sw.Stop(); }
经过使用事务封装了写入操做,当咱们从新运行代码,发现数据写入的速度大大提升了,只需4.5109秒,因为一个事务只需分配一次锁资源,减小了分配锁和数据库联接的耗时。
固然,咱们能够也使用SqlBulkCopy实现大量数据的写入操做,具体实现代码以下:
var sw = Stopwatch.StartNew(); //// Creates a database connection. using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN2"].ToString())) { conn.Open(); using (var bulkCopy = new SqlBulkCopy(conn)) { //// Maping the data columns. bulkCopy.ColumnMappings.Add("user_login", "user_login"); bulkCopy.ColumnMappings.Add("user_pass", "user_pass"); bulkCopy.ColumnMappings.Add("user_nicename", "user_nicename"); bulkCopy.ColumnMappings.Add("user_email", "user_email"); bulkCopy.ColumnMappings.Add("user_url", "user_url"); bulkCopy.ColumnMappings.Add("user_registered", "user_registered"); bulkCopy.ColumnMappings.Add("user_activation_key", "user_activation_key"); bulkCopy.ColumnMappings.Add("user_status", "user_status"); bulkCopy.ColumnMappings.Add("display_name", "display_name"); bulkCopy.DestinationTableName = "dbo.jk_users"; //// Insert data into datatable. bulkCopy.WriteToServer(dataRows); } sw.Stop(); }
上面,咱们经过事务和SqlBulkCopy实现数据批量写入数据库中,但事实上,每次咱们调用cmd.ExecuteNonQuery()方法都会产生一个往返消息,从客户端应用程序到数据库中,因此咱们想是否存在一种方法只发送一次消息就完成写入的操做呢?
若是,你们使用SQL Server 2008,它提供一个新的功能表变量(Table Parameters)能够将整个表数据聚集成一个参数传递给存储过程或SQL语句。它的注意性能开销是将数据聚集成参数(O(数据量))。
如今,咱们修改以前的代码,在SQL Server中定义咱们的表变量,具体定义以下:
-- =============================================
-- Author: JKhuang
-- Create date: 08/16/2012
-- Description: Declares a user table paramter.
-- =============================================
CREATE TYPE jk_users_bulk_insert AS TABLE ( user_login varchar(60), user_pass varchar(64), user_nicename varchar(50), user_email varchar(100), user_url varchar(100), user_activation_key varchar(60), user_status int, display_name varchar(250) )
上面,咱们定义了一个表参数jk_users_bulk_insert,接着咱们定义一个存储过程接受表参数jk_users_bulk_insert,具体定义以下:
-- =============================================
-- Author: JKhuang
-- Create date: 08/16/2012
-- Description: Creates a stored procedure, receive
-- a jk_users_bulk_insert argument.
-- =============================================
CREATE PROCEDURE sp_insert_jk_users @usersTable jk_users_bulk_insert READONLY AS INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, user_url, user_activation_key, user_status, display_name, user_registered) SELECT user_login, user_pass, user_nicename, user_email, user_url, user_activation_key, user_status, display_name, GETDATE() FROM @usersTable
接下咱们在客户端代码中,调用存储过程而且将表做为参数方式传递给存储过程。
var sw = Stopwatch.StartNew(); using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN2"].ToString())) { conn.Open(); //// Invokes the stored procedure. using (var cmd = new SqlCommand("sp_insert_jk_users", conn)) { cmd.CommandType = CommandType.StoredProcedure; //// Adding a "structured" parameter allows you to insert tons of data with low overhead var param = new SqlParameter("@userTable", SqlDbType.Structured) { Value = dt }; cmd.Parameters.Add(param); cmd.ExecuteNonQuery(); } } sw.Stop();
如今,咱们从新执行写入操做发现写入效率与SqlBulkCopy至关。
本文经过博客系统用户表设计的例子,介绍咱们在设计过程当中容易犯的错误和代码的缺陷,例如:SQL注入、数据库资源释放等问题;进而使用一些经常使用的代码优化技巧对代码进行优化,而且经过分析数据库写入的性能开销(链接时间、解析器、数据库链接、约束处理、VARCHAR和磁盘IO),咱们使用存储过程、数据库事务、SqlBulkCopy和表参数等方式下降数据库的开销。