1.测试环境准备
bash
CREATE USER hist_test IDENTIFIED BY hist_test;
GRANT CONNECT, RESOURCE TO hist_test;
GRANT SELECT_CATALOG_ROLE TO hist_test;
GRANT ANALYZE ANY TO hist_test;
GRANT EXECUTE ON DBMS_STATS TO hist_test;
ALTER USER hist_test QUOTA UNLIMITED ON USERS;
2.构造一个状态列严重倾斜的表,status = 1 占绝大多数,其余值各少量。
bash
CREATE TABLE t_hist_test (
id NUMBER,
status NUMBER,
info VARCHAR2(100)
);
-- 插入倾斜数据:status=1 有 100000 行,status=2..100 各 10 行
BEGIN
FOR i IN 1..100000 LOOP
INSERT INTO t_hist_test VALUES(i, 1, 'bulk_' || i);
END LOOP;
FOR j IN 2..100 LOOP
FOR k IN 1..10 LOOP
INSERT INTO t_hist_test VALUES(100000 + j*10 + k, j, 'rare_' || j);
END LOOP;
END LOOP;
COMMIT;
END;
/
CREATE INDEX idx_hist_status ON t_hist_test(status);
3.步骤一:无直方图时的基线行为
bash
先用 ANALYZE(仅测试用,生产禁用)或显式指定 SIZE 1 收集统计信息,确保 status 列没有直方图。
sql
-- 清除可能存在的直方图并重新收集(SIZE 1 表示不建直方图)
EXEC DBMS_STATS.GATHER_TABLE_STATS('HIST_TEST', 'T_HIST_TEST', method_opt => 'FOR COLUMNS SIZE 1 STATUS');
PL/SQL procedure successfully completed.
bash
-- 确认直方图状态为 NONE
SELECT column_name, histogram, num_buckets
FROM dba_tab_col_statistics
WHERE owner='HIST_TEST' AND table_name='T_HIST_TEST' AND column_name='STATUS';
COLUMN_NAME
--------------------------------------------------------------------------------
HISTOGRAM NUM_BUCKETS
------------------------------ -----------
STATUS
NONE 1
bash
执行查询并观察计划:
sql
-- 查询倾斜值(大量数据)
EXPLAIN PLAN FOR SELECT * FROM t_hist_test WHERE status = 1;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Plan hash value: 100264795
-------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1010 | 19190 | 6 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED| T_HIST_TEST | 1010 | 19190 | 6 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | IDX_HIST_STATUS | 1010 | | 2 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------
2 - access("STATUS"=1)
14 rows selected.
-- 查询稀有值(少量数据)
EXPLAIN PLAN FOR SELECT * FROM t_hist_test WHERE status = 50;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Plan hash value: 100264795
-------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1010 | 19190 | 6 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED| T_HIST_TEST | 1010 | 19190 | 6 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | IDX_HIST_STATUS | 1010 | | 2 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------
2 - access("STATUS"=50)
14 rows selected.
预期:没有直方图时,优化器假设 status 分布均匀(选择性 = 1/NDV ≈ 1/100),两个查询的预估行数(E-Rows)会非常接近(约 1000 行),很可能对两个查询都选择索引扫描,或者都选择全表扫描,无法区分数据量的巨大差异。
4.步骤二:生成频率直方图(Frequency)
bash
由于 status 的 NDV(不同值数量)约为 100,小于默认桶数 254,Oracle 应自动生成频率直方图。
sql
-- 强制对 status 收集直方图(默认 SIZE AUTO 在列被查询后也会自动收集)
SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS ('HIST_TEST', 'T_HIST_TEST', method_opt => 'FOR COLUMNS SIZE AUTO STATUS');
PL/SQL procedure successfully completed.
-- 验证直方图类型
SELECT
column_name,
histogram,
num_buckets
FROM
dba_tab_col_statistics
WHERE
owner = 'HIST_TEST'
AND table_name = 'T_HIST_TEST'
AND column_name = 'STATUS';
COLUMN_NAME HISTOGRAM NUM_BUCKETS
-------------------- ------------------------------ -----------
STATUS FREQUENCY 100
-- 预期 HISTOGRAM = FREQUENCY,NUM_BUCKETS ≈ 100
查看直方图桶详情:
sql
SELECT
endpoint_number,
endpoint_value
FROM
dba_tab_histograms
WHERE
owner = 'HIST_TEST'
AND table_name = 'T_HIST_TEST'
AND column_name = 'STATUS'
ORDER BY
endpoint_number;
在频率直方图中,endpoint_number 是累积频率。如果 status=1 的桶的 endpoint_number 远大于前一个桶,说明优化器能识别出它是"Popular Value"(高频值)。
ENDPOINT_NUMBER ENDPOINT_VALUE
--------------- --------------
100000 1
100010 2
100020 3
100030 4
100040 5
100050 6
100060 7
100070 8
100080 9
100090 10
100100 11
ENDPOINT_NUMBER ENDPOINT_VALUE
--------------- --------------
100110 12
100120 13
100130 14
100140 15
100150 16
100160 17
100170 18
100180 19
100190 20
100200 21
100210 22
ENDPOINT_NUMBER ENDPOINT_VALUE
--------------- --------------
100220 23
100230 24
100240 25
100250 26
100260 27
100270 28
100280 29
100290 30
100300 31
100310 32
100320 33
ENDPOINT_NUMBER ENDPOINT_VALUE
--------------- --------------
100330 34
100340 35
100350 36
100360 37
100370 38
100380 39
100390 40
100400 41
100410 42
100420 43
100430 44
ENDPOINT_NUMBER ENDPOINT_VALUE
--------------- --------------
100440 45
100450 46
100460 47
100470 48
100480 49
100490 50
100500 51
100510 52
100520 53
100530 54
100540 55
ENDPOINT_NUMBER ENDPOINT_VALUE
--------------- --------------
100550 56
100560 57
100570 58
100580 59
100590 60
100600 61
100610 62
100620 63
100630 64
100640 65
100650 66
ENDPOINT_NUMBER ENDPOINT_VALUE
--------------- --------------
100660 67
100670 68
100680 69
100690 70
100700 71
100710 72
100720 73
100730 74
100740 75
100750 76
100760 77
ENDPOINT_NUMBER ENDPOINT_VALUE
--------------- --------------
100770 78
100780 79
100790 80
100800 81
100810 82
100820 83
100830 84
100840 85
100850 86
100860 87
100870 88
ENDPOINT_NUMBER ENDPOINT_VALUE
--------------- --------------
100880 89
100890 90
100900 91
100910 92
100920 93
100930 94
100940 95
100950 96
100960 97
100970 98
100980 99
ENDPOINT_NUMBER ENDPOINT_VALUE
--------------- --------------
100990 100
5.步骤三:对比执行计划变化(核心验证)
bash
再次执行步骤一中的两个查询,观察 E-Rows(预估行数)和实际执行计划。
sql
EXPLAIN PLAN FOR
SELECT
*
FROM
t_hist_test
WHERE
status = 1;
SELECT
*
FROM
TABLE (DBMS_XPLAN.DISPLAY);
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Plan hash value: 1532874825
---------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 100K| 1855K| 103 (1)| 00:00:01 |
|* 1 | TABLE ACCESS FULL| T_HIST_TEST | 100K| 1855K| 103 (1)| 00:00:01 |
---------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1 - filter("STATUS"=1)
13 rows selected.
EXPLAIN PLAN FOR SELECT * FROM t_hist_test WHERE status = 50;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Plan hash value: 100264795
-------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 190 | 2 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED| T_HIST_TEST | 10 | 190 | 2 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | IDX_HIST_STATUS | 10 | | 1 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------
2 - access("STATUS"=50)
预期:有了频率直方图后:
status = 1:优化器预估行数接近 100000,可能选择全表扫描(因为返回大量数据,索引成本高)。
status = 50:优化器预估行数接近 10,很可能选择索引扫描。
如果两次查询的计划确实不同,且符合数据分布,说明直方图生效。
**7.注意事项**
```bash
1.SIZE AUTO 的触发条件:Oracle 默认只在列被查询过之后,才会在下次统计信息收集时为该列生成直方图。如果直接收集统计信息但从未查询过该表,可能不会生成直方图。
2.ANALYZE 不生成直方图:测试中若使用 ANALYZE TABLE ... COMPUTE STATISTICS,不会产生直方图,必须用 DBMS_STATS。
3.绑定变量的干扰:如果测试 SQL 使用绑定变量,直方图的效果依赖于绑定变量窥探。对于分布差异极大的值,可能需要观察多个 child cursor 的执行计划,而不是只看第一次硬解析的结果。
4.生产环境慎用 SIZE 254 强制:除非明确确认某列需要高精度直方图,否则优先使用 SIZE AUTO,避免统计信息收集开销和潜在的计划波动。