如何避免许多数据库往返和大量无关数据?

时间:2011-08-18 06:25:03

标签: sql optimization language-agnostic data-retrieval

我使用过各种应用程序并多次遇到这种情况。到目前为止,我还没有弄清楚什么是最好的方法。

以下是该方案:

  • 我有一个桌面或网络应用程序
  • 我需要从数据库中检索简单文档。该文档包含一般细节和项目详细信息,因此数据库:

GeneralDetails表:

| DocumentID | DateCreated | Owner     |
| 1          | 07/07/07    | Naruto    |
| 2          | 08/08/08    | Goku      |
| 3          | 09/09/09    | Taguro    |

ItemDetails

| DocumentID | Item        | Quantity  |
| 1          | Marbles     | 20        |
| 1          | Cards       | 56        |
| 2          | Yo-yo       | 1         |
| 2          | Chess board | 3         |
| 2          | GI Joe      | 12        |
| 3          | Rubber Duck | 1         |

如您所见,这些表具有一对多的关系。现在,为了检索所有文件及其各自的项目,我总是做两个中的任何一个:

方法1 - 许多往返(伪代码):

 Documents = GetFromDB("select DocumentID, Owner " +
                       "from GeneralDetails") 
 For Each Document in Documents
{
    Display(Document["CreatedBy"])
    DocumentItems = GetFromDB("select Item, Quantity " + 
                              "from ItemDetails " + 
                              "where DocumentID = " + Document["DocumentID"] + "")
    For Each DocumentItem in DocumentItems
    {
        Display(DocumentItem["Item"] + " " + DocumentItem["Quantity"])
    }
}

方法2 - 许多无关数据(伪代码):

DocumentsAndItems = GetFromDB("select g.DocumentID, g.Owner, i.Item, i.Quantity " + 
                              "from GeneralDetails as g " +
                              "inner join ItemDetails as i " +
                              "on g.DocumentID = i.DocumentID")
//Display...

我在大学期间使用第一种方法进行桌面应用时,性能并不差,所以我意识到它没问题。

直到有一天,我看到一篇文章“让网络变得更快”,它说许多往返数据库的往返很糟糕;所以从那时起我就使用了第二种方法。

在第二种方法中,我通过使用内部联接来一次检索第一个和第二个表来避免往返,但它会产生不必要的或冗余的数据。查看结果集。

| DocumentID | Owner     | Item        | Quantity  |
| 1          | Naruto    | Marbles     | 20        |
| 1          | Naruto    | Cards       | 56        |
| 2          | Goku      | Yo-yo       | 1         |
| 2          | Goku      | Chess board | 3         |
| 2          | Goku      | GI Joe      | 12        |
| 3          | Taguro    | Rubber Duck | 1         |

结果集包含多余的DocumentIDOwner。它看起来像一个非标准化的数据库。

现在,问题是,如何避免往返,同时避免冗余数据?

10 个答案:

答案 0 :(得分:4)

ActiveRecord和其他ORM使用的方法是选择第一个表,将ID一起批处理,然后在IN子句中使用这些ID进行第二次选择。

  

SELECT * FROM ItemDetails WHERE DocumentId IN([逗号分隔ID列表])

优点:

  • 没有冗余数据

缺点:

  • 两个查询

一般而言,第一种方法称为“N + 1查询问题”,解决方案称为“急切加载”。我倾向于看到你的“方法2”更好,因为数据库的延迟通常胜过冗余数据在数据传输速率上的大小,但是YRMV。与软件中的几乎所有内容一样,这是一种权衡。

答案 1 :(得分:3)

内连接更好,因为数据库有更多的优化可能性。

通常,您无法创建不会产生冗余结果的查询。为此,关系模型限制性太强。我会忍受它:数据库负责优化这些案例。

如果您确实遇到性能问题(主要是因为网络瓶颈),您可以编写存储过程,这会使查询和非规范化。在您的示例中,您创建了一个结果,如:

