从数据库检索数据的最快方法

30

我正在使用C#和Sql Server 2008开发ASP.NET项目。

我有三张表:

Users DataFields DataField Values

每个用户都有一个特定的数据字段值,并且这个值存储在DataFieldsValues中。

现在我想要展示像这样的报告:

enter image description here

我创建了UserDataField对象。在DataField对象中,有方法string GetValue(User user),用于获取某个用户的某个字段的值。

然后我有List<User> usersList<DataField> fields列表,我做如下操作:

string html = string.Empty;
html += "<table>";
html += "<tr><th>Username</th>";
foreach (DataField f in fields)
{
   html += "<th>" + f.Name + "</th>";
}
html += "</tr>"

foreach (User u in users)
{
   html += "<tr><td>" + u.Username + "</td>"
   foreach (DataField f in fields)
   {
      html += "<td>" + f.GetValue(u) + "</td>";
   }
   html += "</tr>"
}
Response.Write(html);

这个方法可以正常工作,但是非常缓慢,我指的是20个用户和10个数据字段。在性能方面有没有更好的方法来实现这个功能?

编辑:对于类中的每个参数,我使用以下方法检索值:

public static string GetDataFromDB(string query)
{
    string return_value = string.Empty;
    SqlConnection sql_conn;
    sql_conn = new SqlConnection(ConfigurationManager.ConnectionStrings["XXXX"].ToString());
    sql_conn.Open();
    SqlCommand com = new SqlCommand(query, sql_conn);
    //if (com.ExecuteScalar() != null)
    try
    {
        return_value = com.ExecuteScalar().ToString();
    }
    catch (Exception x)
    {
    }
    sql_conn.Close();
    return return_value;
} 

例如:

public User(int _Id)
{
this.Id = _Id
this.Username = DBAccess.GetDataFromDB("select Username from Users where Id=" + this.Id)
 //...
}

1
我希望你不要在循环内部打开/关闭连接,因为这将大大减缓数据检索的过程。 - Mana
6
你可以使用 StringBuilder 替代字符串来做一件事情。 - Suraj Singh
3
为什么不使用SQL连接来在一次查询中获取所有数据呢? - scheien
1
这个页面上有很多很好的评论和建议。但是,如果你想让你的代码在保持架构和代码大部分不变的情况下快速运行,那么看看我的答案吧。当然,还有很多其他可以改进架构等方面的方法,但它们会更加复杂,需要更多时间去理解(虽然学习新东西也没有什么不好的)。 - Chris Leyva
2
值得注意的是,在所有其他方面之上,你组合字符串的方式本质上是缓慢的(O(n^2))。使用StringBuilder类会更好。--哎呀,Chris Lava已经解决了这个问题。 - RBarryYoung
显示剩余6条评论
10个回答

23

以下是两个有帮助的建议。第一个建议将显著提高您的性能。第二个建议也有所帮助,但在您的情况下可能不会使您的应用程序更快。

建议1

您经常调用GetDataFromDB(string query)方法。这很糟糕,因为每次都会创建新的SqlConnection和SqlCommand。这需要时间和资源。而且,如果有任何网络延迟,那么就会乘以您所做的调用数量。因此,这只是一个糟糕的想法。

我建议您调用该方法一次,并使其填充一个集合,例如Dictionary<int, string>,以便您可以从用户ID键快速查找您的用户名值。

像这样:

// In the DataField class, have this code.
// This method will query the database for all usernames and user ids and
// return a Dictionary<int, string> where the key is the Id and the value is the 
// username. Make this a global variable within the DataField class.
Dictionary<int, string> usernameDict = GetDataFromDB("select id, username from Users");

// Then in the GetValue(int userId) method, do this:
public string GetValue(int userId)
{
    // Add some error handling and whatnot. 
    // And a better name for this method is GetUsername(int userId)
    return this.usernameDict[userId];
}

建议2

