SQL Server - 加快大表的计数
Posted
技术标签:
【中文标题】SQL Server - 加快大表的计数【英文标题】:SQL Server - Speed up count on large table 【发布时间】:2012-11-29 15:19:36 【问题描述】:我有一张包含接近 3000 万 条记录的表格。只有几列。 'Born'
列之一的值不超过 30 个,并且上面定义了一个索引。我需要能够过滤该列并有效地翻阅结果。
现在我有(例如,如果我正在搜索的年份是“1970” - 它是我的存储过程中的一个参数):
WITH PersonSubset as
(
SELECT *, ROW_NUMBER() OVER (ORDER BY Born asc) AS Row
FROM Person WITH (INDEX(IX_Person_Born))
WHERE Born = '1970'
)
SELECT *, (SELECT count(*) FROM PersonSubset) AS TotalPeople
FROM PersonSubset
WHERE Row BETWEEN 0 AND 30
这种类型的每个查询(仅使用Born
参数)都会返回超过 100 万个结果。
我注意到最大的开销是用于返回总结果的计数。如果我从 select 子句中删除 (SELECT count(*) FROM PersonSubset) AS TotalPeople
,整个过程会加快很多。
有没有办法加快该查询中的计数。我关心的是返回分页结果和总数。
【问题讨论】:
尝试在 CTE 中使用COUNT(*) OVER()
,而不是尝试在输出的每一行上运行子查询。
@AaronBertrand 我在我的系统上设置了 OP 的查询,它首先运行子查询(产生单行结果),然后嵌套循环连接(1 次执行)查询的其余部分。结果显示为具有“表‘人’。扫描计数 2”。我可以肯定地说,子查询没有在“每一行”上运行。
@DavidB 我没有说 会 是结果 - 我只是评论了他似乎在 试图 做的事情。
这种情况可能值得考虑索引视图,例如SELECT Born, COUNT_BIG(*) FROM Person
感谢您的建议。尝试了下面@t-clausen.dk 建议的COUNT OVER,但没有运气。还有什么我可以尝试的吗?
【参考方案1】:
更新了以下 cmets 中的讨论
这里问题的原因是IX_Person_Born
索引的cardinality很低。
SQL 索引非常擅长快速缩小值范围,但是当您有大量具有相同值的记录时,它们就会出现问题。
你可以把它想象成电话簿的索引——如果你想找到“Smith, John”,你首先会发现有很多以 S 开头的名字,然后是一页又一页叫 Smith 的人,然后是很多约翰。你最终扫描了这本书。
这很复杂,因为电话簿中的索引是聚集的——记录是按姓氏排序的。相反,如果您想找到每个叫“约翰”的人,您需要进行大量查找。
这里有 3000 万条记录,但只有 30 个不同的值,这意味着最好的索引仍然返回大约 100 万条记录 - 在这种规模下,它还可能是表扫描。这 100 万个结果中的每一个都不是实际记录 - 它是从索引到表的查找(电话簿类比中的页码),这使得它变得更慢。
高基数指数(比如完整的出生日期)而不是年份会快得多。
这是所有 OLTP 关系数据库的普遍问题:low cardinality + huge datasets = slow queries
,因为索引树没有多大帮助。
简而言之:使用 T-SQL 和索引获取计数没有明显更快的方法。
你有几个选择:
1。数据聚合
OLAP/Cube 汇总或自己做:
select Born, count(*)
from Person
group by Born
优点是多维数据集查找或检查缓存非常快。问题是数据会过时,您需要一些方法来解决这个问题。
2。并行查询
分成两个查询:
SELECT count(*)
FROM Person
WHERE Born = '1970'
SELECT TOP 30 *
FROM Person
WHERE Born = '1970'
然后在并行服务器端运行这些,或将其添加到用户界面。
3。无 SQL
这个问题是 no-SQL 解决方案相对于传统关系数据库的一大优势。在无 SQL 系统中,Person
表在许多廉价服务器之间联合(或分片)。当用户搜索每个服务器时,同时检查。
此时,技术变革可能已经结束,但可能值得研究,因此我将其包括在内。
我过去在使用这种大小的数据库时也遇到过类似的问题,并且(取决于上下文)我使用了选项 1 和 2。如果这里的总数用于分页,那么我可能会选择选项2 和 AJAX 调用以获取计数。
【讨论】:
关于拆分查询的好建议。很可能甚至会做 2 个单独的 SP 并分别缓存结果,因此慢速计数仅在加载第一页时起作用。我确信计数是缓慢的部分 - 查看我在@David B 答案下的评论。 来自执行计划:IX_Person_Born 上 Index Seek 中查询成本的 74% @MRT - 我希望 30 行的子集返回比计数快得多,但 1.8 秒对于计数来说非常慢,即使有数百万条记录。我假设您需要计数的是总页数,在这种情况下,您不需要与分页数据同时获取它 - 在 async/AJAX 调用(或类似调用)中获取计数并显示在您这样做时,在分页控件上加载图标。用户会“感觉”页面很快,即使他们必须等待一秒钟左右才能看到总数。 这是一个很好的 UI 建议。 Sill 我不介意找到更快的计算结果的方法。如果唯一的查询条件上有索引,理论上似乎应该是可能的...... @MRT 如果大部分工作都在IX_Person_Born
上,那么您可能有正确的计划,但需要查看的是索引 - 碎片和过时的统计信息可能会导致索引变慢.您的问题的一部分可能是由于数据的基数与特殊性 - 每个索引页面只有 30 个值用于 3000 万条记录,涵盖大约 100 万条记录,这意味着索引增加的价值比唯一或接近的要少得多-独特的价值可能。您可以将其想象为在电话簿中查找姓氏并发现“琼斯”有 20 页。【参考方案2】:
DECLARE @TotalPeople int
--does this query run fast enough? If not, there is no hope for a combo query.
SET @TotalPeople = (SELECT count(*) FROM Person WHERE Born = '1970')
WITH PersonSubset as
(
SELECT *, ROW_NUMBER() OVER (ORDER BY Born asc) AS Row
FROM Person WITH (INDEX(IX_Person_Born))
WHERE Born = '1970'
)
SELECT *, @TotalPeople as TotalPeople
FROM PersonSubset
WHERE Row BETWEEN 0 AND 30
您通常不能采用慢速查询,将其与快速查询结合起来,然后以快速查询结束。
“Born”列的其中一个具有不超过 30 个不同的值,并且在其上定义了一个索引。
SQL Server 没有使用索引或统计信息,或者索引和统计信息不够有用。
这是一个绝望的措施,它将迫使 Sql 采取行动(以使写入非常昂贵的潜在成本 - 衡量这一点,并阻止对 Person 表的架构更改,同时视图存在)。
CREATE VIEW dbo.BornCounts WITH SCHEMABINDING
AS
SELECT Born, COUNT_BIG(*) as NumRows
FROM dbo.Person
GROUP BY Born
GO
CREATE UNIQUE CLUSTERED INDEX BornCountsIndex ON BornCounts(Born)
通过在视图上放置聚集索引,您可以使其成为系统维护的副本。此副本的大小远小于 3000 万行,并且包含您要查找的确切信息。我不必更改查询以使其使用视图,但如果您愿意,您可以在查询中随意使用视图的名称。
【讨论】:
原始查询耗时 2017ms。计数只需要 1831 毫秒。原始不计总数:632ms。所以你认为没有办法加快速度? 应在开发人员/企业/评估中自动考虑该视图。您可能需要在较低版本上显式使用视图(并使用NOEXPAND
)。附言新颖的想法!
喜欢“绝望的措施”的想法。如果我找不到任何“查询时间”改进,肯定会测试它
@AaronBertrand 您对索引视图的评论是在我测试可能性的同时发布的。这给了我一些安慰 - 对于这类问题有一种通用的专业方法 - 我们不仅仅是在编造东西。【参考方案3】:
WITH PersonSubset as
(
SELECT *, ROW_NUMBER() OVER (ORDER BY Born asc) AS Row
FROM Person WITH (INDEX(IX_Person_Born))
WHERE Born = '1970'
)
SELECT *, **max(Row) AS TotalPeople**
FROM PersonSubset
WHERE Row BETWEEN 0 AND 30
为什么不这样呢?
编辑,不知道为什么粗体不起作用:
【讨论】:
感谢您的建议。不幸的是,效率与 count(*) 相同 你不能在代码块中使用markdown,这就是你的粗体不起作用的原因【参考方案4】:这是一种使用系统 dmv 的新颖方法,如果您可以通过“足够好”的计数,您不介意为 [Born] 的每个不同值创建一个索引,并且您不介意有一点感觉里面有点脏。
为每一年创建一个过滤索引:
--pick a column to index, it doesn't matter which.
CREATE INDEX IX_Person_filt_1970 on Person ( id ) WHERE Born = '1970'
CREATE INDEX IX_Person_filt_1971 on Person ( id ) WHERE Born = '1971'
CREATE INDEX IX_Person_filt_1972 on Person ( id ) WHERE Born = '1972'
然后使用 sys.partitions 中的 [rows] 列来获取行数。
WITH PersonSubset as
(
SELECT *, ROW_NUMBER() OVER (ORDER BY Born asc) AS Row
FROM Person WITH (INDEX(IX_Person_Born))
WHERE Born = '1970'
)
SELECT *,
(
SELECT sum(rows)
FROM sys.partitions p
inner join sys.indexes i on p.object_id = i.object_id and p.index_id =i.index_id
inner join sys.tables t on t.object_id = i.object_id
WHERE t.name ='Person'
and i.name = 'IX_Person_filt_' + '1970' --or at @p1
) AS TotalPeople
FROM PersonSubset
WHERE Row BETWEEN 0 AND 30
Sys.partitions 不能保证在 100% 的情况下是准确的(通常是准确的或非常接近的)如果您需要过滤除 [Born] 之外的任何内容,此方法将不起作用
【讨论】:
以上是关于SQL Server - 加快大表的计数的主要内容,如果未能解决你的问题,请参考以下文章