| DocumentID | Owner     | Items                                   | Quantity    |
| 1          | Naruto    | Marbles, Cards                          | 20, 56      |
| 2          | Goku      | Yo-yo, Chess board, GI Joe, Rubber Duck | 1, 3, 12, 1 |

但这当然不符合第一个普通形式 - 所以你需要在客户端解析它。 如果您使用具有XML支持的数据库(如Oracle或MS SQL Server),您甚至可以在服务器上创建XML文件并将其发送给客户端。

但无论你做什么,请记住:过早优化是万恶之源。在你不是百分之百确定之前不要做这种事情,你真的面临一个你可以解决的问题。

答案 2 :(得分:2)

您可以读取第一个表,从第二个表中提取所需行的键,然后通过第二个表选择它们。

这样的东西
DocumentItems = GetFromDB("select Item, Quantity " + 
                          "from ItemDetails " + 
                          "where DocumentID in (" + LISTING_OF_KEYS + ")")

答案 3 :(得分:1)

你的第二种方法绝对是一种方法。 但是您不必选择不会使用的列。 因此,如果您只需要ItemQuantity,请执行以下操作:

DocumentsAndItems = GetFromDB("select i.Item, i.Quantity " + 
                          "from GeneralDetails as g " +
                          "inner join ItemDetails as i " +
                          "on g.DocumentID = i.DocumentID")

(我想你还有其他条件要放在查询的where部分,否则不需要加入。)

答案 4 :(得分:1)

如果您使用的是.NET和MS SQL Server,这里的简单解决方案是研究使用MARS(多活动结果集)。以下是从MARS演示的Visual Studio 2015帮助中直接提取的示例代码块:

using System;
using System.Data;
using System.Data.SqlClient;

class Class1
{
  static void Main()
  {
     // By default, MARS is disabled when connecting
     // to a MARS-enabled host.
     // It must be enabled in the connection string.
     string connectionString = GetConnectionString();

     int vendorID;
     SqlDataReader productReader = null;
     string vendorSQL = 
       "SELECT VendorId, Name FROM Purchasing.Vendor";
     string productSQL = 
       "SELECT Production.Product.Name FROM Production.Product " +
       "INNER JOIN Purchasing.ProductVendor " +
       "ON Production.Product.ProductID = " + 
       "Purchasing.ProductVendor.ProductID " +
       "WHERE Purchasing.ProductVendor.VendorID = @VendorId";

   using (SqlConnection awConnection = 
      new SqlConnection(connectionString))
   {
      SqlCommand vendorCmd = new SqlCommand(vendorSQL, awConnection);
      SqlCommand productCmd = 
        new SqlCommand(productSQL, awConnection);

      productCmd.Parameters.Add("@VendorId", SqlDbType.Int);

      awConnection.Open();
      using (SqlDataReader vendorReader = vendorCmd.ExecuteReader())
      {
        while (vendorReader.Read())
        {
          Console.WriteLine(vendorReader["Name"]);

          vendorID = (int)vendorReader["VendorId"];

          productCmd.Parameters["@VendorId"].Value = vendorID;
          // The following line of code requires
          // a MARS-enabled connection.
          productReader = productCmd.ExecuteReader();
          using (productReader)
          {
            while (productReader.Read())
            {
              Console.WriteLine("  " +
                productReader["Name"].ToString());
            }
          }
        }
      }
      Console.WriteLine("Press any key to continue");
      Console.ReadLine();
    }
  }
  private static string GetConnectionString()
  {
    // To avoid storing the connection string in your code,
    // you can retrive it from a configuration file.
    return "Data Source=(local);Integrated Security=SSPI;" + 
      "Initial Catalog=AdventureWorks;MultipleActiveResultSets=True";
  }
 }