这里有另一种可以改进事情的方式,尽管在这种情况下略微——使用 StringBuilder 类。这样可以获得显著的性能提升(这里有一个概述:http://support.microsoft.com/kb/306822)。

SringBuilder sb = new StringBuilder();
sb.Append("<table><tr><th>Username</th>");
foreach (DataField f in fields)
{
    sb.Append("<th>" + f.Name + "</th>");
}

// Then, when you need the string
string html = sb.ToString();

如果你需要更多解释,请告诉我,但你所要求的是非常可行的。我们可以解决这个问题!

只要你做这两个简单的改变,你就能获得很好的性能。我保证。


2
我只打开了一个与数据库的SqlConnection并将其保存在会话中。这显著提高了性能。StringBuilder也有所改善。这个答案包含了这两个建议,尽管它们也都在其他答案中提到过。非常感谢您所有宝贵的建议! - enb081
@enb081 - 将开放式数据库连接放入会话中是一个不好的想法。.net中的SQL Server数据库驱动程序具有内置的连接池。通过在会话中存储打开的连接,您以一种效率和效果远低于重新实现此功能的方式。我认为其他帖子的作者试图表达的是,您需要减少对数据库执行的查询数量,而不是打开连接的数量。 - Aaron Carlson
@AaronCarlson 为什么在 Session 中放置一个开放的数据库连接是个坏主意? - enb081
@enb081 - 首先,会话是每个用户的。将一个打开的连接放入每个用户的会话中意味着这些连接永远不会返回到连接池中。数据库连接不可序列化,因此您只能支持inproc会话状态,这将限制您的应用程序在服务器群中运行的能力。我首先建议您阅读有关连接池化的内容。 - Aaron Carlson
@ChrisLava - 如果您更新答案并将打开连接排除在性能问题之外,那将很有帮助。我认为您将打开数据库连接和查询一起归为性能问题的方式是导致OP将一个打开的连接存储在会话状态中的原因之一。 - Aaron Carlson
显示剩余3条评论

19
您选择的数据库设计被称为实体-属性-值,这种设计因其性能问题而广为人知。SQL Server团队已发布了一份关于EAV设计的指南白皮书,请参阅最佳实践:面向性能和可扩展性的语义数据建模
但是,您已经采用了这种设计,现在该怎么办呢?重要的是将对数据库的无数调用减少到一个单一调用,并执行一个单一的集合定向语句来检索数据。关键在于表值参数
declare @users as UsersType;

insert into @users (UserId) values (7), (42), (89);

select ut.Id, 
  ut.Username, 
  df.Name as DataFieldName, 
  dfv.Value
from Users ut
join @users up on ut.Id = up.UserId
join DataFieldValues dfv on ut.Id = dfv.UserId
join DataFields df on dfv.DataFieldId = df.Id
order by ut.Id;

完整的示例请参见SqlFiddle

严格来说,使用PIVOT运算符可以检索所需形状的结果(将数据字段名称转置为列名),但我强烈建议不要这样做。 PIVOT本身就是性能上的困境,但当您添加所需结果集的动态性质时,基本上不可能实现它。传统的每个属性一行的结果集很容易解析成表格,因为按用户ID所需的顺序保证了相关属性集之间的干净分隔。


14

这很慢,因为在底层你要对数据库进行 20 x 10 = 200 次查询。正确的做法是一次性加载所有数据。

你应该发布有关加载数据方式的详细信息。如果你正在使用 Entity Framework,则应使用称为 Include 命令的 Eager Loading。

// Load all blogs and related posts
var blogs1 = context.Blogs
                      .Include(b => b.Posts)
                      .ToList();

一些示例可以在这里找到:http://msdn.microsoft.com/en-us/data/jj574232.aspx

编辑:

看起来您没有使用.NET Framework提供给您的工具。如今,对于像您这样的简单场景,您不必自己进行数据库访问。此外,您应该避免像您所做的那样连接字符串HTML。

我建议您使用现有的ASP.NET控件和Entity Framework重新设计您的应用程序。

这里有一个带有逐步说明的示例供您参考:http://www.codeproject.com/Articles/363040/An-Introduction-to-Entity-Framework-for-Absolute-B


我不想使用现有的ASP.NET控件,因为我想在HTML方面更加灵活,并按照我需要的方式进行塑形。 - enb081
1
另外,请关注我的情况。我没有一个简单的查询可以放入SQLDataSource控件中。 - enb081
5
如果你使用.NET的正确方式,就不会遇到这个问题。为了优化性能,最好的方法是减少数据库调用。其中一种方法是开始使用Entity Framework来简化数据访问。 - Kaspars Ozols
除非您要多次使用“blogs1”的内容,否则调用“.ToList()”只是浪费时间和内存。 - Jon Hanna
嗯,这取决于您对结果集的处理方式。该帖子是关于由于执行多个数据库查询而导致性能问题,因此在我的示例中,我包括了调用.ToList()以确保所有数据都在一次查询中读取。 - Kaspars Ozols
我同意你的答案,但是说实话,我完全不同意关于使用Entity Framework就完全不会出现这个问题的评论。当你不知道自己在做什么的时候,使用Entity Framework也可能犯同样的错误。 - eglasius

6
正如Remus Rusanu所说,您可以使用PIVOT关系运算符以所需格式获取所需的数据。就PIVOT的性能而言,我发现它取决于表的索引和数据集的变化性和大小。我非常有兴趣听他对PIVOT的看法,因为我们都在这里学习。这里有一个很好的关于PIVOT vs JOINS的讨论here
如果DataFields表是静态集,则您可能不需要担心动态生成SQL,并且可以构建自己的存储过程;如果它确实变化,则您可能需要承受动态SQL的性能损失(这里有一篇关于此的优秀文章)或使用不同的方法。
除非您需要进一步使用数据,否则请尽量将返回的集合保持最小限度,这是减少开销的好方法,因为除非您的数据库位于与Web服务器相同的物理服务器上,否则所有内容都需要通过网络传输。
确保尽可能少地执行单独的数据调用,将减少您花费在建立和断开连接上的时间。
当循环控制基于(可能相关的)数据集时,请始终仔细检查循环内的数据调用,因为这会导致JOIN。
当您尝试使用SQL进行实验时,请尝试熟悉执行计划,这将帮助您找出为什么查询运行缓慢的原因,请查看这些资源以获取更多信息。
无论您选择哪种方法,都需要找出代码中的瓶颈所在,即使是简单的执行步骤也可以帮助您解决问题,因为它将使您自己看到问题所在,这也将使您能够自己识别可能存在的问题,并建立良好的设计选择习惯。
Marc Gravel在这里提出了一些有关c#数据读取的有趣观点here,虽然这篇文章有点旧,但值得一读。

数据透视表(PIVOT) 根据您提供的数据示例,以下代码将获取您需要的内容,而无需在查询中进行递归操作:

--Test Data
DECLARE @Users AS TABLE ( Id int
                        , Username VARCHAR(50)
                        , Name VARCHAR(50)
                        , Email VARCHAR(50)
                        , [Role] INT --Avoid reserved words for column names.
                        , Active INT --If this is only ever going to be 0 or 1 it should be a bit.
                        );

DECLARE @DataFields AS TABLE ( Id int
                        , Name VARCHAR(50)
                        , [Type] INT --Avoid reserved words for column names.
                        );

DECLARE @DataFieldsValues AS TABLE ( Id int
                        , UserId int
                        , DataFieldId int
                        , Value VARCHAR(50)
                        );

INSERT INTO @users  ( Id
                    , Username
                    , Name
                    , Email
                    , [Role]
                    , Active) 
VALUES (1,'enb081','enb081','enb081@mack.com',2,1),
       (2,'Mack','Mack','mack@mack.com',1,1),
       (3,'Bob','Bobby','bob@mack.com',1,0)


INSERT INTO @DataFields  
                    ( Id
                    , Name
                    , [Type]) 
VALUES (1,'DataField1',3),
       (2,'DataField2',1),
       (3,'DataField3',2),
       (4,'DataField4',0)

INSERT INTO @DataFieldsValues  
                    ( Id
                    , UserId
                    , DataFieldId
                    , Value) 
VALUES (1,1,1,'value11'),
       (2,1,2,'value12'),
       (3,1,3,'value13'),
       (4,1,4,'value14'),
       (5,2,1,'value21'),
       (6,2,2,'value22'),
       (7,2,3,'value23'),
       (8,2,4,'value24')

--Query
SELECT *
FROM
(   SELECT  ut.Username, 
            df.Name as DataFieldName, 
            dfv.Value
    FROM @Users ut
    INNER JOIN @DataFieldsValues dfv 
        ON ut.Id = dfv.UserId
    INNER JOIN @DataFields df 
        ON dfv.DataFieldId = df.Id) src
PIVOT
(   MIN(Value) FOR DataFieldName IN (DataField1, DataField2, DataField3, DataField4)) pvt

--Results
Username    DataField1  DataField2  DataField3  DataField4
enb081      value11     value12     value13     value14
Mack        value21     value22     value23     value24

记住最重要的是自己尝试,因为我们所建议的可能会受到你所在地的因素影响,而我们并不清楚。


1
如果OP最终决定使用PIVOT,尽管需要动态SQL和结果的变量(在编译时未知),那么这就是正确的方法 :) 我给你点赞。 - Remus Rusanu

