MySQL源码:Range和Ref优化的成本评估
在开始介绍index merge/ROR优化之前,打算先介绍MySQL是如何对range/ref做成本评估的。MySQL是基于成本(cost)模型选择执行计划,在多个range,全表扫描,ref之间会选择成本最小的作为最终的执行计划。仍然强烈建议先阅读登博的slide:《查询优化浅析》,文中
在开始介绍index merge/ROR优化之前,打算先介绍MySQL是如何对range/ref做成本评估的。MySQL是基于成本(cost)模型选择执行计划,在多个range,全表扫描,ref之间会选择成本最小的作为最终的执行计划。仍然强烈建议先阅读登博的slide:《查询优化浅析》,文中较为详细的介绍MySQL在range优化时成本的计算。
本文将继续介绍range/ref执行计划选择的一些不容忽略的细节。希望看客能够通过此文能够了解更多细节。
目录
- 0. 成本计算的总原则
- 1. range成本的计算与分析
- 1.1 range返回的记录数
- 1.2 CPU COST
- 1.3 IO COST
- 1.4 全表扫描的成本
- 1.5 关于range执行计划的分析
- 1.6 验证
- 1.7 一些限制
- 2. ref成本的计算与分析
- 2.1 ref返回的记录数
- 2.2 CPU COST
- 2.3 IO COST
- 2.4 全表扫描的成本
- 2.5 关于ref执行计划的分析
- 2.6 验证
- 3. 上面计算的局限性
- 4. 案例中使用的数据和表
0. 成本计算的总原则
MySQL的一个执行计划,有两部分成本,CPU成本(CPU COST)和IO成本(IO COST)。CPU COST是指查询出纪录后,需要做过滤等处理的时候的CPU消耗,IO COST是指,从存储引擎读取数据时需要做的IO消耗。
总成本 = CPU COST + IO COST
补充说明:(1) IO成本计算不考虑缓存的影响。因为在优化器本身是无法预知需要的数据到底在内存中还是磁盘上。
1. range成本的计算与分析
MySQL使用一颗SEL_ARG的树形结构描述了WHERE条件中的range,如果有多个range,则使用递归的方式遍历SEL_ARG结构,在前面详细的介绍range的红黑树结构,以及MySQL如何遍历之。
接上文,这里将看看,遍历到最后,MySQL如何计算一个简单range的成本。
1.1 range返回的记录数
MySQL首先计算range需要返回都少纪录,通过函数check_quick_select返回对某个索引做range查询大约命中多少条纪录。
found_records= check_quick_select(param, idx, *key, update_tbl_stats);
1.2 CPU COST
#define TIME_FOR_COMPARE 5 // 5 compares == one read double cpu_cost= (double) found_records / TIME_FOR_COMPARE;
1.3 IO COST
对于InnoDB的二级索引,且不是覆盖扫描:
found_read_time := number of ranges + found_records
这里,found_records是主要部分,number of ranges表示一共有多少个range,这是一个修正值,表示IO COST不小于range的个数。
1.4 全表扫描的成本
具体的,对于InnoDB表,我们来看:
read_time= number of total page + (records / TIME_FOR_COMPARE + 1) + 1.1;
对于InnoDB取值为:主键索引(数据)所使用的page数量(stat_clustered_index_size)
对于MyISAM取值为:stats.data_file_length/IO_SIZE + file->tables
1.5 关于range执行计划的分析
这里来看看,range的选择度(selectivty)大概为多少的时候,会放弃range优化,而选择全表扫描。下面时一个定量的分析:
(1) 假设总记录数为R;range需要返回的纪录数为r
(2) 假设该表的总页面数(IO COST)为P;单个页面纪录数为c
\[r+1\frac{r}{5} > P + \frac{R}{5} + 1 + 1.1 \]
\[ \frac{r}{R} > \frac{1}{6} + \frac{5}{6} * \frac{P}{R} + \frac{5.5}{6*R} \]
\[ \frac{r}{R} > \frac{1}{6} + \frac{5}{6} * \frac{1}{c} \frac{5.5}{6*R} \]
在我的测试案例中,P=4,R=1016 ,有
\[ \frac{r}{R} > 0.171 \]
也就是说这个案例中,如果选择度(selectivity)高于17.1%就会放弃range优化,而走全表扫描。这里纪录数超过1016*0.171=173时将放弃range优化。
1.6 验证
MySQL通过函数check_quick_select返回range可能扫描的记录数,所以,这里通过对该函数设置断点,并手动设置返回值,通过此来验证上面对selectivity的计算,详细地:
(gdb) p head->file->stats.records $1 = 1016 (gdb) p head->file->scan_time() $3 = 4 (gdb) p 1016*(1.0/6+(5.0/6)*(4.0/1016)+5.5/(6*1016)) $43 = 173.58333333333329 (gdb) b check_quick_select Breakpoint 5 at 0x679377: file opt_range.cc, line 7436. (gdb) c Continuing. 遇到断点: (gdb) return 173 看到: root@test 05:07:52>explain select * from users where reg_date >= '2012-09-20 12:00:00'; +----+-------------+-------+-------+---------------+-------------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+---------------+-------------+---------+------+------+-------------+ | 1 | SIMPLE | users | range | ind_regdate | ind_regdate | 9 | NULL | 173 | Using where | +----+-------------+-------+-------+---------------+-------------+---------+------+------+-------------+ (gdb) return 174 看到 root@test 05:08:05>explain select * from users where reg_date >= '2012-09-20 12:00:00'; +----+-------------+-------+------+---------------+------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+------+---------+------+------+-------------+ | 1 | SIMPLE | users | ALL | ind_regdate | NULL | NULL | NULL | 1016 | Using where | +----+-------------+-------+------+---------------+------+---------+------+------+-------------+
上面可以看到,如果range命中的记录数超过173的时候,就会放弃range,选择全表扫描。
1.7 一些限制
(1) 无论时InnoDB还是MyISAM的scan_time,range返回的记录数都不是精确值,而且对于InnoDB,总记录数也不是精确值,所以上面只是一个High level的预估。
(2) 上面案例中,条纪录很短,所以看到总page很少,实际情况,单条纪录更大,也就是上面的单个页面纪录数为c更小,所以通常选择度更高的时候,才会选择全表扫描。
2. ref成本的计算与分析
2.1 ref返回的记录数
ref优化的时候,计算返回的记录数从代码上来看要复杂很多,但是思想很简单。
思路:在range优化阶段,任何等值都会当作范围条件(参考1,参考2)。
对于kp1 = const and kp2 = const这类ref,MySQL将直接使用range优化时返回的结果,这个结果是通过存储引擎接口records_in_range返回。
还有一类较为特殊的ref,kp1 = const and kp2 > const,对于此类ref,range优化的时候,会使用两个索引列,但是ref只能用一个索引列。这时,ref首先根据索引统计信息(show index from users中Cardinality的值)预估。因为这里有range优化的值,还会做一次修正,因为range使用了更多的索引字段。修正逻辑为:如果发现索引统计信息太过保守(例如数据分布不均匀时,遇到一个热点),这时会用range优化的值修正。
所以,返回的纪录数,使用如下代码获取:
records= keyinfo->rec_per_key[max_key_part-1] if(records quick_rows[key]...) records= (double)table->quick_rows[key];
2.2 CPU COST
CPU COST := records/(double) TIME_FOR_COMPARE;
2.3 IO COST
ref在做IO成本评估的时候,基本同range相同,ref命中多少纪录则需要多少个IO COST。但是跟range优化打不同的是,这里做了一个修正(range优化并没有做),也是IO COST最坏不会超过全表扫描IO消耗的3倍(或者总记录数除以10),有下面的代码:
s->worst_seeks= min((double) s->found_records / 10, (double) s->read_time*3); IO COST := record_count*min(tmp,s->worst_seeks);
这里record_count是前一次关联后的记录数。tmp是当前ref命中的记录数。这个修正的逻辑是很好理解的:即使加上索引扫描其io cost仍然是有限度的。因为range的评估并没有加上这个修正,所以就导致了一些奇怪的事情发生了,后面我们再详细分析这一点。
2.4 全表扫描的成本
简单版本(不考虑多表关联):
scan_time() + s->records/TIME_FOR_COMPARE
scan_time()为存储引擎返回的全表扫描IO次数;s->records为存储引擎维护的单表总纪录数。
复杂版本(有多表关联):
假设前面关联后的纪录数为record_count,当前表的where条件将过滤后剩余3/4的纪录(不满足where条件的为1/4),并将这个值记为rnd_records。
(s->records - rnd_records)/TIME_FOR_COMPARE + record_count * (rnd_records/TIME_FOR_COMPARE)
这里假设将过滤1/4数据,实际代码中还将做一次修正,如果有range计算,假设其命中q条纪录,那么就认为将过滤s->records-q条纪录。
2.5 关于ref执行计划的分析
上面的分析,可以看到,ref成本有一部分是取min函数的,为了分析ref和全表扫描的临界条件,为了简化做下面的假设:
(1) scan_time()*3 records / 10 (2) scan_time()*3 <p>第一个条件表示约30条纪录一个page;第二个条件是ref命中的记录数为总页面的3倍。</p> <p>那么放弃ref全表扫描的条件是:</p> <pre class="brush:php;toolbar:false">scan_time()*3 + r/5 > scan_time() + R/5 即: scan_time()*2 > (R-r)/5 scan_time() > (R-r)/10 具体的:
(1) 假设总记录数为R;ref需要返回的纪录数为r
(2) 假设该表的总页面数(IO COST)为P;单个页面纪录数为c
那么range的代价超过全表扫描代价,则有:
\[3*P + \frac{r}{5} > P + \frac{R}{5} \]
\[\frac{r}{R} > 1 - \frac{10*P}{R}\]
\[\frac{r}{R} > 1 - \frac{10}{c}\]
在我的测试案例中,P=6.4,R=900 ,有
\[ \frac{r}{R} > 0.929 \]
对于具体的案例,由于取整的问题,会和上面有小小的偏差:
3*((int)6.39) + r/5 > 6.39453125 + 900/5 r > 841.97
2.6 验证
这里再通过gdb修改r的值来验证,因为ref命中纪录的预估是取range的计算值,所以:
gdb) set s->table->quick_rows[1]=841 (gdb) c root@test 04:37:16>explain select * from users where reg_date = '2012-09-21 12:00:00'; +----+-------------+-------+------+---------------+-------------+---------+-------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+-------------+---------+-------+------+-------------+ | 1 | SIMPLE | users | ref | IND_REGDATE | IND_REGDATE | 9 | const | 841 | Using where | +----+-------------+-------+------+---------------+-------------+---------+-------+------+-------------+ 1 row in set (47.61 sec) (gdb) set s->table->quick_rows[1]=842 (gdb) c root@test 04:38:46>explain select * from users where reg_date = '2012-09-21 12:00:00'; +----+-------------+-------+------+---------------+------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+------+---------+------+------+-------------+ | 1 | SIMPLE | users | ALL | IND_REGDATE | NULL | NULL | NULL | 900 | Using where | +----+-------------+-------+------+---------------+------+---------+------+------+-------------+
另一个结论是,如果当条记录很小,单个页面的记录数很多的话,只有选择度(selectivity)非常高的时候,MySQL才会放弃ref,走全表扫描,这也是,Vadim在2006年吐槽MySQL的一点。
3. 上面计算的局限性
上面的推倒尝试介绍一些通用的情况,但是实际上优化器中计算ref/range的成本时,会有一些不同:
(1) 无论时InnoDB还是MyISAM的scan_time,range返回的记录数都不是精确值,而且对于InnoDB,总记录数也不是精确值,所以上面只是一个High level的预估
(2) 上面案例中,条纪录很短,所以看到总page很少,实际情况,单条纪录更大,也就是上面的单个页面纪录数为c更小,所以通常选择度更高的时候,才会选择全表扫描。
(3) 上面的计算,都不是覆盖扫描的情况,覆盖扫描的时候,成本计算与上面略有不同
(4) 上面都是使用gdb修改某些值的方式来验证。如果想通过创建一个表,够造某个索引的区分度/选制度,因为scan_time和返回的记录数都是预估的,这样的方式是不行的
4. 案例中使用的数据和表
CREATE TABLE `users` ( `id` int(11) NOT NULL, `nick` varchar(32) DEFAULT NULL, `reg_date` datetime DEFAULT NULL, KEY `IND_NICK` (`nick`), KEY `IND_REGDATE` (`reg_date`), KEY `IND_ID` (`id`) ) ENGINE=MyISAM for id in `seq 1 886`; \ do mysql -uroot test -e \ "insert into users values($id,char(round(ord('A') + rand()*(ord('z')-ord('A')))),\ '2012-09-21 12:00:00')" ;done for id in `seq 887 900`; \ do mysql -uroot test -e \ "insert into users values($id,char(round(ord('A') + rand()*(ord('z')-ord('A')))),\ '2012-09-20 12:00:00')" ;done

핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

AI Hentai Generator
AI Hentai를 무료로 생성하십시오.

인기 기사

뜨거운 도구

메모장++7.3.1
사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전
중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기
강력한 PHP 통합 개발 환경

드림위버 CS6
시각적 웹 개발 도구

SublimeText3 Mac 버전
신 수준의 코드 편집 소프트웨어(SublimeText3)

뜨거운 주제











선형 복잡성에서 로그 복잡성까지 조회 시간을 줄이는 인덱스를 구축하여 MySQL 쿼리 성능을 최적화할 수 있습니다. SQL 삽입을 방지하고 쿼리 성능을 향상하려면 PREPAREDStatements를 사용하세요. 쿼리 결과를 제한하고 서버에서 처리되는 데이터의 양을 줄입니다. 적절한 조인 유형 사용, 인덱스 생성, 하위 쿼리 사용 고려 등 조인 쿼리를 최적화합니다. 쿼리를 분석하여 병목 현상을 식별하고, 캐싱을 사용하여 데이터베이스 로드를 줄이고, 오버헤드를 최소화합니다.

PHP에서 MySQL 데이터베이스를 백업하고 복원하는 작업은 다음 단계에 따라 수행할 수 있습니다. 데이터베이스 백업: mysqldump 명령을 사용하여 데이터베이스를 SQL 파일로 덤프합니다. 데이터베이스 복원: mysql 명령을 사용하여 SQL 파일에서 데이터베이스를 복원합니다.

