联系我们
18591797788
hubin@rlctech.com
北京市海淀区中关村南大街乙12号院天作国际B座1708室
18681942657
lvyuan@rlctech.com
上海市浦东新区商城路660号乐凯大厦26c-1
18049488781
xieyi@rlctech.com
广州市越秀区东风东路华宫大厦808号1608房
029-81109312
service@rlctech.com
西安市高新区天谷七路996号西安国家数字出版基地C座501
在生产数据库运维、数据质量摸底、测试数据清理前的预估场景中,我们经常需要快速统计某类数据量,但又怕全表扫描拖垮数据库。
今天就带大家拆解一条专为生产环境设计的采样统计 SQL,从 Hint 优化、模糊查询细节,到数据库采样原理,一次性讲透所有技术点!
SELECT /*+ NO_REWRITE NO_PARALLEL DYNAMIC_SAMPLING(0) QUERY_TIMEOUT(10000000) OPT_PARAM('USE_DEFAULT_OPT_STAT','TRUE') */
SUM(CASE WHEN (`APPNT_NAME_xxx` like '%测试%' escape '\\') THEN 1 ELSE 0 END)
FROM `exx_xxx`.`exx_xxx` SAMPLE BLOCK(0.003507) SEED(1) `a`
Hint 作用
NO_REWRITE 禁止查询改写,保持 SQL 原样执行
NO_PARALLEL 禁止并行执行,使用单线程
DYNAMIC_SAMPLING(0) 关闭动态采样,不在执行时额外采集统计信息
QUERY_TIMEOUT(10000000) 设置查询超时为 10,000,000 微秒(10 秒)
OPT_PARAM('USE_DEFAULT_OPT_STAT','TRUE') 使用默认的优化器统计信息
SUM(CASE WHEN (`APPNT_NAME_xxx` like '%测试%' escape '\\') THEN 1 ELSE 0 END)
对 APPNT_NAME_xxx 字段做模糊匹配,查找包含 "测试" 的记录
escape '\' 指定转义字符为反斜杠(处理特殊字符)
匹配到则计 1,否则计 0,最终 SUM 得到符合条件的记录总数
FROM `exx_xxx`.`exx_xxx` SAMPLE BLOCK(0.003507) SEED(1) `a`
exx_xxx.exx_xxx:契约表
SAMPLE BLOCK(0.003507):块级采样,只扫描约 0.3507% 的数据块(而非全表扫描),大幅减少 IO
SEED(1):固定随机种子为 1,确保每次采样结果可重复
a:表别名
这条 SQL 的目的是:通过采样(约 0.35% 的数据块)快速估算 exx_xxx 表中投保人姓名包含"测试"二字的记录数。这通常用于数据质量检查或测试数据清理前的摸底统计,用采样代替全表扫描以降低对生产库的性能影响。
NO_REWRITE 禁止查询改写,保持 SQL 原样执行
NO_PARALLEL 禁止并行执行,使用单线程
DYNAMIC_SAMPLING(0) 关闭动态采样,不在执行时额外采集统计信息
QUERY_TIMEOUT(10000000) 设置查询超时为 10,000,000 微秒(10 秒)
OPT_PARAM('USE_DEFAULT_OPT_STAT','TRUE') 使用默认的优化器统计信息
其中模式字符串支持两个通配符:
| 通配符 | 含义 | 示例 |
|---|---|---|
% |
匹配任意长度的任意字符(包括零个字符) | '%测试%' 匹配任何位置包含"测试"的字符串 |
_ |
匹配恰好一个任意字符 | '张_' 匹配"张三"、"张四",但不匹配"张三丰" |
LIKE '测试%' -- 可以利用索引(前缀匹配)
LIKE '%测试%' -- 无法利用索引(需逐行扫描)
LIKE '%测试' -- 无法利用索引(需逐行扫描)
ESCAPE '\\' 的作用ESCAPE 子句定义一个转义字符,使通配符 % 和 _ 可以被当作普通字符匹配。
这里 '\\' 是 SQL 字符串中反斜杠的写法(\ 本身需要转义),所以实际转义字符是 \。
举例说明:如果要搜索 USER_NAME 中真的包含 % 字符(而不是通配):
-- 不用 ESCAPE:% 被当作通配符,匹配所有以"折扣"开头的
USER_NAME LIKE '折扣%'
-- 用 ESCAPE:\% 表示字面量 %,精确匹配包含"折扣%"这个字符串的
USER_NAME LIKE '%折扣\%%' ESCAPE '\\'
-- ↑ ↑↑
-- | |└── 通配符%(匹配任意后缀)
-- | └─── \% 被转义为字面量 %
-- └──────── 通配符%(匹配任意前缀)
SELECT * FROM products
WHERE product_name LIKE '%#%%' ESCAPE '#';
-- #% → 字面量 %
-- 效果和前面用 \ 完全一样
找名称中同时包含 % 和 _ 的数据:
SELECT * FROM products_xx
WHERE product_name LIKE '%\%%' ESCAPE '\'
AND product_name LIKE '%\_%' ESCAPE '\';
数据库不是一行一行存数据的,而是把很多行打包成一个"块"(Block/Page)存在磁盘上。
磁盘上的数据组织方式:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Block 1 │ │ Block 2 │ │ Block 3 │ ... │ Block N │
│ 第1~100行 │ │ 第101~200行 │ │ 第201~300行 │ │ 最后几行 │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
全表扫描 = 读取所有 Block = 全部 IO 开销;
SAMPLE BLOCK 做了什么?
不读所有 Block,只随机抽取约 0.003507% 的 Block 来读。
全部 Block(假设有 100,000 个):
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■... (100,000个)
SAMPLE BLOCK(0.003507) 后实际读取的(约 3~4 个):
□□□□□■□□□□□□□□□□□□□□□□■□□□□□□□□□□■□□□□...
↑ ↑ ↑
被选中 被选中 被选中
只有被选中的 Block 才会从磁盘读出来,其余全部跳过
SAMPLE BLOCK(0.003507) SEED(1)
采样是随机的,那每次执行选中的 Block 可能不同,结果就会变化。SEED(1) 就是固定随机数种子为 1,保证:
第一次执行:随机选中 Block 5, 1023, 8847, ... → 结果:42
第二次执行:随机选中 Block 5, 1023, 8847, ... → 结果:42(一模一样)
第三次执行:随机选中 Block 5, 1023, 8847, ... → 结果:42(还是一样)
如果去掉 SEED:
第一次执行:随机选中 Block 5, 1023, 8847, ... → 结果:42
第二次执行:随机选中 Block 77, 555, 9012, ... → 结果:38(变了)
第三次执行:随机选中 Block 200, 3001, 7788, ... → 结果:45(又变了)
SEED 的好处:结果可重复、可验证,别人用同样的 SQL 能得到一模一样的数。
OceanBase/Oracle 支持两种采样方式:
| 方式 | 语法 | 粒度 | 速度 | 精度 |
|---|---|---|---|---|
| 块采样 | SAMPLE BLOCK(n) |
以 Block 为单位抽取 | 快(跳过整个 Block) | 略低(Block 内数据可能分布不均) |
| 行采样 | SAMPLE(n) |
以行为单位抽取 | 慢(需要逐行判断是否抽中) | 较高 |
块采样 SAMPLE BLOCK(n):
读 Block 5 的全部行 → 读 Block 1023 的全部行 → ...
↑ 整块读,IO 连续,磁盘效率高
行采样 SAMPLE(n):
Block 1: 第1行(不要) 第2行(要) 第3行(不要) ...
Block 2: 第1行(要) 第2行(不要) ...
↑ 每个 Block 都得读,只是行级筛选,IO 没减少
本 SQL 用的是 SAMPLE BLOCK,追求的是速度优先,因为目的只是粗略估算"测试"数据的数量,不需要精确到个位数.
采样返回的是部分数据的统计值,要估算全表数据需要换算:
假设采样结果:SUM = 3(在采样的 Block 中找到 3 条包含"测试"的记录)
采样比例:0.003507%
估算全表数量 = 3 / 0.003507% = 3 / 0.00003507 ≈ 85,543 条
这是一个估算值,实际数字可能有偏差,但量级是对的;