5
你是如何访问数据库的?如果你正在使用EF,例如,请检查Profiler中这些查询生成的SQL。不要在foreach循环中每次建立连接。
我也不会在服务器端构建HTML。只需返回对象以供页面数据源控件使用即可。

5

确保不要在每个循环中连接数据库。

从我所看到的,f.GetValue(u) 部分是一个从数据库获取字符串值的方法。

将数据一次性放入对象中,并执行与 f.GetValue(u) 相同的操作。


1
例如,您有20个循环使用f.GetValue(u)...每个循环都将连接到数据库以获取一些数据。从数据库中获取数据的应用程序所执行的步骤将比从本地存储的对象列表中获取数据需要更多时间。 - evilom
1
@enb081 这会提高性能,因为创建新的SqlConnection会增加应用程序的时间和内存。特别是如果有一些网络延迟,这会使情况更糟。如果您查看我的答案,我将查询结果放在了Dictionary中,这将让您非常快速地查找用户名。希望对您有所帮助。 - Chris Leyva

4
最严重的问题是:需要大量的数据库往返。每次获取一个值都会有一次请求通过网络发送并等待结果。
如果必须先在代码中获得用户列表,则确保:
  1. 通过单个 db 调用检索用户列表中的所有信息。如果你有一组用户 ID,可以将其与一个表值参数一起发送。
  2. 如果上述未包括字段值,请将用户 ID 列表和字段 ID 列表发送到 2 个表值参数,以一次性检索所有内容。