MySQL 테이블에 데이터를 삽입하는 방법은 무엇입니까? 데이터베이스에 연결: mysqli를 사용하여 데이터베이스에 대한 연결을 설정합니다. SQL 쿼리 준비: 삽입할 열과 값을 지정하는 INSERT 문을 작성합니다. 쿼리 실행: query() 메서드를 사용하여 삽입 쿼리를 실행하면 확인 메시지가 출력됩니다.

MySQL 8.4(2024년 최신 LTS 릴리스)에 도입된 주요 변경 사항 중 하나는 "MySQL 기본 비밀번호" 플러그인이 더 이상 기본적으로 활성화되지 않는다는 것입니다. 또한 MySQL 9.0에서는 이 플러그인을 완전히 제거합니다. 이 변경 사항은 PHP 및 기타 앱에 영향을 미칩니다.

PHP에서 MySQL 저장 프로시저를 사용하려면: PDO 또는 MySQLi 확장을 사용하여 MySQL 데이터베이스에 연결합니다. 저장 프로시저를 호출하는 문을 준비합니다. 저장 프로시저를 실행합니다. 결과 집합을 처리합니다(저장 프로시저가 결과를 반환하는 경우). 데이터베이스 연결을 닫습니다.

PHP를 사용하여 MySQL 테이블을 생성하려면 다음 단계가 필요합니다. 데이터베이스에 연결합니다. 데이터베이스가 없으면 작성하십시오. 데이터베이스를 선택합니다. 테이블을 생성합니다. 쿼리를 실행합니다. 연결을 닫습니다.

