1. 当你按下"执行"时,数据库在做什么?

每次我们在SSMS里点击执行按钮时,SQLServer其实在进行一场紧张的计算博弈。想象一下你是公交车调度员,面对数以千计的乘车需求,你需要决定发车时间、路线规划、车型选择。优化器就是这个决策者,而统计信息就是它手中的乘客需求清单。

上周我遇到一个真实的案例:某电商平台的订单查询突然变慢10倍。经过排查发现,商品状态字段的统计信息过期,导致优化器错误地选择全表扫描。这让我深刻意识到——统计信息是优化器的眼睛,而直方图和密度向量就是这只眼睛的两种关键视觉模式。

2. 直方图:分桶艺术的极致表现

2.1 直方图的构造原理

直方图就像把全校学生的身高数据绘制成统计表。假设要统计2万名学生,SQLServer默认会创建200个直方图桶(最大可到201)。让我们用真实的数据来演示:

-- 创建测试表(技术栈:SQLServer 2019)
CREATE TABLE Products (
    ProductID INT PRIMARY KEY,
    Price DECIMAL(10,2) NOT NULL,
    CategoryID INT NOT NULL
);

-- 插入价格呈阶梯分布的测试数据(设置不同区间的价格分布)
INSERT INTO Products 
SELECT TOP 5000 
    n = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)),
    Price = CASE 
        WHEN n%10 = 0 THEN 100.00
        WHEN n%5 = 0 THEN 200.00
        ELSE n*10.00 
    END,
    CategoryID = n%50+1
FROM sys.all_columns a CROSS JOIN sys.all_columns b;

-- 手动创建统计信息
CREATE STATISTICS st_Price ON Products(Price) WITH FULLSCAN;

现在用DBCC查看直方图:

DBCC SHOW_STATISTICS('Products', 'st_Price') WITH HISTOGRAM;

典型输出节选:

RANGE_HI_KEY   RANGE_ROWS    EQ_ROWS      DISTINCT_RANGE_ROWS  
100.00         0             500          0
200.00         4500          500          4
......

这里的秘密在于:

  • RANGE_HI_KEY 每个区间的上限值
  • RANGE_ROWS 该区间内的行数
  • DISTINCT_RANGE_ROWS 区间内不同值的数量

2.2 查询优化的实战演示

当执行范围查询时:

SELECT * FROM Products 
WHERE Price BETWEEN 150.00 AND 300.00;

优化器会遍历直方图的各个区间,计算满足条件的总行数。如果发现覆盖大量直方图区间,可能选择索引扫描;若只需要少量区间,则可能选择索引查找。

3. 密度向量:不为人知的隐式指南

3.1 密度是如何计算的

密度向量就像班级花名册中的地区分布表。它的计算公式令人着迷:

密度 = 1 / 唯一值总数

但这个简单的公式背后隐藏着重大玄机。来看具体示例:

DBCC SHOW_STATISTICS('Products', 'st_Price');

在输出的头信息中,重点观察:

All density   Average Length  Columns
0.02          8               CategoryID
0.005         8               ProductID
0.5           8               Price

这里的密度参数说明:

  • Price字段的密度为0.5,说明该字段共有1/0.5=2个唯一值
  • CategoryID的密度为0.02,对应50个不同值(1/0.02=50)

3.2 多条件查询的秘密武器

当查询条件涉及多个字段时,优化器就会亮出密度向量这张底牌。例如:

SELECT * FROM Products
WHERE CategoryID = 5 AND Price > 100.00;

优化器会这样计算:

  1. 用CategoryID的密度(0.02)估算该条件的行数:总行数5000 * 0.02 = 100行
  2. 结合Price字段的统计信息进一步过滤
  3. 将两个条件的估算结果进行综合计算

4. 两种统计方式的生死较量

4.1 等值查询的决战场

当我们精确查询某个具体值时,直方图和密度向量会擦出怎样的火花?

SELECT * FROM Products 
WHERE Price = 200.00;

这时:

  1. 直方图派:直接查找RANGE_HI_KEY等于200的桶,使用EQ_ROWS值(500)
  2. 密度向量派:总行数5000 * 密度0.5 = 2500(明显错误!)

这里暴露了关键差异:直方图能精确统计高频值,而密度向量采用平均值假设。

4.2 范围查询的精度比拼

当查询条件是范围时:

SELECT * FROM Products 
WHERE Price > 150.00 AND Price < 250.00;

直方图会:

  1. 找出包含150和250的区间
  2. 计算每个区间的覆盖比例
  3. 累加估算行数

密度向量则是: 总行数 * 密度 * 范围比例 → 5000 * 0.5 * 0.2(假设)= 500

这时直方图的优势展露无遗,特别是当数据分布不均匀时。

5. 应用场景的生存法则

5.1 什么时候信任直方图?

  • 数据存在明显的热点值
  • 查询条件使用高频值
  • 范围查询的边界明确

例如电商网站的爆款商品查询,就非常适合依赖直方图统计。

5.2 密度向量更适用的战场

  • 多条件联合查询
  • 数据分布较为均匀的字段
  • 统计信息更新延迟时

比如用户画像系统中的年龄段筛选,数据如果是均匀分布的,密度向量反而更高效。

6. 实践中的血泪教训

6.1 自动更新的双面性

某次线上事故:统计信息自动更新后,执行计划突然变为全表扫描。经查是某个时间点的自动采样导致统计信息失真。

解决办法:

-- 修改统计信息更新策略
UPDATE STATISTICS Products st_Price 
WITH FULLSCAN, PERSIST_SAMPLE_PERCENT = ON;

6.2 组合统计信息的威力

当关联字段存在强相关性时,需要创建联合统计信息:

CREATE STATISTICS st_CategoryPrice 
ON Products(CategoryID, Price);

这可以让优化器了解两个字段的联合分布情况。

7. 技术选择的哲学思考

7.1 直方图的优势清单

  • 精确反映数据分布特征
  • 擅长处理偏态数据
  • 支持范围查询估算

7.2 密度向量的存在价值

  • 计算成本低廉
  • 内存占用小
  • 支持多条件计算
  • 快速响应数据变化

8. 必须牢记的黄金准则

  1. 定期健康检查:每月检查sys.dm_db_stats_properties
  2. 警惕采样陷阱:对于超过1GB的表,优先使用FULLSCAN
  3. 索引重建后必做:重建重要字段的统计信息
  4. 监控谓词选择:关注执行计划中的实际行数vs估算行数

9. 来自实战的终极建议

  • 为高频查询字段建立覆盖索引的同时,必须监控其统计信息
  • 使用查询存储(Query Store)捕获执行计划变更
  • 对于关键业务表,设置统计信息更新告警
-- 查询统计信息更新时间
SELECT 
    name AS stats_name,
    STATS_DATE(object_id, stats_id) AS last_updated
FROM sys.stats
WHERE object_id = OBJECT_ID('Products');

10. 总结:统计即力量

通过对直方图和密度向量的深入探索,我们发现:优化器就像一个精明但视力欠佳的决策者,统计信息就是它的眼镜。只有正确维护这副眼镜,才能确保它做出最优决策。

在实践中要注意:

  • 直方图擅长描述细节但维护成本高
  • 密度向量简便但可能失真
  • 二者需要配合使用才能发挥最大效果