这应该会有很大的改善。使用这两个特定查询后,您已经摆脱了网络噪音,可以专注于必要时改进索引。
另一个收益是整个字符串拼接。第一步是使用 StringBuilder 进行替换。下一步是直接写入输出流,这样就不需要在内存中保存所有数据...但你不太可能需要这么做;如果由于数据过多需要这样做,浏览器处理它也会有问题。

注:这不是 OP 的情况,而是对于那些需要批量快速操作的人,你需要使用批量导出功能:http://technet.microsoft.com/en-us/library/ms175937.aspx


4

在表的主键字段上使用Indexed,并在后台代码中使用字符串生成器。


2

FAST... USE

  1. stored procedures
  2. use reader

    SqlDataReader dbReader = mySqlCommand.ExecuteReader();
    
    //if reader has row values
    if (dbReader.HasRows) // while(xxx) for more rows return
    {
         //READ DATA
    }
    
  3. DO PROPER INDEXES if need go for partitions...

  4. Use and HINTs for SELECT NOLOCK work for me

查询提示(Transact-SQL) http://technet.microsoft.com/en-us/library/ms181714.aspx

锁定提示 http://technet.microsoft.com/en-us/library/aa213026(v=sql.80).aspx

如果我调用存储过程,那么我唯一会使用LINQ。