希望这会让你走上理解的道路。关于往返的主题有许多不同的哲学,其中很大程度上取决于您正在编写的应用程序类型以及您要连接的数据存储。如果这是一个内部网项目并且没有大量的并发用户,那么大量往返数据库的往返不是您认为的问题或担忧,除了它看起来如何看待您的声誉没有更精简的代码! (GRIN) 如果这是一个Web应用程序,那么这是一个不同的故事,你应该尽量确保你不会太频繁地回到井里,如果完全可以避免的话。 MARS是解决这个问题的一个很好的答案,因为一切都是从服务器一次性回来的,然后由你来迭代返回的数据。 希望这对你有用!

答案 5 :(得分:1)

答案取决于你的任务。

1.如果要生成列表/报告,则需要带有冗余数据的方法2。您通过网络传输更多数据,但节省了生成内容的时间。

2.如果要先显示常规列表,然后通过用户单击显示详细信息,则最好使用方法-1。生成和发送有限的数据集将非常快。

3.如果要将所有数据预加载到应用程序中,则可以使用XML。它将提供所有非冗余数据。但是,还有一个额外的编程,在SQL中使用XML编码并在客户端进行解码。

我会做这样的事情来在SQL端生成XML:

;WITH t AS (
    SELECT g.DocumentID, g.Owner, i.Item, i.Quantity
    FROM GeneralDetails AS g
    INNER JOIN ItemDetails AS i 
    ON g.DocumentID = i.DocumentID
)
SELECT 1 as Tag, Null as Parent, 
    DocumentID as [Document!1!DocumentID],
    Owner as [Document!1!Owner],
    NULL as [ItemDetais!2!Item],
    NULL as [ItemDetais!2!Quantity]
FROM t GROUP BY DocumentID, Owner
UNION ALL
SELECT 2 as Tag, 1 as Parent, DocumentID, Owner, Item, Quantity
FROM t 
ORDER BY [Document!1!DocumentID], [Document!1!Owner], [ItemDetais!2!Item], [ItemDetais!2!Quantity]
FOR XML EXPLICIT;

答案 6 :(得分:1)

据我所知,你有很多选择

  1. Concat您的字符串,以便您的所有项目都显示没有冗余数据。即“#34;大理石,卡片"
  2. 将您的查询作为压缩的XML文件返回,然后程序可以将其解析为数据库。
    • 这为您提供了只有一次旅行的优势,但您也可以在一个文件中获得可能非常庞大的所有数据。
  3. 这个项目将是我个人的偏好,实现一种延迟加载的形式。
    • 这意味着"额外的"数据仅在需要时加载。因此,虽然这确实有多次旅行,但旅行只是为了获得所需的数据。

答案 7 :(得分:0)

在我的应用程序中,有大约200个表单/屏幕和一个包含~300个表的数据库,我从来不需要第一种方法或第二种方法。

在我的应用程序中,用户经常会在屏幕上看到两个网格(表格),彼此相邻:

  • 带有文档列表的主GeneralDetails表(通常有搜索功能使用一堆不同的过滤器限制结果)。

  • 所选文档的ItemDetails表格中的数据。不适用于所有文件。仅适用于一份当前文件。当用户在第一个网格中选择不同的文档时,我(重新)运行查询以检索所选文档的详细信息。仅适用于一个选定的文件。

因此,master和details表之间没有连接。 并且,没有循环来检索所有主文档的详细信息。

为什么您需要在客户端上获取所有文档的详细信息?

我想说,最佳实践归结为常识:

通过网络仅传输您需要的数据总是好的,没有冗余。最好保持查询/请求的数量尽可能低。而不是在循环中发送许多请求,发送一个将返回所有必要行的请求。然后,如果确实需要,则在客户端上切片并切块。

如果需要以某种方式处理一批文档及其详细信息,这是一个不同的故事,到目前为止,我总是设法在服务器端执行此操作,而不将所有这些数据传输到客户端。 / p>

