SQL Server - 加速大表的计数

时间:2012-11-29 15:19:36

标签: sql sql-server performance

我有一张接近 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,整个过程就会加速。

有没有办法加快查询中的计数。我关心的是返回分页结果和总计数。

4 个答案:

答案 0 :(得分:7)

更新了以下评论中的讨论

此处问题的原因是IX_Person_Born索引的cardinality非常低。

SQL索引非常擅长快速缩小值,但是当您有大量具有相同值的记录时,它们会出现问题。

你可以把它想象成电话簿的索引 - 如果你想找到“史密斯,约翰”你首先发现有许多以S开头的名字,然后是名为史密斯的页面和页面,然后很多约翰斯。你最终扫描了这本书。

这是复杂的,因为电话簿中的索引是聚集的 - 记录按姓氏排序。如果你想要找到所谓的“约翰”,你会做很多的查找。

这里有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

这个问题是无SQL解决方案相对于传统关系数据库的一大优势。在无SQL系统中,Person表在许多廉价服务器上联合(或分片)。当用户搜索每个服务器时,同时检查。

此时技术变革可能已经结束,但可能值得调查,所以我已将其纳入其中。

我过去曾遇到类似问题的这类大小的数据库,并且(取决于上下文)我已经使用了选项1和2.如果这里的总数是分页,那么我可能会选择2和AJAX调用以获得计数。

答案 1 :(得分: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万行,并且它具有您正在寻找的确切信息。我没有必要更改查询以使其使用视图,但如果您愿意,可以在查询中自由使用视图的名称。

答案 2 :(得分:1)

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

为什么不那样?

编辑,不知道为什么粗体不起作用:<

答案 3 :(得分:1)

这是一个使用系统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]列来获取rowcount。

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]之外的任何东西,这种方法将不起作用