SQL Server 高性能写入的一些经验总结
转自:http://www.jb51.net/article/31162.htm
本篇博文将针对一些常用的数据库性能调休方法进行介绍,而且,为了编写高效的SQL代码,我们需要掌握一些基本代码优化的技巧,所以,我们将从一些基本优化技巧进行介绍
1.1.1 摘要
在开发过程中,我们不时会遇到系统性能瓶颈问题,而引起这一问题原因可以很多,有可能是代码不够高效、有可能是硬件或网络问题,也有可能是数据库设计的问题。
本篇博文将针对一些常用的数据库性能调休方法进行介绍,而且,为了编写高效的SQL代码,我们需要掌握一些基本代码优化的技巧,所以,我们将从一些基本优化技巧进行介绍。
本文目录
代码中的问题
数据库性能开销
使用存储过程
使用数据库事务
使用SqlBulkCopy
使用表参数
1.1.2 正文
假设,我们要设计一个博客系统,其中包含一个用户表(User),它用来存储用户的账户名、密码、显示名称和注册日期等信息。
由于时间的关系,我们已经把User表设计好了,它包括账户名、密码(注意:这里没有考虑隐私信息的加密存储)、显示名称和注册日期等,具体设计如下:
-- 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
)
图1 Users表设计
上面,我们定义了Users表,它包含账户名、密码、显示名称和注册日期等10个字段,其中,ID是一个自增的主键,user_resistered用来记录用户的注册时间,它设置了默认值GETDATE()。
接下来,我们将通过客户端代码实现数据存储到Users表中,具体的代码如下:
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方式实现数据写入功能,但大家是否发现代码存在问题或可以改进的地方呢?
首先,我们在客户端代码中,创建一个数据库连接,它需要占用一定的系统资源,当操作完毕之后我们需要释放占用的系统资源,当然,我们可以手动释放资源,具体实现如下:
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检查资源是否已经释放了,具体实现如下:
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语句:定义一个范围,将在此范围之外释放一个或多个对象。
@"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语句
•添加数据库新架构
•LINQ to SQL
接下来,我们将通过参数化SQL语句防御SQL注入攻击,大家也可以使用其他的方法防御SQL注入攻击,具体实现代码如下:
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()来计算写入代码的执行时间,具体代码如下:
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
VARCHAR是数据库常用的类型,但它也可能导致意想不到的性能开销;每次我们存储可变长度的列,那么SQL Server必须做更多的内存管理;字符串可以很容易地消耗数百字节的内存的,如果我们在一个VARCHAR列中设置索引,那么SQL Server执行B-树搜索时,就需要进行O(字符串长度)次比较,然而,整数字段比较次数只受限于内存延迟和CPU频率。
磁盘IO
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秒。
图3数据写入时间
使用数据库事务
想想数据是否可以延长写入到数据库中,是否可以批量地写入呢?如果允许延迟一段时间才写入到数据库中,那么我们可以使用Transaction来延迟数据写入。
数据库事务是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。 SQL Server确保事务执行成功后,数据写入到数据库中,反之,事务将回滚。
如果我们对数据库进行十次独立的操作,那么SQL Server就需要分配十次锁开销,但如果把这些操作都封装在一个事务中,那么SQL Server只需要分配一次锁开销。
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 数据写入时间
使用SqlBulkCopy
通过使用事务封装了写入操作,当我们重新运行代码,发现数据写入的速度大大提高了,只需4.5109秒,由于一个事务只需分配一次锁资源,减少了分配锁和数据库联接的耗时。
当然,我们可以也使用SqlBulkCopy实现大量数据的写入操作,具体实现代码如下:
//// 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();
}
图5 数据写入时间
上面,我们通过事务和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
接下我们在客户端代码中,调用存储过程并且将表作为参数方式传递给存储过程。
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相当。
1.1.3总结
本文通过博客系统用户表设计的例子,介绍我们在设计过程中容易犯的错误和代码的缺陷,例如:SQL注入、数据库资源释放等问题;进而使用一些常用的代码优化技巧对代码进行优化,并且通过分析数据库写入的性能开销(连接时间、解析器、数据库连接、约束处理、VARCHAR和磁盘IO),我们使用存储过程、数据库事务、SqlBulkCopy和表参数等方式降低数据库的开销。
[1] http://beginner-sql-tutorial.com/sql-query-tuning.htm
[2] http://www.dzone.com/links/r/sql_optimization_tipsquestions.html
[3] http://blackrabbitcoder.net/archive/2010/11/11/c.net-little-wonders---a-presentation.aspx
[4] http://www.altdevblogaday.com/2012/05/16/sql-server-high-performance-inserts/1.1.1 摘要
在开发过程中,我们不时会遇到系统性能瓶颈问题,而引起这一问题原因可以很多,有可能是代码不够高效、有可能是硬件或网络问题,也有可能是数据库设计的问题。
本篇博文将针对一些常用的数据库性能调休方法进行介绍,而且,为了编写高效的SQL代码,我们需要掌握一些基本代码优化的技巧,所以,我们将从一些基本优化技巧进行介绍。
本文目录
代码中的问题
数据库性能开销
使用存储过程
使用数据库事务
使用SqlBulkCopy
使用表参数
1.1.2 正文
假设,我们要设计一个博客系统,其中包含一个用户表(User),它用来存储用户的账户名、密码、显示名称和注册日期等信息。
由于时间的关系,我们已经把User表设计好了,它包括账户名、密码(注意:这里没有考虑隐私信息的加密存储)、显示名称和注册日期等,具体设计如下:
-- 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
)
图1 Users表设计
上面,我们定义了Users表,它包含账户名、密码、显示名称和注册日期等10个字段,其中,ID是一个自增的主键,user_resistered用来记录用户的注册时间,它设置了默认值GETDATE()。
接下来,我们将通过客户端代码实现数据存储到Users表中,具体的代码如下:
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方式实现数据写入功能,但大家是否发现代码存在问题或可以改进的地方呢?
首先,我们在客户端代码中,创建一个数据库连接,它需要占用一定的系统资源,当操作完毕之后我们需要释放占用的系统资源,当然,我们可以手动释放资源,具体实现如下:
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检查资源是否已经释放了,具体实现如下:
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语句:定义一个范围,将在此范围之外释放一个或多个对象。
@"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语句
•添加数据库新架构
•LINQ to SQL
接下来,我们将通过参数化SQL语句防御SQL注入攻击,大家也可以使用其他的方法防御SQL注入攻击,具体实现代码如下:
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()来计算写入代码的执行时间,具体代码如下:
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
VARCHAR是数据库常用的类型,但它也可能导致意想不到的性能开销;每次我们存储可变长度的列,那么SQL Server必须做更多的内存管理;字符串可以很容易地消耗数百字节的内存的,如果我们在一个VARCHAR列中设置索引,那么SQL Server执行B-树搜索时,就需要进行O(字符串长度)次比较,然而,整数字段比较次数只受限于内存延迟和CPU频率。
磁盘IO
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秒。
图3数据写入时间
使用数据库事务
想想数据是否可以延长写入到数据库中,是否可以批量地写入呢?如果允许延迟一段时间才写入到数据库中,那么我们可以使用Transaction来延迟数据写入。
数据库事务是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。 SQL Server确保事务执行成功后,数据写入到数据库中,反之,事务将回滚。
如果我们对数据库进行十次独立的操作,那么SQL Server就需要分配十次锁开销,但如果把这些操作都封装在一个事务中,那么SQL Server只需要分配一次锁开销。
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 数据写入时间
使用SqlBulkCopy
通过使用事务封装了写入操作,当我们重新运行代码,发现数据写入的速度大大提高了,只需4.5109秒,由于一个事务只需分配一次锁资源,减少了分配锁和数据库联接的耗时。
当然,我们可以也使用SqlBulkCopy实现大量数据的写入操作,具体实现代码如下:
//// 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();
}
图5 数据写入时间
上面,我们通过事务和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
接下我们在客户端代码中,调用存储过程并且将表作为参数方式传递给存储过程。
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相当。
1.1.3总结
本文通过博客系统用户表设计的例子,介绍我们在设计过程中容易犯的错误和代码的缺陷,例如:SQL注入、数据库资源释放等问题;进而使用一些常用的代码优化技巧对代码进行优化,并且通过分析数据库写入的性能开销(连接时间、解析器、数据库连接、约束处理、VARCHAR和磁盘IO),我们使用存储过程、数据库事务、SqlBulkCopy和表参数等方式降低数据库的开销。
[1] http://beginner-sql-tutorial.com/sql-query-tuning.htm
[2] http://www.dzone.com/links/r/sql_optimization_tipsquestions.html
[3] http://blackrabbitcoder.net/archive/2010/11/11/c.net-little-wonders---a-presentation.aspx
[4] http://www.altdevblogaday.com/2012/05/16/sql-server-high-performance-inserts/
SQL Server 高性能写入的一些经验总结的更多相关文章
- [转]SQL Server 高性能写入的一些总结
本文转自:http://www.cnblogs.com/rush/archive/2012/08/31/2666090.html 1.1.1 摘要 在开发过程中,我们不时会遇到系统性能瓶颈问题,而引起 ...
- SQL Server 高性能写入的一些总结
1.1.1 摘要 在开发过程中,我们不时会遇到系统性能瓶颈问题,而引起这一问题原因可以很多,有可能是代码不够高效.有可能是硬件或网络问题,也有可能是数据库设计的问题. 本篇博文将针对一些常用的数据库性 ...
- SQL Server 高性能写入的一些总结(转)
1.1.1 摘要 在开发过程中,我们不时会遇到系统性能瓶颈问题,而引起这一问题原因可以很多,有可能是代码不够高效.有可能是硬件或网络问题,也有可能是数据库设计的问题. 本篇博文将针对一些常用的数据库性 ...
- SQL SERVER 日志写入原理浅析
昨天看到网上有一个关于SQL SERVER 课件,便随手下载了下来看看主要讲了些什么内容,于是看到了下面两个PPT页面 由于第一张PPT上的内容不太准确(日志文件中没有“日志页”的概念,只有VLF的概 ...
- 【机器学习 Azure Machine Learning】Azure Machine Learning 访问SQL Server 无法写入问题 (使用微软Python AML Core SDK)
问题情形 使用Python SDK在连接到数据库后,连接数据库获取数据成功,但是在Pandas中用 to_sql 反写会数据库时候报错.错误信息为:ProgrammingError: ('42000' ...
- 从SQL Server到MySQL,近百亿数据量迁移实战
从SQL Server到MySQL,近百亿数据量迁移实战 狄敬超(3D) 2018-05-29 10:52:48 212 沪江成立于 2001 年,作为较早期的教育学习网站,当时技术选型范围并不大:J ...
- SQL Server 服务器磁盘测试之SQLIO篇(二)
上次放出了一篇文章,针对磁盘卷簇大小默认4KB和自定义64KB进行了测试,测试内容为随机和顺序读写,大小为8KB和64KB,有人觉得这并没有照顾到SQL Server所有的IO使用情景.这篇测试文章, ...
- SQL Server安全(11/11):审核(Auditing)
在保密你的服务器和数据,防备当前复杂的攻击,SQL Server有你需要的一切.但在你能有效使用这些安全功能前,你需要理解你面对的威胁和一些基本的安全概念.这篇文章提供了基础,因此你可以对SQL Se ...
- SQL Server中的事务日志管理(8/9):优化日志吞吐量
当一切正常时,没有必要特别留意什么是事务日志,它是如何工作的.你只要确保每个数据库都有正确的备份.当出现问题时,事务日志的理解对于采取修正操作是重要的,尤其在需要紧急恢复数据库到指定点时.这系列文章会 ...
随机推荐
- HTML5初学---坦克大战基础
让小球动起来,根据键盘的W(上),D(右),S(下),A(左):键的点击移动小球 <!DOCTYPE html> <html> <head> <meta ch ...
- 手机端图片插件可缩放 旋转 全屏查看photoswipe
官方介绍PhotoSwipe 是专为移动触摸设备设计的相册/画廊.兼容所有iPhone.iPad.黑莓6+,以及桌面浏览器.底层实现基于HTML/CSS/JavaScript,是一款免费开源的相册产品 ...
- javascript中判断变量时变量值为 0 的特殊情况
有时候我们在js中会直接判断变量是否存在值,下面列举一些情况: var a = 0; var b = 1; var c = ' '; var d; console.log( a ? 1 : null) ...
- Tomcat的类加载机制
一个功能健全的Web服务器,要解决如下几个问题: 部署在同一个服务器上的两个Web应用程序使用的Java 类库可以实现相互隔离.不能要求一个类库在一个服务器中只有一份,服务器应当保证两个应用程序的类 ...
- 2014年spark开发者大赛火热进行中!
“发现最有正能量的网络达人”,Spark开发者大赛火热进行! 2014年9月30日,2014 Spark开发者大赛在北京正式启动.本次大赛由Spark亚太研究院联合国内领先的IT技术创新与发展的互联网 ...
- HDU 1811 Rank of Tetris 【拓扑排序 + 并查集】
自从Lele开发了Rating系统,他的Tetris事业更是如虎添翼,不久他遍把这个游戏推向了全球. 为了更好的符合那些爱好者的喜好,Lele又想了一个新点子:他将制作一个全球Tetris高手排行榜, ...
- codeforces Round #440 B Maximum of Maximums of Minimums【思维/找规律】
B. Maximum of Maximums of Minimums time limit per test 1 second memory limit per test 256 megabytes ...
- HTTP状态代码集
所有 HTTP 状态代码及其定义. 代码 指示 2xx 成功 200 正常:请求已完成. 201 正常:紧接 POST 命令. 202 正常:已接受用于处 ...
- IDEA的Maven项目找不到class
- 某道我xjb想的题
Function 时限:5s 空限:256M (都是单点) Discription 现在你有一个函数: inline int f(int x){ int tot=0,alr=0,now; while( ...