如果出于某种原因需要将所有主文档的列表连同所有文档的详细信息一起提供给客户端,我将进行两次查询而没有任何循环:

SELECT ... FROM GeneralDetails

SELECT ... FROM ItemDetails

这两个查询将返回两个数据数组,如果需要,我会将主内部结构中的主 - 详细数据组合在客户端的内存中。

答案 8 :(得分:0)

您可以通过分别从两个表中检索所需的数据来进一步优化此过程。之后,您可以遍历记录或连接表以生成与来自SQL Server的结果集相同的结果集。

使用ORM,您可以在两次往返中分别检索实体 - 一次检索GeneralDetails,另一次检查ItemDetails后检索GeneralDetails.DocumentId。尽管如此,有两次往返DB的往返方式优于其他两种方法。

这是一个NHibernate示例:

void XXX()
{
    var queryGeneral = uow.Session.QueryOver<GeneralDetails>();
    var theDate = DateTime.Now.Subtract(5);
    queryGeneral.AndRestrictionOn(c => c.SubmittedOn).IsBetween(theDate).And(theDate.AddDays(3));

    // Whatever other criteria applies.

    var generalDetails = queryGeneral.List();

    var neededDocIds = generalDetails.Select(gd => gd.DocumentId).Distinct().ToArray();

    var queryItems = uow.Session.QueryOver<ItemDetails>();
    queryItem.AndRestrictionOn(id => id.DocumentId).IsIn(neededDocs);

    var itemDetails = queryItems.List();

    // The records from both tables are now in the generalDetails and itemDetails lists so you can manipulate them in memory...
}

我相信(没有实例)ADO.NET数据集实际上可以保存到DB的第二次往返。你甚至不需要加入结果;这是编码风格和工作流程的问题,但通常你可以通过同时使用两个结果集来更新你的UI,

void YYY()
{
    var sql = "SELECT *  FROM GeneralDetails WHERE DateCreated BETWEEN '2015-06-01' AND '2015-06-20';";
    sql += @"
            WITH cte AS (
                SELECT DocumentId FROM GeneralDetails WHERE DateCreated BETWEEN '2015-06-01' AND '2015-06-20'
            )
            SELECT * FROM ItemDetails INNER JOIN cte ON ItemDetails.DocumentId = cte.DocumentId";

    var ds = new DataSet();

    using (var conn = new SqlConnection("a conn string"))
    using (var da = new SqlDataAdapter())
    {
        conn.Open();
        da.SelectCommand = conn.CreateCommand();
        da.SelectCommand.CommandText = sql;
        da.Fill(ds);
    }

    // Now the two table are in the dataset so you can loop through them and do your stuff...
}
  • 注意:我仅在示例中编写了上述代码,未经过测试!

答案 9 :(得分:0)

自从我问这个问题以来,我意识到还有其他方面可以优化我的应用程序来检索数据。在这种情况下,我将执行以下操作:

  • 问问自己,我真的需要检索许多文件及其子项吗?通常在UI中,我在列表中显示记录,仅当用户需要子项时(如果用户点击记录),我将检索它们。

  • 如果真的有必要显示许多子项目,例如帖子/评论,我只会提供一些帖子,想一想分页,或提供“加载更多”功能。

总而言之,我可能最终会进行延迟加载,只在用户需要时检索数据。

避免往返数据库服务器的解决方案尽管不能保证性能提升,因为它需要在数据库服务器和应用程序中进行更多处理,但是要检索多个记录集,一个结果发送到父文档,一个结果发布到子项目,请参阅伪代码:

 recordSets = GetData
     ("select * from parentDocs where [condition] ;
        select * from subItems where [condition]")

 //join the parent documents and subitems here

我可能需要一个临时表来显示父文档,所以我可以将它用于第二个查询中的条件,因为我只需要检索所选父文档的子项。

我还应该指出,做一个基准测试比仅仅应用原则更好,因为它实际上是个案基础。