SQL Server 中对大型数据集的慢速不同查询
Posted
技术标签:
【中文标题】SQL Server 中对大型数据集的慢速不同查询【英文标题】:Slow distinct query in SQL Server over large dataset 【发布时间】:2009-04-16 06:27:13 【问题描述】:我们使用 SQL Server 2005 来跟踪大量不断传入的数据(每秒 5-15 次更新)。在它投入生产几个月后,我们注意到其中一个表已经开始花费大量时间来查询。
表格有 3 列:
id
-- 自动编号(集群)
typeUUID
-- 在插入发生之前生成的 GUID;用于将类型组合在一起
typeName
-- 类型名称 (duh...)
我们运行的其中一个查询在 typeName
字段上是不同的:
SELECT DISTINCT [typeName] FROM [types] WITH (nolock);
typeName
字段上有一个非聚集、非唯一的升序索引。该表目前包含大约 2 亿条记录。当我们运行这个查询时,查询用了 5m 58s 才返回!也许我们不了解索引是如何工作的……但我认为我们并没有对它们有太多误解。。
为了进一步测试,我们运行了以下查询:
SELECT DISTINCT [typeName] FROM (SELECT TOP 1000000 [typeName] FROM [types] WITH (nolock)) AS [subtbl]
这个查询在大约 10 秒后返回,正如我所料,它正在扫描表。
这里有什么我们遗漏的吗?为什么第一个查询需要这么长时间?
编辑:啊,对不起,第一次查询返回 76 条记录,谢谢九边。
跟进:谢谢大家的回答,现在对我来说更有意义(我不知道为什么以前没有......)。如果没有索引,它会跨 200M 行进行表扫描,使用索引,它会跨 200M 行进行索引扫描...
SQL Server 确实更喜欢索引,它确实提供了一点性能提升,但没有什么值得兴奋的。重建索引确实将查询时间从 6m 缩短到 3m 多一点,这是一个改进,但还不够。我只是要向我的老板推荐我们规范化表结构。
再次感谢大家的帮助!!
【问题讨论】:
您通常期望有多少种不同的类型? 老实说,听起来您的设计存在根本缺陷。 “传入”表中有 200M 条记录?在它们出现一段时间后,你不能把它们推到别的地方吗?在不了解您的应用程序的情况下很难提供更好的建议,但听起来您可能需要进行一些认真的重构。 是的,我们确实有很多数据正在处理,目前是 4 个月的数据。我们需要对数据进行分区,但我们还没有做到。 我无法思考 typeUUID 列实际上可能对什么有用......将它与 200M 行、20 次插入/秒的基数 76 的单列索引结合起来,并声称“性能问题”,这在我看来就是“回到绘图板”:( >>但是应该颠倒差异,因为第一个>>查询使用的是索引 NO - 它不是。如果您将所有行扫描到 DISTINCT 上,您将始终进行全表扫描 - 没有索引会对此有所帮助。 【参考方案1】:您确实误解了索引。即使它确实使用了索引,它仍然会对 200M 条目进行索引扫描。这将需要很长时间,加上执行 DISTINCT (导致排序)所需的时间,而且运行起来是一件坏事。在查询中看到 DISTINCT 总是会引发危险信号,并让我仔细检查查询。在这种情况下,您可能遇到了规范化问题?
【讨论】:
毫无疑问,这部分是数据规范化问题,但我们之前已经遇到了数据规范化的性能问题。我们几乎没有领先于传入的数据。 这是一个索引扫描,但不应该扫描索引(至少在这种情况下)只命中树的节点,而不是扫描叶子? 索引可能有大量碎片,增加了扫描时间。你有任何维护工作吗?你应该每晚都用这么多数据做那件事。 (假设你可以安排时间。) 嗯...这是一个很好的观点。我们目前不做这样的维护,但你是对的,这么多数据应该有夜间维护。明天我得试试。 完全正确-如果进行诸如“WHERE typename = xyz”之类的查询,索引将有所帮助-但对于选择不同的* from ...,索引根本无济于事。如果您有 200 M 行,则必须扫描所有 200 M 行.....【参考方案2】:我怀疑 SQL Server 甚至会尝试使用索引,它必须做几乎相同数量的工作(给定窄表),读取所有 200M 行,无论它是查看表还是索引。如果typeName
上的索引被聚集,它可能会减少所花费的时间,因为它不需要在分组之前进行排序。
如果您的类型的基数很低,那么维护一个包含不同type
值列表的汇总表怎么样?插入/更新主表的触发器将对汇总表进行检查,并在找到新类型时插入新记录。
【讨论】:
+1;插入触发器比我想的要好(在主之后添加第二个 INSERT,插入到所述汇总表中,并捕获/忽略 UNIQUE 约束违规)。 我也在想同样的事情。如果您需要经常运行不同的查询,请执行汇总表。删除行后,您还需要添加一个 DELETE 触发器来清理表。或者,如果不是什么大问题,可以安排一个 SQL 作业来每晚更新汇总表。 (删除已删除的类型。) 一个想法是否涉及DELETE:让汇总表有一个引用计数列;触发 INSERT 增加它并在 DELETE 减少它。这应该工作得很好。【参考方案3】:使用DISTINCT
关键字时,SQL Server 优化器存在问题。解决方案是通过单独拆分不同的查询来强制它保持相同的查询计划。
所以我们进行了如下查询:
SELECT DISTINCT [typeName] FROM [types] WITH (nolock);
并将其分解为以下内容:
SELECT typeName INTO #tempTable1 FROM types WITH (NOLOCK)
SELECT DISTINCT typeName FROM #tempTable1
另一种解决方法是使用GROUP BY
,它会获得不同的优化计划。
【讨论】:
添加更多关于这如何改变执行计划的信息可能会更好。【参考方案4】:正如其他人已经指出的那样 - 当您对表执行 SELECT DISTINCT (typename) 时,无论如何您最终都会进行全表扫描。
所以这实际上是限制需要扫描的行数的问题。
问题是:您需要 DISTINCT 类型名来做什么?你的 200M 行中有多少是不同的?你只有少数(最多几百个)不同的类型名吗??
如果是这样 - 你可以有一个单独的表 DISTINCT_TYPENAMES 或其他东西,并通过执行全表扫描来填充它们,然后在将新行插入主表时,始终检查它们的类型名是否已经在 DISTINCT_TYPENAMES 中,如果没有,添加它。
这样,您将拥有一个单独的小表,其中仅包含不同的 TypeName 条目,查询和/或显示速度将非常快。
马克
【讨论】:
是索引扫描,不是表扫描(我已经验证过了)。我的理解是,如果索引构建正确,它只会扫描索引,而不是整个表。 它可以并且确实扫描索引而不是表。但这不是索引旨在解决的问题,因此全索引扫描无法比全表扫描更快地解决此查询。 并且在这种情况下,非聚集索引将已经包含两个字段(实际查找的“typeName”和“id”),因此非聚集索引几乎是反正整张桌子……【参考方案5】:循环方法应该使用多次搜索(但会丢失一些并行性)。对于与总行数(低基数)相比具有相对较少不同值的情况,可能值得一试。
想法来自这个question:
select typeName into #Result from Types where 1=0;
declare @t varchar(100) = (select min(typeName) from Types);
while @t is not null
begin
set @t = (select top 1 typeName from Types where typeName > @t order by typeName);
if (@t is not null)
insert into #Result values (@t);
end
select * from #Result;
看起来还有其他一些方法(特别是递归 CTE @Paul White):
different-ways-to-find-distinct-values-faster-methods
sqlservercentral Topic873124-338-5
【讨论】:
【参考方案6】:我的第一个想法是统计。要查找最后更新:
SELECT
name AS index_name,
STATS_DATE(object_id, index_id) AS statistics_update_date
FROM
sys.indexes
WHERE
object_id = OBJECT_ID('MyTable');
编辑:重建索引时更新统计信息,我看到没有维护
我的第二个想法是索引还在吗? TOP 查询仍应使用索引。 我刚刚在一张有 5700 万行的表上进行了测试,并且都使用了索引。
【讨论】:
是的,索引在那里,它正在使用索引。 :( 那是我检查的第一件事。它正在扫描索引,但我不知道为什么扫描索引的唯一字段要花这么长时间...【参考方案7】:索引视图可以使这更快。
create view alltypes
with schemabinding as
select typename, count_big(*) as kount
from dbo.types
group by typename
create unique clustered index idx
on alltypes (typename)
在基表的每次更改上保持最新视图的工作应该适度(当然,取决于您的应用程序 - 我的观点是它不必每次都扫描整个表或执行像那样贵得离谱的东西。)
或者,您可以制作一个包含所有值的小表格:
select distinct typename
into alltypes
from types
alter table alltypes
add primary key (typename)
alter table types add foreign key (typename) references alltypes
外键将确保所有使用的值都出现在父表alltypes
中。问题在于确保alltypes
确实不 包含子types
表中未使用的值。
【讨论】:
【参考方案8】:我应该试试这样的:
SELECT typeName FROM [types] WITH (nolock)
group by typeName;
和其他人一样,我会说您需要规范化该列。
【讨论】:
【参考方案9】:索引可帮助您快速找到一行。但是您要求数据库列出整个表的所有唯一类型。索引对此无能为力。
您可以运行一个夜间作业,该作业运行查询并将其存储在不同的表中。如果您需要最新的数据,您可以存储夜间扫描中包含的最后一个 ID,并合并结果:
select type
from nightlyscan
union
select distinct type
from verybigtable
where rowid > lastscannedid
另一种选择是将大表规范化为两个表:
talbe1: id, guid, typeid
type table: typeid, typename
如果类型的数量相对较少,这将非常有益。
【讨论】:
【参考方案10】:我可能会遗漏一些东西,但如果通过负载开销来创建具有不同值的视图并改为查询它会更有效吗?
如果结果集明显更小,并且每次写入时填充它的开销明显较小,这将几乎立即响应选择,尽管考虑到视图的性质本身可能微不足道。
它确实提出了一个问题,与您想要不同的频率相比,写入次数和速度的重要性。
【讨论】:
以上是关于SQL Server 中对大型数据集的慢速不同查询的主要内容,如果未能解决你的问题,请参考以下文章
从 sql server 迁移到大型数据集的 sqlite 的最快方法
在 SQL Server 中对大型表进行分区的最佳方法是啥?