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

时间:2013-12-05 11:43:21

标签: c# asp.net sql .net sql-server-2008

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

我有三张桌子:

Users DataFields DataField Values

每个用户对每个数据字段都有一个特定值,该值存储在DataFieldsValues中。

现在我想显示一个如下所示的报告:

enter image description here

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

然后我有用户列表List<User> users和DataFields List<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)
 //...
}

10 个答案:

答案 0 :(得分:20)

以下是2条有用的建议。第一个建议是显着改善您的表现。第二个建议也会有所帮助,但可能不会让你的应用更快。

建议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();

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

如果您进行这两项简单的更改,您将获得出色的表现。我保证。

答案 1 :(得分:18)

您选择的数据库设计名为Entity-Attribute-Value,这是一种以其性能问题而闻名的设计。 SQL Server团队已发布白皮书以获取有关EAV设计的指导,请参阅Best Practices for Semantic Data Modeling for Performance and Scalability

唉,你已经有了设计,你现在可以做些什么呢?重要的是将对dB的miriad调用减少到单个调用,并执行一个面向集合的语句来检索数据。游戏名称为Table Valued Parameters

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本身就是一个性能泥潭,但是当你添加所需结果集的动态性质时,基本上不可能将它拉下来。由一行每个属性组成的传统结果集很容易解析为一个表,因为用户标识所需的顺序保证了相关属性集之间的干净中断。

答案 2 :(得分: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控件和实体框架重新设计应用程序。

以下是为您提供分步说明的示例: http://www.codeproject.com/Articles/363040/An-Introduction-to-Entity-Framework-for-Absolute-B

答案 3 :(得分:6)

正如Remus Rusanu所说,你可以使用PIVOT关系运算符以你需要的格式获得你想要的数据,就PIVOT的性能而言,我发现它将取决于你的表的索引以及数据集的可变性和大小。我非常有兴趣听取他对PIVOTs的看法,因为我们都在这里学习。关于PIVOT vs JOINS here的讨论很多。

如果DataFields表是静态集,那么您可能不需要担心动态生成SQL,并且可以自己构建存储过程;如果它确实有所不同,您可能需要采用动态SQL (here is an excellent article on this)的性能影响或使用不同的方法。

除非您进一步需要数据,否则尝试将返回的设置保持在显示所需的最小值,这是减少开销的好方法,因为除非您的数据库位于同一物理服务器上,否则所有内容都需要通过网络。网络服务器。

确保尽可能少地执行单独的数据通话,以减少您花费在提升和断开连接上的时间。

当循环控件基于(可能是相关的?)数据集时,你应该总是仔细检查循环内的数据调用,因为这会尖叫JOIN。

当您尝试使用SQL尝试熟悉执行计划时,这些将帮助您找出运行速度慢的原因,请查看these resources以获取更多信息。

无论你接近什么,你决定你需要弄清楚你的代码中的瓶颈在哪里,基本的步骤可以帮助你解决这个问题,因为它可以帮助你自己查看问题所在,这也将允许你可以自己确定你的方法可能出现的问题,并建立良好的设计选择习惯。

Marc Gravel对c#数据阅读提出了一些有趣的观点here这篇文章有点陈旧但值得一读。

PIVOTing您的数据。(抱歉Remus ;-)) 基于您提供的数据示例,以下代码将获得您所需的内容,而不会进行查询内递归:

--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

要记住的最重要的事情就是为自己尝试一些事情,因为我们建议的任何内容都可能因您网站上我们不知道的因素而改变。

答案 4 :(得分:5)

您是如何访问数据库的?例如,如果您使用EF,请使用Profiler从这些查询中检查生成的SQL。不要每次都在foreach循环中建立连接。

我也不会在服务器端构建html。只需返回页面数据源控件的对象。

答案 5 :(得分:5)

确保您没有为每个循环建立数据库连接。

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

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

答案 6 :(得分:4)

对表的主键字段和使用字符串构建器后面的代码使用Indexed。

答案 7 :(得分:4)

那里最糟糕的问题是:大量往返数据库。每次获得值时,请求都会通过网络进行,并等待结果。

如果您必须先在代码中包含用户列表,请确保:

  1. 您可以在单个数据库调用中检索用户列表中的所有信息。如果 你有一组用户ID,发送你可以发送一个有价值的表 参数。
  2. 如果上面没有包含字段值,请发送 2个表值中的用户ID列表和字段ID列表 一次性检索所有参数。
  3. 这应该会产生巨大的变化。通过这两个特定的查询,您可以将网络噪声排除在外,并且可以在必要时集中精力改进索引。

    您将获得的另一个好处是整个串联字符串。第一步是用StringBuilder替换。下一步是直接写入输出流,因此您不需要将所有数据保存在内存中......但您不太可能需要它;如果由于数据过多而导致无论如何都会遇到浏览器处理问题。

    PS。不是OP场景,但对于那些需要批量速度的人来说,您需要批量导出:http://technet.microsoft.com/en-us/library/ms175937.aspx

答案 8 :(得分:2)

... FAST 使用

  1. 存储过程
  2. 使用阅读器

    SqlDataReader dbReader = mySqlCommand.ExecuteReader();
    
    //if reader has row values
    if (dbReader.HasRows) // while(xxx) for more rows return
    {
         //READ DATA
    }
    
  3. 如果需要分区,请使用正确的索引...

  4. SELECT NOLOCK的使用和提示为我工作

  5. 查询提示(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文件结构....

答案 9 :(得分:-1)

而不是DataReader使用DataAdapterDataset。执行所有查询一次,如下所示:

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

这将打开sqlconnection一次只触发所有这三个查询,并在数据集中返回三个不同的数据表,然后使用您的方法进行渲染。