搜索LINQ to SQL

但我是老派的……

自Entity Framework 1.0以来,我就摆脱了它们,因为当你做学校项目时,它是好的……

但作为计算实例非常昂贵……

将所有内容都读入内存中,然后做些什么??? 为什么我要支付SQL费用?使用一些JSON文件结构就好了....


2
你对于Entity Framework的说法是错误的。为了访问一个列的值,你不需要将所有数据读入内存中。EF已经足够智能,可以加载最少量的数据,只需使用正确的LINQ语句限制返回的数据即可:var relatedPostIds = context.Blogs.SelectMany(b => b.Posts).Select(p => p.ID).ToList(); - Kaspars Ozols
我和我的团队在五年前使用EF模拟请求来访问公司数据库,我们尝试了最大的表,其约为970M行。提取10行需要17秒,这是老派的方法,需要0秒。 我在分析公司有十年的经验。一年前,一位野心勃勃的高级.NET开发人员加入我们,想要教我们如何使用新的EF,并重建所有代码。我们询问他失败的原因,但他们浪费了三名程序员4个月的时间,却无法回答。他们甚至没有办法运行我能够轻易执行的查询...尽管我一直在使用EF方法,但对于简单的事情,我也可以使用其他方法。 - Valentin Petkov
微软关心的唯一问题是如何教人们使用EF并在Azure中消耗更多的计算实例。当您分析Azure价格结构时,您会看到哪个部分最昂贵。然后,如果您正在支付,就开始设计应用程序以消耗更少的资源。嗯,如果有人支付EF,那就轻松获胜! - Valentin Petkov
1
最后两条评论更像是发泄情绪而不是有争议和有用的内容。我并不是说EF是万能解决方案,但在大多数场景下它可以让软件开发变得更加容易。如果你使用非常庞大的数据库,可能会遇到性能问题,但对于大多数情况,EF都能正常工作。 - Kaspars Ozols
1
EF在过去5年中发生了很大变化。虽然它比老派的ORM慢,但通过正确的设置和编写正确的查询,其差异并不明显。EF最大的性能问题是在批量插入/更新操作方面。否则,您只需考虑小于1秒的差异。显然,非常复杂的查询仍然最好通过存储过程/视图来处理,这些仍然可以从EF中执行。 - Keith
显示剩余2条评论

-1

不要使用DataReader,而是使用DataAdapterDataset。按照下面的示例一次性执行所有查询:

  string SqlQuery ="Select * from Users;Select * From DataFields;Select * From DataFieldsValues;";

这将只打开sqlconnection一次,执行这三个查询并在数据集中返回三个不同的数据表,然后使用您的呈现方法。


2
Down-vote,因为尽管它可以在一次调用中运行所有3个操作,但它并没有利用查询引擎事先准备好的用户和组件之间的数据连接,因此会让用户在检索到数据后需要额外的工作。 - DRapp
1
我完全同意DRapp的观点。此外,您现在已将逻辑合并为一个查询,使其更难理解和维护。在数据库上执行这3个查询不会导致性能问题。您可以创建3个查询字符串(一个用于用户,一个用于数据字段,一个用于数据字段值),并在同一SqlConnection中分别执行这些查询。 - Chris Leyva

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接