普通视图
基于Dify搭建AI智能体应用
1.Dify概述
Dify是一个低代码/无代码的AI应用开发平台,它通过可视化的智能体工作流将大型语言模型与你已有的工具和数据连接起来,你可以构建一个流程,让AI智能体自动完成一连串操作。
2.本地部署Dify
本文采用docker的方式本地部署dify 1.0.1,整个部署操作,都需要在全程国际联网的环境下进行,且要尽量保证docker中不运行其他的容器
2.1 安装docker-compose 2.x
dify的编排文件采用的是docker-compose 2.x版本规范,因此如果没有安装或者使用的是3.x版本,需要下载一个docker-compose 2.x
wget https://github.com/docker/compose/releases/download/v2.39.2/docker-compose-linux-x86_64 下载完成后,放入/opt下
2.2 部署dify
先从github拉取dify源码到/opt/dify目录下
git clone https://github.com/langgenius/dify.git切换到dify/docker目录下,将默认文件.env.example重命名复制一份
cd difycd dockercp .env.example .env从dify/docker目录下,使用刚刚下载的docker-compose-linux-x86_64启动
/opt/docker-compose-linux-x86_64 up -d第一次启动,需要下载许多镜像
![]()
当全部镜像下载完成后,会启动,直到全部启动成功
![]()
浏览器访问虚拟机地址的80,即可进入,第一次进入需要设置管理员用户名和密码
![]()
如果设置管理员时,弹窗提示无权限:
Setup failed: PermissionDenied (persistent) at write => permission denied Context: service: fs path: privkeys/5a438d1c-8c8b-43c2-a83e-1478fd3df017/private.pem Source: Permission denied (os error 13)
则需要返回到dify/docker目录内执行chmod -R 777 volumes/放开权限
成功注册管理员后,会进入主页面
![]()
2.3 配置大模型
先配置大模型,从主界面设置进入
![]()
需要安装OpenAI,DeepSeek等大模型应用,如果想要的大模型应用没有,可以使用OpenAI-API-compatible,前提是其适配了OpenAI的协议
![]()
安装完成后,将自己的API KEY填入对应的大模型应用中
3.智能体案例
待续
免费领取2个月天翼云电脑8核16G
昨天收到了个短信,可以免费领取2个月云电脑,奈何以前贼想拥有的时候咋不来个活动让我先体验一把,前几个月已经整了个长期的。。。
想玩玩的可以领取一下,微信扫码跳转到小程序可以领取2个月,8核16G内存120G硬盘,有需要的朋友可以体验一把。
我之前自己的是每1个小时不连接的话就会自动休眠,搞了个不关机插件可以达到长期运行,这个免费试用的好像是不自动休眠,我昨天领取到现在也没显示休眠。
![]()
蜀道难 — 有偿招募 Google Play 测试用户
懒惰,有时候带来的负面效果,后期想要修复的时候,要付出的代驾比当时处理要复杂的多。更恐怖的是,哪怕付出了这么多的代码,依然无法达到最开始的效果。
当 google playstore 开发者给我发邮件提示账号快过期的时候,并没有太留意,后来就给忘了。当然,gmail 的邮箱已经收到了数次提醒,但是由于那段时间不怎么翻墙,导致并没有看到这些邮件,等看到邮件的时候账号已经被用了。
看到这个停用的原因,真的是让人崩溃,这 tmd,当时但凡翻墙了,也就是点几下鼠标的事情。现在好了,重新注册账号依然面临一系列的问题。appid 被占用,旧应用无法下架,无法转让。现在连上线发布都需要面临另外一个问题,那就是需要开启封闭测试,只有封闭测试通过之后才能有发布正式版的权限。
google play开发者给出的答案是:
针对新创建的个人账号的测试要求简介 测试是应用开发流程中不可或缺的一环。通过持续对应用运行测试,您可以在公开发布应用之前验证其正确性、功能行为和易用性。这能让您及时解决发现的技术问题或用户体验问题,最大限度地降低这些问题对用户的影响,从而确保您在 Google Play 中发布的是应用的最佳版本。如果开发者在发布应用之前经常使用 Play 管理中心的测试工具进行测试,他们的应用将能够带给用户更优质的使用体验,从而在 Google Play 上赢得更高的评分,取得更大的成就。 为了帮助所有开发者确保提供高品质的应用,我们提出了新的测试要求。如果开发者使用的是 2023 年 11 月 13 日之后创建的个人账号,则其应用需要先经过测试,然后才能在 Google Play 上发布和分发。若应用未经过测试,系统会停用 Play 管理中心内的部分功能,例如正式版(测试和发布 > 正式版)和预注册(测试和发布 > 测试 > 预注册),直到开发者满足相关要求为止。 测试要求概览 如果您使用的是新创建的个人开发者账号,则必须对您的应用运行封闭式测试,且至少有 12 名测试人员在过去至少 14 天内选择持续参与测试。满足上述条件后,您便可以在 Play 管理中心的信息中心申请正式版发布权限,以便最终在 Google Play 上分发您的应用。申请时,您需要回答一些问题,帮助我们了解您的应用、测试流程及正式版发布准备情况。 下文详细介绍了不同类型的测试轨道和相关要求,以及关于申请正式版发布权限的更多详情。 了解不同的测试轨道和相关要求 Play 管理中心提供不同类型的测试轨道,以便您逐步扩大测试范围并改进应用,力求达到适合面向数十亿 Google Play 用户发布的水平。 内部测试:在完成应用设置之前,您可以自行将 build 快速分发给少量可信测试员。这有助于您排查问题并收集早期反馈。通常,build 被添加到 Play 管理中心几秒钟后,就会向测试人员开放。虽然内部测试并非强制性要求,但我们建议您从这里开始。 封闭式测试:利用封闭式测试,您可以与由您控管的众多用户分享应用。这样一来,您可以在发布前修复问题,并确保应用符合 Google Play 政策的要求。您必须先运行封闭式测试,然后才能申请发布正式版应用。当您申请正式版发布权限时,必须至少有 12 名测试人员选择参与您的封闭式测试。他们必须在过去 14 天内选择持续参与。完成应用设置后,您即可启动封闭式测试。 开放式测试:让您可在 Google Play 中发布测试版应用。进行开放式测试时,任何人都可以加入您的测试计划并向您提交非公开反馈。请先确保您的应用和商品详情已经准备好在 Google Play 上架,然后再选择该选项。如果您拥有正式版发布权限,则可以使用开放式测试。 正式版:让您可通过 Google Play 面向数十亿用户发布应用。您需要先运行符合我们条件的封闭式测试,然后才能申请将应用发布为正式版。在申请时,您还需要回答一些与封闭式测试相关的问题。当您申请正式版发布权限时,必须至少有 12 名测试人员选择参与您的封闭式测试。他们必须在过去 14 天内选择持续参与。
如果开发者使用的是 2023 年 11 月 13 日之后创建的个人账号,则其应用需要先经过测试,然后才能在 Google Play 上发布和分发。若应用未经过测试,系统会停用 Play 管理中心内的部分功能
测试要求概览
如果您使用的是新创建的个人开发者账号,则必须对您的应用运行封闭式测试,且至少有 12 名测试人员在过去至少 14 天内选择持续参与测试。
重要提示:向测试人员强调,他们需要选择持续参与至少为期 14 天的封闭式测试。
说实话就是现在google 的做法是越来越看不懂了,关键是很多操作也不知道后续该如何操作。这就很蛋疼了,这种事情当然更好的办法是去咸鱼之类的找个专门做测试的。
测试资格需要加入这个 google 群组(需要加入群组之后才能参与测试):guimiquan@googlegroups.com
https://groups.google.com/g/guimiquan
我也没咋用过这个东西,不清楚怎么去处理这个破玩意儿。有时候感觉老外设计的东西,的确是不怎么符合自己的使用习惯。
ap测试地址:
https://play.google.com/apps/testing/gma.dayi.app
app商店地址:
https://play.google.com/store/apps/details?id=gma.dayi.app
网上依然能找到这种测评服务了,价格大约 200+。
现在的问题在于重新注册开发者账号话费了 30 美元,现在不上架恶心,上架也恶心。既然如此那还是折腾一下吧,不过鉴于自己手上没那么多的设备,想请各位宝子帮帮忙测试一下,想参与的加一下 google 的群组。需要 12 名测试人员,根据提供的测试账号数量,在测试完成后给于相应的报酬,如果有时间,或者闲着没事的希望可以板帮我哦。
目前我自己还在修改一些问题,希望大家除了加下 google group,顺便加下群哦,QQ群:777692924![]()
需要大家开始帮忙测试了会统一通知一下的。
都说蜀道难,现在发版比蜀道还难,蜀道难都是看得到的难,而这发版,完全是不知道的陷阱。
最后说下测试报酬哈,鉴于价格大约都是 200+,12 个账号,按照每个账号 20 元红包来计算(提供几个测试账号,会获得相应账号数量的红包)。如果不想要红包的,可以寄写真照片(3 张),如果喜欢的话。
如果种种原因,到时最后测试没通过,红包依然会发放,不过金额就变成 10 元了哈。毕竟不想让大家白费精力,这个钱属实不多。聊表心意,主要是这个应用也没啥盈利能力,全靠用爱发电,小女子先行写过啦。
果然一体检全身毛病
年底了,公司安排年度体检,之前一直没通知,上周才知道,就约了个周六去了,公司在美年大健康订购的职工体检套餐,只需要每个人在线预约,然后到时间去就行了,约的周六早上八点,我这附近还没有这个体检机构,只能坐地铁去最近的庐阳区的一家,周六还起了个大早,六点半就爬起来了,收拾好坐一个小时地铁,刚好七点五十到那,好巧,还碰到了另外两个同事。
到那也是先抽血,然后就是一些常规检查,还有就是彩超和DR,这几个还算比较有用的吧,前前后后也搞了一上午的时间,正好套餐里带了个早餐,体检完吃个饭,算是午饭了吧,哈。
今天上午发来短信说是体检报告出来了,就下载来看了下,好家伙,去年都还没啥呢,今年这一年造的,出现了各种小毛病,特别是脂肪肝和肾结石前兆有点出乎意料了,归根结底还是吃出来的病,人只要一胖就各种毛病,特别是我们这工作,还经常久坐,都想找坐办公室的工作,坐办公室的工作也不是那么好干的啊。
看了下报告,都指向一个原因,胖,需要多运动,后面晚上吃饭一定得注意点了,尽量少吃,然后出去走走活动一下。
![]()
第一次漂泊的一点点勇气
完美主义
小时候跟姐姐学的用扑克牌算命,洗牌就那么一张一张的抽出来排上去,从 1 开始一直排到 13,不同的花色对应着好或者不好,每个数字代表不用的意义,事业啊、婚姻啊、学历啊等等。有时候,为了抽到一排红心,自己会将扑克牌在洗牌之后重新排号,按照一个特定的顺序,抽几张扔几张。最后总是能拿到一排红心。
慢慢长大之后发现,不要说一排红心,哪怕能拿到一颗红心已经实属不易。为了这颗所谓的红心,要付出的实在是太多太多。
反常的气温,忽然又升高到了十七度。给人一种暖春的感觉,周五的时候,对象说给宝子约了周六的牙医。宝子的这个牙,已经成了一种非常严重的问题。由于之前一直吃手,后来用这种手段干预,戴手套,贴嘴等等,虽然不吃了,但是舌头还是不自觉的就往前顶,现在牙齿已经有些变形了,甚至连骨头都开始过度生长。
去妇幼、齐鲁去问诊,给的建议都是等牙齿换完再处理,但是现在似乎依然等不到那个时候了,再不干预,以后要该起来就更难了。原本想着,简单的咨询一下,但是在经过一系列的检查之后,觉得还算靠谱,给的医疗建议也在接受范围之内。决定不再折腾了直接在这里处理,费用六千五。为这些所谓的坏习惯的付出依然不止这些。
有时候懒惰真的会付出代驾,小的时候想下狠心纠正这种坏习惯,但是宝子的姥姥各种觉得残忍,阻挠。最后的结果就是,虽然当时是痛快了。后患却没那么容易消除。虽然现在自己依然不是一个完美主义者,但是,这种过失,现在想起来却也时常后悔。
周末,有时候感觉时间是真的少,各种乱七八糟的事情就占据掉了大半。剩下一点点的时间,来处理下那些乱七八糟微不足道的事情。之前 google play store 的账号,因为长时间没登录被停用了,导致原有的闺蜜圈 app 也被下架。在重新注册开发者账号,想要重新发布应用的时候,提示 appid 被占用了,给 google 发邮件申请转移,给的答案是账号可以解除封禁,可以登录,但是转移却是一直失败的,根本没有转移权限。
既然如此,那暂时也就不再尝试使用原来的 appid 了,毕竟,这个流程一直持续下去,也不知道会到猴年马月能结束,就酱紫吧。完美主义,自己坚持有个 p 用,还得条件允许才能完美。
除了 google play,其实还有一个平台是自己之前也想上的,那就是鸿蒙,uniapp 刚支持打包鸿蒙 应用的时候,自己就尝试过向鸿蒙的迁移。然而,由于项目框架较老,需要做的工作不止一点点,需要先将 vue2 升级到 vue3,然后在将 vue3 版本打包成鸿蒙的 app。
升级这一步就不是很顺利,作为一个初学前端(vue 框架)的菜鸟,最开始项目建立的时候,代码结构设计的并不是非常好,并且硬编码了很多 vue 2 only的一些代码。升级到 vue3 之后,勉强编译通过,运行到了鸿蒙系统上。
再后来,这件事情也确实没什么动力,就不了了之了。然而,就在上周又接到了一个广东深圳的电话,接起来之后说是鸿蒙开发者中心的。问有没有app 升级或者开发计划,说看到在应用商店上架的闺蜜圈 app了,并且说帮忙给建立技术支持服务群,协助将项目从 vue2 到鸿蒙系统 app 的发版。 ![]()
在接到这一通电话之后,总觉得不做点什么真的对不起鸿蒙生态的付出。(这个电话打了很多次了,一直没接)之前,自己的那种完美主义追求,想要在国内的各大应用市场上架。然而在多年以后,所有的手机应用市场都关上了针对个人开发者的大门,除了华为。国内的个人开发者,真的连狗都不如。
在自己开发第一款 app 的时候,国内应用商店华为、小米、锤子、魅族还是针对个人开发者开放的,只是现在小米把个人开发者推出了门外,魅族也关闭了那扇大门,锤子死了。只剩下华为还算是对个人开发者开放,更何况现在,人家都找上门了,自己又有什么理由不做出点努力呢?
而至于完美主义,现在依然不可能了,如果要做,也只能部分完美。让那些自己有能力去完美的地方,能稍微完美一点吧。
为了能升级到 vue3 和支持最新版的鸿蒙开发工具,将 hbuilder 升级到了最新版,切换到原来 vue3 的分支,不得不说,最新版的 hbuilder 在鸿蒙的支持上友好了很多。
配置好一系列工具和插件之后,甚至应不需要在使用鸿蒙开发者工具打包就可以直接运行到模拟器了,虽然提示只支持 arm 架构的模拟器,但是运行是完全没有问题的。
当然,现在升级最大的优势在于,通过 cursor 可以帮忙解决大部分的 vue代码升级问题。
的确减少了自己的大部分工作量,只需要关注那些 ai 解决不了的问题就 ok 了。两天陆陆续续的修复,最终还是在鸿蒙系统上运行起来了,也修复了大部分的错误,当然,这个升级之后的功能,还需要进一步的细致测试。
实际运行效果:
有的事情,开始固然是艰难了一些,甚至,很长时间都看不到方向,然而,做了也就那样,没什么做不了。也没什么做不到。
网上总是说 hbuiler 这不好,那不好,性能太差,不如原生。有哪有啊完美无瑕的工具或者框架,如果通过这个工具或者框架实现了自己的目的,那么这个框架或者工具就是足够优秀的,哪怕不完美。国外的东西不见得就是好的,国内的东西也不见得就是不好。很多程序员为了争论 emacs 和 vi 到底哪个更好,能口诛笔伐。甚至连 vi 党和 vim 党都能同室操戈,我作为一个实用主义党是在不明白这种争论的意义和价值。
当然,其实这些年我说 hbuiler 好不是一次了,我也是目光短浅,没用过 flutter 之类的其他的跨平台语言。仅仅局限于自己的鼠目寸光,与我而言,这解决了我的问题,就足够了。通过自学,两个月的时间,能让我通过这门语言或者工具来做一款产品,这就够了。
只是,现在我站在了自己写的屎山代码上,有太多的东西需要优化,有太多的结构需要调整。
或许,是时候放弃完美了,对于用户来说,你的代码是不是屎并没那么重要,只要给用户呈现的不是
就完了。
28岁破壳日冒泡
半个月没有更新了,回归了平静的生活,经过上次公司“放假”回老家待了几天,带回去的东西都收拾的差不多了又独自一人返回合肥了,开始了宿舍集体生活。
回来的第二天就找房东把之前租房那里的钥匙还回去了,退租过程也很和谐,水电燃气费用在回老家之前,媳妇已经全部计算好并且充值进去了,退租的时候房东又每个表都当面确认了一遍,也没啥问题,然后就是一手交钱一手交钥匙,过程也比较愉快。
由于宿舍这里已经有宽带了,自己原来办理的360包年的300M就不需要了,上周六吧,又跑了一趟联通营业厅,把宽带销户掉,八月底交的一年的费用,应该还剩个近300块,注销之后营业员说下个月10号之后才能把剩余的费用退回,只能下个月再去一趟了,还好可以退,不然白瞎一年的宽带费用了,回老家之前还想着挂闲鱼租给有需要的人呢,这下可以直接注销,挺好。
由于这次封网时间较长,回来之后也是开启了贼忙碌的工作,一个人的生活好像没有了色彩一样,两点一线,每天就发视频给家里人讲讲话,其他时间感觉说话都变少了。
平平无奇的日子也没啥好玩的事情可以记录了,今天是人生中的第28个破壳日,也是冒个泡吧,来了一周了也没出去吃饭,都是回来自己倒腾随便做点吃的,今天在各位家人的赞助下,也是吃上了喜姐炸串的大鸡腿,也是吃上肉了,哈哈。
![]()
绽放
意义
中午吃完饭,打开电脑下载了点乱七八糟的东西,期间传来了 iphone 的两次短信提示音。平时垃圾短信、垃圾电话一大堆所以也没去看到底是什么东西。等到要出门上班的时候,看了下 p70 手机,推送栏有一条消息:“……驾驶 中型以上……汽车…… 超速 ……不超过 20% ……”,都不用仔细看就知道是超速了。
距离上次收到超速短信已经过了数年,最近几年除了路边停车收到的罚单,以及对象在快速路上超速吃了一张罚单。从来没再收到过超速罚单,倒不是因为没超速,只是山东高速的容错率还是蛮高的。
看了下拍的现场图片,应该是在原来的济潍高速被拍的。区间限速路段 22km 长度,限速 120km/h,实际的区间速度为 134km/h。之前跑 G20的时候,基本区间限速都是压着 140 左右跑,但是却没被拍过。只有回老家的时候才会跑济潍高速,跑的时候一般是压着 130 跑,然而这次稍微快了点。这一下,两百块没了,说实话就是这两百干啥不好呢,为什么要超速呢。
带着家人跑高速的时候,对象总是提示说,不要超速,被罚款太不值了。有时候被说,心里也感觉不爽,但是真的压着 120 跑,总是觉得有些憋屈。然而,等真被拍了之后,交了两百块钱,现在也会想着 200 花的有意义么?平均时速降 2km/h 就完全可以省掉这 200 快了。也常在网上看到有人说,限速 120 超速 20% 不要紧,然而,这个超速 20% 以内不要紧,不是所有的高速都适用。如果要超速,还是贴着 10% 之内超吧,毕竟,交的罚款都赶上来回的油费了。
周日带着宝子去上网球课,终于有重新能达到隔网接球了。跟着前一个教练学的内容,扔了一年多的钱,学了个不标准的动作。现在每节课都在纠正之前的错误,也在承担之前这错误的姿势带来的影响。目前已经又消耗掉 20 节课时费了。好在现在纠正的还算可以,最起码就是球能打实了,能听到打球的声音了。总说及时止损,有时候却因为自己的懒惰和松懈,浪费了太多的时间在那些错误的地方,然而这些错误最终的买单人还是自己。
有时候也不得不思考究竟什么是有意义的,学习?工作?玩耍?还是什么其他的东西?
然而,有时候想想这些都没什么意义,或许真正有意义的事情是做自己喜欢的事情,让自己快乐。然而,这种快乐又从何而来呢?是兴趣爱好还是什么其他的东西?亦或者说是一种无所谓的坚持?
坚持有意义吗?答案就是我也不知道有没有意义,只是从某种意义上在坚持一件事情。最近晚上跳绳的时候,有时候会一边看电视一边跳,这么跳的好处是看到入迷的时候,跳绳反而觉得没那么累了,甚至很容易就跳到 4000+。
昨晚跳的时候,看的第一集电视剧已经过半,在第一集结束之后,继续后面一集。既然开始跳了,自然是要坚持下去的。就这样,刚好差不多一个小时多一点的时间,完成了一万个。
虽然最开始的 4000 还能有点感觉,但是到了 6000+以后反而没什么太大的感觉了,除了有点出汗,并没有太吃力。或许这就是坚持的意义吧,只有牛马有个一个好身体,才能赚更多的牛马费不是吗?也只有更多的牛马费,才能给宝子营造更好的生活环境。
总说年轻人不愿意结婚生子,之前忘了在谁那里看到的一个专家的言论说,那年轻人不愿意生孩子,并不是年轻人没有责任心,而是现在的年轻人责任心比上一辈高太多了,知道为自己的孩子负责,知道为他们创造更好的生活条件。自己没有能力的时候,不想生而不养。
而这个好的条件,有需要付出多少的努力?然而,这些所谓的努力又有多少事有意义的?对于多数人来说,从来不会希望你过得比他们好。上周回去奔丧的时候,在殡仪馆,大姐夫跟二姑家的堂哥在那里聊天,自己去厕所的时候路过。堂哥玩笑了一句:“发财了,不认识亲戚了?”
我幽怨的回了一句:“嗐,发个屁才啊,不单工资没涨,还降工资了……”
简单的聊了几句就去洗手间了。努力或许从来都没什么意义,别人看到的都是你现在的表象,至于是不是他们希望的,大概率并不是。而之所以说这句话,大概率是因为这次回去自己没有开大白而是开粉皮回去的。毕竟这个时候在一堆车牌号鲁 V 和鲁 G 的车里,找到一辆鲁 B 的车还是蛮容易的,自己下车的时候他们大概率也是看到了。虽然不是什么豪车,但是也能戳痛某些人吧。不管承认不承认,有时候看到别人开着跑车出现,自己的内心也会触动,那种内心的由衷的羡慕,因为这些东西大概率自己这辈子无法体会了。而这些东西,不是自己努力就能得到的,如果要按这个来平评判,自己的努力就没有任何的意义。
穷,固然会被人看不起,但是你过得比他好,大概率他会记恨你。
于是,多数时候,我总是习惯展示给他们自己想看的那个自己。更像一个虚伪的骗子,哪怕是自己的家人,有时候竟然也会带着一份伪装,只是不知道这份伪装如果真的去掉,会变成什么样子?大概率不是自己想要的样子。
自己不断追求各种所谓的意义,以前却很少追求快乐。在某些时候,甚至为了单纯的快乐竟然有一些负罪的感觉。就想那个拉磨的驴,偶然从磨盘上下来之后,也会怀疑自己存在的意义和价值。内心的不自信,太多的时候需要所谓的外界的认可来证明自己的价值。诚然,这种病态从来没有真正消除过。
跟大多数人一样,人生的轨迹似乎也没什么太多的变化。上学、工作、结婚、生子,在父辈的言语中,听到的最多的就是多生几个孩子。这一辈子什么都不会留下,能留下的只有自己的孩子。然而,留下那么多孩子干嘛呢?如果给不了好的生活,宁可不要孩子。自己摸爬滚打从按个小村子里爬出来,难道要让自己的孩子继续重蹈覆辙吗?
四叔家的姐姐得了脑瘤,在鬼门关走了一遭之后,心态变了很多。自己很喜欢这个小姐姐,小时候经常在一起,长大了之后见的少了,但是感觉依然很亲切。然而也只有过年的时候才有时间去看看她,身体瘦削,这几年气色好了很多。她说:“有时候读书太多也不好,你看你跑那么远都见不到你。”
“这哪里算远,一点都不远”我答道,我握着她的手,能感到那只手冰冰凉凉。我想努力的给她暖一下,就这么紧紧的握着。
也许从来没什么所谓的感同身受,哪怕经历过同样的事情,每个人依然有每个人的感觉。而自己能做的,也仅仅是尽量去体会他人的内心的感觉。
人生,或许从来没什么意义。也许只有自己能给自己的人生赋予意义,快乐活着,痛快生活。
只是,说起来从来都简单,然而,追寻却从来不易。
如何下载电报 Telegram 视频
多数时候,要下载电报的视频并不需要太复杂的操作,直接点击视频资源的 save to gallery 就可以保存视频了。
然而,并不是所有的群组都能保存视频,有的群组甚至连截屏和分享的权限都没有,自然也就没法下载视频了。
这种群组不单没有开放下载群贤,连复制和转发的权限都没有。所以要想下载这种视频,之前我尝试了几种办法,包括但不限于:
1.通过浏览器插件下载
2.通过 tg 的开发者账号权限,通过接口获取数据
3.tdl
然而,第一种方法,最大的问题在于很多插件都是需要付费使用的。最多能下载四五个视频,超过这个数量就要收费了。另外,今天又试了一下发现插件失效了,应该是本地插件没有更新导致的。
之前用的时候还是可以的。
第二种方法最大的局限在于注册 tg 开发者大概率会直接失败,基于 tg bot 的实现方式,没有开发者账号是无法实现的。而注册失败,就一个弹窗提示 error,就很沙雕。也不告诉你为什么,看网上分析说什么各种风控,就一个破聊天工具,哪里那么多的风控?
原因: • 当前 IP 地址地理位置受限,或是当前ip被滥用导致被feng控,使用代理可能会被 Telegram 拦截。 • 账户状态异常,例如被标记为垃圾账号、未绑定手机号、账号过新等。 • 多次重复尝试触发风控机制,系统会暂时禁止继续操作。 • 浏览器环境异常,如关闭了 JavaScript、使用了隐私插件或禁用了 Cookie。 解决方案: • 使用稳定、纯净的住宅代理 IP • 避免使用数据中心代理、JC节点这类 IP 容易被识别为爬虫来源。 • 建议使用真实住宅宽带出口的代理(如美国、欧洲地区),确保 IP 没有黑历史 • 优先选择长期未被用于 Telegram 操作的 IP,降低触发风控的概率 • 使用指纹浏览器 • 隔离的浏览器环境 • 每个指纹配置都是独立的“浏览器环境”
在尝试了无数次之后,只能放弃这种方法。
直到某一天发现了 tdl,不得不说,这是个好东西啊。
温馨提示: 此处隐藏内容需要发表评论,并且审核通过后才能查看。
(发表评论请勾选 在此浏览器中保存我的显示名称、邮箱地址和网站地址,以便下次评论时使用。)
(请仔细检查自己的昵称和评论内容,以免被识别为垃圾评论而导致无法正常审核。)
Features: Single file start-up Low resource usage Take up all your bandwidth Faster than official clients Download files from (protected) chats Forward messages with automatic fallback and message routing Upload files to Telegram Export messages/members/subscribers to JSON
不过在实际使用的时候,发现几个问题,就是直接用 channel 的方式下载,下载失败了,所以,尝试导出记录的方式下载:
导出的 js 文件会包含所有的媒体资源:
命令参考上图的写法即可,导出之后,使用 json 文件进行下载即可:
tdl.exe dl -f result1.json --skip-same
JVM开篇
申请免费通配符证书
之前有个项目,用了个域名带下划线,结果申请证书的时候就悲剧了,嗯,你问为什么不买个通配符的?当然是为了省钱,不想花钱。
刚开始还以为是阿里的问题,后来去腾讯发现也不行,换了几家都不行。最后搜索发现这么个情况:
由于受CAB出台的新规(证书中所包含的域名不能有下划线)影响,从2018年12月7日起,所有新签发的证书的域名中不能包含下划线,在2018年12月7日之前如有签发过下划线的域名,则需要在2019年1月14日进行强制吊销。 如有证书用户受到影响,可以通过下述方法进行解决: 1.请用户将含有下划线的域名进行调整,然后CA对该老证书进行重签发,将新的标准FQDN添加到冲签发的证书中。 2.用Wildcard对原老证书进行替换(但如果老证是EV SSL证书,则Wildcard证书不适用于本解决方案,因为Wildcard是OV类型证书,不支持EV)。 域名命名规范 (RFC标准) 互联网的核心技术规范由IETF(互联网工程任务组)通过一系列名为RFC(意见征求稿)的文档来定义。关于域名如何命名的规则,主要在 RFC 1035 中明确规定。 合法字符集:RFC 1035规定,域名中的“标签”(例如 www、example、com 都是独立的标签)只能使用以下字符: 字母 a-z (不区分大小写) 数字 0-9 连字符 - 关键限制:连字符 - 不能出现在标签的开头或结尾。 根据这个标准,下划线 _ 根本不在允许的字符集之内。所以,从技术规范上讲,server_name.com 本身就是一个无效的域名。
所以,虽然之前能创建这种二级或者三级域名,但是,在申请对应的证书的时候就悲剧了。
所以为了不影响业务,尝试申请免费的通配符证书,还是通过 acmes.sh 搞吧。
1.安装
curl https://get.acme.sh | sh -s email=my@example.com
2.配置环境变量:
export Ali_Key="LTA**************6yn" export Ali_Secret="q435*************EBSaDba5"
3.申请证书:
acme.sh --issue --dns dns_ali -d example.com -d *.example.com --debug
需要注意的是,通配符证书只能通过配置信息自动校验,不能通过添加解析的方式校验,所以要配置 key 和 secret。如果是不同的解析服务商,设置不同的环境变量即可。
zhongling@MacBookPro .acme.sh % export Ali_Key="LTA**************6yn" export Ali_Secret="q435*************EBSaDba5" zhongling@MacBookPro .acme.sh % ./acme.sh --issue -d lang.bi -d '*.lang.bi' -k ec-256 --dns dns_ali --dnssleep 60 [2025年11月21日 星期五 16时53分34秒 CST] Using CA: https://acme.zerossl.com/v2/DV90 [2025年11月21日 星期五 16时53分34秒 CST] Multi domain='DNS:lang.bi,DNS:*.lang.bi' [2025年11月21日 星期五 16时53分41秒 CST] Getting webroot for domain='lang.bi' [2025年11月21日 星期五 16时53分41秒 CST] Getting webroot for domain='*.lang.bi' [2025年11月21日 星期五 16时53分41秒 CST] Adding TXT value: fcTxHx2osERz8mqJFaV2c0yKvo6vUSMA4SH1FR95PMQ for domain: _acme-challenge.lang.bi [2025年11月21日 星期五 16时53分44秒 CST] The TXT record has been successfully added. [2025年11月21日 星期五 16时53分44秒 CST] Sleeping for 60 seconds to wait for the the TXT records to take effect [2025年11月21日 星期五 16时54分46秒 CST] lang.bi is already verified, skipping dns-01. [2025年11月21日 星期五 16时54分46秒 CST] Verifying: *.lang.bi [2025年11月21日 星期五 16时54分47秒 CST] Processing. The CA is processing your order, please wait. (1/30) [2025年11月21日 星期五 16时54分52秒 CST] Success [2025年11月21日 星期五 16时54分52秒 CST] Removing DNS records. [2025年11月21日 星期五 16时54分52秒 CST] Removing txt: fcTxHx2osERz8mqJFaV2c0yKvo6vUSMA4SH1FR95PMQ for domain: _acme-challenge.lang.bi [2025年11月21日 星期五 16时54分54秒 CST] Successfully removed [2025年11月21日 星期五 16时54分54秒 CST] Verification finished, beginning signing. [2025年11月21日 星期五 16时54分54秒 CST] Let's finalize the order. [2025年11月21日 星期五 16时54分54秒 CST] Le_OrderFinalize='https://acme.zerossl.com/v2/DV90/order/7SLmDTCNs_Qw7zls2HFDpA/finalize' [2025年11月21日 星期五 16时54分56秒 CST] Order status is 'processing', let's sleep and retry. [2025年11月21日 星期五 16时54分56秒 CST] Sleeping for 15 seconds then retrying [2025年11月21日 星期五 16时55分12秒 CST] Polling order status: https://acme.zerossl.com/v2/DV90/order/7SLmDTCNs_Qw7zls2HFDpA [2025年11月21日 星期五 16时55分13秒 CST] Downloading cert. [2025年11月21日 星期五 16时55分13秒 CST] Le_LinkCert='https://acme.zerossl.com/v2/DV90/cert/bvCTHYFrpbcye-ASpKoS5g' [2025年11月21日 星期五 16时55分15秒 CST] Cert success. -----BEGIN CERTIFICATE----- MIID/zCCA4WgAwIBAgIQS5gLQdZXhrEHdsgVdwPdgzAKBggqhkjOPQQDAzBLMQsw CQYDVQQGEwJBVDEQMA4GA1UEChMHWmVyb1NTTDEqMCgGA1UEAxMhWmVyb1NTTCBF Q0MgRG9tYWluIFNlY3VyZSBTaXRlIENBMB4XDTI1MTEyMTAwMDAwMFoXDTI2MDIx OTIzNTk1OVowFzEVMBMGA1UEAxMMaGFpa2VodWkubmV0MFkwEwYHKoZIzj0CAQYI KoZIzj0DAQcDQgAEehCGvspbOuBBQjuauz9ghdv9bmvPGJmlz/LttbMjBlBi31Wh **************************************************************** qaiMNTAnBgNVHREEIDAeggxoYWlrZWh1aS5uZXSCDiouaGFpa2VodWkubmV0MAoG CCqGSM49BAMDA2gAMGUCMHlmfYvfKEWtJ/CM7UNx6sJPwzu5fU1c5j8v2Oj4REQh /KE0yJHo3YZkXegvxlSAPAIxAOPw+ZwRsatCaRL8yEGp4mX0umkKx+XbtTlus5NK aBIOcZiS307CH5mXKOb1jXMPpg== -----END CERTIFICATE----- [2025年11月21日 星期五 16时55分15秒 CST] Your cert is in: /Users/zhongling/.acme.sh/lang.bi_ecc/lang.bi.cer [2025年11月21日 星期五 16时55分15秒 CST] Your cert key is in: /Users/zhongling/.acme.sh/lang.bi_ecc/lang.bi.key [2025年11月21日 星期五 16时55分15秒 CST] The intermediate CA cert is in: /Users/zhongling/.acme.sh/lang.bi_ecc/ca.cer [2025年11月21日 星期五 16时55分15秒 CST] And the full-chain cert is in: /Users/zhongling/.acme.sh/lang.bi_ecc/fullchain.cer zhongling@MacBookPro .acme.sh % start /Users/zhongling/.acme.sh/ zsh: command not found: start zsh: permission denied: /Users/zhongling/.acme.sh/ zhongling@MacBookPro .acme.sh % start /Users/zhongling/.acme.sh/ zsh: command not found: start zhongling@MacBookPro .acme.sh % open /Users/zhongling/.acme.sh/ zhongling@MacBookPro .acme.sh % open /Users/zhongling/.acme.sh/
最终,省去了更换域名的麻烦。先将就用着吧。
面试笔记:MySQL 相关03 – SQL语法与查询优化
感谢订阅陶其的个人博客!
SQL语法与查询优化
回目录: 《面试笔记:MySQL 相关目录》
上一篇: 《面试笔记:MySQL 相关02 – 索引》
01. 子查询与连接(join)查询
子查询:是嵌套在主查询中的查询,按“返回结果类型”和“是否依赖主查询”分类,核心是 “子结果驱动主查询”;
连接查询:是多表关联,核心是 “通过关联条件合并表数据”,按“是否保留不匹配行”分类,底层依赖驱动表和被驱动表的关联逻辑。
以下给出了4种核心子查询和5种核心连接查询,以及一种内部优化查询。
01. 标量子查询
返回单个值。
- 写法格式:子查询返回 1 行 1 列,可作为主查询的字段或条件值;
-- 示例:查询订单数最多的用户名称(子查询返回最大订单数) SELECT username FROM user WHERE user_id = (SELECT MAX(user_id) FROM order WHERE status = 1); - 性能开销:极低,仅执行 1 次子查询,结果直接代入主查询;
- 适用场景:主查询需依赖单个聚合值(如最大值、平均值)或特定单行数据。
02. 列子查询
返回单列多行。
- 写法格式:子查询返回 1 列 N 行,主查询用
IN/NOT IN/ANY/ALL匹配;-- 示例:查询有未支付订单的用户(子查询返回未支付订单的用户ID列表) SELECT username FROM user WHERE user_id IN (SELECT DISTINCT user_id FROM order WHERE status = 0); - 性能开销:中低,MySQL 5.7 + 会将其优化为 “半连接”(避免逐行匹配),子查询结果会被物化(存入临时表);
- 适用场景:主查询需匹配多个候选值(如 “在某个集合中”),子查询结果集不宜过大(建议 1 万行内)。
03. 行子查询
返回多行多列。
- 写法格式:子查询返回 N 行 N 列(通常 1-2 列),主查询用IN匹配行数据;
-- 示例:查询与“用户张三的上海地址”完全匹配的用户(返回id和city两列) SELECT username FROM user WHERE (user_id, city) IN (SELECT user_id, city FROM user_addr WHERE username = '张三' AND city = '上海'); - 性能开销:中,子查询结果集需按行匹配,建议结果集控制在 1 千行内;
- 适用场景:主查询需匹配 “多字段组合条件”(如联合主键、多维度筛选)。
04. 关联子查询
依赖主查询字段。
- 写法格式:子查询用主查询的字段作为条件(
EXISTS是典型用法),需用表别名区分;-- 示例:查询有有效订单的用户(子查询依赖主查询的user_id,找到匹配即停止) SELECT username FROM user u WHERE EXISTS (SELECT 1 FROM order o WHERE o.user_id = u.user_id AND o.status = 1); - 性能开销:中高,主查询每扫描 1 行,子查询触发 1 次(类似 “嵌套循环”);若子查询关联字段无索引,会导致全表扫描(如 10 万行主表触发 10 万次子查询);
- 适用场景:主查询需 “存在性校验”(如 “是否有相关数据”),优先用
EXISTS(找到匹配行立即停止)替代IN(需遍历全量结果集)。
05. 内连接
INNER JOIN,默认连接方式。
- 写法格式:仅保留两表中 “关联条件完全匹配” 的行(取交集),
INNER可省略;-- 示例:查询用户及其有效订单(仅返回有订单的用户和匹配的订单) SELECT u.username, o.order_no FROM user u INNER JOIN order o ON u.user_id = o.user_id -- 关联条件(必须写ON),INNER 可省略 WHERE o.status = 1; - 性能开销:低,MySQL 会自动选择 “小表作为驱动表”,若关联字段有索引,被驱动表可快速匹配(避免全表扫描);
- 适用场景:需获取多表 “交集数据”(如用户 + 关联订单、商品 + 关联分类),是业务中最常用的连接方式。
06. 左外连接
LEFT JOIN / LEFT OUTER JOIN
- 写法格式:保留左表所有行,右表无匹配时用
NULL填充,OUTER可省略;-- 示例:查询所有用户及其订单(无订单的用户也会显示,订单字段为NULL) SELECT u.username, o.order_no FROM user u LEFT JOIN order o ON u.user_id = o.user_id WHERE o.status = 1 OR o.order_no IS NULL; -- 注意:过滤右表需包含NULL - 性能开销:中低,驱动表是左表(需全扫左表),右表关联字段有索引则性能接近内连接;无索引则性能下降明显;
- 适用场景:需保留主表全量数据(如用户表),关联从表(如订单表)的可选数据(如 “统计所有用户的订单情况,包括无订单用户”)。
07. 右外连接
RIGHT JOIN / RIGHT OUTER JOIN
- 写法格式:保留右表所有行,左表无匹配时用
NULL填充,逻辑与左连接相反;-- 示例:查询所有订单及其所属用户(无匹配用户的订单也显示,用户字段为NULL) SELECT u.username, o.order_no FROM user u RIGHT JOIN order o ON u.user_id = o.user_id WHERE o.create_time > '2024-01-01'; - 性能开销:与左连接一致,驱动表是右表(需全扫右表);
- 适用场景:需保留从表全量数据(如订单表),关联主表(如用户表)的可选数据(如 “统计所有近 3 个月订单,包括已删除用户的订单”)。
08. 全外连接
FULL JOIN / FULL OUTER JOIN
- 写法格式:保留左右两表所有行,无匹配时用
NULL填充;MySQL 不直接支持FULL JOIN,需用UNION合并左连接和右连接;-- 示例:查询所有用户和所有订单的关联数据(保留双方无匹配的行) SELECT u.username, o.order_no FROM user u LEFT JOIN order o ON u.user_id = o.user_id UNION -- 去重合并 SELECT u.username, o.order_no FROM user u RIGHT JOIN order o ON u.user_id = o.user_id; - 性能开销:高,需执行两次连接 + 去重操作,数据量大时效率低;
- 适用场景:极少用,仅需 “完整保留两表所有数据” 的特殊场景(数据对账、全量统计)。
09. 交叉连接
CROSS JOIN,笛卡尔积
- 写法格式:无关联条件,两表数据完全组合(行数 = 左表行数 × 右表行数),
CROSS JOIN可省略;-- 示例:用户表(3行)和商品表(2行)交叉连接,结果为6行 SELECT u.username, p.product_name FROM user u CROSS JOIN product p; - 性能开销:极高,行数呈指数级增长(1 万行 ×1 万行 = 1 亿行),几乎不用;
- 适用场景:仅用于 “全组合场景”(如生成所有用户 + 所有商品的测试数据),必须配合
WHERE过滤(否则会撑爆内存)。
10. 半连接
半连接不是独立的查询语法,而是 MySQL 5.7 + 默认启用的对列子查询(如IN子查询)的内部优化手段,目的是减少子查询与主查询的匹配开销。
用于优化“主查询匹配子查询结果集”的场景(如IN/EXISTS列子查询)。
核心逻辑是 “只判断匹配与否,不返回子查询的完整数据”,避免逐行匹配的低效问题。
简单说:普通子查询是 “子查询返回所有结果→主查询逐行比对”,半连接是 “主查询与子查询表直接关联→找到匹配行就停止”,本质是将子查询转化为类似JOIN的关联逻辑,提升性能。
触发条件:子查询是“列子查询”(返回单列多行),且子查询表与主查询表无关联(非关联子查询),例如WHERE id IN (SELECT uid FROM t2 WHERE status=1)。
写法格式:无特殊语法,仍用普通IN/EXISTS子查询写法,MySQL 会自动触发半连接优化(可通过EXPLAIN查看type列是否有SIMPLE/HASH JOIN,而非SUBQUERY)。
-- 示例:查询有未支付订单的用户(MySQL会自动用半连接优化)
SELECT username FROM user u WHERE u.user_id IN (SELECT DISTINCT o.user_id FROM order o WHERE o.status = 0);
性能开销
- 低 – 中:远优于未优化的子查询(避免子查询结果集物化后逐行匹配)。
- 优化逻辑:MySQL 会选择以下方式之一执行半连接:
- 哈希半连接:对小表(子查询表)建哈希表,主表逐行匹配哈希键(适合大表)。
- 嵌套循环半连接:小表驱动大表,找到匹配行立即停止扫描(适合小表)。
- 物化半连接:子查询结果存入临时表并建索引,主表关联临时表(适合子查询结果集较小)。
适用场景
- 主查询需判断 “字段是否在子查询结果集中”(如
IN/EXISTS)。 - 子查询表数据量适中(1 万行内最佳),且子查询有过滤条件(如
WHERE status=1),能减少结果集大小。 - 替代场景:若子查询结果集极大(10 万行 +),建议手动改写为
JOIN(半连接优化效果会下降)。
注意项:
- 半连接是 MySQL 自动优化,无需专门手动去写语法,但需知道 “
IN子查询在 5.7 + 会被优化为半连接”,避免面试官问 “IN和JOIN哪个快” 时只说表面结论。 - 实践中,若
EXPLAIN显示子查询类型为SUBQUERY(未触发半连接),可通过 “子查询加DISTINCT” 或 “改写为JOIN” 强制优化(如上述示例加DISTINCT让结果集唯一,更易触发半连接)。
总结
1. 子查询 vs 连接查询:
- 简单场景(如单值匹配、存在性校验)用子查询(写法简洁);
- 复杂多表关联(如 3 表以上、需筛选多字段)用连接查询(性能更优,易优化);
- 关联子查询尽量改写为JOIN(避免嵌套循环,如
EXISTS适合小表,JOIN适合大表)。
2. 性能优化关键:
- 连接查询:关联字段必须建索引(如
order.user_id),小表驱动大表(减少循环次数); - 子查询:避免
NOT IN(NULL值会导致结果异常),用NOT EXISTS替代; - 外连接:右表过滤条件写在
ON(关联时过滤),左表过滤条件写在WHERE(关联后过滤)。
3. Java 开发实践场景:
- 列表查询(如用户列表 + 订单数):用
LEFT JOIN+GROUP BY; - 详情查询(如订单详情 + 用户信息):用
INNER JOIN; - 存在性校验(如判断用户是否有未支付订单):用
EXISTS子查询。
02. 子查询与join性能对比
子查询与join性能对比及适用场景
![]()
关键结论:
- 简单场景(单表校验、单值匹配)用子查询(简洁);
- 多表关联、复杂聚合、主表数据量大时,优先用 JOIN(性能更稳定)。
03. 复杂查询的执行逻辑
1. group by/having
GROUP BY 和 HAVING 是 MySQL 中用于数据分组聚合的核心语法,执行逻辑: “数据过滤→分组→聚合→二次过滤” 。
1. 核心执行逻辑
GROUP BY 的作用是 “按指定字段将数据分组”;
HAVING 则是 “对分组后的结果进行过滤”。
整体执行流程分四步,顺序不可颠倒:
1) 原始数据过滤(WHERE 子句);
- 先通过
WHERE筛选符合条件的原始数据(如WHERE status = 1过滤无效数据),减少后续处理的数据量。 - 原理:
WHERE是 “分组前过滤”,仅保留满足条件的行,不涉及聚合操作,可利用索引快速筛选(如status字段有索引时,直接定位有效行)。
2) 分组操作(GROUP BY 子句);
- 按
GROUP BY后的字段(如GROUP BY user_id)将上一步过滤后的数据分组,相同值的行被归为一组。 - 底层处理:
- 若分组字段有索引,MySQL 会直接按索引顺序分组(无需额外排序,效率高);
- 若无索引,会创建临时表存储分组数据,或对数据进行文件排序(
Using temporary或Using filesort,可通过EXPLAIN查看)。
3) 聚合计算(聚合函数);
- 对每个分组执行聚合函数(如
COUNT(*)统计行数、SUM(amount)计算总和),生成每个分组的聚合结果。 - 原理:聚合函数仅作用于分组内的数据,每个分组最终输出一行结果(包含分组字段和聚合值)。
4) 分组结果过滤(HAVING 子句);
- 用
HAVING筛选聚合后的分组(如HAVING COUNT(*) > 5保留行数超 5 的组),最终返回符合条件的分组。 - 原理:
HAVING是 “分组后过滤”,可直接使用聚合函数(因聚合结果已生成)。
2. 关键区别与实践注意
1) WHERE 与 HAVING 的核心区别
WHERE作用于分组前的原始数据,不能使用聚合函数(如WHERE COUNT(*) > 5会报错);HAVING作用于分组后的结果,可以使用聚合函数(如HAVING SUM(amount) > 1000合法)。
2) 性能优化实践(重点)
- 优先用
WHERE过滤:尽量在分组前通过WHERE减少数据量(如GROUP BY user_id HAVING user_id > 100可改为WHERE user_id > 100 GROUP BY user_id,减少分组计算量)。 - 分组字段加索引:避免临时表和文件排序(如
GROUP BY create_time时,给create_time建索引,EXPLAIN中无Using temporary即为优化生效)。 - 避免
SELECT非分组字段:MySQL 5.7+ 默认开启ONLY_FULL_GROUP_BY模式,SELECT后只能出现GROUP BY字段或聚合函数(如SELECT user_id, username, COUNT(*)若username未在GROUP BY中,会报错,需规范写法)。
3. 典型场景举例
- 统计高频用户:查询 “订单数超 10 单的用户及其总消费”,用
GROUP BY user_id HAVING COUNT(*) > 10。 - 按时间分组分析:查询 “每月订单金额超 10 万的月份”,用
GROUP BY month(create_time) HAVING SUM(amount) > 100000。
2. limit 分页
LIMIT 分页是 MySQL 中用于从查询结果中截取指定范围数据的核心语法(如 LIMIT offset, size 取第 offset + 1 到 offset + size 行),其执行逻辑围绕 “全量查询→排序→截取” 展开,实际使用中需重点关注性能问题。
1. 基本执行逻辑
LIMIT 本身不参与数据筛选,而是对 “查询 + 排序后” 的结果集进行截取,核心流程分三步:
1) 执行主查询获取原始数据
- 先执行
WHERE、JOIN等条件筛选,得到所有符合条件的行(如SELECT * FROM order WHERE status=1筛选有效订单)。
2) 排序处理(若有 ORDER BY)
- 若包含
ORDER BY(分页几乎必带,否则结果无序),MySQL 会按指定字段排序:- 若排序字段有索引(如
ORDER BY create_time且create_time有索引),直接利用索引顺序获取有序数据(效率高); - 若无索引,需在内存或磁盘中进行 “文件排序”(
Using filesort,可通过EXPLAIN查看),排序过程会消耗额外 CPU/IO。
- 若排序字段有索引(如
3) 截取指定范围数据
- 从排序后的完整结果集中,跳过前 offset 行,返回接下来的 size 行(如 LIMIT 20, 10 即跳过前 20 行,返回 10 行)。
2. 核心性能问题:offset 过大导致效率低下
当 offset 很大时(如 LIMIT 100000, 10),性能会急剧下降,原因是:
- MySQL 必须先扫描并排序前 100010 行数据,然后丢弃前 100000 行,仅返回最后 10 行,大量计算被浪费;
- 若排序无索引(依赖文件排序),会产生临时文件,进一步加剧性能损耗(10 万行数据排序可能耗时数百毫秒)。
3. 实践优化方案(重点,体现工程经验)
针对 offset 过大的问题,结合 Java 开发中常见的分页场景(如列表页、历史记录查询),优化手段如下:
1) 基于 “主键 / 唯一索引” 分页(最常用)
利用主键或唯一索引的有序性,通过 WHERE 条件直接定位起始位置,避免全量扫描:
-- 代替 LIMIT 100000, 10(低效)
SELECT * FROM order
WHERE id > 100000 -- 上一页最后一条数据的id
ORDER BY id
LIMIT 10;
原理:主键索引是有序的,id > 100000 可直接定位到起始行,无需扫描前 10 万行,性能提升 10 倍以上。
2) 避免 SELECT *,只查必要字段
减少数据传输量和内存占用,尤其大表(如包含 text 字段的表):
-- 只查需要的字段(如订单号、金额、时间)
SELECT order_no, amount, create_time FROM order
WHERE id > 100000
LIMIT 10;
3) 确保排序字段有索引
分页必须带 ORDER BY,且排序字段需建索引(如 create_time 索引),避免文件排序:
-- 给 create_time 建索引:CREATE INDEX idx_order_create_time ON order(create_time)
SELECT * FROM order
WHERE status=1
ORDER BY create_time DESC -- 利用索引排序
LIMIT 20, 10;
4) 限制最大分页页数
业务上避免允许用户访问过大页数(如 “只显示前 100 页”),超过则提示 “数据过多,请缩小范围”(如电商平台常见做法),从源头减少大 offset 场景。
04. order by 的排序原理
MySQL 的ORDER BY用于对查询结果按指定字段排序,其核心原理是 “利用索引有序性直接取数” 或 “无索引时通过内存 / 磁盘排序”,性能差异主要源于是否能借助索引避免额外排序操作。
1. 核心排序原理
1. 利用索引排序
Using index,高效。
若排序字段(或多字段排序的前缀)有有序索引(如 B + 树索引,本身按字段值有序存储),MySQL 会直接沿索引顺序读取数据,无需额外排序,这是最优情况。
- 原理:B + 树索引的叶子节点按索引字段值升序(或降序,取决于建索引时的指定)排列,
ORDER BY字段与索引顺序一致时,可直接通过索引定位并返回有序数据,避免排序开销。 - 示例:
订单表order有索引idx_create_time(create_time升序),执行SELECT id, create_time FROM order ORDER BY create_time时,MySQL 直接沿idx_create_time的叶子节点顺序取数,无需排序(EXPLAIN中Extra列显示Using index)。
2. 文件排序
Using filesort,低效。
若排序字段无索引,或排序顺序与索引顺序不一致(如索引是升序,ORDER BY用降序且无对应降序索引),MySQL 会触发 “文件排序”:先将数据加载到内存 / 临时文件,完成排序后再返回结果。
-
执行步骤:
- 从表中读取符合
WHERE条件的行(无索引时全表扫描); - 将 “排序字段值 + 行指针(指向原始数据位置)” 存入
sort_buffer(排序缓冲区); - 若
sort_buffer装不下所有数据,会将数据分块,先在内存中排序,再写入临时文件(磁盘); - 最后合并所有分块的排序结果,得到全局有序的结果集,再通过行指针取原始数据返回。
- 从表中读取符合
-
示例:
订单表order的create_time无索引,执行SELECT * FROM order ORDER BY create_time时,MySQL 会先扫描全表,将create_time和行指针存入sort_buffer,排序后再取完整数据(EXPLAIN中Extra列显示Using filesort)。
2. 影响排序性能的关键因素
- 是否使用索引:索引排序性能远高于文件排序(毫秒级 vs 秒级,数据量大时差距更大)。
sort_buffer大小:由sort_buffer_size参数控制(默认 256KB),若数据量超过该值,会触发磁盘临时文件排序(IO 开销剧增)。- 查询字段多少:
SELECT *会导致sort_buffer中存储更多字段(增加内存占用),而只查必要字段可减少sort_buffer压力,加速排序。
3. 实践优化建议
1. 给排序字段建合适的索引:
- 单字段排序:直接给排序字段建索引(如
ORDER BY create_time→ 建idx_create_time)。 - 多字段排序(如
ORDER BY a, b DESC):建联合索引(a, b DESC),索引顺序与排序顺序完全一致(避免索引失效)。
2. 避免SELECT *,只查必要字段:
- 例如,列表页只需
id、name、create_time,则SELECT id, name, create_time ...,减少sort_buffer中存储的数据量,避免磁盘排序。
3. 控制排序数据量:
先用WHERE过滤无效数据(如WHERE status=1),减少进入排序阶段的行数(如从 100 万行滤到 1 万行,排序效率提升 100 倍)。
4. 大结果集排序用分页配合索引:
如 “按时间排序分页查询订单”,建(create_time, id)联合索引,用WHERE create_time > ?定位分页起点,避免全量排序。
4. 总结
ORDER BY的优化核心是 “能用上索引就避免文件排序”:索引排序依赖 B + 树的有序性,高效低耗;文件排序需内存 / 磁盘排序,性能差。实际开发中,通过 “建匹配索引 + 精简查询字段 + 提前过滤数据” 可显著提升排序性能,这也是处理订单列表、用户日志等需排序场景的常规优化手段。
05. 慢查询的定位与分析
MySQL 的慢查询(执行时间超过阈值的 SQL)是性能优化的核心切入点,定位与分析需结合日志工具、执行计划和业务场景。
1. 慢查询的定位
慢查询的定位:通过日志捕获目标 SQL。
定位慢查询的核心是开启慢查询日志,记录执行时间超标的 SQL,为后续分析提供依据。
1. 慢查询日志配置(5.7 版本)
通过修改my.cnf(或my.ini)配置,开启并定制日志规则:
slow_query_log = 1 # 开启慢查询日志(默认:0,关闭)
slow_query_log_file = /var/log/mysql/slow.log # 日志文件路径(需MySQL有写入权限)
long_query_time = 1 # 慢查询阈值(单位秒,默认10秒,建议生产设1-2秒)
log_queries_not_using_indexes = 1 # 记录未使用索引的查询(即使未超阈值,可选)
log_output = FILE # 日志输出方式(FILE/table,5.7默认:FILE)
配置后重启 MySQL 生效,也可通过SET GLOBAL动态开启(无需重启,适合临时排查):
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;
2. 日志收集工具
-
mysqldumpslow(自带工具):简单统计慢查询,适合快速定位高频问题:
# 查看访问次数最多的10条慢查询 mysqldumpslow -s c -t 10 /var/log/mysql/slow.log # 查看总执行时间最长的10条慢查询 mysqldumpslow -s t -t 10 /var/log/mysql/slow.log优势:轻量,适合初步筛选;
不足:无法分析趋势或复杂统计。 -
pt-query-digest(Percona 工具包):生产环境常用,可按执行时间、频率、用户等维度分析,输出 SQL 模板、平均耗时、扫描行数等关键信息:
pt-query-digest /var/log/mysql/slow.log > slow_analysis.txt优势:能识别重复 SQL(如参数不同但结构相同的查询),定位 “隐形高频慢查询”(单条耗时 0.8 秒,但每秒执行 100 次,总耗时 80 秒)。
2. 慢查询的分析
慢查询的分析:通过执行计划定位根因。
捕获慢查询后,需用EXPLAIN分析其执行逻辑,重点关注是否全表扫描、是否用对索引、是否有额外排序 / 临时表。
1. 核心分析工具:EXPLAIN
对慢查询执行EXPLAIN,查看关键字段:
type:访问类型(性能从好到差:const>eq_ref>ref>range>ALL)。ALL表示全表扫描(需优先优化)。key:实际使用的索引(NULL表示未用索引)。rows:预估扫描行数(值越大,效率越低)。Extra:额外信息(关键警告:Using filesort(需排序且无索引)、Using temporary(需创建临时表)、Using where;Using filesort(全表扫描后排序,性能极差))。
2. 常见慢查询原因及分析(结合EXPLAIN)
- 全表扫描(
type=ALL,key=NULL):- 原因:查询条件无索引(如:
WHERE status=1但status无索引),或索引失效(如:WHERE name LIKE '%abc'左模糊匹配导致索引失效)。
- 原因:查询条件无索引(如:
JOIN关联无索引(rows极大):- 原因:被驱动表的关联字段无索引(如
t1 JOIN t2 ON t2.uid = t1.id,t2.uid无索引),导致被驱动表全表扫描。
- 原因:被驱动表的关联字段无索引(如
- 排序无索引(
Extra=Using filesort):- 原因:
ORDER BY字段无索引,或排序顺序与索引不一致(如索引是create_time ASC,但查询用ORDER BY create_time DESC且无降序索引)。
- 原因:
- 子查询嵌套(
type=DEPENDENT SUBQUERY):- 原因:关联子查询导致主表每行触发一次子查询(如
WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.uid = t1.id)),无索引时性能骤降。
- 原因:关联子查询导致主表每行触发一次子查询(如
3. 慢查询的优化方向
分析出原因后,针对性优化:
1. 加索引:
- 为查询条件、
JOIN关联字段、排序字段建索引(如:WHERE+ORDER BY字段建联合索引,如idx_status_create_time (status, create_time))。
2. 改写 SQL:
- 子查询改
JOIN(如关联子查询改写为INNER JOIN,减少嵌套循环); - 避免左模糊匹配(
LIKE '%abc'改为LIKE 'abc%',或用全文索引); - 大分页改范围查询(
LIMIT 100000,10改为WHERE id > 100000 LIMIT 10)。
3. 控制数据量:
- 用
WHERE提前过滤无效数据(如WHERE create_time > '2024-01-01'),减少扫描和排序行数。
4. 8.0 版本的优化
- 慢查询日志支持表存储:除文件外,可将日志写入
mysql.slow_log表(log_output = TABLE),支持 SQL 查询分析(如按时间范围统计)。 - 日志内容更丰富:新增
query_id(唯一标识)、rows_affected等字段,方便关联性能模式(Performance Schema)数据。 - 动态调整更灵活:
long_query_time支持小数(如 0.5 秒),且修改后立即生效(无需重启连接)。
5. 总结
慢查询定位与分析的核心流程是:开启日志捕获 → 工具统计筛选 → EXPLAIN分析执行计划 → 针对性优化(索引 / 改写 SQL)。
实践中,需结合业务场景(如订单表慢查询多与user_id、create_time索引相关),优先解决 “高频 + 高耗时” 的慢查询。
06. 复杂SQL的拆分与改写
大表分页优化:延迟关联、书签分页。
MySQL 大表分页是业务中高频痛点(如订单列表、日志查询),普通LIMIT offset, size在offset较大时(如LIMIT 100000,10)性能极差:因 MySQL 需扫描前 10 万行再丢弃,仅取最后 10 行,IO 开销剧增。
复杂 SQL 拆分与改写的核心思路是减少无效扫描行数,常用优化手段包括延迟关联、书签分页。
1. 普通分页的性能瓶颈
普通分页 SQL 示例(订单表order,按create_time逆序):
SELECT * FROM `order` ORDER BY create_time DESC LIMIT 100000, 10;
问题:当offset=100000时,MySQL 需先遍历索引找到第 10 万行的位置(即使有create_time索引),再回表读取 10 行数据,前 10 万行的扫描属于无效开销,数据量越大越慢。
2. 关键优化方法
1. 延迟关联(延迟回表)
概念:先通过覆盖索引获取目标数据的主键(或唯一键),再关联原表获取完整字段,避免 “早期回表” 带来的大量 IO。
原理:利用索引的 “覆盖查询” 特性(只查主键,无需回表)快速定位目标行,再通过主键关联原表取数,减少扫描和回表的数据量。
优化后 SQL 示例:
-- 子查询用索引获取主键(覆盖索引,无需回表)
SELECT o.*
FROM `order` o
JOIN (SELECT id FROM `order` ORDER BY create_time DESC LIMIT 100000, 10) tmp
ON o.id = tmp.id;
- 优势:子查询仅扫描索引获取id(轻量),关联原表时仅取 10 行完整数据,性能比普通分页提升 5-10 倍(10 万 + offset 场景)。
- 适用场景:需展示完整字段、支持任意跳页(如第 100 页、第 200 页)的列表。
2. 书签分页(游标分页 / 键集分页)
概念:用 “上一页最后一条记录的唯一标识”(书签,如id或create_time+id)作为条件,替代offset直接定位下一页起点,完全避免无效扫描。
原理:利用主键 / 唯一索引的有序性,通过WHERE条件跳过前面数据,直接从书签位置取数。
优化后 SQL 示例:假设上一页最后一条记录的create_time='2024-01-01 10:00:00'、id=100000,下一页查询:
SELECT *
FROM `order`
WHERE create_time < '2024-01-01 10:00:00'
OR (create_time = '2024-01-01 10:00:00' AND id < 100000)
ORDER BY create_time DESC, id DESC
LIMIT 10;
- 关键:用
create_time+id作为书签(create_time可能重复,需加id保证唯一性),通过索引直接定位,无offset开销。 - 优势:性能极致(无论翻多少页,扫描行数固定为
LIMIT size); - 局限:不支持 “跳页”(如直接到第 100 页),仅适合 “上一页 / 下一页”“加载更多” 场景(APP / 移动端常用)。
3. 覆盖索引分页
覆盖索引分页:若查询字段(如id, order_no, create_time)全部包含在索引中,直接用索引查询,无需回表:
-- 索引`idx_create_time_order_no (create_time DESC, order_no, id)`覆盖所有查询字段
SELECT id, order_no, create_time
FROM `order`
ORDER BY create_time DESC
LIMIT 100000, 10;
4. 限制最大 Offset
业务上禁止offset超过阈值(如 10000),引导用户通过筛选条件(如 “按时间范围筛选”)缩小数据范围,避免大 offset 查询。
5. 分表分页
超大规模表(亿级)需水平分表(如按create_time分表),分页时先定位分表,再在分表内分页,避免跨表扫描。
3. 总结
大表分页优化的核心是 “用索引定位替代无效扫描”:
- 需支持跳页选延迟关联,通过覆盖索引减少回表;
- 移动端 / 加载更多选书签分页,用唯一键直接定位;
- 极致性能选覆盖索引分页,避免回表开销。
07. JOIN优化
小表驱动大表、 避免 cross join。
1. JOIN 优化的核心逻辑
MySQL 中 JOIN 的底层实现以Nested Loop Join(嵌套循环连接)为主:外层循环遍历 “驱动表” 的数据,内层循环用驱动表的字段去 “被驱动表” 中匹配数据。。就像Java里的双层for循环。
优化的核心是减少外层循环次数 + 降低内层匹配成本,这也是 “小表驱动大表” 的原理基础。
2. 优化1:小表驱动大表
1. 原理
“小表” 指参与 JOIN 的数据集更小的表(而非物理表大小)。
用小表做驱动表(外层循环),大表做被驱动表(内层循环),可显著减少外层循环次数,从而降低整体 IO 和匹配开销。
- 举例:小表有 100 行,大表有 100 万行
- 小表驱动大表:外层循环 100 次,内层每次匹配大表 → 总匹配 100 次;
- 大表驱动小表:外层循环 100 万次,内层每次匹配小表 → 总匹配 100 万次;
- 性能差异一目了然。
2. 实践判断与示例
- 如何判断 “小表”:
- 通过
EXPLAIN看rows字段(预估扫描行数),行数少的就是 “小表”; - 或业务逻辑中明确数据量更小的表(如字典表、配置表)。
- 通过
- SQL 示例(用户表
user是小表,订单表order是大表):
-- 推荐:小表user驱动大表order(INNER JOIN中MySQL会自动优化,但LEFT JOIN需显式控制)
SELECT u.username, o.order_no
FROM user u
INNER JOIN `order` o ON u.id = o.user_id;
-- LEFT JOIN需注意:左表是驱动表,因此左表必须是小表
SELECT u.username, o.order_no
FROM user u -- 小表放左表(驱动表)
LEFT JOIN `order` o ON u.id = o.user_id;
3. 注意点
INNER JOIN:MySQL 优化器会自动选择小表作为驱动表,无需手动调整;LEFT JOIN:驱动表固定为左表,因此必须将小表放在左表位置(否则会用大表驱动小表,性能暴跌);RIGHT JOIN:驱动表固定为右表,需将小表放在右表位置(实际开发中建议用LEFT JOIN替代,更符合阅读习惯)。
3. 优化2:避免 CROSS JOIN(笛卡尔积)
1. CROSS JOIN 的危害
CROSS JOIN会返回两张表的笛卡尔积(行数 = 表 A 行数 × 表 B 行数),若表 A 有 1 万行、表 B 有 10 万行,结果会有 10 亿行,直接导致数据库 CPU/IO 打满、查询超时。
2. 常见触发场景
- 显式使用
CROSS JOIN关键字且无ON条件; JOIN时遗漏ON关联条件(如SELECT * FROM A JOIN B);WHERE条件无法过滤笛卡尔积(如关联字段值全为NULL)。
3. 如何避免
- 任何
JOIN必须加ON关联条件(即使是INNER JOIN); - 禁止显式使用无关联条件的
CROSS JOIN; - 若需关联但无直接字段,通过业务逻辑补充关联条件(如时间范围、状态过滤)。
4. 优化补充
1. 被驱动表的关联字段必须加索引
被驱动表的ON字段(如order.user_id)需建索引,否则内层循环会全表扫描,即使小表驱动大表也会很慢。
2. 避免SELECT *
只查询需要的字段,减少数据传输量(尤其大表有大字段如TEXT时)。
3. 提前过滤数据
用WHERE先过滤驱动表和被驱动表的无效数据(如WHERE o.status=1),减少参与 JOIN 的行数。
5. 总结
MySQL JOIN 优化的核心是:
- 小表驱动大表:利用嵌套循环的特性减少外层循环次数,
LEFT JOIN需手动控制驱动表; - 杜绝笛卡尔积:
JOIN必须加ON条件,避免CROSS JOIN; - 索引兜底:被驱动表的关联字段加索引,是
JOIN性能的基础保障。
08. count的性能差异
count(1)、count(*)、count(字段)
1. count 的本质
count 的本质:统计 “非 NULL 行数”。
count()是聚合函数,核心作用是统计查询结果集中符合条件的 “非 NULL 行数”,不同参数的差异在于 “统计范围” 和 “是否需要判断 NULL”,这直接决定性能。
2. 三种 count 的区别与性能对比
1. count(*)
统计所有行数(含 NULL)。
逻辑:专门用于统计表 / 结果集的总行数。
MySQL 对其有特殊优化:会优先选择最小的非聚集索引(覆盖索引)扫描,若没有则用聚集索引,避免全表扫描。
特点:包含 NULL 值(因为不判断字段,只统计行数),性能最优。
2. count(1)
统计所有行数(占位符)。
逻辑:用常量1作为占位符,统计所有行数(无论字段是否为 NULL)。
MySQL 优化器会将其与count(*)视为等价,底层执行计划完全相同。
特点:性能与count(*)几乎无差异,属于 “语法糖”。
3. count(字段)
统计字段非 NULL 的行数。
逻辑:需逐行检查该字段是否为 NULL,仅统计非 NULL 的行数。性能分两种情况:
- 字段是索引字段:用索引扫描(比全表快),但需判断
NULL; - 字段是非索引字段:全表扫描 + 判断
NULL,性能最差。
特点:性能低于count(*)/count(1),且结果可能不等于总行数(因为字段可能为 NULL)。
3. 性能排序
从快到慢:count(*) ≈ count(1) > ount(索引字段) > count(非索引字段)
关键原因:
count(*)/count(1)无需判断字段NULL,且 MySQL 会选最优索引;count(字段)需额外判断NULL,非索引字段还需全表扫描。
4. 实践建议
- 统计总行数:优先用
count(*)(语义最清晰,MySQL 优化最好),而非count(1)或count(主键); - 统计字段非
NULL行数:若需此逻辑,确保字段加索引(如count(user_name)需user_name有索引); - 避免误区:
- 认为
count(主键)更快:实际count(*)会选更小的索引,比主键索引(聚集索引)扫描更快; - 认为
count(1)比count(*)快:MySQL 优化后两者无区别,count(*)更符合语义。
- 认为
5. 总结
- 核心差异:
count(*)/count(1)统计总行数(含NULL),性能最优;count(字段)统计非NULL行数,性能较差; - 实践选择:统计总数用
count(*),统计非NULL字段用count(索引字段); - 优化关键:利用 MySQL 对
count(*)的索引优化,避免count(非索引字段)的全表扫描。
6. 扩展
统计user表email非null的总行数,email是普通索引,是SELECT COUNT(email) FROM user快,还是SELECT COUNT(*) FROM user WHERE email IS NOT NULL快?
答:基本相同,但是不推荐使用后者,推荐使用COUNT(email)。因为这个全程是索引,虽然需要索引扫描,但是后者使用了WHERE条件,同样也是需要进行索引扫描,同时还多出了一步条件过滤。同时COUNT(email)语义更明确。
09. IN和JOIN哪个快
先明确两者的底层逻辑
IN:属于 “子查询 / 值列表匹配”,本质是将子查询结果加载到内存后做匹配(或直接匹配值列表),适合 “单字段匹配” 场景;JOIN:属于 “多表关联”,底层以Nested Loop Join为主(小表驱动大表),通过关联字段索引直接匹配,适合 “多字段关联取值” 场景。
![]()
IN的优势:语法简单,适合 “单字段匹配” 的简单场景;JOIN的优势:支持多字段关联,大结果集下利用索引更高效,是复杂关联的首选。
10. MySQL的函数
MySQL 函数按功能可分为内置函数(系统提供,重点)和自定义函数(用户编写)。
1. 核心内置函数
1. 字符串函数
处理文本数据。
用于字符串拼接、截取、替换等,是业务中最常用的函数类别。
CONCAT(str1, str2,...):字符串拼接(如:拼接用户姓名和手机号:CONCAT(username, '-', mobile));SUBSTRING(str, pos, len):截取子串(如:取手机号后 4 位:SUBSTRING(mobile, 8, 4));REPLACE(str, old, new):替换字符串(如:清理内容中的特殊字符:REPLACE(content, '<', ''));LENGTH(str):获取字符串长度(如:校验用户名长度:WHERE LENGTH(username) > 6)。
实践:用户信息格式化、文本内容清洗场景高频使用。
2. 数值函数
处理数字运算。
用于数值计算、取整、进制转换等。
ROUND(num, n):四舍五入(如:金额保留 2 位小数:ROUND(amount, 2));ABS(num):取绝对值(如:计算差值的绝对值:ABS(score1 - score2));CEIL(num)/FLOOR(num):向上 / 向下取整(如:订单数量向上取整:CEIL(total/10));SUM(num)/AVG(num):求和 / 平均值(报表统计核心函数)。
实践:金额计算、数据统计场景必备。
3. 日期时间函数
处理时间数据。
用于时间获取、格式化、差值计算,是业务中仅次于字符串函数的高频函数。
NOW()/CURDATE():获取当前时间(含时分秒)/ 当前日期(仅年月日);DATE_FORMAT(date, fmt):时间格式化(如:订单时间转字符串:DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s'));DATEDIFF(date1, date2):计算日期差值(如:用户注册天数:DATEDIFF(NOW(), register_time));DATE_ADD(date, INTERVAL expr unit):时间增减(如:计算 7 天后的日期:DATE_ADD(NOW(), INTERVAL 7 DAY))。
实践:订单时间筛选、会员有效期计算、报表时间维度统计。
4. 聚合函数
数据统计分析。
用于对多行数据进行聚合计算,需结合GROUP BY使用。
COUNT(*):统计总行数(用户数、订单数统计);SUM(field):求和(销售额、销量统计);MAX(field)/MIN(field):最大值 / 最小值(最高订单金额、最早注册时间);GROUP_CONCAT(field):分组拼接字符串(如:查询用户的所有订单号:GROUP_CONCAT(order_no))。
实践:报表系统、数据看板的核心函数。
5. 条件函数
逻辑判断。
用于实现 SQL 中的条件分支,替代复杂的WHERE判断。
IF(condition, val1, val2):简单条件判断(如:判断用户状态:IF(status=1, '正常', '禁用'));CASE WHEN:复杂条件分支(如:订单状态映射:CASE status WHEN 0 THEN '待支付' WHEN 1 THEN '已支付' ELSE '已取消' END);IFNULL(val, default):NULL值替换(如:用户昵称为空时显示默认值:IFNULL(nickname, '游客'))。
实践:查询结果格式化、动态状态展示。
6. 其他常用函数
UUID():生成唯一标识符(分布式场景临时 ID 生成);INSTR(str, substr):查找子串位置(如:判断内容是否含关键词:INSTR(content, 'MySQL') > 0);CAST(val AS type):类型转换(如:字符串转数字:CAST(score AS UNSIGNED))。
2. 面试高频考点
函数使用的性能陷阱。
1. 函数操作索引字段会导致索引失效
这是面试必问点!若对索引字段使用函数,MySQL 无法使用索引,会触发全表扫描。
- 反例:
WHERE DATE(create_time) = '2024-01-01'(对索引字段create_time用DATE()函数,索引失效); - 正例:
WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02'(直接匹配字段范围,利用索引)。这个之所以能利用索引,是因为没有使用函数破坏索引的有序性,同时MySQL将给出的查询条件值'2024-01-01'和'2024-01-02'隐式的转换为了DATETIME类型,从而避免了将整个表的create_time字段转成其他类型,保证了原索引的可用性。
2. 聚合函数的NULL处理
COUNT(field)会忽略NULL值,COUNT(*)统计所有行(含NULL);SUM(field)中NULL值会被视为 0,但建议用IFNULL(field, 0)显式处理(避免语义歧义)。
3. 自定义函数的慎用场景
- 自定义函数(
CREATE FUNCTION)可实现复杂逻辑,但执行效率低(无法并行执行),且可能导致锁表,高并发场景建议用存储过程或应用层代码替代。
3. 总结
MySQL 函数是提升 SQL 灵活性的核心工具。
- 分类记忆:重点掌握字符串、日期、聚合、条件函数的常用用法;
- 性能陷阱:索引字段避免使用函数,否则索引失效;
- 实践优先:简单逻辑用内置函数,复杂逻辑优先应用层处理(避免 SQL 臃肿)。
回目录: 《面试笔记:MySQL 相关目录》
上一篇: 《面试笔记:MySQL 相关02 – 索引》
喜欢面试笔记:MySQL 相关03 – SQL语法与查询优化这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。
面试笔记:MySQL 相关02 – 索引
感谢订阅陶其的个人博客!
索引
回目录: 《面试笔记:MySQL 相关目录》
上一篇: 《面试笔记:MySQL 相关01 – 基础核心》
下一篇: 《面试笔记:MySQL 相关03 – SQL语法与查询优化》
01. 各种树的原理和特性
学习树,是为了便于学习索引。索引的核心作用是“加速查询”,而高效的树结构正是实现这一点的关键。
答:
1. “树”形象的理解
可以从“现实中的树”类比数据结构的“树”,它和路边的树长得很像,只是“倒过来”了。
想象一棵简化的苹果树:
- 最底下的 “树根” 是起点,对应数据结构中树的根节点(只有一个);
- 从树根往上长的 “树干” 会分杈出 “树枝”,这些树枝就是父节点;
- 树枝再分杈出更细的枝桠,这些细枝桠就是子节点(一个父节点可以有多个子节点);
- 最顶端的 “苹果”(没有再分杈的部分),就是叶子节点。
数据结构里的 “树”,就是这样一种 “分层、有分支” 的结构: - 所有节点(根、父、子、叶子)都像 “果实” 一样,存储着数据(或索引);
- 节点之间的 “连接”(比如根到父、父到子)像 “树枝”,表示数据之间的关系;
- 整个结构是 “单向” 的:只能从根往下找子节点,不能从子节点反推回根(就像苹果不会自己长回树枝)。
举个更具体的例子:
- 如果用树存 “班级学生名单”,根节点可以是 “三年级”;
- 根节点的子节点(父节点)可以是 “三班”“五班”;
- “三班” 的子节点可以是 “男生组”“女生组”;
- “男生组” 的子节点(叶子节点)就是具体的学生:“小明”“小李”…
这种结构的核心好处是:查找效率高。
比如想找 “三年级三班男生组的小明”,你不用遍历所有学生,只需从根(三年级)→ 三班 → 男生组 → 小明,一步一步按 “分支” 找,比在一堆乱序的名单里翻快得多。
2. 二叉查找树
“不合适”的树(Binary Search Tree)
特性:每个节点最多 2 个子节点(左小右大),查询时从根节点开始,比当前节点小就走左子树,大就走右子树。
问题:容易 “失衡”。比如插入一串递增数据,会退化成链表(左子树为空,只有右子树),查询效率从 O (logn) 暴跌到 O (n)(和遍历链表一样慢)。
为什么 MySQL 不用:数据库索引需要稳定高效的查询,二叉树的失衡问题无法满足。
![]()
3. 平衡二叉树
又叫:AVL树/红黑树
解决 “失衡”,但仍有局限
特性:在二叉查找树基础上,通过旋转保持 “平衡”(左右子树高度差不超过 1),确保查询效率稳定在 O (logn)。
问题:还是 “二叉”(每个节点最多 2 个子节点),导致树的 “高度过高”。
比如存 100 万条数据,平衡二叉树的高度大概是 20 层(2^20≈100 万)。
为什么 MySQL 不用:索引数据存在磁盘上,每次查询需要从磁盘读数据(一次 IO 读一个 “页”,比如 4KB)。树高 20 就意味着最多要读 20 次磁盘,IO 成本太高。
![]()
4. B 树
多路平衡查找树,降低高度
特性:
- “多路”:每个节点可以有多个子节点(比如 100 个),不再是二叉。
- “平衡”:所有叶子节点在同一层,避免某条路径过长。
- 节点存 “数据”:每个节点不仅存索引键,还直接存对应的数据(或数据地址)。
优势:高度大大降低。比如每个节点有 100 个子节点,存 100 万数据,树高只需 3 层(100^3=100 万),最多 3 次 IO 就能查到数据,比平衡二叉树高效得多。
为什么 MySQL 不完全用它:范围查询不方便。比如查 “id 从 100 到 1000”,B 树需要回溯父节点找下一个范围,效率低。
![]()
5. B + 树
MySQL 索引的 “标准答案”
B + 树是 B 树的变种,专门为数据库索引优化设计,核心特性完美适配索引需求:
非叶子节点只存索引,不存数据:
- 非叶子节点仅保留索引键(比如 id),不存实际数据,这样一个节点能存更多索引键,子节点数量更多(比如 200 个),树高更低(100 万数据只需 2-3 层),IO 次数更少。
叶子节点存完整数据,且首尾相连:
- 所有实际数据只存在叶子节点,且叶子节点之间用链表连接(形成有序链表)。
对索引的核心价值:
- 单值查询快:和 B 树一样,通过索引键快速定位到叶子节点,IO 少。
- 范围查询无敌:比如查 “id>100”,找到第一个 id=100 的叶子节点后,直接顺着链表往后扫,不用回溯父节点,效率极高(MySQL 中范围查询很频繁,这是关键)。
![]()
6. MySQL 为什么选 B + 树做索引?
- 树高低,IO 次数少(磁盘读写效率高);
- 叶子节点有序相连,范围查询(如 between、in)效率远超其他树;
- 非叶子节点只存索引,节点存储密度高,进一步降低树高。
02. 索引的作用
答:
-
加速查询,降低IO:索引通过有序结构(如B+树)将分散的数据按索引键排序,查询时无需全表扫描,而是通过索引快速定位目标数据所在的磁盘位置,大幅减少磁盘IO次数(比如从扫描全表100万行降到扫描几十行);
-
保证数据唯一性,强化业务约束:通过唯一索引(含主键索引),数据库会强制约束索引键的值不重复,直接实现 “业务唯一标识” 需求(如用户 ID、订单号不可重复),避免手动校验的繁琐和风险。
-
优化排序与分组操作:索引本身是有序的,当查询包含
order by(排序)、group by(分组)时,可直接利用索引的有序性避免全表数据的额外排序(减少内存 / 磁盘临时表开销),提升这类操作的效率。
03. 常见索引类型及其特性
答:
1. 主键索引
Primary Key
特性:
- 强制唯一性(表中唯一标识一行数据),且不允许NULL值;
- 一张表只能有一个主键索引;
- InnoDB 中主键索引是聚簇索引(叶子节点直接存储整行数据),无需回表,查询效率最高。
适用场景:作为表的唯一标识(如用户 ID、订单 ID),确保数据唯一性并加速行查询。
2. 唯一索引
Unique Index
特性:
- 确保索引列的值唯一,但允许NULL值(且NULL可出现多次);
- 一张表可创建多个唯一索引;
- 基于 B + 树结构,非聚簇索引(叶子节点存主键值,需回表查数据);
- 查询时找到一个匹配值就停止扫描(无需确认是否有重复),比普通索引少了 “继续校验” 的步骤,回表前的索引定位效率更高。
适用场景:需唯一约束但非主键的字段(如手机号、邮箱,允许未填写即NULL)。
3. 普通索引
Normal Index
特性:
- 最基础的索引类型,无唯一性约束,允许重复值和NULL;
- 基于 B + 树结构,非聚簇索引,仅用于加速查询,不影响数据本身的约束;
- 查询时需扫描所有匹配的索引节点(可能有多个重复值),再批量回表取数据,比唯一索引多了 “扫描重复索引” 的开销。
适用场景:高频查询的非唯一字段(如商品分类、用户昵称),单纯提升查询效率。
4. 联合索引
Composite Index
特性:
- 由多个字段组合创建(如(a, b, c)),遵循最左前缀原则(查询需包含最左字段才能命中索引);
- 索引按第一个字段排序,第一个字段相同则按第二个,以此类推;
- 可覆盖多字段查询,减少回表(如查询a, b时,若联合索引包含a, b,则无需回表);
- 效率依赖查询条件:仅命中最左前缀且为覆盖索引时(无需回表),效率接近普通索引;若未命中全部前缀或需回表,效率会下降。
- 整体比普通索引慢的核心:索引结构是多字段排序,定位时需匹配多个字段的有序性,逻辑比单字段索引复杂。
适用场景:多字段组合查询(如 “按用户 ID + 订单状态查询”),需合理设计字段顺序(区分度高的字段放左侧)。
5. 前缀索引
Prefix Index
特性:
- 仅对字符串字段的前 N 个字符创建索引(如index(name(10))),大幅节省存储空间,但索引选择性低(重复率高);
- 适用于长字符串(如 URL、长文本),但可能降低索引选择性(重复率升高),且无法用于order by/group by或覆盖索引。
- 查询时需扫描更多索引节点才能定位目标,且无法使用覆盖索引(必须回表),额外增加 IO 开销。
适用场景:长字符串字段的模糊查询(如like ‘abc%’),平衡空间与查询效率。
6. 全文索引
Full-Text Index
特性:
- 针对大文本内容(如文章、评论)的关键词搜索,针对大文本分词匹配(而非like的字符匹配,非 B + 树精确查找,需先分词、匹配倒排索引,再关联原表数据);
- 效率远高于like ‘%关键词%’,但仅支持MATCH() AGAINST()语法,且有最小 / 最大词长限制。
- 逻辑复杂度远高于其他索引,仅适合关键词检索,单值精确查询效率远低于 B + 树结构的索引。
适用场景:全文检索需求(如博客系统的文章关键词搜索)。
7. 索引效率排序
这些索引类型,按照查询速度和效率,从高到低排序
基于InnoDB引擎、但是精确查询场景:
查询效率从高到低排序:主键索引 > 唯一索引 > 普通索引 > 联合索引(命中最左前缀 + 覆盖索引)> 前缀索引 > 全文索引
但是并非绝对,依旧需要结合使用场景进行判断。
排序前提:默认是「单值精确查询」,若为「范围查询」,主键索引和唯一索引的差距会缩小(范围查询需扫描多个节点)。
04. 聚簇索引与非聚簇索引
答:
聚簇索引与非聚簇索引的核心区别是:数据存储位置。
聚簇索引,又叫做聚集索引,它的叶子节点存储整行数据;
非聚簇索引的叶子节点仅存储 “索引键 + 主键值”,需通过主键回表查询完整数据(覆盖索引除外)。
![]()
05. 最左前缀原则
答:
联合索引的最左前缀原则:
查询条件必须从联合索引的最左字段开始匹配,且连续匹配,才能用到索引的对应部分。
举个例子:联合索引(name, age, score)
-
能命中索引的情况(从左到右连续匹配):
where name = '张三'(只用name,命中索引的name部分);where name = '张三' and age = 20(用name+age,命中索引的name+age部分);where name = '张三' and age = 20 and score > 90(用name+age+score,命中索引的全部三部分)。
-
不能命中索引的情况(跳过左字段或不连续):
where age = 20(跳过最左的name,完全无法命中);where name = '张三' and score > 90(跳过中间的age,只能命中name部分,score无法利用索引)。
联合索引含范围查询或模糊查询能否命中索引?
对于范围查询(大于、小于等)会导致“当前字段右侧的索引字段失效”,但左侧已匹配的字段仍能命中,(当前字段也能命中);
对于模糊查询,如果是前缀模糊(例如:"快乐大本营",like '快乐%'),能命中当前字段及左侧的索引,但当前字段右侧的索引失效;如果是后缀/全模糊查询(例如:%大本营、%乐大%),会导致当前字段及右侧字段的索引都失效,仅当前字段左侧已匹配索引有效。
06. 索引的维护成本
答:
MySQL索引的维护成本本质是“空间换时间”的代价,主要集中在“写入性能损耗”、“存储空间占用”、“索引碎片维护”。索引数量越多,结构越复杂,则维护成本越高。
1. 写入操作(插入/更新/删除)的性能损耗
- 写入数据时,不仅要修改表数据,还需同步维护索引的 B + 树结构。
- 插入:可能触发 B + 树节点分裂(如节点满时),需重新组织索引排序;若为唯一索引,还需额外校验唯一性(扫描索引确认无重复)。
- 更新:若更新的是索引列,需先删除旧索引条目,再插入新索引条目,相当于两次索引操作;非索引列更新不影响索引,但仍需维护聚簇索引的物理顺序(InnoDB)。
- 删除:不会立即释放索引空间,仅标记为 “删除”,后续需通过碎片整理回收,且可能触发 B + 树节点合并(如节点数据过少时)。
2. 存储空间的额外占用
- 索引需独立存储 B + 树结构,一张表的索引越多、字段越长,占用的磁盘空间越大。
- 示例:大表的联合索引、长字符串的前缀索引,可能占用与数据本身相当的存储空间;聚簇索引虽无需额外存数据,但非聚簇索引(如唯一、普通索引)需存储主键值,叠加后空间开销显著。
3. 索引碎片与定期维护开销
- 频繁插入 / 删除后,B + 树会产生 “空洞”(已删除但未释放的空间),导致索引碎片增多。
- 碎片会降低查询效率(磁盘 IO 增多),需定期执行维护操作(如
OPTIMIZE TABLE、重建索引),这些操作会锁表或占用大量 IO 资源,影响业务高峰期性能。
4. 优化器的决策负担
- 表中索引过多时,MySQL 优化器需遍历所有可能的索引组合,评估最优查询计划,导致查询解析时间变长(尤其复杂查询场景)。
5. 如何优化维护策略
- 不同索引的维护成本差异:联合索引 > 普通索引 > 唯一索引 > 主键索引(结构越复杂,维护时 B + 树调整逻辑越繁琐);前缀索引、全文索引的维护成本高于单字段索引(前者需处理部分字符,后者需维护分词倒排索引)。
- 平衡维护成本的核心原则:避免 “过度索引”,仅为高频查询字段创建索引;优先选择窄索引(短字段、少字段组合),减少存储空间和维护开销;定期监控索引使用率,删除无效索引(如长期未被使用的索引)。
07. 回表查询与索引覆盖
答:
回表查询是 “非聚簇索引查询后需二次查聚簇索引拿完整数据” 的过程;
索引覆盖是 “查询字段(即所需的返回字段)全在索引中,无需二次查询” 的优化场景。
二者是 “需回表” 与 “免回表” 的对立关系,直接影响查询效率。
1. 回表查询(Bookmark Lookup)
1. 定义
当使用非聚簇索引(如唯一索引、普通索引、联合索引)查询时,若查询字段未完全包含在该索引中,需先通过非聚簇索引找到 “索引键 + 主键值”,再用主键值查询聚簇索引(主键索引) ,才能获取整行完整数据,这个 “二次查询” 的过程就是回表。
2. 核心逻辑(结合 InnoDB 引擎)
- 非聚簇索引的叶子节点仅存储 “索引键 + 主键值”,不存完整行数据。
- 若查询需要非索引字段(如用唯一索引
email查user表的主键id和非索引字段name),必须通过主键值回聚簇索引 “兜底”,才能拿到name。
3. 示例
-- 表结构:id(主键)、email(唯一索引)、name(非索引字段)
select id, name from user where email = 'a@test.com';
第一步:通过唯一索引email(非聚簇)找到主键id=100;
第二步:用id=100查询聚簇索引,拿到name字段,完成查询;
这两步共同构成回表查询,额外增加了一次聚簇索引查询的 IO 开销。
2. 索引覆盖(Covering Index)
1. 定义
当查询的所有字段(包括筛选条件、返回字段)都包含在某一个索引中时,则无需回表,仅通过该索引就能获取所有需要的数据,这个索引就是 “覆盖索引”,也叫紧凑索引,对应的查询就是覆盖索引查询。
2. 核心优势
避免回表,减少一次磁盘 IO(聚簇索引查询),大幅提升效率;
索引数据量远小于全表数据,查询时扫描的数据量更少。
3. 示例
-- 联合索引:idx_email_name(email, name)(包含email和name字段)
select email, name from user where email = 'a@test.com';
查询的筛选字段email、返回字段name均在联合索引中;
直接通过该联合索引就能拿到所有需要的数据(返回字段),无需回聚簇索引,实现 “一次查询完成”。
3. 补充项
触发条件对比:
- 回表:非聚簇索引 + 查询字段超出索引范围;
- 索引覆盖:查询字段(筛选 + 返回)完全匹配某一索引(单字段索引或联合索引)。
实战价值:
索引覆盖是优化回表开销的核心手段,设计索引时可将高频查询字段加入联合索引(如idx_userid_status(user_id, status)),避免回表;
聚簇索引查询天然支持 “索引覆盖”(叶子节点存完整数据),无需回表,这也是其查询效率最高的原因之一。
易混点:
索引覆盖的关键是 “字段全包含”,与索引类型无关(单字段索引、联合索引均可作为覆盖索引);
前缀索引无法实现索引覆盖(仅存字段前 N 个字符,无法返回完整字段值)。
4. 不能触发索引覆盖示例
SELECT id, name, email FROM user WHERE email = 'xxx@qq.com';
-- 在user表中,id是主键,email是唯一索引,name是普通索引
结论:不能触发索引覆盖。
索引覆盖的核心要求是查询的所有字段(筛选条件 + 返回字段)必须完全包含在同一个索引中,跨索引无法实现覆盖。
如何修改才能触发索引覆盖?
- 创建联合唯一索引
unique index idx_email_name(email, name) - 筛选字段
email、返回字段email、name、id(主键会自动包含在非聚簇索引中),全部包含在这个联合索引里; - 查询时无需回表,直接通过该联合索引就能获取所有需要的数据,触发索引覆盖。
08. 如何合理使用索引?
以业务查询为导向,平衡查询效率与维护成本,避免 “过度索引” 或 “无效索引”。
- 优先为 “高频查询字段” 建索引,低频查询不建;
- 联合索引需遵循 “最左前缀 + 区分度优先(区分度高,重复值少)” 原则;
- 避免索引失效场景,确保索引被正确使用;
- 利用 “索引覆盖” 减少回表,提升效率;
- 根据字段特性选择合适的索引类型;
- 定期维护索引,清理无效索引。
合理使用索引的核心是 “按需设计、避免失效、注重维护”。
“索引不是越多越好,而是越合适越好”。
09. 索引失效场景
MySQL 索引失效的本质是:查询条件破坏了索引的有序性或匹配规则;
常见场景集中在:“字段操作”、“类型不匹配”、“查询语法不当”三类;
避免核心是:让查询条件贴合索引设计规则。
以下是常见索引失效场景 + 避免方法:
1. 对索引字段做函数 / 运算操作
失效场景:查询时对索引字段用函数(如substr、date_format)或数学运算,会导致 MySQL 无法利用索引的有序性,只能全表扫描。
- 示例:
where substr(name, 1, 3) = '张三'(name是普通索引)、where age + 1 = 20(age是普通索引)。
避免方法:将函数 / 运算移到等号右侧,或提前计算结果。
- 优化后:
where name like '张三%'、where age = 19。
2. 隐式类型转换
失效场景:查询条件中字段类型与传入值类型不匹配,MySQL 会自动做类型转换,导致索引失效。
- 示例:
where phone = '13800138000'(phone是int类型,字符串转数字)、where id = '100'(id是int,字符串转数字)。
避免方法:确保查询值类型与字段类型完全一致。 - 优化后:
where phone = 13800138000、where id = 100。
3. 模糊查询(非前缀匹配)
失效场景:like的模糊查询若以%开头(后缀模糊 / 全模糊),会破坏索引有序性,导致索引失效;前缀模糊(%在末尾)可正常使用索引。
- 示例:
where name like '%张三'(后缀模糊,失效)、where name like '%张三%'(全模糊,失效)。
避免方法:优先用前缀模糊查询;若需全模糊,改用全文索引(如fulltext index)或应用层分词。 - 优化后:
where name like '张三%'(前缀模糊)、MATCH(name) AGAINST('张三')(全文索引)。
4. 联合索引不满足最左前缀原则
失效场景:联合索引(如(a, b, c))需从左到右连续匹配,跳过左侧字段或不连续匹配,会导致索引失效或部分失效。
- 示例:
where b = 2 and c = 3(跳过最左a,全失效)、where a = 1 and c = 3(跳过中间b,仅a部分生效,c失效)。
避免方法:查询条件需包含联合索引的最左字段,且按索引字段顺序匹配;若高频查询b + c,可单独建联合索引(b, c)。 - 优化后:
where a = 1 and b = 2 and c = 3(全生效)、where b = 2 and c = 3(改用(b, c)联合索引)。
5. 范围查询后字段失效
失效场景:联合索引中,某字段用>、<、>=、<=、between做范围查询后,其右侧的索引字段会失效。
- 示例:
where a = 1 and b > 2 and c = 3(联合索引(a, b, c),c失效)。
避免方法:将范围查询字段放在联合索引的最右侧;若需多字段范围查询,改用覆盖索引或拆分查询。 - 优化后:
where a = 1 and b > 2(仅用a + b索引)、建联合索引(a, b, c)并确保查询字段覆盖(select a, b, c,避免回表)。
6. or连接非索引字段
失效场景:or连接的查询条件中,若有一个字段无索引,会导致整个查询无法使用索引(MySQL 会选择全表扫描)。
- 示例:
where name = '张三' or address = '北京'(name有索引,address无索引,全失效)。
避免方法:确保or连接的所有字段都有索引;或改用union all拆分查询(需字段一致)。 - 优化后:
where name = '张三' union all where address = '北京'(address补建索引)。
7. is not null/not in/not exists
失效场景:对索引字段用is not null(部分场景失效)、not in、not exists,会破坏索引的匹配逻辑,导致失效(is null通常可使用索引)。
- 示例:
where name is not null(name是普通索引,可能失效)、where id not in (1,2,3)(id是主键索引,大数据量下失效)。
避免方法:is not null改用union拼接非空结果;not in改用left join ... on ... is null;小数据量not in可接受,大数据量必优化。 - 优化后:
where id in (select id from user) union ...(is not null替代)、select * from user u left join tmp t on u.id = t.id where t.id is null(not in替代)。
8. 索引选择性差(失效等价场景)
失效场景:索引字段重复率极高(如 “性别” 字段,仅男 / 女),MySQL 优化器会判断 “全表扫描比索引查询更快”,主动放弃使用索引。
- 示例:
where gender = '男'(gender建了索引,但全表 80% 是男性,索引失效)。
避免方法:不针对低选择性字段建单字段索引;若需查询,将其作为联合索引的右侧字段(如(age, gender)),通过高选择性字段先过滤。
9. 补充
- 验证索引是否失效的核心方法:用
explain分析执行计划,若key字段为NULL或非目标索引,说明索引失效。 - 特殊情况:
Innodb的聚簇索引(主键)即使有上述场景(如is not null),通常也不会完全失效,因聚簇索引的物理存储特性,优化器更倾向使用。 - 避免失效的核心原则:不破坏索引的有序性、不改变字段的原始形态、让查询条件贴合索引设计。
10. explain执行计划分析
explain 是 MySQL 分析 SQL 执行计划的核心工具。
通过输出 12 个字段(面试重点关注 7 个核心字段),可判断索引是否生效、查询是否全表扫描、是否存在文件排序 / 临时表等性能问题。
1. explain 核心作用
- 判断 SQL 是否使用了目标索引(避免索引失效);
- 识别全表扫描、文件排序、临时表等低效操作;
- 分析表的连接顺序、查询类型(简单 / 复杂查询);
- 预估查询扫描的行数,评估查询效率。
2. 7 个核心字段解析
1. type:访问类型
核心,判断查询效率的关键
含义:表示 MySQL 如何访问表中的数据(即查询方式),取值决定查询效率,从优到差排序:
system > const > eq_ref > ref > range > index > ALL
关键取值解读:
system:表中只有 1 行数据(如系统表),效率最高(罕见);const:通过主键 / 唯一索引查询,匹配 1 行数据(如where id=100),高效;eq_ref:多表连接时,被连接表通过主键 / 唯一索引匹配,每行只返回 1 行(如join on 主键);ref:通过普通索引 / 联合索引前缀匹配,返回多行匹配数据(如where name=’张三’,name 是普通索引);range:范围查询(>、<、between、in),只扫描索引的某一范围(比ref差,但比全表扫描好);index:扫描整个索引树(索引全扫描),比ALL好(索引数据量小于全表);ALL:全表扫描(最差),需避免(通常是索引失效或未建索引)。
判断标准:type至少要达到range级别,最优是ref或const,出现ALL说明存在性能问题。
2. key:实际使用的索引
含义:表示 MySQL 实际选择的索引(若为NULL,说明未使用任何索引,索引失效或无合适索引);
判断标准:若key不是你设计的目标索引(如预期用idx_email,但key为NULL),说明索引失效,需排查原因(结合索引失效场景)。
3. rows:预估扫描行数
含义:MySQL 优化器预估的、查询需要扫描的行数(非精确值,但可反映效率);
判断标准:行数越少越好,若rows远大于表中实际数据量,可能是统计信息过时(需执行analyze table更新),或索引设计不合理。
4. extra:额外执行信息
核心!暴露性能隐患
含义:记录 SQL 执行的额外操作,重点关注 “好的标识” 和 “坏的标识”:
- 优质标识:
Using index:触发覆盖索引,无需回表(高效,面试加分点);Using index condition:索引下推(ICP),减少回表次数(高效)。
- 性能隐患标识(必须避免):
Using filesort:需在内存 / 磁盘中排序(未利用索引有序性,如order by字段无索引);Using temporary:创建临时表存储中间结果(如group by无索引、多表连接无合适索引,性能极差);Using where:全表扫描后过滤数据(type=ALL时出现,说明无索引可用);Using join buffer:多表连接时未用索引,使用连接缓冲区(低效)。
5. id:查询执行顺序
含义:表示查询中每个select子句的执行顺序(数字越大越先执行,相同数字按从上到下顺序);
应用场景:复杂查询(子查询、join)中,判断表的连接顺序是否合理(如小表驱动大表)。
6. select_type:查询类型
含义:区分简单查询和复杂查询,面试高频取值:
SIMPLE:简单查询(无子查询、无union);SUBQUERY:子查询(select中嵌套select);DERIVED:派生表(from中嵌套select);UNION:union连接的第二个及以后的查询。
面试价值:说明查询的复杂程度,复杂查询(如多层子查询)可能导致优化器选择低效执行计划,需考虑拆分 SQL。
7. table:当前查询的表
含义:显示 SQL 查询的表名(或别名),多表连接时按id顺序显示表的执行顺序。
加项
-
如何判断索引是否生效:
看key字段是否为目标索引(非NULL),同时type不是ALL/index; -
如何判断查询是否高效:
type≥range+key非NULL+rows值小 +extra无filesort/temporary; -
常见问题排查:
- 若
type=ALL+key=NULL:索引失效或未建索引,排查索引失效场景; - 若
extra有filesort:order by/group by字段未建索引,需添加索引; - 若
extra有temporary:group by无索引或多表连接无合适索引,优化索引设计。
- 若
-
explain extended:在explain基础上增加filtered字段(过滤行数占比),filtered越高说明过滤效果越好; -
explain format=json:输出 JSON 格式的详细执行计划,适合复杂查询分析; -
执行计划是 “预估” 而非 “实际”:优化器可能因统计信息过时、索引选择性差等误判,需结合实际执行耗时验证。
11. 升序索引与降序索引
在 MySQL 中,索引的升序(ASC)和降序(DESC)是通过创建索引时指定排序方向来设置的,核心目的是匹配查询中ORDER BY的排序需求,避免额外的文件排序(Using filesort)或反向扫描带来的额外开销。
在ORDER BY排序时,如果排序字段存在索引,但是索引的顺序与排序的顺序不一致,则排序时导致优化器选择“反向扫描索引”,性能虽然比文件排序快非常多,但是依旧不如排序和索引同向的效果好。
比如:create_time字段存在索引idx_create_time,索引是升序(ASC)的,但是查询排序时,需要逆序排序(比如ORDER BY create_time DESC),那么优化器选择“反向扫描索引”。
1. 升序 / 降序索引的创建语法
MySQL 中创建索引时,默认是升序(ASC),可显式指定ASC(升序)或DESC(降序)。语法如下:
-- 升序索引(默认,可省略ASC)
CREATE INDEX idx_create_time_asc ON 表名(create_time ASC);
-- 降序索引(显式指定DESC)
CREATE INDEX idx_create_time_desc ON 表名(create_time DESC);
升序索引(ASC):索引中字段值按从小到大排序,适用于ORDER BY 字段 ASC的查询。
降序索引(DESC):索引中字段值按从大到小排序,适用于ORDER BY 字段 DESC的查询(如 “按创建时间逆序排序的列表”)。
这里面还存在着版本差异:
2. 5.7版本
5.7版本的限制与实践。
5.7 版本对降序索引的支持是 “语法层面” 的,实际存储仍为升序,查询时若用ORDER BY 字段 DESC,优化器会 “反向扫描升序索引”(而非直接使用降序索引),虽然比全表扫描 + 文件排序高效,但仍有一定开销。
场景:查询列表经常按create_time逆序排序(ORDER BY create_time DESC)
在 5.7 中,即使创建DESC索引,实际效果与ASC索引差异不大,但仍建议显式创建DESC索引(为 8.0 升级兼容,且优化器可能优先选择):
-- 5.7中创建“伪降序索引”(语法支持,实际升序存储)
CREATE INDEX idx_create_time_desc ON order(create_time DESC);
-- 查询时,优化器会反向扫描该索引,避免文件排序
SELECT id, order_no FROM order
WHERE status = 1
ORDER BY create_time DESC
LIMIT 10;
5.7 的替代方案(核心优化)
若ORDER BY create_time DESC还需配合WHERE条件(如status=1),建议创建联合索引,将过滤字段放前面,排序字段放后面(显式指定DESC):
-- 联合索引:先过滤(status),再排序(create_time DESC)
CREATE INDEX idx_status_create_time_desc ON order(status, create_time DESC);
-- 该索引可同时优化WHERE和ORDER BY,避免全表扫描和文件排序
SELECT id, order_no FROM order
WHERE status = 1
ORDER BY create_time DESC
LIMIT 10;
3. 8.0 版本
8.0 版本的关键优化:真正支持降序索引。
8.0 版本开始,DESC索引会真正按降序存储,查询时若ORDER BY 字段 DESC,可直接匹配降序索引,无需反向扫描,性能比 5.7 提升明显(尤其大数据量排序场景)。
场景:同样按create_time逆序排序
在 8.0 中,创建DESC索引后,ORDER BY create_time DESC会直接使用该索引,EXPLAIN中Extra列无Using filesort:
-- 8.0中创建“真降序索引”(实际按create_time从大到小存储)
CREATE INDEX idx_create_time_desc ON order(create_time DESC);
-- 查询时直接匹配索引,无需排序
SELECT id, order_no FROM order
ORDER BY create_time DESC
LIMIT 10;
联合索引的降序支持(8.0 增强)
8.0 允许联合索引中部分字段指定DESC,更精准匹配复杂排序场景。
例如 “按status升序、create_time降序排序”:
-- 联合索引:status升序,create_time降序
CREATE INDEX idx_status_asc_create_time_desc ON order(status ASC, create_time DESC);
-- 完全匹配ORDER BY,无文件排序
SELECT id, order_no FROM order
ORDER BY status ASC, create_time DESC
LIMIT 10;
4. 实践建议
1. 优先匹配查询的排序方向:
- 若查询常用
ORDER BY create_time DESC(如订单列表默认 “最新在前”),直接创建create_time DESC索引(8.0 最佳,5.7 次之)。 - 若排序字段同时有
ASC和DESC需求(极少),可分别创建升序和降序索引(但需平衡索引维护成本)。
2. 联合索引的排序字段放最后:
- 索引遵循 “最左前缀原则”,过滤字段(
WHERE条件)放前面,排序字段(ORDER BY)放后面,例如WHERE status=1 ORDER BY create_time DESC,联合索引为(status, create_time DESC)。
3. 避免过度创建降序索引:
- 降序索引与升序索引的维护成本相同(都会占用磁盘空间,影响写入性能),仅对 “高频逆序排序” 的字段创建降序索引,低频场景无需单独创建(依赖优化器反向扫描即可)。
5. 总结
- 创建方式:通过
ASC(默认)或DESC关键字指定,语法简单。 - 版本差异:5.7 仅语法支持降序索引(实际升序存储),8.0 真正支持(按降序存储,性能更优)。
- 核心价值:匹配
ORDER BY的排序方向,避免文件排序,尤其在大数据量的列表查询(如订单、日志)中,能显著提升性能。
回目录: 《面试笔记:MySQL 相关目录》
上一篇: 《面试笔记:MySQL 相关01 – 基础核心》
下一篇: 《面试笔记:MySQL 相关03 – SQL语法与查询优化》
喜欢面试笔记:MySQL 相关02 – 索引这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。
面试笔记:MySQL 相关01 – 基础核心
感谢订阅陶其的个人博客!
基础核心
回目录: 《面试笔记:MySQL 相关目录》
下一篇: 《面试笔记:MySQL 相关02 – 索引》
01. 整体架构
![]()
以5.7版本为基础,8.0的更改会特别标注。
MySQL 采用分层架构,整体可分为 4 层。
连接层、服务层、存储引擎层、文件系统层。
1. 连接层
客户端连接层
- 负责接收客户端(如Java程序、Navicat等)的TCP/IP连接请求,处理身份认证(用户名、密码、主机权限校验)。
- 提供连接池机制,复用已建立的连接(避免频繁 TCP 握手 / 挥手的开销),同时管理连接状态(如空闲超时断开)。
- 核心组件:连接器(验证用户名密码、权限)、连接池(复用已建立的连接,减少握手开销)。
- 8.0版本变化:
- 认证插件升级:默认使用
caching_sha2_password替代 5.7 的mysql_native_password,加密强度更高,需客户端(如 JDBC 驱动)适配支持。 - 连接池优化:增强连接复用效率,减少空闲连接占用的资源,同时支持 “连接属性动态修改”(无需重启连接即可调整部分参数)。
- 认证插件升级:默认使用
2. 服务层
核心处理层
- 所有存储引擎共享的核心层,负责 SQL 的 “解析 – 优化 – 执行” 全流程,不直接操作数据,仅通过接口调用存储引擎。
- 包含查询缓存(默认关闭)、解析器、优化器、执行器等核心逻辑。
- 统一处理日志(如 binlog)、权限二次校验(执行 SQL 前再次确认权限)等通用逻辑。
- 核心流程:接收 SQL 后,先查查询缓存(若命中直接返回)→ 解析器生成语法树(检查 SQL 语法)→ 优化器生成最优执行计划(如选择索引、连接方式)→ 执行器调用存储引擎接口执行计划。
- 8.0版本变化:
- 移除查询缓存组件:因 5.7 中查询缓存命中率极低(数据更新会清空对应表缓存),8.0 直接删除该模块,简化服务层逻辑,避免无效开销。
- 优化器增强:架构层面支持更多优化规则(如复杂 JOIN 的执行计划调整)。
- binlog 默认开启:5.7 中 binlog 默认关闭,8.0 默认启用,且架构上支持 binlog 的 “即时回放”(加速主从同步),无需额外配置。
3. 存储引擎层
数据存储层
- 采用 “插件式架构”,可动态加载不同存储引擎(如 InnoDB、MyISAM),负责数据的实际存储、读取和事务管理。
- 与服务层通过统一的
handler接口交互,服务层无需关心底层数据存储格式(如 InnoDB 的表空间、MyISAM 的文件存储)。 - 默认存储引擎为 InnoDB,架构上支持事务、行锁等特性的底层实现。
- 8.0版本变化:
- 强化 InnoDB 的架构适配:移除其他低效存储引擎(如 Federated),仅保留 InnoDB、MyISAM、Memory 等常用引擎,聚焦 InnoDB 的性能优化。
- 架构层面支持 InnoDB 的新特性:如自增 ID 持久化(通过 redo log 架构实现)、隐藏索引(架构上支持索引的 “逻辑禁用”,不影响物理存储)。
- 锁机制架构优化:行锁的范围判断逻辑在架构层面更精准,减少锁冲突(依赖与服务层的接口交互优化)。
4. 文件系统层
- 负责将数据、日志等持久化到磁盘,依赖操作系统的文件系统(如 ext4、NTFS)。
- 存储文件类型包括:表空间文件(InnoDB 的
.ibd)、日志文件(redo log(重做日志)、binlog(二进制日志)、undo log(回滚日志))、配置文件(my.cnf)等。 - 8.0版本变化:
- 日志文件架构调整:redo log 默认存储路径优化,支持更大的日志文件大小(默认单个文件 1GB,5.7 默认 48MB),提升崩溃恢复效率。
- 表空间文件优化:默认使用独立表空间(5.7 需手动配置),且架构上支持 “表空间加密”(透明数据加密 TDE),文件存储更安全。
- 自增 ID 存储架构变更:5.7 中自增 ID 存于内存,8.0 架构上改为写入 redo log,重启后可通过日志恢复自增 ID 序列,避免重复。
02. 核心组件
1. 连接器
Connection Manager
核心功能:负责客户端连接的建立、管理与身份认证,是客户端与 MySQL 交互的 “入口”。
- 接收 TCP 连接请求后,校验用户名、密码及客户端主机权限(基于mysql.user表),通过后分配连接线程。
- 维护连接状态(如空闲、活跃),默认空闲超时时间为 8 小时(wait_timeout参数控制),超时后自动断开。
- 认证依赖
mysql_native_password插件(默认),加密强度一般,兼容性好。 - 8.0 版本变化:
- 默认认证插件升级为
caching_sha2_password,采用 SHA-256 加密,安全性更高(需客户端驱动支持,如 JDBC 需 8.0 + 版本)。 - 优化连接复用机制,减少空闲连接的资源占用,支持 “连接属性动态修改”(如无需重连即可调整部分会话参数)。
- 默认认证插件升级为
2. 查询缓存
Query Cache
核心功能:缓存 SQL 语句与结果集,相同 SQL(字节级一致)可直接返回缓存结果,减少重复计算。
- 存在但默认关闭(
query_cache_type=OFF),需手动开启;缓存以表为单位,表数据更新(增删改)时会清空该表所有缓存。 - 局限性明显:仅适用于静态数据(如配置表),高并发写场景下命中率极低,反而因缓存维护消耗资源。
- 8.0 版本变化:
- 彻底移除查询缓存组件(相关参数如
query_cache_size失效),原因是其实际应用价值低,移除后简化了服务层逻辑,减少无效开销。
- 彻底移除查询缓存组件(相关参数如
3. 解析器
Parser
核心功能:对 SQL 语句进行语法分析,生成 “语法树”,确保 SQL 符合语法规则。
- 解析过程包括:词法分析(识别关键字、表名、字段名等)→ 语法分析(检查 SQL 结构是否合法,如
SELECT后是否有字段、WHERE是否搭配条件)。 - 若语法错误(如关键字拼写错误),直接返回报错(如 “you have an error in your SQL syntax”)。
- 8.0 版本变化:
- 核心功能不变,但扩展了对新语法的支持(如
WITH RECURSIVE递归查询、降序索引语法),解析效率略有优化(减少语法树生成的内存占用)。
- 核心功能不变,但扩展了对新语法的支持(如
4. 优化器
Optimizer
核心功能:基于语法树生成 “最优执行计划”,目标是最小化执行成本(CPU、IO 开销)。
- 优化逻辑包括:选择合适的索引(如判断全表扫描 vs 索引扫描更快)、调整多表连接顺序(小表驱动大表减少中间结果集)、简化表达式(如
a=1 and a=2直接判定为无效)。 - 依赖表统计信息(如行数、数据分布),但统计信息更新不及时可能导致执行计划偏差。
- 8.0 版本变化:
- 增强统计信息:引入 “直方图”(Histogram),更精准记录数据分布(如字段值的频率),优化器对非均匀分布数据的索引选择更合理。
- 优化规则扩展:支持复杂 JOIN(如多表嵌套连接)的执行计划调整,子查询优化更彻底(减少 “派生表” 的临时表开销)。
5. 执行器
Executor
核心功能:根据优化器生成的执行计划,调用存储引擎接口执行操作,并返回结果。
- 执行前再次校验权限(避免连接建立后权限被修改导致的安全问题)。
- 例如:检查用户是否有目标表的
SELECT权限。
- 例如:检查用户是否有目标表的
- 通过统一接口(如
handler::read_row)调用存储引擎,获取数据后进行过滤、聚合等处理(如WHERE条件过滤、GROUP BY分组)。 - 8.0 版本变化:
- 优化接口调用效率,减少与存储引擎的交互次数(如批量读取数据)。
- 支持 “即时执行”(Instant Execution),对简单查询可跳过部分优化步骤,直接执行,提升响应速度。
6. 日志组件
核心日志模块
负责记录 MySQL 的操作和状态,支撑数据恢复、主从同步等功能。
1. binlog(二进制日志)
- 5.7 版本:默认关闭,需手动开启(
log_bin=ON)。记录所有数据修改操作(增删改、DDL),格式支持STATEMENT(语句)、ROW(行)、MIXED(混合),用于主从同步和时间点恢复。 - 8.0 版本:默认开启。格式默认
ROW(更安全),支持 “即时回放”(Binlog Instant),主从同步时可跳过部分无效日志,提升同步效率。
2. redo log(重做日志,InnoDB 依赖)
- 5.7 版本:InnoDB 专属,记录数据页的物理修改,采用 “循环写” 机制(固定大小文件),保证崩溃后数据可恢复(先写日志再写磁盘,即 WAL 机制)。
- 8.0 版本:默认单个日志文件大小从 48MB 增至 1GB,减少日志切换频率;支持 “并行写入”,提升高并发下的日志写入效率。
3. undo log(回滚日志,InnoDB 依赖)
- 5.7 版本:记录数据修改前的状态,用于事务回滚和 MVCC(多版本并发控制),默认随表空间存储,可能因长期积累导致空间膨胀。
- 8.0 版本:支持 “undo log 自动回收”(通过
innodb_undo_log_truncate参数),无需手动清理,减少维护成本。
7. 权限组件
核心功能:管理用户权限,控制对数据库、表、字段的操作权限。
- 权限存储在
mysql库的系统表中(如user、db、tables_priv),权限修改后需通过FLUSH PRIVILEGES刷新或重启生效(静态权限)。 - 支持库级、表级、列级权限,但缺乏细粒度的动态权限(如管理特定日志的权限)。
- 8.0 版本变化:
- 引入 “动态权限”(如
BINLOG_ADMIN、BACKUP_ADMIN),权限修改后即时生效,无需刷新或重启。 - 权限检查逻辑优化,结合角色(Role)管理(5.7 后期引入但不完善,8.0 强化),可批量分配权限,简化权限管理。
- 引入 “动态权限”(如
8. 总结
MySQL 核心组件的核心逻辑(连接、解析、优化、执行、日志、权限)在 5.7 和 8.0 中保持一致。
8.0 的改进集中在:移除低效组件(查询缓存)、增强安全性(认证插件)、优化性能(优化器、日志)、简化维护(动态权限、undo 回收)等。
03. 存储引擎
MySQL 的存储引擎是负责数据存储、读取及底层特性实现的核心模块,采用 “插件式” 设计,不同引擎支持的功能(如事务、锁机制)差异显著。
1. InnoDB(默认)
InnoDB 是 MySQL 最常用(默认)的存储引擎,以事务支持、高并发为核心优势,5.7 和 8.0 均将其作为默认引擎,版本间优化集中在性能、可靠性和功能扩展。
核心通用特性(5.7 和 8.0 共通)
- 支持事务(ACID 特性):通过 redo log(保证持久性)、undo log(保证原子性和隔离性)实现。
- 行级锁:仅锁定修改的行(而非全表),适合高并发写场景(如电商订单更新)。
- 聚簇索引:数据与主键索引物理存储在一起,查询主键时效率极高。
- 外键约束:支持表间外键关联(如orders表关联users表的user_id)。
5.7 版本 InnoDB 特性
- 自增 ID(AUTO_INCREMENT):存储在内存中,重启 MySQL 后可能因未持久化导致重复(需依赖 binlog 恢复,但存在风险)。
- 索引限制:仅语法支持降序索引(DESC),实际仍按升序存储,查询降序数据时需额外排序。
- 锁机制:间隙锁(Gap Lock)范围较宽泛,可能导致高并发下锁冲突增加(如批量插入相邻 ID 时)。
- undo log:默认随表空间存储,长期运行后可能因未自动回收导致磁盘空间膨胀(需手动清理)。
8.0 版本 InnoDB 关键改进
- 自增 ID 持久化:将自增 ID 写入 redo log,重启后可通过日志恢复,彻底解决 5.7 的重复问题。
- 索引增强:
- 真正支持降序索引(DESC),查询降序排序数据时无需额外排序,直接使用索引。
- 新增 “隐藏索引”(INVISIBLE):可标记索引为隐藏(不影响物理存储),用于临时禁用索引测试性能(无需删除重建)。
- 锁优化:间隙锁范围更精准,减少非必要锁定(如批量插入时仅锁定实际需要的区间),降低锁冲突。
- undo log 自动回收:支持innodb_undo_log_truncate参数(默认开启),自动收缩过大的 undo log,减少人工维护成本。
- 表空间加密:支持透明数据加密(TDE),表空间文件(.ibd)加密存储,提升数据安全性。
2. MyISAM
MyISAM 是早期 MySQL 的默认引擎,因不支持事务和行锁,逐渐被 InnoDB 替代,5.7 和 8.0 中仍保留但应用场景有限。
核心通用特性(5.7 和 8.0 共通)
- 不支持事务和外键:仅适合无需事务保证的场景。
- 表级锁:写操作(增删改)会锁定全表,读操作需等待写锁释放,并发写性能差。
- 独立文件存储:数据存于.MYD文件,索引存于.MYI文件,可直接复制文件迁移表。
5.7 版本:仍有部分场景使用(如只读日志表),但已明确不推荐用于核心业务。
8.0 版本:进一步弱化 MyISAM,默认配置下性能优化倾向 InnoDB,且移除了部分对 MyISAM 的冗余支持(如全文索引的部分优化仅针对 InnoDB)。
3. Memory
内存引擎
数据存储在内存中,适合临时数据处理,性能极快但数据易失(重启丢失)。
5.7 版本特性
- 支持哈希索引(默认)和 B + 树索引,哈希索引适合等值查询(=),不支持范围查询(>、<)。
- 表大小受
max_heap_table_size限制(默认 16MB),超出后会报错。
8.0 版本改进
- 优化内存分配机制,减少小表的内存浪费。
- 支持动态调整
max_heap_table_size(无需重建表),更灵活适配临时数据大小。
4. CSV
逗号分割值引擎
数据以 CSV 格式文件存储(.csv),适合数据交换(如与 Excel、文本文件交互)。
5.7 版本特性
- 表结构存于
.frm文件,数据存于.csv文件,可直接用文本编辑器查看 / 修改。 - 不支持索引和事务,仅适合简单的导入导出场景。
8.0 版本改进
- 增强兼容性:支持 CSV 文件中包含换行符(需特殊处理),减少导入导出时的格式错误。
5. 总结
- InnoDB:5.7 已具备事务、行锁核心能力,8.0 通过自增 ID 持久化、索引优化、锁细化等提升可靠性和性能,是所有业务的首选。
- MyISAM:仅适合只读、低并发场景,8.0 中进一步被边缘化。
- Memory/CSV:作为辅助引擎,8.0 主要优化了易用性(如动态调整内存表大小),核心功能无本质变化。
04. InnoDB存储引擎
InnoDB作为MySQL的默认且常用的搜索引擎,有如下的核心特性:
1. 事务支持(ACID)
- 完全支持事务的原子性(A)、一致性(C)、隔离性(I)、持久性(D)。
2. 锁机制
- 支持行级锁 + 间隙锁(Next-Key Lock),行级锁仅锁定修改行(高并发友好),间隙锁防止幻读。
- 8.0 变化:间隙锁范围更精准,减少非必要锁定(如批量插入相邻 ID 时仅锁实际区间),降低锁冲突概率。
3. 聚簇索引
- 数据与主键索引物理存储在一起,形成 B + 树结构,主键查询可直接获取数据。
4. 自增 ID(AUTO_INCREMENT)
- 自增 ID 存储在内存中,MySQL 重启后可能因未持久化导致序列重复(需依赖 binlog 部分恢复)。
- 8.0 变化:自增 ID 写入 redo log 持久化,重启后可通过日志恢复序列,彻底解决重复问题。
5. 索引特性
- 支持 B + 树索引(默认)、全文索引、前缀索引;仅语法支持降序索引(
DESC),实际仍按升序存储。 - 8.0 变化:
- 真正支持降序索引,查询降序排序数据时无需额外排序,直接复用索引。
- 新增隐藏索引(
INVISIBLE),可临时禁用索引(不删除),方便测试索引性能影响。
6. 日志依赖(redo log/undo log)
- redo log:循环写的物理日志,记录数据页修改,保证崩溃恢复。
- undo log:记录数据修改前的状态,用于事务回滚和 MVCC(多版本并发控制),默认随表空间存储,易膨胀需手动清理。
- 8.0 变化:
- redo log 默认单个文件大小从 48MB 增至 1GB,减少日志切换开销。
- undo log 支持自动回收(
innodb_undo_log_truncate默认开启),无需手动清理。
7. 外键约束
- 支持表间外键关联(如:
orders.user_id关联users.id),保证数据引用完整性。
8. 安全性增强
- 无表空间加密功能,数据文件(
.ibd)以明文存储。 - 8.0 变化:新增透明数据加密(TDE),支持表空间文件加密存储,提升敏感数据安全性(如用户密码、支付信息表)。
05. 三大范式和反范式
MySQL 的三大范式(1NF、2NF、3NF)是关系型数据库设计的基础原则,目的是减少数据冗余、保证数据一致性、避免插入 / 更新 / 删除异常;
反范式是有意打破范式规则,有策略地保留适量冗余,目的是减少多表 JOIN,提升查询效率(尤其读多写少场景)。
1. 第一范式(1NF)
第一范式(1NF):字段原子化,不可再分。
- 核心要求:表中所有字段的值必须是 “原子性” 的(不可再拆分为更小的数据单元)。
- 目的:避免同一字段存储多维度信息,导致查询和修改混乱。
- 例子:
- 反例:用户表的
address字段存储 “中国 – 北京 – 朝阳区”(可拆分为country、city、district); - 正例:拆分为
country、city、district三个字段,每个字段不可再分。
- 反例:用户表的
- 实践:1NF 是基础,几乎所有业务表都需满足(如:订单表的
phone字段不存储 “固话 + 手机”,而是单独字段)。
2. 第二范式(2NF)
第二范式(2NF):消除 “部分依赖”,非主属性完全依赖主键。
- 核心要求:在 1NF 基础上,表的主键必须是 “联合主键”(多字段组成),且所有非主属性必须完全依赖于整个主键,不能仅依赖主键的一部分(即消除 “部分依赖”)。
- 目的:避免因主键部分字段变化导致的数据异常(如修改部分主键后,非主属性需联动更新)。
- 例子:
- 反例:订单项表(联合主键
order_id+product_id)中,product_name仅依赖product_id(主键的一部分),属于部分依赖; - 正例:
product_name应存储在产品表中,订单项表只存product_id,通过关联产品表获取名称(非主属性quantity、price完全依赖order_id+product_id)。
- 反例:订单项表(联合主键
- 实践:多对多关系的中间表(如 “用户 – 角色” 关联表)需满足 2NF,避免冗余存储角色名称等信息。
3. 第三范式(3NF)
第三范式(3NF):消除 “传递依赖”,非主属性不依赖其他非主属性。
- 核心要求:在 2NF 基础上,所有非主属性必须直接依赖于主键,不能依赖于其他非主属性(即消除 “传递依赖”)。
- 目的:避免因某个非主属性变化,导致其他非主属性需联动更新(如 A 依赖主键,B 依赖 A,则 B 传递依赖主键)。
- 例子:
- 反例:用户表(主键
user_id)中,area_name依赖area_id,area_id依赖user_id,则area_name传递依赖user_id; - 正例:
area_name应存储在区域表中,用户表只存area_id,通过关联区域表获取名称(非主属性仅直接依赖user_id)。
- 反例:用户表(主键
- 实践:用户表、商品表等核心表需满足 3NF,避免存储 “部门名称”“分类名称” 等可通过关联获取的字段。
4. 反范式
反范式是有意打破范式规则,有策略地保留适量冗余,目的是减少多表 JOIN,提升查询效率(尤其读多写少场景)。
- 核心思路
通过在表中冗余存储其他表的字段,避免查询时关联多个表(JOIN操作耗时,尤其大数据量时)。 - 例子:
- 电商商品列表页需展示 “商品名称 + 分类名称”,若严格遵循 3NF,需关联
product表和category表; - 反范式优化:在
product表中冗余category_name字段,查询时直接从product表获取,无需 JOIN。
- 电商商品列表页需展示 “商品名称 + 分类名称”,若严格遵循 3NF,需关联
- 适用场景
- 高频查询、低频更新:如商品详情页(查询频繁,分类名称很少修改),冗余后查询性能提升 10 倍以上;
- 多表关联复杂:如订单列表需关联用户表、商品表、物流表,冗余 “用户名”“商品名” 后,查询从多表 JOIN 简化为单表查询;
- 统计分析场景:报表系统需聚合多维度数据,冗余存储聚合结果(如 “每月销售额”),避免实时计算。
- 注意事项
- 冗余字段需同步更新:如
category_name修改后,需同步更新product表中的冗余字段(可通过触发器、Java 代码事务保证); - 控制冗余范围:只冗余高频查询的核心字段(如名称、状态),避免表过大(如冗余大文本字段会增加存储和 IO 成本)。
- 冗余字段需同步更新:如
5. 总结
- 范式:适合写多读少、数据一致性要求高的场景(如订单系统、用户中心),通过减少冗余降低更新异常风险;
- 反范式:适合读多写少、查询性能敏感的场景(如电商列表、报表),通过可控冗余提升查询效率。
- 实际开发中,很少严格遵循某一范式,而是混合使用(如核心交易表用 3NF 保证一致性,查询表用反范式提升性能)。
06. DDL、DML、DCL、DQL
MySQL 中 DDL、DML、DCL、DQL 是按操作类型划分的四大类 SQL 语言,分别对应:
- DDL:数据库结构定义;
- DML:数据操纵;
- DCL:权限控制;
- DQL:数据查询。
1. DQL:数据查询语言
Data Query Language。
- 定义:用于从数据库中查询数据,不修改数据或结构,是业务系统中最频繁的操作。
- 核心命令:
SELECT(含WHERE、JOIN、GROUP BY、ORDER BY、LIMIT等子句)。 - 作用:从表中提取所需数据,支撑业务展示(如列表页、详情页)、统计分析(如报表)等场景。
- 实践要点:
- 是 Java 开发中最常用的 SQL(如查询用户信息、订单列表),需结合索引优化(如
WHERE条件加索引、避免SELECT *); - 复杂查询(多表关联、聚合)需用
EXPLAIN分析执行计划,避免全表扫描或文件排序。
- 是 Java 开发中最常用的 SQL(如查询用户信息、订单列表),需结合索引优化(如
2. DML:数据操纵语言
Data Manipulation Language。
- 定义:用于修改表中的数据(增、删、改),会改变数据内容,但不改变表结构。
- 核心命令:
INSERT(新增);UPDATE(修改);DELETE(删除)。
- 作用:处理业务数据的生命周期(如创建订单、更新状态、删除无效记录)。
- 实践要点:
- 操作会触发事务(默认自动提交,可通过
BEGIN手动控制),需保证原子性(如订单创建时同时扣减库存,失败则回滚); - 批量操作优化:
INSERT用VALUES (),(),()批量插入(比单条循环高效),DELETE/UPDATE避免全表操作(加WHERE条件,如DELETE FROM log WHERE create_time < '2024-01-01'); - 高频写入场景(如日志)需控制频率,避免锁表影响查询。
- 操作会触发事务(默认自动提交,可通过
3. DDL:数据定义语言
Data Definition Language。
- 定义:用于定义或修改数据库、表、索引等结构,会改变数据库的元数据(结构信息)。
- 核心命令:
CREATE(创建,如CREATE TABLE、CREATE INDEX);ALTER(修改,如ALTER TABLE ADD COLUMN);DROP(删除,如DROP TABLE);TRUNCATE(清空表)。
- 作用:初始化数据库结构(如建表、加字段)、调整表结构(如新增索引、扩展字段)。
- 实践要点:
- 执行时可能锁表(尤其
ALTER TABLE在 InnoDB 中,大表修改会阻塞读写),生产环境需在低峰期执行,大表建议用在线 DDL 工具(如 pt-online-schema-change); - 谨慎使用
DROP和TRUNCATE(不可逆,TRUNCATE会清空数据且不触发事务回滚); - 索引相关 DDL(
CREATE INDEX)需评估必要性(索引提升查询但降低写入性能)。
- 执行时可能锁表(尤其
4. DCL:数据控制语言
Data Control Language。
- 定义:用于管理数据库用户权限和事务控制,控制谁能操作数据、操作范围。
- 核心命令:`
- GRANT`(授予权限);
REVOKE(撤销权限);COMMIT(提交事务);ROLLBACK(回滚事务)。
- 作用:保障数据安全(如限制应用账号只能操作指定表)、控制事务一致性。
- 实践要点:
- 权限遵循 “最小原则”:应用程序账号只授予
SELECT/INSERT/UPDATE等必要权限,禁止DROP/GRANT等高风险权限,避免用 root 账号直接连接业务系统; - 事务控制(
COMMIT/ROLLBACK)需在 Java 代码中配合业务逻辑(如分布式事务场景,确保多库操作一致性)。
- 权限遵循 “最小原则”:应用程序账号只授予
5. 总结
四类语言分工明确:
- DQL:查数据(业务展示核心);
- DML:改数据(业务操作核心);
- DDL:改结构(架构维护核心);
- DCL:控权限(安全保障核心)。
07. 数据库表字段类型
1. 核心字段类型及适用场景
1. 数值型:适合存储数字(整数、小数)
| 类型 | 特点(长度 / 范围) | JDK8对应类型 | 适用场景 |
|---|---|---|---|
TINYINT |
1 字节,范围 – 128 ~ 127 (无符号 0 ~ 255) |
Byte |
状态标识(如status:0 – 禁用、1 – 正常)、性别(0 – 女、1 – 男) |
INT |
4 字节,范围 – 21 亿 ~ 21 亿 | Integer |
普通 ID(如user_id、order_id,中小规模业务足够)、数量(如count) |
BIGINT |
8 字节,范围 ±9e18, 大约±90亿亿 |
Long |
大整数 ID(如分布式 ID、雪花 ID)、 高频增长数据(如万亿级订单量) |
DECIMAL(M,D) |
高精度小数 (M 总长度,D 小数位) |
java.math.BigDecimal |
金额(如amount:DECIMAL(10,2)表示最多 10 位,2 位小数)、利率 |
FLOAT/DOUBLE |
单 / 双精度浮点 (有精度损失) |
Float/Double |
非精确计算(如温度、重量,允许微小误差) |
2. 字符串型:适合存储文本
| 类型 | 特点(长度 / 范围) | JDK8对应类型 | 适用场景 |
|---|---|---|---|
CHAR(N) |
固定长度(N 字节,0 ~ 255), 空格填充 |
String |
长度固定的字符串(如手机号CHAR(11)、身份证号CHAR(18)) |
VARCHAR(N) |
可变长度(0 ~ 65535), 存储实际长度 |
String |
长度不固定的文本(如用户名VARCHAR(50)、地址VARCHAR(200)) |
TEXT |
大文本(最大 64KB) | String |
较长文本(如商品描述、用户备注,不适合建索引) |
MEDIUMTEXT/LONGTEXT |
更大文本(16MB/4GB) | String |
超大文本(如文章内容、日志详情,慎用,会拖慢查询) |
3. 日期时间型:适合存储时间
| 类型 | 特点(长度 / 范围) | JDK8对应类型 | 适用场景 |
|---|---|---|---|
DATETIME |
8 字节,范围 1000 ~ 9999 年, 无时区 |
java.time.LocalDateTime(不推荐 Date) |
业务时间(如订单创建时间create_time,不受服务器时区影响) |
TIMESTAMP |
4 字节,范围 1970 ~ 2038 年, 受时区影响 |
java.time.LocalDateTime(不推荐 Date) |
记录系统时间(如最后更新时间update_time,自动随时区转换) |
DATE |
3 字节,仅日期(年月日) | java.time.LocalDate(不推荐 Timestamp) |
生日、到期日(如birthday、expire_date) |
TIME |
3 字节,仅时间(时分秒) | java.time.LocalTime |
时段记录(如会议时长、打卡时间) |
4. 特殊类型:针对性场景
| 类型 | 特点(长度 / 范围) | JDK8对应类型 | 适用场景 |
|---|---|---|---|
ENUM |
枚举(存储整数,显示字符串) | String |
固定可选值(如pay_type:ENUM('WECHAT','ALIPAY','CARD')) |
SET |
集合(多选,最多 64 个值) | String |
多选项(如tags:SET('hot','new','discount')) |
BLOB |
二进制数据(如图片、文件) | byte[](字节数组) |
小型二进制(如头像缩略图,大型文件建议存 OSS,库中只存 URL) |
2. 经典面试点
1. 数值型:精度与范围陷阱
-
金额为什么用
DECIMAL而非FLOAT?
FLOAT是浮点型,存在精度损失(如0.1 + 0.2 = 0.300000004),而DECIMAL是精确小数,适合金额等强精度场景。 -
INT和BIGINT怎么选?
中小业务(千万级数据)用INT足够;分布式系统(如订单 ID 用雪花 ID,长度 18 位)必须用BIGINT,避免溢出。
2. 字符串型:长度与性能
-
CHAR和VARCHAR的核心区别?
CHAR固定长度,查询快但浪费空间(如手机号用CHAR(11)比VARCHAR(11)高效,无需计算长度);
VARCHAR节省空间,但查询需额外解析长度,适合长度波动大的场景(如地址)。 -
为什么不建议
VARCHAR(255)滥用?
MySQL 中VARCHAR(255)在某些引擎(如InnoDB)中会按 255 字节分配临时内存,即使实际数据很短,也会浪费内存(尤其排序、JOIN时),建议按实际需求设长度(如用户名VARCHAR(50))。 -
TEXT类型的坑?
TEXT字段不适合建索引(即使建索引也只能取前 N 个字符),且查询时会额外 IO,大文本建议拆分表(如商品表存desc_id,关联单独的product_desc表存TEXT内容)。
3. 日期时间型:时区与范围
-
DATETIME和TIMESTAMP怎么选?
业务时间(如订单创建时间)用DATETIME(固定值,不受服务器时区影响);系统时间(如最后修改时间)用TIMESTAMP(自动更新,适配多时区部署)。 -
TIMESTAMP的 2038 年问题?
因范围是 1970 ~ 2038 年,长期系统需注意(可改用DATETIME规避)。
4. 枚举类型ENUM的利弊
- 优点:存储高效(用整数存字符串),约束数据合法性(只能选定义的值);
- 缺点:修改枚举值需
ALTER TABLE(DDL 操作,大表阻塞),不适合值频繁变化的场景(如活动状态,建议用TINYINT+ 字典表替代)。
3. 总结
字段类型选择的核心原则:“够用即可,避免冗余”
- 数字优先选小类型(如状态用
TINYINT而非INT); - 字符串按长度固定与否选
CHAR/VARCHAR,避免大文本字段拖累主表; - 日期根据时区需求选
DATETIME/TIMESTAMP; - 金额、ID 等关键字段必须保证精度和范围,避免溢出或精度丢失。
回目录: 《面试笔记:MySQL 相关目录》
下一篇: 《面试笔记:MySQL 相关02 – 索引》
喜欢面试笔记:MySQL 相关01 – 基础核心这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。
送别
周二早上,刚换好衣服准备去上班。手机响了,一个陌生的号码,归属地显示的是潍坊。之前也会接到各种陌生号码的电话,为了解决 iphone 的骚扰电话,开了自动录音功能。但是,这个功能并不稳定,录音有时候显示不完整,有时候对方打了很多次电话,手机却连响都不会响。周末的时候把这个功能关掉了。
电话接起来,对面传来一个熟悉的声音,是四叔家的哥哥:“二大爷(二伯)走了,明天的公事……”
“好的,我知道了,我明天回”简单的答复之后就挂断了电话。
快到中午的时候,又接到电话,通知了下时间,早上八点半。这个时间,如果早上从青岛走,六点之前就得出发。决定下班之后,往回赶。工作日的晚上,路上的车并不多。七点多出发,到家的时候也九点多了,好在提前远程开了空调,到家的时候,温度也到了 20 多度了。
客厅的桌子上还放着国庆走的时候落在上面的零食,袋子开着,并没封口。试了一下,已经潮了。把桌子上的东西都收拾了一下扔到了垃圾桶里。
好在冰箱里还有上次回来的时候放的饮料,就住一晚。没开电视,也没开饮水机,两瓶魔爪下肚之后,感觉也没那么渴了。躺床上,刷会儿手机,眨眼已经十一点多了,想着早上不能起太晚,简单洗刷就睡了。
睁眼看了下时间,七点,爬起来收拾下,准备出门。收拾好一切一定七点半了,刚要出门的时候姐姐打电话问出门了没。
开上车,去古城煎包吃个早餐。到小学门口的时候,依然堵的不动了。果断调头往另外一个方向走,只是,这县城的生活节奏,慢吞吞,一个红绿灯都过不了几辆车。那种闲庭信步的悠闲感,真的适应不了。
要了两个煎包,一碗豆浆。到角落的桌子边坐下来,开始吃,一口下去,竟然齁咸。一向味道不错的煎包,今天的确有失水准。好在还有碗豆浆,草草吃完,继续往殡仪馆赶。
等自己赶到的时候,依然很多然都到了。只是,现在很多人都变了模样,年轻的孩子们,自己甚至都已经认不出谁是谁。自己同辈的姐姐、哥哥们却基本都没怎么变样,很多人也老了很多。
这是自己第三次来这个地方,第一次是送自己的父亲,第二次是送对象的爷爷,这第三次是送自己的二伯。
第一次来的时候,依然太久远了,一眨眼二十年了。很多的事情都忘记了,记忆中仅剩的一点点记忆,就是领骨灰出来的时候,给的骨灰盒太小。烧完的骨灰,竟然没法完全装到骨灰盒里。有那么一块骨头还翘在外面。二伯用脚踩着骨灰盒才把骨灰盒给扣上,那小小的骨灰盒啊,竟然死了都那么不体面。因为父亲生病欠了太多的钱,那时也确实买不起更好的骨灰盒,更好的骨灰盒会更大一些。
现在二伯自己也走了,国庆期间自己又去探望了一次,说稍微好转了一点。尽管如此,但是这么久了没从重症监护室出来,总觉得,也是早晚的事情,这一天总是会来的。
到现在,父亲兄弟四人,现在也仅剩下四叔一人了。二伯在老家没有什么家产,也没房子,最终的选择就是县城的公墓。之前总是远远的看到过公墓,或者在电视上看到。自己走进公墓之后才发现,真的是一排一排,写满了名字。所有的人,最后终将成为一抔黄土,埋没在这黄土之中,两代之后,再也没人记得这个人曾经存在过。此时,这是个就真的从这个世界消失了。
一切结束后,回程的路上,想着去壳牌加油站加个油,预付卡还有两百多,依然不够一次了。又充了500 进去,结果到加油站 发现加油站给围起来了,不知道是在装修还是干嘛。这 500 块钱,不知道猴年马月才能再用上了。
今天早上出门的时候,看到绿化带的月季依然坚强的绽放着,虽然已是寒冬,哪怕不合时宜,只是赶上了这样的时间,也要努力的绽放。至于其他的,由他去吧。
Cloudflare 全球大故障 “500“席卷整个互联网
又是计划赶不上变化的一周,启程回家
发这篇博文的时候已经躺在了河南老家的床上,本来还没想这周回来的,前几天还计划这周末再去哪里玩一下,下周22号再回来了,结果突然来了个变故,不知道是哪位人才想出来的歪点子,由于第十五届全运会的原因,近期我们这不让干活,从5号到22号,本来好好的摸鱼几天,周三晚上公司突然通知开会,让从13号开始一直到23号,直接放假了,然后工作日不记工资,也就是周四周五,还有下周5天,美名其曰放假了,这不是纯纯停薪留职嘛,这样搞和临时工有什么区别,这次既然有这个开头,后面我感觉再有这种长时间的不让操作的时间说不定都会实行这种方式,那到时候次数多了可就不像这次这么坦然接受了。
这整得正好赶上搬家的时间,既然已经这样了,那就更改计划,直接开始收拾东西,该装车装车,大件装不下的发快递,昨天差不多收拾了一下又寄了300块的德邦,其他一些重的东西都装在车上了,然后我的东西都搬到给我安排的公司宿舍了,前几天也去看了下宿舍环境,本来觉得空间啥的还都挺大的,唯一美中不足的就是进去没有个门,结果送过去东西之后也是摆了一地还没收拾显得乱七八糟也没那么大空间了。。。过几天回去再收拾吧。
上午差不多收拾完东西还带宝宝去中科大拍了个照片,也是证明来过了,以后可是要冲着这目标去的,哈哈。
收拾完又打扫了下租房子这里的卫生,喊房东过来看了下,整得干干净净的也没说啥,上次整的洗手台她也能接受,说挺好,不掉就行。中午又在外面简单吃了个饭,12点多开始加满油出发,不得不说非节假日跑高速就是舒服,一路畅通无阻,差不多430公里,四个半小时就到了,到家天还没黑,挺好,不得不说后备箱+副驾驶+一个后座,塞的满满当当,回来卸车的时候用三轮车拉都拉了三车,回来待个三四天吧,计划周二高铁返程合肥,就先留宝宝她们在家吧。
![]()