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 - 加快大表的计数的主要内容,如果未能解决你的问题,请参考以下文章

如何加快创建大表 AlphaNumeric 代码的 SQL 脚本

在sql server 2008中查询大表[重复]

复制ms访问单个大表到sql server两张小表

在 SQL Server 2005 中从小表批量复制到大表

SQL Server - 合并大表而不锁定数据

从 SQL Server 中的大表中删除大部分数据的策略