繁体   English   中英

sql异常非常慢……如何加快速度?

[英]Sql exceptions are very slow… how to speed up?

我有一段代码将数据导入到sql表中。 当传入数据出现问题时(例如,错误的数据类型或将被截断),它将生成一个预期的异常,但是它非常慢。 如果它们都很好,我可以在几秒钟内导入10,000行,但是有时源数据需要一点点tlc,因此应用程序会导入这些行并报告异常,以便对其进行修复。 例如,如果所有10,000行的一个字段都需要缩短,因为该字段将被截断,那么它将花费大约10分钟而不是10或20秒。

增加的时间并不是因为catch块中正在运行任何代码...即使在catch块中根本没有代码,它也会这样做。 有人知道这仅仅是它的方式吗,还是可以做些什么使它更快?

编辑:好的,根据请求,我在下面添加了一些代码,但是我想您很快就会明白为什么我认为一开始没有必要这样做:)

    public void ImportRow(DataRow r, SqlConnection conn, SqlTransaction trx)
    {
        var sqlCmd = _importCommand.CreateSqlCommand(r);
        SqlCommand sCom = new SqlCommand(sqlCmd, conn, trx);

        try
        {
            sCom.ExecuteNonQuery();
        }
        catch (Exception e)
        {

        }
    }

_importCommand.CreateSqlCommand(r)仅返回一个字符串(您可能已经在SqlCommand构造函数参数中使用它来推断出该字符串)。 如您所见,我还没有向catch块添加任何代码。 如果没有异常,它将执行sCom.ExecuteNonQuery(); 速度很快,但是如果有例外,则会有短暂的停顿。 ImportRow方法是从另一个函数循环调用的,但是我肯定地确定此方法的try块产生了滞后,因此其他代码无关。

当您在批量插入操作中遇到错误时,必须回滚成功插入的记录。 根据经验,与撤消操作相比,回滚过程中操作的撤消总是比较慢,因为它必须回读日志(为写而不是读进行了优化) 补偿了该操作。 但是,由此无法解释10秒到10分钟之间的差异。 因此,除非您能提供某种证据证明服务器需要10分钟,否则我必须得出结论,这就是您的应用程序代码在这10分钟内正在做什么。

更新

让我们尝试全部失败的10k插入,然后将其与成功的10k插入进行比较:

set nocount on;
use master;
if db_id('test') is not null
begin
    alter database test set single_user with rollback immediate;
    drop database test;
end
go

create database test;
go

use test;
go

create table good (a int, b char(1000));
go

declare @start datetime = getdate(), @i int =0;
begin transaction;
while @i < 10000
begin
    insert into good (a) values (@i);
    set @i += 1;
end
commit;
declare @end datetime = getdate();
select 'good: ', datediff(ms, @start, @end);
go

create table bad (a int, b char(1000), constraint fail check (a<0));
go

declare @start datetime = getdate(), @i int =0;
begin transaction;
while @i < 10000
begin
    insert into bad (a) values (@i);
    set @i += 1;
end
commit;
declare @end datetime = getdate();
select 'bad: ', datediff(ms, @start, @end);
go

在我的测试机上,“好”情况得到约600ms,而坏情况得到1400ms。 因此,例外情况使时间加倍,但几乎没有分钟。

接下来,让我们做同样的事情,但是要从托管客户端进行:

    static void Main(string[] args)
    {
        try
        {
            using (SqlConnection conn = new SqlConnection(@"..."))
            {
                conn.Open();
                Stopwatch sw = new Stopwatch();
                sw.Start();
                using (SqlTransaction trn = conn.BeginTransaction())
                {
                    SqlCommand cmd = new SqlCommand(
                       "insert into good (a) values (@i)", conn, trn);
                    SqlParameter p = new SqlParameter("@i", SqlDbType.Int);
                    cmd.Parameters.Add(p);
                    for (int i = 0; i < 10000; ++i)
                    {
                        p.Value = i;
                        cmd.ExecuteNonQuery();
                    }
                    trn.Commit();
                    sw.Stop();
                }
                Console.WriteLine("Good: {0}", sw.Elapsed);

                int excount = 0;
                sw.Reset();
                sw.Start();

                using (SqlTransaction trn = conn.BeginTransaction())
                {
                    SqlCommand cmd = new SqlCommand(
                        "insert into bad (a) values (@i)", conn, trn);
                    SqlParameter p = new SqlParameter("@i", SqlDbType.Int);
                    cmd.Parameters.Add(p);
                    for (int i = 0; i < 10000; ++i)
                    {
                        p.Value = i;
                        try
                        {
                            cmd.ExecuteNonQuery();
                        }
                        catch (SqlException s)
                        {
                            ++excount;
                        }
                    }
                    trn.Commit();
                    sw.Stop();
                }
                Console.WriteLine("Bad: {0} [Exceptions: {1}]",  
                   sw.Elapsed, excount);
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
    }

结果(零售版本和调试版本的时间相似):

Good: 00:00:00.8601303
Bad: 00:01:57.8987760 [Exceptions: 10000]

因此,时间已从原始SQL Server时间1.4秒缩短到将近2分钟。 因此,结案时,异常代价很高,对吗? 没那么快。 尝试运行未连接调试器的应用程序(Ctrl + F5):

Good: 00:00:00.6640281
Bad: 00:00:02.3746845 [Exceptions: 10000]

回到2秒。 因此,真正的罪魁祸首不是SQL Server,C#异常处理或SqlClient层。 调试器 这是正常现象,因为调试器会做一些相当侵入性的事情,并且会在每次抛出的CLR异常上运行大量代码。 如果您的代码除了抛出异常外什么也不做,那么结果可能会产生很大的影响。 这被称为“ 第一次机会异常处理”其他人之前也曾说过

但是,如果正在调试应用程序,则调试器会在程序执行之前看到所有异常。 这是第一次机会异常与第二次机会异常之间的区别:调试器获得了第一次看到异常的机会(因此得名)。 如果调试器允许程序继续执行并且不处理异常,则程序将照常看到异常。

再一次证明,在处理性能问题时,最好是进行测量

如果您对客户执行相同的检查,您将能够快速报告那些发现良好记录的客户,并迅速报告错误。

您可以动态推断要写入的表的布局,从而需要重复的表布局规范,但是除非您的组件本质上非常通用,否则我认为这样做不值得。

一些技巧:

  1. 创建一个参数化查询, 准备并重用它。 这样,SQL Server可以缓存执行计划
  2. 如果你有大量的数据要导入的,考虑使用批量导入功能( MERGEOPENROWSET是我对跨平台好玩目前最喜欢的) -它们比使用单独的更新/插入语句快很多

异常会造成如此大的减慢,这似乎很奇怪,除非您当然要回滚事务,在这种情况下,我完全可以理解问题的减慢。

无论如何,您可以尝试的一件事是从表定义中推断出列的长度,并在将数据插入数据库之前将其截断。 因此,您只会插入良好的数据。

例外情况很慢,因为在抛出和捕获时,需要完成很多记账工作,需要完成所有定稿工作,而所有堆栈框架计算通常要花费更长的时间。 我认为您根本无法通过使用某些内置方法来加快此速度。

您必须自己运行检查,而不是SQL才能返回错误。 我知道这有点乏味,但是无法加快速度。

另一种选择是在并行线程中插入行。 这将加快速度,因为一行中的异常不会降低所有挂起的行的速度。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM