本文提供的建议和示例适用于创建和更新专用 SQL 池中有关表的查询优化统计信息。

为何使用统计信息

专用 SQL 池对数据了解得越多,其针对数据执行查询的速度就越快。 将数据载入专用 SQL 池之后,收集有关数据的统计信息是最重要的查询优化任务之一。

专用 SQL 池查询优化器是基于成本的优化器。 此优化器会对各种查询计划的成本进行比较,并选择成本最低的计划。 在大多数情况下,所选计划也是执行速度最快的计划。

例如,如果优化器估计查询筛选的日期会返回一行数据,则它会选择一个计划。 如果优化器估计选定的日期会返回 1 百万行数据,则它会返回另一个计划。

自动创建统计信息

启用数据库的 AUTO_CREATE_STATISTICS 选项时,专用 SQL 池会分析传入的用户查询中是否缺少统计信息。

如果缺少统计信息,查询优化器将在查询谓词或联接条件中各个列上创建统计信息,以改进查询计划的基数估计。

默认情况下,自动创建统计信息目前处于开启状态。

可运行以下命令来检查是否为专用 SQL 池配置了 AUTO_CREATE_STATISTICS:

SELECT name, is_auto_create_stats_on
FROM sys.databases

如果专用 SQL 池未配置 AUTO_CREATE_STATISTICS,建议通过运行以下命令来启用此属性:

ALTER DATABASE <yourdatawarehousename>
SET AUTO_CREATE_STATISTICS ON

