1、深入了解MySQL中内部组件架构(连接器,查询缓存,分析器,优化器,执行器等)
2、深入了解MYSQL之InnoDB一次数据更新流程
3、深入了解Buffer Pool原理与优化(未)
4、深入了解写入一行数据在磁盘如何存储(未)
一文解决SQL调优实战
生产经验篇(1)——删库,怎么修复?
生产经验篇(2)——真实环境的MySQL机器配置规划
生产经验篇(3)——生产MySQL全链路压测
生产经验篇(4)——多个Buffer Pool来优化数据库的并发性能,chunk运行期间动态调整buffer pool(未)
生产经验篇(5)——基于当前机器合理设计bufferpool(未)
解释:主要分为两部分:Server层,Store层,分别为:
作用:
表现形式为:
mysql ‐h host[数据库地址] ‐u root[用户] ‐p root[密码] ‐P 3306[端口]
当我们输入上面那条命令之后,MYSQL客户端工具**(navicat,mysql front,jdbc,SQLyog等)**会使用跟服务端建立连接,然后经历TCP连接(三次握手),连接器会去验证你的用户名密码是否正确。然后会进行相应的数据返回。
1、如果用户名或密码不对,你就会收到一个"Access denied for user"的错误,然后客户端程序结束执行。
2、如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限。
一个用户建立成功之后,即使管理员对其权限进行修改,也不会有影响,只有断开连接之后,才会进行更新权限,而在没有断开连接之前,只会更新在系统表空间的mysql的user表中。
其中修改密码权限等的SQL为:
1、CREATE USER ‘username’@‘host’ IDENTIFIED BY ‘password’; //创建新用户
2、grant all privileges on . to ‘username’@‘%’; //赋权限,%表示所有(host)
3、flush privileges //刷新数据库
4、update user set password=password(”123456″) where user=’root’;(设置用户名密码)
5、show grants for root@“%”; 查看当前用户的权限
当我们连接成功之后,如果我们没有对当前连接进行任何操作,那么他就会进入一个空闲状态,我们可以使用show processlist去查看
解释:Command代表当前连接的状态
daemon:守护线程,代表后台挂着
query:代表正在查询
sleep:代表空闲状态
。。。以此类推
当我们的server端长时间不接受到客户端的请求,那么连接器就会断开,默认是由wait_timout控制,默认为8h。
由图上可知,28800s/60/60=8h。
当我们想设置这个时间的时候,也可以使用以下命令:
set global wait_timeout=28800; 设置全局服务器关闭非交互连接之前等待活动的秒数
当我们断开连接之后,客户端再请求服务端,那么就会报错:
Lost connection to MySQL server during query
然后,如果我们想继续操作,重连即可。
因为我们在开发过程中,一般使用的是连接池(长连接),而MySQL是使用临时内存去管理连接对象的,也就是个内存,那么长期下来,肯定会有内存过大问题,可以参考下HashMap本地缓存是不是也是这个道理。
那么我们怎么解决这个问题呢?
1、定期断开长连接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。
2、如果你用的是 MySQL 5.7 或更新版本,可以在每次执行一个比较大的操作后,通过执行mysql_reset_connection 来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。
扩展:
长连接:
短连接:
可以通过配置my.cnf文件加入skip_ssl指令 关闭ssl,配置:
[mysqld]
skip_ssl
show global variables like 'have_ssl%';
mysql -h localhost -u root -p'489773' -- protocol=tcp
https://www.processon.com/view/link/62fa96706376897acca09661
客户端
服务端
show databases; 显示所有数据库
use dbname; 打开数据库:
show tables; 显示数据库mysql中所有的表;
describe user; 显示表mysql数据库中user表的列信息);
1、首先MySQL会把执行的SQL语句与执行SQL的返回值以key-value对的形式直接缓存在内存中。key:查询的语句,value:查询的结果。
2、如果key在缓存中能直接查询到,那么就直接把返回值(value)返回给客户端。
3、如果不在缓存,那么就会走下面的流程,然后把他缓存到内存当中。
1、静态表(指的是很少会更新的表,比如配置表,字典表等)
2、如果需要开启,开启把my.cnf的参数**query_cache_type **设置成 DEMAND。
如下:
# query_cache_type有3个值
# 0代表关闭查询缓存OFF,
# 1代表开启ON,2(DEMAND)代表当sql语句中有SQL_CACHE 关键词时才缓存
query_cache_type=2
3、默认情况下,一个表中,是可以指定要使用查询缓存的语句,可以用SQL_CACHE显示去指定。如下:
select SQL_CACHE * from test where id=10;
4、也可以去查询当前实例是否开启缓存机制
show global variables like "%query_cache_type%";
5、也可以监控查询缓存的命中率
show status like'%Qcache%'; //查看运行的缓存信息
Qcache_free_blocks:表示查询缓存中目前还有多少剩余的blocks,如果该值显示较大,则说明查询缓存中的内存碎片 过多了,可能在一定的时间进行整理。
Qcache_free_memory:查询缓存的内存大小,通过这个参数可以很清晰的知道当前系统的查询内存是否够用,是多 了,还是不够用,DBA可以根据实际情况做出调整。
Qcache_hits:表示有多少次命中缓存。我们主要可以通过该值来验证我们的查询缓存的效果。数字越大,缓存效果越理想。
Qcache_inserts:表示多少次未命中然后插入,意思是新来的SQL请求在缓存中未找到,不得不执行查询处理,执行 查询处理后把结果insert到查询缓存中。这样的情况的次数,次数越多,表示查询缓存应用到的比较少,效果也就不理 想。当然系统刚启动后,查询缓存是空的,这很正常。
Qcache_lowmem_prunes:该参数记录有多少条查询因为内存不足而被移除出查询缓存。通过这个值,用户可以适当的 调整缓存大小。
Qcache_not_cached:表示因为query_cache_type的设置而没有被缓存的查询数量。
Qcache_queries_in_cache:当前缓存中缓存的查询数量。
Qcache_total_blocks:当前缓存的block数量。
1、如果对当前表进行更新,那么内存中的数据就要全部清空。
2、对于更新压力大的数据库来说,查询缓存效率低
3、所以8.0之后把查询缓存删除了
MYSQL中的SQL解析其实本质上跟我们平常写的Java的.java结尾的文件或者C/C++中的.cpp文件本质上是一样的,他们都是编译器去解析的,主要分为:
百度百科介绍:
词法解析状态机是在词法解析的扫描阶段执行的过程,下图2-1-1是状态解析token的执行过程:
对应状态机 | 备注 |
---|---|
MY_LEX_START | 开始解析token |
MY_LEX_CHAR | 解析单个字符例如*、:、; |
MY_LEX_IDENT | 解析字符串,匹配关键词,例如“table”、“select” 等 |
MY_LEX_IDENT_SEP | 找到字符’.’ |
MY_LEX_IDENT_START | 从’.'开始解析token |
MY_LEX_REAL | 不完全实数 |
MY_LEX_HEX_NUMBER | hex字符串 |
MY_LEX_BIN_NUMBER | bin字符串 |
MY_LEX_CMP_OP | 不完全比较运算符 |
MY_LEX_LONG_CMP_OP | 不完全比较运算符 |
MY_LEX_STRING | 字符串 |
MY_LEX_COMMENT | Comment |
MY_LEX_END | 结束 |
MY_LEX_NUMBER_IDENT | 数字 |
MY_LEX_INT_OR_REAL | 完全整数或不完全实数 |
MY_LEX_REAL_OR_POINT | 解析.返回不完全实数,或者字符’.’ |
MY_LEX_BOOL | 布尔 |
MY_LEX_EOL | 如果是eof,则设置状态end结束, |
MY_LEX_LONG_COMMENT | 长注释 |
MY_LEX_END_LONG_COMMENT | 备注结束 |
MY_LEX_SEMICOLON | 分隔符; |
MY_LEX_SET_VAR | 检查:= |
MY_LEX_USER_END | 结束’@’ |
MY_LEX_HOSTNAME | 解析hostname |
MY_LEX_SKIP | 空格 |
MY_LEX_USER_VARIABLE_DELIMITER | 引号字符 |
MY_LEX_SYSTEM_VAR | 例如解析user@hostname,解析到@ |
MY_LEX_IDENT_OR_KEYWORD | 判断返回字符串状态或者键盘键值 |
MY_LEX_IDENT_OR_HEX | hex-数字 |
MY_LEX_IDENT_OR_BIN | bin-数字 |
MY_LEX_IDENT_OR_NCHAR | 判断返回字符状态,或字符串状态 |
MY_LEX_STRING_OR_DELIMITER | 判断返回字符串状态或者空格字符状态 |
我们开始调试,首先要先启动下mysql8.0.30。然后准备两个终端:一个终端用于操作mysql语句、另外一个终端用于调试使用,如下图。
可以用lldb进行调试:
lldb -p 进程ID
词法解析调用过程:
https://www.processon.com/view/link/62fb03805653bb3108f40e4f
根据上图我们可以得知,Mysql8.0.30会调用MYSQLlex方法进行词法解析、MYSQLlex中会调用lex_one_token进行单个token解析。如果我们要调试可以对lex_one_token去下一个断点。
(lldb)b lex_one_token
下断点后在mysql操作终端操作做一条语句例如“select * from t1;”,此时调试终端会捕获到断点,调试到下图。
根据上图我们得知第一个状态机为MY_LEX_START,执行状态机进入switch后,会通过yyPeek方法获取一个字符(下图)。来判断这个字符是否为空格,不是空格后,通过“state = state_map[c];” 返回一个状态机。判断时通过state_map解析。
由于获取单个字符是s,s对应state_map中的状态机是MY_LEX_IDENT,MY_LEX_IDENT状态机会去匹配对应的关键词返回token。第一个匹配的关键词就是select。
根据图我们得知通过find_keyword方法可以匹配对应的token。第一次匹配“select”后我们得到一个token(748),748这个token对应SELECT_SYM,可以在/mysql-8.0.30/sql/sql_yacc.h文件中查找到。此时m_ptr参数值为“ * from t1”,该参数由返回前调用lip->yyUnget()进行左移。lip->next_state的状态再次设置为MY_LEX_START。
当我们再次调用lex_one_token时,处理MY_LEX_START状态机时,会过滤调一个空格字符。继续执行获取,获取到“”字符,又会将状态机设置为MY_LEX_END_LONG_COMMENT,然后执行状态机会设置为MY_LEX_CHAR,返回时下一次的状态设置成了MY_LEX_START。最后返回一个token(42),其实这个42是ASCII到“”。此时m_ptr参数值为“ from t1”。再次执行MY_LEX_START过程,会设置状态机为MY_LEX_IDENT,执行MY_LEX_IDENT状态机后会返回token(452),可以在/mysql-8.0.30/sql/sql_yacc.h文件中查找到。对应FROM。再次执行后会返回状态机IDENT_QUOTED,最终返回状态机MY_LEX_EOL,最终返回MY_LEX_END结束。
state_map是验证状态机的关键步骤,初始化该过程主要在/mysql-8.0.20/mysys/sql_chars.cc文件的init_state_maps方法中,方法实现如下:
bool init_state_maps(CHARSET_INFO *cs) {
uint i;
uchar *ident_map;
enum my_lex_states *state_map = nullptr;
lex_state_maps_st *lex_state_maps = (lex_state_maps_st *)my_once_alloc(
sizeof(lex_state_maps_st), MYF(MY_WME));
if (lex_state_maps == nullptr) return true; // 空指针OOM
cs->state_maps = lex_state_maps;
state_map = lex_state_maps->main_map;
if (!(cs->ident_map = ident_map = (uchar *)my_once_alloc(256, MYF(MY_WME))))
return true; // OOM
hint_lex_init_maps(cs, lex_state_maps->hint_map);
/* 填充状态以获得更快的解析器 */
for (i = 0; i < 256; i++) {
if (my_isalpha(cs, i))
state_map[i] = MY_LEX_IDENT; //字符串状态机
else if (my_isdigit(cs, i))
state_map[i] = MY_LEX_NUMBER_IDENT;
else if (my_ismb1st(cs, i))
/* To get whether it's a possible leading byte for a charset. */
state_map[i] = MY_LEX_IDENT;
else if (my_isspace(cs, i))
state_map[i] = MY_LEX_SKIP; //空格状态机
else
state_map[i] = MY_LEX_CHAR; //字符状态机
}
state_map[(uchar)'_'] = state_map[(uchar)'$'] = MY_LEX_IDENT;
state_map[(uchar)'\''] = MY_LEX_STRING;
state_map[(uchar)'.'] = MY_LEX_REAL_OR_POINT;
state_map[(uchar)'>'] = state_map[(uchar)'='] = state_map[(uchar)'!'] =
MY_LEX_CMP_OP; //操作符合匹配状态机
state_map[(uchar)'<'] = MY_LEX_LONG_CMP_OP;
state_map[(uchar)'&'] = state_map[(uchar)'|'] = MY_LEX_BOOL;
state_map[(uchar)'#'] = MY_LEX_COMMENT;
state_map[(uchar)';'] = MY_LEX_SEMICOLON;
state_map[(uchar)':'] = MY_LEX_SET_VAR;
state_map[0] = MY_LEX_EOL; //结束标志状态机
state_map[(uchar)'/'] = MY_LEX_LONG_COMMENT;
state_map[(uchar)'*'] = MY_LEX_END_LONG_COMMENT; //*字符匹配状态机
state_map[(uchar)'@'] = MY_LEX_USER_END; //@字符匹配状态机
state_map[(uchar)'`'] = MY_LEX_USER_VARIABLE_DELIMITER;
state_map[(uchar)'"'] = MY_LEX_STRING_OR_DELIMITER;
/*
创建第二个映射以加快查找标识符的速度
*/
for (i = 0; i < 256; i++) {
ident_map[i] = (uchar)(state_map[i] == MY_LEX_IDENT ||
state_map[i] == MY_LEX_NUMBER_IDENT);
}
/* Special handling of hex and binary strings */
state_map[(uchar)'x'] = state_map[(uchar)'X'] = MY_LEX_IDENT_OR_HEX;
state_map[(uchar)'b'] = state_map[(uchar)'B'] = MY_LEX_IDENT_OR_BIN;
state_map[(uchar)'n'] = state_map[(uchar)'N'] = MY_LEX_IDENT_OR_NCHAR;
return false;
}
代码中能快速匹配状态机,就是因为初始化好了一堆的状态机map,根据字符可以匹配不同的状态机。状态机的宏在mysql-8.0.30/include/sql_chars.h文件中。
关键代码lex_one_token解析:
static int lex_one_token(Lexer_yystype *yylval, THD *thd) {
uchar c = 0;
bool comment_closed;
int tokval, result_state;
uint length;
enum my_lex_states state;
Lex_input_stream *lip = &thd->m_parser_state->m_lip; //获得输入信息
const CHARSET_INFO *cs = thd->charset(); //获得字符集
const my_lex_states *state_map = cs->state_maps->main_map; //获得状态
const uchar *ident_map = cs->ident_map; //字符串分割符合
lip->yylval = yylval; // 全局状态
lip->start_token(); //初始化token字符串
state = lip->next_state; //获得下一个状态
lip->next_state = MY_LEX_START; //设置下一个状态
for (;;) { //循环解析状态机
switch (state) {
case MY_LEX_START: // 开始解析token
while (state_map[c = lip->yyPeek()] == MY_LEX_SKIP) { //解析token,判断是否为空格
if (c == '\n') lip->yylineno++;
lip->yySkip(); //处理空格
}
/* Start of real token */
lip->restart_token(); //设置m_tok_start和m_cpp_tok_start
c = lip->yyGet(); //获得单个字符,并设置m_cpp_ptr,并且m_ptr移位
state = state_map[c]; //如果是字符串返回MY_LEX_IDENT 状态
break;
//...
case MY_LEX_IDENT: //解析字符串关键词,比如select、tables等
const char *start;
if (use_mb(cs)) {
result_state = IDENT_QUOTED;
switch (my_mbcharlen(cs, lip->yyGetLast())) {
case 1:
break;
case 0:
if (my_mbmaxlenlen(cs) < 2) break;
/* else fall through */
default:
int l =
my_ismbchar(cs, lip->get_ptr() - 1, lip->get_end_of_query());
if (l == 0) {
state = MY_LEX_CHAR;
continue;
}
lip->skip_binary(l - 1);
}
while (ident_map[c = lip->yyGet()]) { //循环获取字符串
switch (my_mbcharlen(cs, c)) {
case 1:
break;
case 0:
if (my_mbmaxlenlen(cs) < 2) break;
/* else fall through */
default:
int l;
if ((l = my_ismbchar(cs, lip->get_ptr() - 1,
lip->get_end_of_query())) == 0)
break;
lip->skip_binary(l - 1);
}
}
} else {
for (result_state = c; ident_map[c = lip->yyGet()]; result_state |= c)
;
/* If there were non-ASCII characters, mark that we must convert */
result_state = result_state & 0x80 ? IDENT_QUOTED : IDENT;
}
length = lip->yyLength();
start = lip->get_ptr();
if (lip->ignore_space) {
/*
If we find a space then this can't be an identifier. We notice this
below by checking start != lex->ptr.
*/
for (; state_map[c] == MY_LEX_SKIP; c = lip->yyGet()) {
if (c == '\n') lip->yylineno++;
}
}
if (start == lip->get_ptr() && c == '.' && ident_map[lip->yyPeek()]) //判断字符是否为'.' '
lip->next_state = MY_LEX_IDENT_SEP;
else { // '(' must follow directly if function
lip->yyUnget();
if ((tokval = find_keyword(lip, length, c == '('))) { //查找token
lip->next_state = MY_LEX_START; // Allow signed numbers
return (tokval); // 返回token
}
lip->yySkip(); // next state does a unget
}
yylval->lex_str = get_token(lip, 0, length);
//...
return (result_state); // IDENT or IDENT_QUOTED
//...
case MY_LEX_EOL: //'\0'结束符
if (lip->eof()) {
lip->yyUnget(); // Reject the last '\0'
lip->set_echo(false);
lip->yySkip();
lip->set_echo(true);
/* Unbalanced comments with a missing '*' '/' are a syntax error */
if (lip->in_comment != NO_COMMENT) return (ABORT_SYM);
lip->next_state = MY_LEX_END; // 设置下一个状态机为MY_LEX_END,继续循环
return (END_OF_INPUT); // 返回token
}
}
}
}
在解析select * from t1; 语句过程中,mysql5会多一步MY_LEX_OPERATOR_OR_IDENT的过程。在mysql 8.0.30中优化了该过程,如下图。
语法分析具体实现算法细节:https://blog.csdn.net/weixin_43894553/article/details/110347056
1、算术表达式
比如1+2*(3-1)+3,他会被解析成抽象语法树,如下图
https://www.processon.com/view/link/62fb3b110791293111b2e1d3
2、xml表达式
<order>
<id>1id>
<sku>手机sku>
<address>
<city>广州city>
address>
<user>
<id>1001id>
<name>zhzname>
user>
order>
他的抽象语法树(AST)是
https://www.processon.com/view/link/62fb3d5b1e085305de1c09d4
3、程序表达式
while(b != 0){
if (a > b)
a = a-b;
else{
b = b-a;
}
}
return a;
抽象语法树:
https://www.processon.com/view/link/62fb4159f346fb3fe99d97c8
表达式:
sum=0
for i in Math.random(0,100)
sum=sum+i
end
https://www.processon.com/view/link/62fb40560791293111b30df8
例如,四则运算表达式的文法为:
文法****1.1
E->T|EAT
T->F|TMF
F->(E)|i
A->+|-
M->*|/
改为LL(1)后为:
文法****1.2
E->TE’
E’->ATE’|e_symbol
T->FT’
T’->MFT’|e_symbol
F->(E)|i
A->+|-
M->*|/
例如,当在开发语言时,可能在开始的时候,选择LL(1)文法来描述语言的语法规则,编译器前端生成LL(1)语法树,编译器后端对LL(1)语法树进行处理,生成字节码或者是汇编代码。但是随着工程的开发,在语言中加入了更多的特性,用LL(1)文法描述时,感觉限制很大,并且编写文法时很吃力,所以这个时候决定采用LR(1)文法来描述语言的语法规则,把编译器前端改生成LR(1)语法树,但在这个时候,你会发现很糟糕,因为以前编译器后端是对LL(1)语树进行处理,不得不同时也修改后端的代码。
抽象语法树的第一个特点为:不依赖于具体的文法。无论是LL(1)文法,还是LR(1),或者还是其它的方法,都要求在语法分析时候,构造出相同的语法树,这样可以给编译器后端提供了清晰,统一的接口。即使是前端采用了不同的文法,都只需要改变前端代码,而不用连累到后端。即减少了工作量,也提高的编译器的可维护性。
抽象语法树的第二个特点为:不依赖于语言的细节。在编译器家族中,大名鼎鼎的gcc算得上是一个老大哥了,它可以编译多种语言,例如c,c++,java,ADA,Object C, FORTRAN, PASCAL,COBOL等等。在前端gcc对不同的语言进行词法,语法分析和语义分析后,产生抽象语法树形成中间代码作为输出,供后端处理。要做到这一点,就必须在构造语法树时,不依赖于语言的细节,例如在不同的语言中,类似于if-condition-then这样的语句有不同的表示方法
在c中为:
if(condition)
{
do_something();
}
在fortran中为:
If condition then
do_somthing()
end if
在构造if-condition-then语句的抽象语法树时,只需要用两个分支节点来表于,一个为condition,一个为if_body。如下图:
在源程序中出现的括号,或者是关键字,都会被丢掉。
...定义段...
%%
...规则段...
%%
...用户子例程段...
mysql的yacc描述⽂件路径:sql/sql_yacc.yy
关键字 | 描述 |
---|---|
token | 标志0-255被保留作为字符值,⾃定义产⽣的token标志从258开始 |
type | 为⾮终结符指明类型 |
left | 表示左结合(left-associative) |
right | 表示右结合(right-associative) |
nonassoc | 表示⽆结合 |
parse-param | 通过%parse-param {param} 来给 yyparse(param)传参数 |
lex-param | 通过%lex-param {param} 来给 yylex(param)传参数 |
start | 指定起始符号 |
pure-parser | 指定希望解析器是可重⼊的 |
expect | 告诉编译器预⻅多少次的移动归约冲突 |
可以看share/messages_to_clients.txt
#1 ./sql/conn_handler/connection_handler_per_thread.cc::handle_connection
#2.1 ./sql/sql_parse.cc::bool do_command(THD *thd)
#2.2 ./sql/sql_parse.cc::dispatch_command
#3 ./sql/sql_parse.cc::mysql_parse
#4.1 ./sql/sql_lex.cc:bool lex_start(THD *thd)
#4.2 ./sql/sql_parse.cc::parse_sql
#5 ./sql_class.cc: bool THD::sql_parser()
#6.1 ./sql_yacc.cc:int MYSQLparse
#6.2 ./sql_lex.cc:bool LEX::make_sql_cmd(Parse_tree_root *parse_tree) {
#1 sql/sql_parse.cc:int mysql_execute_command(THD *thd, bool first_level) {
#2 sql/sql_select.cc:bool Sql_cmd_dml::execute(THD *thd) {
#3 sql/sql_select.cc:bool Sql_cmd_dml::prepare(THD *thd) {
#4 sql/sql_select.cc:bool Sql_cmd_select::precheck(THD *thd) {
#5 auth/sql_authorization.cc::bool check_table_access
#6 auth/sql_authorization.cc::bool check_grant
可以看一下这篇文章:https://blog.csdn.net/taoerchun/article/details/121376459,里面主要讲optimizer_trace的成本计算。或者看后面的SQL优化章节会有optimizer_trace实战
定义:
扩展:
比如我们平常工作中有时候是不是范懵逼,写成(a=1 and b=c) or ((a>c) and (c<1)),其实这个时候优化器会帮我们优化成(a=1 and b=c) or (a>c and c<1)
比如select * from A where ((a<1 and b=b ) and 1=1 )or (a=2 or 1!=1)
此时优化器会把他优化成select * from A where a<1 or a=2
当表达式中只包含常量时,表达式就会优化,比如
a=1+2会被优化成a=3
而abs(a)>2或者**-a>-1**这种就不会被优化器优化
常量表定义:指的是主键索引或者唯一二级索引等值匹配所查询的表称之为常量表。优化器一般在分析一个sql时,会先执行常量表查询,然后再把查询语句中所有涉及到该表的条件全部换成常量,然后再分析其整体sql的成本计算(optimizer_trace)。
比如下面这条sql
SELECT * FROM A INNER JOIN B ON A.uid = B.uid WHERE A.id = 1001;
这个时候优化器会先根据A表的主键索引去查询A表的各个字段常量值,然后换成
SELECT A表的各大字段常量值,B.* FROM A INNER JOIN B ON A表的uid列的常量值 = B.uid
啥意思呢,你可以用下面这个例子来理解,比如
A表中有
id | name | uid |
---|---|---|
1001 | zhz | 1 |
1002 | zhz1 | 2 |
B表中有
uid | name | age |
---|---|---|
1 | zhangsan | 18 |
2 | lisi | 21 |
这个时候我们再去看整条sql:SELECT * FROM A INNER JOIN B ON A.uid = B.uid WHERE A.id = 1001;他是不是会先把A表中1001的数据拿出来,然后最终结果就会变成**select “1001”,“zhz”,1,B.uid,B.name,B.age FROM A INNER JOIN B ON 1 = B.uid **
什么叫做外连接消除呢?
为什么可以转呢?
为什么要有这种转换呢?
SQL语句:SELECT m, n FROM (SELECT m2 + 1 AS m, n2 AS n FROM e2 WHERE m2 > 2) AS t
子查询:SELECT m2 + 1 AS m, n2 AS n FROM e2 WHERE m2 > 2
派生表:t,也就是说子查询本质就是一个生成一个派生表,比如上面哪个子查询,他会生成一个m,n列的符合条件的结果集表,就是派生表。
含义:
比如
含义:
比如
含义:
比如:
含义:
比如
IN/NOT IN
SELECT * FROM a WHERE (a1, a2) IN (SELECT b1, b2 FROM b);
ANY/SOME(同义词)
SELECT * FROM a WHERE a1 > ANY(SELECT b1 FROM b);
等价于
SELECT * FROM a WHERE a1 > (SELECT MIN(b1) FROM b);
ALL
SELECT * FROM a WHERE a1 > ALL(SELECT b1 FROM b);
等价于
SELECT * FROM a WHERE a1 > (SELECT Max(b1) FROM b);
SELECT * FROM e1 WHERE EXISTS (SELECT 1 FROM e2);
不相关标量子查询或者行子查询
SELECT * FROM s1 WHERE order_note = (SELECT order_note FROM s2 WHERE key3 = 'a' LIMIT 1);
执行思路
1、先执行 SELECT order_note FROM s2 WHERE key3 = 'a' LIMIT 1 ,假设结果为A
2、再把上一次的结果拿出来,替换成
SELECT * FROM s1 WHERE order_note = A
相关的标量子查询或者行子查询
SELECT * FROM s1 WHERE order_note = (SELECT order_note FROM s2 WHERE s1.order_no= s2.order_no LIMIT 1);
执行思路:
1、先从外层查询中获取一条记录,本例中也就是先从s1表中获取一条记录。
2、后从上一步骤中获取的那条记录中找出子查询中涉及到的值,本例中就是从s1表中获 取的那条记录中找出s1.order_no列的值,然后执行子查询。
3、最后根据子查询的查询结果来检测外层查询WHERE子句的条件是否成立,如果成立,就把 外层查询的那条记录加入到结果集,否则就丢弃
比如下面这个不相关的in查询
SELECT * FROM tbl_name WHERE column IN (a, b, c ..., ...);
这上面这条sql如果是基于mysql本身不做优化来说,会每一次查询一条记录,对比n次,性能极低,因此mysql为了优化这种sql的改进措施是:不直接将不相关子查询的结果集
临时表写入过程:
最后:
我们以下面这条sql为例:
SELECT * FROM s1 WHERE order_note IN (SELECT order_note FROM s2 WHERE order_no = 'a');
假设子查询的物化表叫做temp_table,对应的列叫做temp_val。那么我们此时是不是相当于可以理解为是上面的sql,等价于下面这条
SELECT s1.* FROM s1 INNER JOIN temp_table ON order_note = temp_val;
也就是说他其实相当于表s1和子查询物化表temp_table进行内连接。那么mysql就会进入下面这一步,去评估内连接不同的连接顺序的成本是多少了,如:
半连接 (semi-join):对于s1表的某条记录来说,我们只关心在s2表中 是否存在与之匹配的记录,而不关心具体有多少条记录与之匹配,最终的结果集中只保留s1表的记录。
举个例子:下面这条sql
SELECT * FROM s1 WHERE order_note IN (SELECT order_note FROM s2 WHERE order_no = 'a');
我们可以把这个查询理解成:对于s1表中的某条记录,如果我们能在s2表(准确的说是 执行完WHERE s2.order_no= 'a’之后的结果集)中找到一条或多条记录,这些记录的 order_note的值等于s1表记录的order_note列的值,那么该条s1表的记录就会被加入到最终的结果集。我们可以发现s1,s2两个表的连接很像下面这条sql
SELECT s1.* FROM s1 INNER JOIN s2 ON s1.order_note = s2.order_note WHERE s2.order_no= 'a';
只不过我们不能保证对于s1表的某条记录来说,在s2表(准确的说是执行完WHERE s2.order_no= 'a’之后的结果集)中有多少条记录满足s1.order_no = s2.order_no这个
条件,他会分成三种情况:
因此我们可以发现in子查询和表內连接不相等,那么mysql为了解决这样一个问题,他出了一种新的东西,也就是半连接。他会优化成
SELECT s1.* FROM s1 SEMI JOIN s2 ON s1.order_note = s2.order_note WHERE order_no= 'a';
半连接的实现方法概述 (自行研究)
SELECT * FROM s1 WHERE key2 IN (SELECT key2 FROM s2 WHERE key3 = 'a');
SELECT s1.* FROM s1 INNER JOIN s2 ON s1.key2 = s2.key2 WHERE s2.key3 = 'a';
mysql内部会转成exist
比如下面这一个sql
SELECT * FROM s1 WHERE order_no IN (SELECT order_no FROM s2 where s1.order_note = s2.order_note) OR insert_time > '2021-03-22 18:28:28'; show WARNINGS;
具体的可以用explain分析。
原SQL | 优化后的SQL |
---|---|
< ANY (SELECT inner_expr …) | < (SELECT MAX(inner_expr) …) |
> ANY (SELECT inner_expr …) | > (SELECT MIN(inner_expr) …) |
< ALL (SELECT inner_expr …) | < (SELECT MIN(inner_expr) …) |
> ALL (SELECT inner_expr …) | > (SELECT MAX(inner_expr) …) |
不相关子查询
SELECT * FROM s1 WHERE EXISTS (SELECT 1 FROM s2 WHERE expire_time= 'a') OR order_no> ‘2022-08-18 01:18:28’0;
优化思路:
1、因为是不相关子查询,所以我们可以知道,他首先执行的是SELECT 1 FROM s2 WHERE expire_time= 'a',假设他有记录,那么编译器会把这条sql优化(重写)成为
SELECT * FROM s1 WHERE true OR order_no> ‘2022-08-18 01:18:28’0;
2、接着我们可以发现还可以优化成
SELECT * FROM s1 WHERE true,
3、至此一条优化完的sql如上!
相关子查询
SELECT * FROM s1 WHERE EXISTS (SELECT 1 FROM s2 WHERE s1.order_note = s2.order_note);
优化思路,由上面哪个sql得知,我们是不能优化的,必须是先把子查询执行,才能执行外层,当然我们可以加个普通索引(idx_order_note)
语句的执行计划
“id”字段的值是否等于我们期望的一个值,如果不是的话,那就继续调用存储引擎的接口,去获取“users”表的下一行数 据。
我是小白弟弟,一个在互联网行业的小白,立志成为一名架构师
https://blog.csdn.net/zhouhengzhe?t=1