시간 복잡도는 입력 크기를 기준으로 알고리즘의 실행 시간을 측정합니다. C++ 프로그램의 시간 복잡성을 줄이는 팁에는 데이터 저장 및 관리를 최적화하기 위한 적절한 컨테이너(예: 벡터, 목록) 선택이 포함됩니다. Quick Sort와 같은 효율적인 알고리즘을 활용하여 계산 시간을 단축합니다. 여러 작업을 제거하여 이중 계산을 줄입니다. 불필요한 계산을 피하려면 조건부 분기를 사용하세요. 이진 검색과 같은 더 빠른 알고리즘을 사용하여 선형 검색을 최적화합니다.

Oracle 데이터베이스와 MySQL은 모두 관계형 모델을 기반으로 하는 데이터베이스이지만 호환성, 확장성, 데이터 유형 및 보안 측면에서 Oracle이 우수하고, MySQL은 속도와 유연성에 중점을 두고 중소 규모 데이터 세트에 더 적합합니다. ① Oracle은 광범위한 데이터 유형을 제공하고, ② 고급 보안 기능을 제공하고, ③ 엔터프라이즈급 애플리케이션에 적합하고, ① MySQL은 NoSQL 데이터 유형을 지원하고, ② 보안 조치가 적고, ③ 중소 규모 애플리케이션에 적합합니다.
