我有一张接近 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
,整个过程就会加速。
有没有办法加快查询中的计数。我关心的是返回分页结果和总计数。
答案 0 :(得分:7)
更新了以下评论中的讨论
此处问题的原因是IX_Person_Born
索引的cardinality非常低。
SQL索引非常擅长快速缩小值,但是当您有大量具有相同值的记录时,它们会出现问题。
你可以把它想象成电话簿的索引 - 如果你想找到“史密斯,约翰”你首先发现有许多以S开头的名字,然后是名为史密斯的页面和页面,然后很多约翰斯。你最终扫描了这本书。
这是复杂的,因为电话簿中的索引是聚集的 - 记录按姓氏排序。如果你想要找到所谓的“约翰”,你会做很多的查找。
这里有3000万条记录,但只有30个不同的值,这意味着最好的索引仍然可以返回大约100万条记录 - 在这种规模下它也可能是一个表扫描。这100万个结果中的每一个都不是实际记录 - 它是从索引到表格的查找(电话簿类比中的页码),这使得它更慢。
高基数指数(比如出生日期),而不是年份会更快。
这是所有OLTP关系数据库的一般问题:low cardinality + huge datasets = slow queries
因为索引树没有多大帮助。
简而言之:使用T-SQL和索引获取计数没有明显更快的方法。
您有几个选择:
OLAP / Cube汇总或自行完成:
select Born, count(*)
from Person
group by Born
专家认为,多维数据集查找或检查缓存非常快。问题是数据会过时,你需要一些方法来解释它。
分为两个查询:
SELECT count(*)
FROM Person
WHERE Born = '1970'
SELECT TOP 30 *
FROM Person
WHERE Born = '1970'
然后在并行服务器端运行它们,或将其添加到用户界面。
这个问题是无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]之外的任何东西,这种方法将不起作用