我现在想知道从数据库中的多个表中检索数据的最佳方法。可悲的是,我找不到任何东西来帮助我理解正确的方法是什么。
假设我有一个名为 ContentPages 的内容页面表。该表包含以下字段:
PageID
PageTitle
PageContent
现在,除了 ContentPages 表之外,我还有表 ContentPagesTags ,它负责存储最能描述页面内容的标签(只是就像在这个网站 - stackoverflow中,你可以在你的问题中应用特定的标签)。 ContentPagesTags 表包含以下字段:
PageID
TagID
ContentPagesTags 表负责页面与附加标记之间的关系。 TagID 字段取自最后一个表 PageTags ,该表存储了可应用于内容页面的所有可能标记。最后一个表结构如下所示:
TagID
TagTitle
这就是它。现在,每当我想要检索从其数据表中提取所需信息的 ContentPage 对象时,我还想加载所有相关标记的数组。默认情况下,我到目前为止所做的是运行两个单独的查询以实现我的目标:
SELECT * FROM ContentPages
然后在返回 ContentPage 对象之前,每页运行下一个查询:
SELECT * FROM ContentPagesTags WHERE PageID = @PageID
PageID 是我正在构建对象的当前页面的ID。
总而言之,我为每个内容页面对象运行(至少)两个查询,以便检索所有需要的信息。在这个特定的例子中,我只展示了我为了从另一个表中提取信息所做的事情,但是随着时间的推移,我发现自己每个对象都运行多个查询以获取我所需的信息(例如,除了我可能的页面标记之外)以及想要选择页面评论,页面草稿和我可能需要的其他信息。最终,这让我查询了多个命令,这使得我的Web应用程序运行速度比预期慢得多。
我非常确定有更好,更快,更有效的方法来处理这些任务。很高兴在这个主题上有所了解,以提高我对不同SQL选择的知识,以及如何处理用户请求的大量数据,而不需要为每个对象转换多个选择。
答案 0 :(得分:1)
我建议将标签放在分隔列表中。您可以使用以下查询在SQL Server中执行此操作:
select cp.*,
stuff((select ', ' + TagTitle
from ContentPagesTags cpt join
PageTags pt
on cpt.TagId = pt.TagId
where cpt.PageId = cp.PageId
for xml path ('')
), 1, 2, '') as Tags
from ContentPages cp;
我认为字符串连接的语法不够直观。其他数据库具有很好的功能(例如listagg()
和group_concat()
)。但是,性能通常非常合理,特别是如果您有适当的索引(包括ContentPagesTags(PageId, TagId)
)。
答案 1 :(得分:1)
在等待我在对原始问题的评论中提出的问题进行澄清时,我至少可以这样说:
来自纯粹的"查询性能"在PageID
关系之外,这些信息在彼此之间没有相关性(即[标签]和[评论]表)方面是完全不同的,但肯定不是逐行的。这些额外表之间的基础。因此,没有什么可以做的,可以在以下的查询级别获得效率:
确保所有子表之间的PageID
外键都返回[ContentPages]
表。
确保每个子表中的PageID
字段都有索引(非群集应该没问题且FILLFACTOR为90 - 100,具体取决于使用模式)。
确保定期执行索引维护。至少要经常重新进行REORGANIZE,并在必要时重新进行REBUILD。
确保表格已正确建模:使用相应的数据类型(即不要使用INT来存储1到10的值,这些值永远不会超过10或者最坏的情况是50,因为它更容易在应用层编码int
;不要将UNIQUEIDENTIFIER用于任何PK或聚簇索引;等等。严重的是:糟糕的数据建模(数据类型和结构)可能会损害某些甚至所有查询的整体性能,使得任何数量的索引或任何其他功能或技巧都无法提供帮助。
如果您有Enterprise Edition,请考虑启用行或页面压缩(是索引的一项功能),尤其是对于[Comments]
这样的表,或者甚至是[ContentPagesTags]
之类的大型关联表它将非常大(就行数而言),因为压缩允许使用较小的固定长度数据类型来存储声明为较大类型的值。含义:如果INT
有BIGINT
(4个字节)或TagID
(8个字节),那么在IDENTITY值需要超过2个字节之前,它将会很短。 SMALLINT
数据类型,在超过INT
数据类型的4个字节之前很长一段时间,但是 SQL Server将存储 1005 的值在2字节空间中,好像它是SMALLINT
。从本质上讲,减少行大小将在每个8k数据页(这是SQL Server读取和存储数据的方式)上容纳更多行,从而减少物理IO并更好地利用缓存在内存中的数据页。
如果并发是(或成为)问题,请查看Snapshot Isolation。
现在,从应用程序/进程的角度来看,您希望减少连接/调用的数量。您可以尝试将某些信息合并到CSV或XML字段中,每个PageID
/ PageContent
行最终为1对1,但这实际上比仅让RDBMS给出的效率低一些你最简单的数据形式。花费额外的时间将INT值转换为字符串然后合并为更大的CSV或XML字符串肯定会更快,只是让应用层花费更多的时间来解包它。
相反,您可以通过返回多个结果集来减少调用次数,而不是增加操作时间/复杂性。例如:
CREATE PROCEDURE GetPageData
(
@PageID INT
)
AS
SET NOCOUNT ON;
SELECT fields
FROM [Page] pg
WHERE pg.PageID = @PageID;
SELECT tag.TagID,
tag.TagTitle
FROM [PageTags] tag
INNER JOIN [ContentPagesTags] cpt
ON cpt.TagID = tag.TagID
WHERE cpt.PageID = @PageID;
SELECT cmt.CommentID,
cmt.Comment
cmd.CommentCreatedOn
FROM [PageComments] cmt
WHERE cmt.PageID = @PageID
ORDER BY cmt.CommentCreatedOn ASC;
通过SqlDataReader.NextResult()循环显示结果集。
但是,仅仅是为了记录,我真的不认为这三个单独的"得到"此信息的存储过程实际上会增加操作的总时间,以填充每个页面。我建议首先对两种方法进行一些性能测试,以确保你没有解决比现实更多的感知/理论问题: - )。
修改强>
注意:
多个结果集(不是SQL Server M.A.R.S.功能"多个活动结果集")并非特定于存储过程。您也可以通过SqlCommand发出多个参数化的SELECT语句:
string _Query = @"
SELECT fields
FROM [Page] pg
WHERE pg.PageID = @PageID;
SELECT tag.TagID,
tag.TagTitle
FROM [PageTags] tag
INNER JOIN [ContentPagesTags] cpt
ON cpt.TagID = tag.TagID
WHERE cpt.PageID = @PageID;
--assume SELECT statement as shown above for [PageComments]";
SqlCommand _Command = new SqlCommand(_Query, _SomeSqlConnection);
_Command.CommandType = CommandType.Text;
SqlParameter _ParamPageID = new SqlParameter("@PageID", SqlDbType.Int);
_ParamPageID.Value = _PageID;
_Command.Parameters.Add(_ParamPageID);
如果您使用SqlDataReader.Read()
,则会出现如下情况。请注意,我有目的地展示了从_Reader
获取值的多种方法,只是为了显示选项。此外,从CPU的角度来看,标签和/或注释的数量实际上是无关紧要的。更多的项目确实等同于更多的内存,但没有办法(除非你使用AJAX一次构建一个项目的一个项目,永远不会将整个集合拉入内存,但我高度怀疑单个页面是否有足够的标签和注释甚至是值得注意的。)
// assume the code block above is right here
SqlDataReader _Reader;
_Reader = _Command.ExecuteReader();
if (_Reader.HasRows)
{
// only 1 row returned from [ContentPages] table
_Reader.Read();
PageObject.Title = _Reader["PageTitle"].ToString();
PageObject.Content = _Reader["PageContent"].ToString();
PageObject.ModifiedOn = (DateTime)_Reader["LastModifiedDate"];
_Reader.NextResult(); // move to next result set
while (_Reader.Read()) // retrieve 0 - n rows
{
TagCollection.Add((int)_Reader["TagID"], _Reader["TagTitle"].ToString());
}
_Reader.NextResult(); // move to next result set
while (_Reader.Read()) // retrieve 0 - n rows
{
CommentCollection.Add(new PageComment(
_Reader.GetInt32(0),
_Reader.GetString(1),
_Reader.GetDateTime(2)
));
}
}
else
{
throw new Exception("PageID " + _PageID.ToString()
+ " does not exist. What were you thinking??!?");
}
您还可以将多个结果集加载到DataSet
,每个结果集都是自己的DataTable
。有关详细信息,请参阅DataSet.Load
// assume the code block 2 blocks above is right here
SqlDataReader _Reader;
_Reader = _Command.ExecuteReader();
DataSet _Results = new DataSet();
if (_Reader.HasRows)
{
_Results.Load(_Reader, LoadOption.Upsert, "Content", "Tags", "Comments");
}
else
{
throw new Exception("PageID " + _PageID.ToString()
+ " does not exist. What were you thinking??!?");
}