当检测到包含联接或存在某个谓词时,这些语句将触发统计信息的自动创建:

  • SELECT
  • INSERT-SELECT
  • UPDATE
  • DELETE
  • 和 EXPLAIN
  • 不在临时或外部表上创建“自动创建统计信息”。

    自动创建统计信息的过程是以同步方式完成的,因此,如果列中缺少统计信息,查询性能可能会轻微下降。 为单个列创建统计信息所需的时间取决于表的大小。

    为了避免可测量的性能降低,应确保在分析系统之前先通过执行基准检验工作负载来创建统计信息。

    统计信息的创建将记录在其他用户上下文中的 sys.dm_pdw_exec_requests 中。

    在创建自动统计信息时,它们将采用以下格式:WA_Sys<以十六进制表示的 8 位列 ID>_<以十六进制表示的 8 位表 ID>。 可以通过运行 DBCC SHOW_STATISTICS 命令查看已创建的统计信息:

    DBCC SHOW_STATISTICS (<table_name>, <target>)
    

    table_name 是包含要显示的统计信息的表的名称。 此表不能为外部表。 目标是要显示统计信息的目标索引、统计信息或列的名称。

    更新统计信息

    最佳实践之一是每天在添加新日期后,更新有关日期列的统计信息。 每次有新行载入专用 SQL 池时,就会添加新的加载日期或事务日期。 这些添加操作会更改数据分布情况并使统计信息过时。

    有关客户表中的国家/地区列的统计信息可能永远不需要更新,因为值的分布通常不会变化。 假设客户间的分布固定不变,将新行添加到表变化并不会改变数据分布情况。

    但是,如果专用 SQL 池只包含一个国家/地区,并且引入了来自新国家/地区的数据,导致存储了多个国家/地区的数据,那么,就需要更新有关国家/地区列的统计信息。

    下面是关于更新统计信息的建议:

    统计信息特性

    在排查查询问题时,首先要询问的问题之一就是 “统计信息是最新的吗?”

    此问题无法根据数据期限得到答案。 如果对基础数据未做重大更改,则最新的统计信息对象有可能非常陈旧。 如果行数有明显变化或给定列的值分布有重大变化,则 需要更新统计信息。

    没有任何动态管理视图可用于确定自上次更新统计信息以来表中的数据是否发生更改。 以下两个查询可帮助确定统计信息是否过时。

    查询 1:找出统计信息中的行计数 (stats_row_count) 与实际行计数 (actual_row_count) 之间的差异。

    select 
    objIdsWithStats.[object_id], 
    actualRowCounts.[schema], 
    actualRowCounts.logical_table_name, 
    statsRowCounts.stats_row_count, 
    actualRowCounts.actual_row_count,
    row_count_difference = CASE
        WHEN actualRowCounts.actual_row_count >= statsRowCounts.stats_row_count THEN actualRowCounts.actual_row_count - statsRowCounts.stats_row_count
        ELSE statsRowCounts.stats_row_count - actualRowCounts.actual_row_count
    percent_deviation_from_actual = CASE
        WHEN actualRowCounts.actual_row_count = 0 THEN statsRowCounts.stats_row_count
        WHEN statsRowCounts.stats_row_count = 0 THEN actualRowCounts.actual_row_count
        WHEN actualRowCounts.actual_row_count >= statsRowCounts.stats_row_count THEN CONVERT(NUMERIC(18, 0), CONVERT(NUMERIC(18, 2), (actualRowCounts.actual_row_count - statsRowCounts.stats_row_count)) / CONVERT(NUMERIC(18, 2), actualRowCounts.actual_row_count) * 100)
        ELSE CONVERT(NUMERIC(18, 0), CONVERT(NUMERIC(18, 2), (statsRowCounts.stats_row_count - actualRowCounts.actual_row_count)) / CONVERT(NUMERIC(18, 2), actualRowCounts.actual_row_count) * 100)
        select distinct object_id from sys.stats where stats_id > 1
    ) objIdsWithStats
    left join
        select object_id, sum(rows) as stats_row_count from sys.partitions group by object_id
    ) statsRowCounts
    on objIdsWithStats.object_id = statsRowCounts.object_id 
    left join
        SELECT sm.name [schema] ,
            tb.name logical_table_name ,
            tb.object_id object_id ,
            SUM(rg.row_count) actual_row_count
        FROM sys.schemas sm
             INNER JOIN sys.tables tb ON sm.schema_id = tb.schema_id
             INNER JOIN sys.pdw_table_mappings mp ON tb.object_id = mp.object_id
             INNER JOIN sys.pdw_nodes_tables nt ON nt.name = mp.physical_name
             INNER JOIN sys.dm_pdw_nodes_db_partition_stats rg     ON rg.object_id = nt.object_id
                AND rg.pdw_node_id = nt.pdw_node_id
                AND rg.distribution_id = nt.distribution_id
        WHERE rg.index_id = 1
        GROUP BY sm.name, tb.name, tb.object_id
    ) actualRowCounts
    on objIdsWithStats.object_id = actualRowCounts.object_id
    

    查询 2: 通过检查每个表中上次更新统计信息的时间,找出统计信息的使用年限。

    如果给定列的值分布有重大变化,则应该更新统计信息,不管上次更新时间为何。

    SELECT
        sm.[name] AS [schema_name],
        tb.[name] AS [table_name],
        co.[name] AS [stats_column_name],
        st.[name] AS [stats_name],
        STATS_DATE(st.[object_id],st.[stats_id]) AS [stats_last_updated_date]
        sys.objects ob
        JOIN sys.stats st
            ON  ob.[object_id] = st.[object_id]
        JOIN sys.stats_columns sc
            ON  st.[stats_id] = sc.[stats_id]
            AND st.[object_id] = sc.[object_id]
        JOIN sys.columns co
            ON  sc.[column_id] = co.[column_id]
            AND sc.[object_id] = co.[object_id]
        JOIN sys.types  ty
            ON  co.[user_type_id] = ty.[user_type_id]
        JOIN sys.tables tb
            ON  co.[object_id] = tb.[object_id]
        JOIN sys.schemas sm
            ON  tb.[schema_id] = sm.[schema_id]
    WHERE
        st.[user_created] = 1;
    

    例如,专用 SQL 池中的日期列往往需要经常更新统计信息。 每次有新行载入专用 SQL 池时,就会添加新的加载日期或事务日期。 这些添加操作会更改数据分布情况并使统计信息过时。

    相反地,客户表上性别列的统计信息可能永远不需要更新。 假设客户间的分布固定不变,将新行添加到表变化并不会改变数据分布情况。

    如果专用 SQL 池只包含一种性别,而新的要求会导致出现多种性别,则需更新性别列的统计信息。

    有关详细信息,请参阅统计信息的通用指南。

    实施统计信息管理

    通常可以扩展数据加载过程,确保在加载结束时更新统计信息,避免/最大程度地减少并发查询之间出现阻塞或资源争用的情况。

    当表更改其大小和/或其值分布时,数据加载最为频繁。 可以通过数据加载实施某些管理过程。

    下面提供了有关更新统计信息的一些指导原则:

  • 确保加载的每个表至少包含一个更新的统计信息对象。 这会在统计信息更新过程中更新表大小(行计数和页计数)信息。
  • 将重点放在参与 JOIN、GROUP BY、ORDER BY 和 DISTINCT 子句的列上。
  • 考虑更频繁地更新“递增键”列(例如事务日期),因为这些值不包含在统计信息直方图中。
  • 考虑较不经常更新静态分布列。
  • 请记住,每个统计信息对象是按顺序更新的。 仅实现 UPDATE STATISTICS <TABLE_NAME> 不一定总很理想 - 尤其是对包含许多统计信息对象的宽型表而言。
  • 有关详细信息,请参阅基数估计

    示例:创建统计信息

    以下示例演示如何使用各种选项来创建统计信息。 用于每个列的选项取决于数据特征以及在查询中使用列的方式。

    使用默认选项创建单列统计信息

    若要基于某个列创建统计信息,需要提供统计信息对象的名称和列的名称。

    此语法使用所有默认选项。 默认情况下,创建统计信息时会提取 20% 的表数据作为样本。

    CREATE STATISTICS [statistics_name] ON [schema_name].[table_name]([column_name]);
    
    CREATE STATISTICS col1_stats ON dbo.table1 (col1);
    

    通过检查每个行创建单列统计信息

    20% 的默认采样率足以应付大多数情况。 不过,可以调整采样率。

    若要采样整个表,请使用此语法:

    CREATE STATISTICS [statistics_name] ON [schema_name].[table_name]([column_name]) WITH FULLSCAN;
    
    CREATE STATISTICS col1_stats ON dbo.table1 (col1) WITH FULLSCAN;
    

    通过指定样本大小创建单列统计信息

    或者,可以以百分比指定样本大小:

    CREATE STATISTICS col1_stats ON dbo.table1 (col1) WITH SAMPLE = 50 PERCENT;
    

    只对某些行创建单列统计信息

    也可以对表中的部分行创建统计信息。 这称为筛选的统计信息。

    例如,在计划查询大型分区表的特定分区时,可以使用筛选的统计信息。 只对分区值创建统计信息,统计信息的准确度会改善,并因而改善查询性能。

    此示例会基于一系列的值创建统计信息。 可以轻松定义这些值以匹配分区中的值范围。

    CREATE STATISTICS stats_col1 ON table1(col1) WHERE col1 > '2000101' AND col1 < '20001231';
    

    若要让查询优化器在选择分布式查询计划时考虑使用筛选的统计信息,查询必须符合统计信息对象的定义。 使用上述示例,查询的 WHERE 子句需要指定介于 2000101 和 20001231 之间的 col1 值。

    使用所有选项创建单列统计信息

    也可以将选项组合在一起。 以下示例使用自定义样本大小创建筛选的统计信息对象:

    CREATE STATISTICS stats_col1 ON table1 (col1) WHERE col1 > '2000101' AND col1 < '20001231' WITH SAMPLE = 50 PERCENT;
    

    有关完整参考,请参阅 CREATE STATISTICS

    创建多列统计信息

    若要创建多列统计信息对象,请使用上述示例,但要指定更多的列。

    用于估计查询结果中行数的直方图只适用于统计信息对象定义中所列的第一个列。

    在此示例中,直方图针对的是 product_category。 跨列统计信息是根据 product_category 和 product_sub_category 计算的:

    CREATE STATISTICS stats_2cols ON table1 (product_category, product_sub_category) WHERE product_category > '2000101' AND product_category < '20001231' WITH SAMPLE = 50 PERCENT;
    

    由于 product_category 与 product_sub_category 之间存在关联,因此在同时访问这些列时,多列统计信息对象相当有用。

    基于表中的所有列创建统计信息

    创建统计信息的方法之一是在创建表后发出 CREATE STATISTICS 命令:

    CREATE TABLE dbo.table1
       col1 int
    ,  col2 int
    ,  col3 int
        CLUSTERED COLUMNSTORE INDEX
    CREATE STATISTICS stats_col1 on dbo.table1 (col1);
    CREATE STATISTICS stats_col2 on dbo.table2 (col2);
    CREATE STATISTICS stats_col3 on dbo.table3 (col3);
    

    使用存储过程基于 SQL 池中的所有列创建统计信息

    专用 SQL 池不提供相当于 SQL Server 中 sp_create_stats 的系统存储过程。 此存储过程基于 SQL 池中尚不包含统计信息的每一列创建单列统计信息对象。

    可参考下列开始进行 SQL 池设计。 可以根据需要任意改写此存储过程。

    CREATE PROCEDURE    [dbo].[prc_sqldw_create_stats]
    (   @create_type    tinyint -- 1 default 2 Fullscan 3 Sample
    ,   @sample_pct     tinyint
    IF @create_type IS NULL
    BEGIN
        SET @create_type = 1;
    IF @create_type NOT IN (1,2,3)
    BEGIN
        THROW 151000,'Invalid value for @stats_type parameter. Valid range 1 (default), 2 (fullscan) or 3 (sample).',1;
    IF @sample_pct IS NULL
    BEGIN;
        SET @sample_pct = 20;
    IF OBJECT_ID('tempdb..#stats_ddl') IS NOT NULL
    BEGIN;
        DROP TABLE #stats_ddl;
    CREATE TABLE #stats_ddl
    WITH    (   DISTRIBUTION    = HASH([seq_nmbr])
            ,   LOCATION        = USER_DB
    WITH T
    SELECT      t.[name]                        AS [table_name]
    ,           s.[name]                        AS [table_schema_name]
    ,           c.[name]                        AS [column_name]
    ,           c.[column_id]                   AS [column_id]
    ,           t.[object_id]                   AS [object_id]
    ,           ROW_NUMBER()
                OVER(ORDER BY (SELECT NULL))    AS [seq_nmbr]
    FROM        sys.[tables] t
    JOIN        sys.[schemas] s         ON  t.[schema_id]       = s.[schema_id]
    JOIN        sys.[columns] c         ON  t.[object_id]       = c.[object_id]
    LEFT JOIN   sys.[stats_columns] l   ON  l.[object_id]       = c.[object_id]
                                        AND l.[column_id]       = c.[column_id]
                                        AND l.[stats_column_id] = 1
    LEFT JOIN    sys.[external_tables] e    ON    e.[object_id]        = t.[object_id]
    WHERE       l.[object_id] IS NULL
    AND            e.[object_id] IS NULL -- not an external table
    SELECT  [table_schema_name]
    ,       [table_name]
    ,       [column_name]
    ,       [column_id]
    ,       [object_id]
    ,       [seq_nmbr]
    ,       CASE @create_type
            WHEN 1
            THEN    CAST('CREATE STATISTICS '+QUOTENAME('stat_'+table_schema_name+ '_' + table_name + '_'+column_name)+' ON '+QUOTENAME(table_schema_name)+'.'+QUOTENAME(table_name)+'('+QUOTENAME(column_name)+')' AS VARCHAR(8000))
            WHEN 2
            THEN    CAST('CREATE STATISTICS '+QUOTENAME('stat_'+table_schema_name+ '_' + table_name + '_'+column_name)+' ON '+QUOTENAME(table_schema_name)+'.'+QUOTENAME(table_name)+'('+QUOTENAME(column_name)+') WITH FULLSCAN' AS VARCHAR(8000))
            WHEN 3
            THEN    CAST('CREATE STATISTICS '+QUOTENAME('stat_'+table_schema_name+ '_' + table_name + '_'+column_name)+' ON '+QUOTENAME(table_schema_name)+'.'+QUOTENAME(table_name)+'('+QUOTENAME(column_name)+') WITH SAMPLE '+CONVERT(varchar(4),@sample_pct)+' PERCENT' AS VARCHAR(8000))
            END AS create_stat_ddl
    FROM T
    DECLARE @i INT              = 1
    ,       @t INT              = (SELECT COUNT(*) FROM #stats_ddl)
    ,       @s NVARCHAR(4000)   = N''
    WHILE @i <= @t
    BEGIN
        SET @s=(SELECT create_stat_ddl FROM #stats_ddl WHERE seq_nmbr = @i);
        PRINT @s
        EXEC sp_executesql @s
        SET @i+=1;
    DROP TABLE #stats_ddl;
    

    若要使用默认设置基于表中的所有列创建统计信息,请执行存储过程。

    EXEC [dbo].[prc_sqldw_create_stats] 1, NULL;
    

    若要通过全面扫描基于表中的所有列创建统计信息,请调用此过程。

    EXEC [dbo].[prc_sqldw_create_stats] 2, NULL;
    

    要对表中的所有列创建示例统计信息,请输入 3 和示例百分比。 此过程使用 20% 的采样率。

    EXEC [dbo].[prc_sqldw_create_stats] 3, 20;
    

    示例:更新统计信息

    要更新统计信息,可以:

  • 更新一个统计信息对象。 指定要更新的统计信息对象名称。
  • 更新表中的所有统计信息对象。 指定表名称,而不是一个特定的统计信息对象。
  • 更新一个特定的统计信息对象

    使用以下语法来更新特定的统计信息对象:

    UPDATE STATISTICS [schema_name].[table_name]([stat_name]);
    
    UPDATE STATISTICS [dbo].[table1] ([stats_col1]);
    

    通过更新特定统计信息对象,可以减少管理统计信息所需的时间和资源。 这需要经过一定的思考,以选择要更新的最佳统计信息对象。

    更新表的所有统计信息

    用于更新表中所有统计信息对象的一个简单方法如下:

    UPDATE STATISTICS [schema_name].[table_name];
    
    UPDATE STATISTICS dbo.table1;
    

    UPDATE STATISTICS 语句很容易使用。 只要记住,这会更新表中的所有统计信息,因此执行的工作可能会超过所需的数量。 如果性能不是一个考虑因素,这是保证拥有最新统计信息的最简单、最全面的操作方式。

    更新表中的所有统计信息时,专用 SQL 池会执行扫描,以便针对每个统计信息对象进行表采样。 如果表很大、包含许多列和许多统计信息,则根据需要更新各项统计信息可能比较有效率。

    有关 UPDATE STATISTICS 过程的实现,请参阅临时表。 实现方法与上述 CREATE STATISTICS 过程略有不同,但最终结果相同。

    有关完整语法,请参阅更新统计信息

    统计信息元数据

    可以使用多个系统视图和函数来查找有关统计信息的信息。 例如,使用 stats-date 函数查看上次创建或更新统计信息的时间,即可了解统计信息对象是否可能已过时。

    统计信息的目录视图

    这些系统视图提供有关统计信息的信息:

    SELECT sm.[name] AS [schema_name] , tb.[name] AS [table_name] , st.[name] AS [stats_name] , st.[filter_definition] AS [stats_filter_definition] , st.[has_filter] AS [stats_is_filtered] , STATS_DATE(st.[object_id],st.[stats_id]) AS [stats_last_updated_date] , co.[name] AS [stats_column_name] , ty.[name] AS [column_type] , co.[max_length] AS [column_max_length] , co.[precision] AS [column_precision] , co.[scale] AS [column_scale] , co.[is_nullable] AS [column_is_nullable] , co.[collation_name] AS [column_collation_name] , QUOTENAME(sm.[name])+'.'+QUOTENAME(tb.[name]) AS two_part_name , QUOTENAME(DB_NAME())+'.'+QUOTENAME(sm.[name])+'.'+QUOTENAME(tb.[name]) AS three_part_name FROM sys.objects AS ob JOIN sys.stats AS st ON ob.[object_id] = st.[object_id] JOIN sys.stats_columns AS sc ON st.[stats_id] = sc.[stats_id] AND st.[object_id] = sc.[object_id] JOIN sys.columns AS co ON sc.[column_id] = co.[column_id] AND sc.[object_id] = co.[object_id] JOIN sys.types AS ty ON co.[user_type_id] = ty.[user_type_id] JOIN sys.tables AS tb ON co.[object_id] = tb.[object_id] JOIN sys.schemas AS sm ON tb.[schema_id] = sm.[schema_id] WHERE 1=1 AND st.[user_created] = 1

    DBCC SHOW_STATISTICS() 示例

    DBCC SHOW_STATISTICS() 显示统计信息对象中保存的数据。 这些数据包括三个组成部分:

    有关统计信息的标头元数据。 直方图显示统计信息对象的第一个键列中的值分布。 密度向量可度量跨列相关性。

    专用 SQL 池使用统计信息对象中的任何数据来计算基数估计值。

    显示标头、密度和直方图

    此简单示例显示了统计信息对象的所有三个组成部分:

    DBCC SHOW_STATISTICS([<schema_name>.<table_name>],<stats_name>)
    
    DBCC SHOW_STATISTICS (dbo.table1, stats_col1);
    

    显示 DBCC SHOW_STATISTICS() 的一个或多个组成部分

    如果只想要查看特定部分,请使用 WITH 子句并指定要查看哪些部分:

    DBCC SHOW_STATISTICS([<schema_name>.<table_name>],<stats_name>) WITH stat_header, histogram, density_vector
    
    DBCC SHOW_STATISTICS (dbo.table1, stats_col1) WITH histogram, density_vector
    

    DBCC SHOW_STATISTICS() 差异

    相比于 SQL Server,在专用 SQL 池中,DBCC SHOW_STATISTICS() 的实现更加严格:

  • 未阐述的功能不受支持。
  • 不能使用 Stats_stream。
  • 不能联接特定统计信息子集的结果。 例如 STAT_HEADER JOIN DENSITY_VECTOR。
  • 不能针对消息隐藏设置 NO_INFOMSGS。
  • 不能在统计信息名称的前后使用方括号。
  • 不能使用列名来标识统计信息对象。
  • 不支持自定义错误 2767。
  • 有关进一步提升查询性能的信息,请参阅监视工作负荷