我使用过各种应用程序并多次遇到这种情况。到目前为止,我还没有弄清楚什么是最好的方法。
以下是该方案:
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 |
结果集包含多余的DocumentID
和Owner
。它看起来像一个非标准化的数据库。
现在,问题是,如何避免往返,同时避免冗余数据?
答案 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)
你的第二种方法绝对是一种方法。
但是您不必选择不会使用的列。
因此,如果您只需要Item
和Quantity
,请执行以下操作:
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)
据我所知,你有很多选择
答案 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
我可能需要一个临时表来显示父文档,所以我可以将它用于第二个查询中的条件,因为我只需要检索所选父文档的子项。
我还应该指出,做一个基准测试比仅仅应用原则更好,因为它实际上是个案基础。