7

我有一张包含近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 count(*) FROM PersonSubset) AS TotalPeople从 select 子句中删除,整个事情会加快很多。

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

4

4 回答 4

11

更新以下评论中的讨论

这里问题的原因是索引的基数非常低。IX_Person_Born

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

你可以把它想象成电话簿的索引——如果你想找到“Smith, John”,你首先会发现有很多以 S 开头的名字,然后是一页又一页叫 Smith 的人,然后很多约翰。你最终扫描了这本书。

这很复杂,因为电话簿中的索引是聚集的——记录是按姓氏排序的。相反,如果您想找到每个名为“John”的人,您将需要进行大量查找。

这里有 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打电话来数数。

于 2012-11-29T15:42:28.227 回答
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 万行,并且包含您要查找的确切信息。我不必更改查询以使其使用视图,但如果您愿意,您可以在查询中自由使用视图的名称。

于 2012-11-29T15:40:37.660 回答
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

为什么不这样?

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

于 2012-11-29T16:05:43.783 回答
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] 列来获取行数。

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] 之外的任何内容,此方法将不起作用

于 2012-12-07T04:23:08.707 回答