【问题标题】:how to improve SQL query performance in my case在我的情况下如何提高 SQL 查询性能
【发布时间】:2010-11-03 21:33:03
【问题描述】:

我有一个表,架构非常简单,一个 ID 列作为唯一主键(uniqueidentifier 类型)和其他一些 nvarchar 列。我目前的目标是,对于 5000 个输入,我需要计算表中已包含哪些输入,哪些未包含。输入是字符串,我有一个将字符串转换为唯一标识符(GUID)的 C# 函数。我的逻辑是,如果存在现有 ID,那么我将字符串视为已包含在表中。

我的问题是,如果我需要从 5000 个输入字符串中找出哪些已经包含在 DB 中,哪些没有,那么最有效的方法是什么?

顺便说一句:我当前的实现是,使用 C# 代码将字符串转换为 GUID,然后调用/实现一个存储过程,该存储过程查询数据库中是否存在 ID 并返回到 C# 代码。

我的工作环境:VSTS 2008 + SQL Server 2008 + C# 3.5。

【问题讨论】:

    标签: c# sql-server optimization


    【解决方案1】:

    尽量确保您最终只运行一个查询 - 即,如果您的解决方案包括对数据库运行 5000 次查询,那么这可能是该操作的最大资源消耗。

    如果您可以将 5000 个 ID 插入到临时表中,则可以编写一个查询来查找数据库中不存在的 ID。

    【讨论】:

    • 是的,我的意思是,确保您只运行一 (1) 个查询。没有一个查询 5000 次! :) 所以要使用临时表选项,我希望解决方案将涉及 1 个插入,然后是 1 个查询。不过,我不是 SQL Server 专家。如果是 Oracle,我将只运行一个查询(即没有插入,没有临时表)并为 5000 个 ID 使用批量绑定。
    • 我故意模棱两可,因为我的建议是一般性的,而不是特定于特定数据库的。 ... Oracle 有一个称为“全局临时表”的功能,它在编写查询时看起来像普通表,但其中的数据是当前会话的本地数据(即通常只保存在内存中),并在会话结束时自动消失。 ...我不了解 SQL Server,抱歉。
    • 临时表解决方案可能更好,因为数据库只需解析和运行 1 个查询,而不是解析和运行 5000 个查询。当然,这可能会被在临时表中插入 5000 行的成本所抵消;这就是为什么我最好的选择是将值批量绑定到查询中,如果可能的话。
    • 我不知道你是否可以在 SQL Server 中拥有一个持久临时表。在 Oracle 中,您只需要创建一次临时表,之后的任何会话都可以使用它。但我再次强调,进行批量插入 + 查询的想法是第二好的选择;第一个选项是将值批量绑定到查询中,并且根本不使用临时表。
    • 您有多种选择。如果不首先回答一些重要问题,您将无法确定哪个是最好的,最重要的是您需要有一个测试策略来比较它们。你在一条可能是也可能不是最优的道路上走了很长一段路。对于 5000 条记录,我怀疑它会是。
    【解决方案2】:

    我的第一个直觉是将你的 5000 个输入泵入一个单列临时表 X,可能对其进行索引,然后使用:

    SELECT X.thecol
    FROM X
    JOIN ExistingTable USING (thecol)
    

    获取存在的那些,并且(如果需要两个集合)

    SELECT X.thecol
    FROM X
    LEFT JOIN ExistingTable USING (thecol)
    WHERE ExistingTable.thecol IS NULL
    

    得到那些缺席的。至少值得进行基准测试。

    编辑:根据要求,这里有一些关于 SQL Server 临时表的优秀文档和教程。 Bill Graziano 有一个简单的介绍,涵盖临时表、表变量和全局临时表。 Randy DyessSQL Master 讨论支持和反对他们的性能问题(但请记住,如果您遇到性能问题,您确实想要对替代品进行基准测试,只是继续理论上的考虑!-)。

    MSDN 上有关于 tempdb(保存临时表的地方)和 optimizing 其性能的文章。

    【讨论】:

    • "单列临时表 X"——如果你能澄清这一点,不胜感激。我认为这一点很重要。临时表是指创建物理表还是?
    • CREATE TABLE #X (thecol VARCHAR(30)) 生成临时表(名称中的前导 # 是临时表的原因)——它的持续时间与创建的过程或会话一样长它。
    • (1) 如果您在所有用户之间共享一个物理表,您的代码将不得不过滤特定于当前会话的行,这需要时间;此外,临时表可能会在服务器上使用更少的资源。
    • 事实证明这个答案可能有点为时过早。问题的实际规模更像是 100 万条记录。在提供答案之前,您需要提出更多问题。
    • 在他的回复中将这种临时表的方法与 MERGE 语句 marc_s cmets 结合起来可能是做你想做的事情的好方法。
    【解决方案3】:

    您需要如何处理表中存在或不存在的条目??

    根据您的需要,也许 SQL Server 2008 中的新 MERGE 语句可以满足您的需求 - 更新已有的内容,插入新的内容,所有这些都整齐地包装到单个 SQL 语句中。看看吧!

    你的陈述应该是这样的:

    MERGE INTO 
        (your target table) AS t
    USING 
        (your source table, e.g. a temporary table) AS s
    ON t.ID = s.ID
    WHEN NOT MATCHED THEN  -- new rows does not exist in base table
      ....(do whatever you need to do)
    WHEN MATCHED THEN      -- row exists in base table
      ... (do whatever else you need to do)
    ;
    

    为了使这个速度非常快,我会从例如加载“新”记录。使用 BULK INSERT 将 TXT 或 CSV 文件放入 SQL Server 中的临时表中:

    BULK INSERT YourTemporaryTable
    FROM 'c:\temp\yourimportfile.csv'
    WITH 
    (
        FIELDTERMINATOR =',',
        ROWTERMINATOR =' |\n'
    )
    

    BULK INSERT 与 MERGE 相结合应该为您提供在这个星球上可以获得的最佳性能 :-)

    马克

    PS:这是来自 TechNet 的关于 MERGE 性能以及为什么它比单个语句更快的说明:

    在 SQL Server 2008 中,您可以使用 MERGE 语句在单个语句中执行多个数据操作语言 (DML) 操作。例如,您可能需要根据在另一个表中发现的差异,通过在一个表中插入、更新或删除行来同步两个表。通常,这是通过执行包含单独的 INSERT、UPDATE 和 DELETE 语句的存储过程或批处理来完成的。 但是,这意味着源表和目标表中的数据都会被多次评估和处理;每个语句至少一次。 通过使用 MERGE 语句,您可以将单个 DML 语句替换为单个语句。这可以提高查询性能,因为操作是在单个语句中执行的,因此可以最大限度地减少源表和目标表中数据的处理次数。但是,性能提升取决于是否有正确的索引、连接和其他考虑因素。本主题提供最佳实践建议,帮助您在使用 MERGE 语句时获得最佳性能。

    【讨论】:

    • 使用场景是,我有一个很大的工作/订单数据库,其中包含已处理的工作/订单,对于新的 5000 个批量订单/工作请求,我将首先查找订单/工作已经处理,如果没有,我将处理未处理的订单/工作。你觉得合并适合我的场景吗?
    • 是的,绝对!这是 MERGE 的 THE 完美场景。您有一个包含新处理作业的表,然后您更新基表,例如设置一个标志,或者添加一行,或者你需要做的任何事情。
    • 是的 - 这就是你必须这样做的方式之前 MERGE 出现:-) 我认为使用 MERGE 会更快一些,因为很多工作在 Microsoft 已经着手确保 MERGE 尽可能优化。
    • 您可以使用普通表或“真正的”临时表 - 恕我直言,差别不大。
    • 不,没有任何印刷文件 - 但有几个视频,例如channel9 介绍了新的 SQL Server 2008 功能,其中 MS 员工提到为了确保 MERGE 非常高效,我们付出了很多工作。
    【解决方案4】:

    第 1 步。确保您有问题需要解决。在很多情况下,一次插入 5000 个插入并不多。

    您确定最简单的方法还不够吗?到目前为止,您测量了哪些性能问题?

    【讨论】:

    • 查询的数量是可配置的,我想让我的解决方案适用于大数字,比如 1M 级别。
    • 不同的问题你会得到不同的答案。那么你需要在你的问题中说1MM。您需要询问批量插入策略。如果您很可能或不太可能获得匹配,答案也会有所不同。
    • 1M不是近期的,目前的场景是每15分钟,有5000个批量查询。有什么建议吗?
    • 是的,一次尝试一个,看看需要多长时间。如果您将它们聚集在一起,它可能会一次性给服务器带来很大的负载,否则它不会遇到这种情况。您也可以尝试完全取消批处理并在获得它们时提交它们。只要它跟上,它可能是最轻的整体负载 - 这可能会工作很长时间。 5K @ 15 分钟。每秒只有 5 次。只要您有一个应用程序将它们排入队列,这可能是一种合理的方式来平衡整体负载。第一条建议是不要解决你还没有解决的问题;先做最简单的事。
    • 巨大的流量是来自其他应用程序的订单状态和详细信息的按需查询,我的查找未处理订单的设计只是为了避免对大订单数据库的按需查询造成影响。有什么建议吗?
    【解决方案5】:

    如果你想要简单,因为 5000 条记录不是很多,那么从 C# 中只需使用一个循环为你想要添加到表中的每个字符串生成一个插入语句。将插入包装在 TRY CATCH 块中。像这样一次性将它们全部发送到服务器:

    BEGIN TRY
    INSERT INTO table (theCol, field2, field3)
    SELECT theGuid, value2, value3
    END TRY BEGIN CATCH END CATCH
    
    BEGIN TRY
    INSERT INTO table (theCol, field2, field3)
    SELECT theGuid, value2, value3
    END TRY BEGIN CATCH END CATCH
    
    BEGIN TRY
    INSERT INTO table (theCol, field2, field3)
    SELECT theGuid, value2, value3
    END TRY BEGIN CATCH END CATCH
    

    如果您在字符串 GUID 上定义了唯一索引或主键,则重复插入将失败。提前检查记录是否不存在只会重复 SQL 无论如何都要执行的工作。

    如果性能真的很重要,那么考虑将 5000 GUIDS 下载到本地站点并在本地进行所有分析。读取 5000 个 GUIDS 应该花费不到 1 秒的时间。这比批量导入临时表(这是您从临时表获得性能的唯一方法)和使用临时表连接进行更新更简单。

    【讨论】:

    • 我认为您使用SQL Server是否返回错误来检查重复的解决方案不是很可靠,因为您可以看到如果我们插入失败可能有很多原因,包括重复值。
    • “如果性能真的很重要,请考虑将 5000 GUIDS 下载到本地站点并在本地进行所有分析。” ——下载是什么意思?我的场景是我有一个大表,有几个 M 已处理的订单,每次批量输入 5000 个订单来检查几个 M 已处理的订单表以找到未处理的订单。
    • 不清楚数据库中的表中有几条 M 记录。将该信息添加到您的问题中!您是对的,执行插入语句时可能存在其他错误。您可以更改我的解决方案以仅吸收密钥违规错误并引发其他错误。你能告诉我,1)导入 5000 条记录需要多长时间,2)表中已经有多少记录?我问,因为如果重复记录的数量很小,比如 10-100,那么不发送 10-100 插入不会节省太多。
    【解决方案6】:

    绝对不要一个接一个。

    我首选的解决方案是创建一个存储过程,其中包含一个可以采用的参数和以下格式的 XML:

    <ROOT>
      <MyObject ID="60EAD98F-8A6C-4C22-AF75-000000000000">
      <MyObject ID="60EAD98F-8A6C-4C22-AF75-000000000001">
      ....
    </ROOT>
    

    然后在带有 NCHAR(MAX) 类型参数的过程中,将其转换为 XML,然后将其用作具有单列的表(我们称之为 @FilterTable)。存储过程如下所示:

    CREATE PROCEDURE dbo.sp_MultipleParams(@FilterXML NVARCHAR(MAX))
    AS BEGIN
        SET NOCOUNT ON
    
        DECLARE @x XML
        SELECT @x = CONVERT(XML, @FilterXML)
    
        -- temporary table (must have it, because cannot join on XML statement)
        DECLARE @FilterTable TABLE (
             "ID" UNIQUEIDENTIFIER
        )
    
        -- insert into temporary table
        -- @important: XML iS CaSe-SenSiTiv
        INSERT      @FilterTable
        SELECT      x.value('@ID', 'UNIQUEIDENTIFIER')
        FROM        @x.nodes('/ROOT/MyObject') AS R(x)
    
        SELECT      o.ID,
                    SIGN(SUM(CASE WHEN t.ID IS NULL THEN 0 ELSE 1 END)) AS FoundInDB
        FROM        @FilterTable o
        LEFT JOIN   dbo.MyTable t
                ON  o.ID = t.ID
        GROUP BY    o.ID
    
    END
    GO
    

    你运行它:

    EXEC sp_MultipleParams '<ROOT><MyObject ID="60EAD98F-8A6C-4C22-AF75-000000000000"/><MyObject ID="60EAD98F-8A6C-4C22-AF75-000000000002"/></ROOT>'
    

    您的结果如下所示:

    ID                                   FoundInDB
    ------------------------------------ -----------
    60EAD98F-8A6C-4C22-AF75-000000000000 1
    60EAD98F-8A6C-4C22-AF75-000000000002 0
    

    【讨论】:

    • 对您的SQL 语句感到困惑, 1. 我对如何从XML 文件的每一行中提取ID 属性感到困惑?我在你的回复中没有找到这样的说法。 2.另一个问题是这究竟是什么意思——“SIGN(SUM(CASE WHEN t.ID IS NULL THEN 0 ELSE 1 END))”?
    • 感谢您的建议,我的输入是一个字符串数组,我不想将其包装到 XML 中以增加额外的开销。你认为批量插入临时表,然后将临时表与真正的大订单表连接起来的解决方案有意义吗?
    • SIGN(...) 基本上会在找到 0 行时返回 0,当找到超过 1 行时返回 1。在你的情况下,过滤器是唯一的,所以这并不是真正需要的,所以你可以删除 SIGN,只留下 SUM(...)。
    • Alex使用临时表的方案是一个可行的方案。但是,如果您考虑从外部 SQL 插入行,那么我认为它不是“好”,原因如下:1)您破坏了并发性:2 个同时执行您的请求的用户将彼此锁定或混合另一个请求,取决于您的事务隔离级别。 2)您的 SP 将成为一个带有连接的选择,它“依赖”另一个表中的数据 - 不好。 ---- 而且我不认为传递 XML 是一种开销:在 C# 中创建输入字符串很容易; SQL 处理得很好。
    • 如果您不喜欢 XML,您可以传递逗号分隔的字符串并创建一个 UDF,将其转换为单列表(只需 google 即可 - 许多人在 SQLServer-2005 之前使用过因为 XML 处理很慢而且代码很丑)
    【解决方案7】:

    由于您使用的是 Sql server 2008,因此您可以使用表值参数。这是一种将表作为参数提供给存储过程的方法。

    使用 ADO.NET,您可以轻松地预填充 DataTable 并将其作为 SqlParameter 传递。 您需要执行的步骤:

    创建自定义 Sql 类型

    CREATE TYPE MyType AS TABLE
    (
    UniqueId INT NOT NULL,
    Column NVARCHAR(255) NOT NULL
    ) 
    

    创建一个接受类型的存储过程

    CREATE PROCEDURE spInsertMyType
    @Data MyType READONLY
    AS 
    xxxx
    

    使用 C# 调用

    SqlCommand insertCommand = new SqlCommand(
       "spInsertMyType", connection);
     insertCommand.CommandType = CommandType.StoredProcedure;
     SqlParameter tvpParam = 
        insertCommand.Parameters.AddWithValue(
        "@Data", dataReader);
     tvpParam.SqlDbType = SqlDbType.Structured;
    

    链接:Table-valued Parameters in Sql 2008

    【讨论】:

    • 传递参数是第一步,下一步你建议我做左加入吗?
    • 您绝对可以使用左连接。此外,您提到使用 c# 函数将字符串转换为 ID。由于表值参数可以包含多个字段,因此可能不需要对数据进行预处理,只需为您希望查询的所有字段发送数据。
    猜你喜欢
    • 1970-01-01
    • 2021-10-28
    • 2015-07-01
    • 2022-01-09
    • 2016-01-17
    • 2010-09-19
    • 1970-01-01
    • 2023-03-24
    相关资源
    最近更新 更多