7

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

这是场景:

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

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。它看起来像一个非规范化的数据库。

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

4

10 回答 10

4

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

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

好处:

  • 无冗余数据

缺点:

  • 两个查询

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

于 2011-08-18T06:59:12.133 回答
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 文件并将其发送到客户端。

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

于 2011-08-18T07:04:15.097 回答
2

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

就像是

DocumentItems = GetFromDB("select Item, Quantity " + 
                          "from ItemDetails " + 
                          "where DocumentID in (" + LISTING_OF_KEYS + ")")
于 2011-08-18T07:05:24.890 回答
1

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

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

(我想您还有其他条件可以放在where查询的一部分中,否则不需要连接。)

于 2016-06-09T12:39:28.430 回答
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";
  }
 }

希望这能让你走上理解之路。关于往返的主题有许多不同的理念,其中大部分取决于您正在编写的应用程序的类型和您要连接的数据存储。如果这是一个 Intranet 项目并且没有大量的同时用户,那么大量往返数据库不是您认为的问题或担忧,除非您的声誉看起来没有更精简的代码!(笑)如果这是一个 Web 应用程序,那就另当别论了,如果可以避免的话,您应该尽量确保不会过于频繁地回到井中。MARS 是解决此问题的一个很好的答案,因为一切都从服务器一次返回,然后由您来遍历返回的数据。

于 2016-06-09T14:33:26.087 回答
1

答案取决于你的任务。

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

2. 如果你想先显示General列表,然后通过用户点击显示细节,那么最好使用Method-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;
于 2016-06-09T18:04:56.680 回答
1

据我所知,您有多种选择

  1. 连接您的字符串,以便您的所有项目都将出现而没有冗余数据。即“弹珠,卡片”
  2. 将您的查询作为压缩的 XML 文件返回,然后您的程序可以像解析数据库一样解析该文件。
    • 这为您提供了仅一次旅行的优势,但您也可以将所有数据放在一个可能很大的文件中。
  3. 这个项目将是我个人的偏好,实现一种延迟加载的形式。
    • 这意味着仅在需要时才加载“附加”数据。因此,虽然这确实有多次行程,但行程只是为了获取所需的数据。
于 2016-06-09T20:56:32.447 回答
0

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

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

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

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

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

为什么您需要了解有关客户的所有文件的详细信息?

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

在网络上只传输您需要的数据,没有冗余总是好的。保持查询/请求的数量尽可能低总是好的。与其在一个循环中发送许多请求,不如发送一个返回所有必要行的请求。如果真的需要,然后在客户端上切片和切块。


如果需要以某种方式处理一批文档及其详细信息,那就另当别论了,到目前为止,我一直设法在服务器端进行处理,而无需将所有这些数据传输到客户端。

如果由于某种原因需要向客户端获取所有主文档的列表以及所有文档的详细信息,我会进行两个查询而无需任何循环:

SELECT ... FROM GeneralDetails

SELECT ... FROM ItemDetails

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

于 2016-06-03T06:31:17.067 回答
0

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

使用 ORM,您可以在两次往返中分别检索实体 - 一次检索GeneralDetails,另一次ItemDetails检索GeneralDetails.DocumentId. 尽管有两次到数据库的往返行程,但它比其他两种方法中的任何一种都进行了优化。

这是一个 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 数据集,您实际上可以将第二次往返保存到数据库。你甚至不需要加入结果;这是编码风格和工作流程的问题,但通常您可以通过同时使用两个结果集来更新您的 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...
}
  • 注意:我上面的代码只是为了例子而写的,没有经过测试!
于 2016-06-08T10:59:11.923 回答
0

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

  • 问问自己,我真的需要检索许多文档及其子项吗?通常在 UI 中,我以列表的形式显示记录,只有当用户需要子项时(如果用户单击记录),我才会检索它们。

  • 如果确实需要显示很多带有子条目的记录,例如帖子/评论,我只会提供一些帖子,考虑分页,或者提供“加载更多”功能。

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

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

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

 //join the parent documents and subitems here

我可能需要在此处为​​父文档创建一个临时表,以便将其用于第二个查询中的条件,因为我只需要检索所选父文档的子项。

我还应该指出,做一个基准测试比立即应用原则要好,因为它确实是一个个案基础。

于 2016-06-10T15:41:05.280 回答