🥳🥳🥳 茫茫人海千千万万,感谢这一刻你看到了我的文章,感谢观赏,大家好呀,我是最爱吃鱼罐头,大家可以叫鱼罐头呦~🥳🥳🥳
如果你觉得这个【重启人生计划】对你也有一定的帮助,加入本专栏,开启新的训练计划,漫长成长路,千锤百炼,终飞升巅峰!无水文,不废话,唯有日以继日,终踏顶峰! ✨✨欢迎订阅本专栏✨✨
❤️❤️❤️ 最后,希望我的这篇文章能对你的有所帮助! 愿自己还有你在未来的日子,保持学习,保持进步,保持热爱,奔赴山海! ❤️❤️❤️
序言
大家好,我是最爱吃鱼罐头,距离离职已经过去一个月了,目前进度为2,打算重新找工作倒计时28天,当然这其中也会去投递面试。
山野万万里,人生路漫漫,日暮酒杯淡饭,一半一半,一半是山川湖海,一半是人间烟火。
今日回顾
真正执行计划时,才发现时间其实过得很快,我也想抓紧时间去努力提升自己,但我感觉焦虑已经悄然而生。这几天都看了招聘的,发现也还有很多公司都在招人,但我知道,机会毕竟有限。以前去面试的时候,其实心里或多或少带着有底气的心里,现在其实没有后路可言。我只能不断抓紧时间去提升自己,抓住每一次机会。人生的不如意,对每个人的当下来说,都是最好的安排。最后还是得拒绝焦虑,拒绝内耗,随遇而安。
挑战之旅第二天,今天早上大概回顾了下jvm的内容,后面就刷day3的算法题,难度简单,稍微想一下还是可以快速做出来的,下午就继续刷了jvm的面试题,明天开始算法和面试题都进入新的阶段。今晚的时间,会去看看链表的视频。
算法回顾
删除有序数组中的重复项 📍
对于这道题的讲解呢,核心的关键在于双指针:
双指针的核心思想是通过使用两个指针在数组中进行遍历,从而达到相应的目的。
最常见的双指针算法的应用有:
- 对撞指针:两个指针分别从数据结构的两端向中间移动,通常用于解决排序数组中寻找某些特定的对或条件问题。例如,找到数组中和为目标值的两个数。
- 快慢指针:通常用于链表相关问题,如检测链表是否有环、找到链表的中间节点等。一个指针每次移动一步,另一个指针每次移动两步。
- 滑动窗口:一个指针表示窗口的开始位置,另一个指针表示窗口的结束位置。这个技术可以用于处理子数组或子串的相关问题,比如计算最大/最小值、找到符合条件的最长子串等。
而双指针的关键在于:
- 指针的起始位置的选取
- 指针的移动方向
- 指针的移动速度
而在昨天的二分查找📍题目中,其实也有双指针的影子,它就是对撞指针的实际应用。下面的几道就是相关快慢指针和滑动窗口的简单应用。
代码实现:
package com.ygt.day3;
/**
* 26. 删除有序数组中的重复项
* https://leetcode.cn/problems/remove-duplicates-from-sorted-array/description/
* 给你一个 非严格递增排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。
* 考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:
* 更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
* 返回 k 。
* 判题标准:
* 系统会用下面的代码来测试你的题解:
* int[] nums = [...]; // 输入数组
* int[] expectedNums = [...]; // 长度正确的期望答案
* int k = removeDuplicates(nums); // 调用
* assert k == expectedNums.length;
* for (int i = 0; i < k; i++) {
* assert nums[i] == expectedNums[i];
* }
* 如果所有断言都通过,那么您的题解将被 通过。
* 输入:nums = [0,0,1,1,1,2,2,3,3,4]
* 输出:5, nums = [0,1,2,3,4]
* 解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
* @author ygt
* @since 2024/8/13
*/
public class RemoveDuplicates {
public static void main(String[] args) {
int[] nums = {0,0,1,1,1,2,2,3,3,4};
System.out.println("删除有序数组中的重复项的答案:" + new RemoveDuplicates().removeDuplicates(nums));
}
/*主要解题思路:需要掌握双指针*/
public int removeDuplicates(int[] nums) {
// 需要注意的一点是,"原地" 即在原数组上操作 --> 所以不能创建一个新数组
/*
这道题的思路:在于双指针的使用,顾名思义,需要两个指针,通过两个指针不断比较,移动指针实现目的。
定义两个指针:fast:快指针 slow:慢指针
开始比较,发现0=0 ,并且f向前移动 第二次开始比较,0!=1,s移动一位,并替换为1
一开始的数组 --> 移动后的数组 --> 移动后的数组
[ 0, 0, 1, 1, 2, 3] [ 0, 0, 1, 1, 2, 3] [ 0, 1, 1, 1, 2, 3]
s f s f s f
第三次开始比较,f一直移动到2处,1!=2,s移动一位,并替换为2 第四次开始比较,f移动到3处,2!=3,s移动一位,并替换为3
--> 移动后的数组 --> 移动后的数组 --> 移动后的数组
[ 0, 1, 1, 1, 2, 3] [ 0, 1, 2, 1, 2, 3] [ 0, 1, 2, 3, 2, 3]
s f s f s f
最后f到达数组末尾,结束
*/
int slow = 0, fast = 1;
// fast只能移动到数组末尾处
while (fast < nums.length) {
if(nums[fast] != nums[slow]) {
nums[++slow] = nums[fast];
}
fast++;
}
// 可以看一下当前数组 --> 可以看到前面正常改成功了,slow后面的是不管的。
//System.out.println(Arrays.toString(nums));
// 最终长度就是slow+1的大小
return slow+1;
}
}
移除元素 📍
对于这道题的讲解呢,也是双指针的快慢指针应用,而与上道题的区别,在于增加了一个val值,并且这道题跟顺序没大关系,只要是等于val的元素,就可以排除掉。
代码实现:
package com.ygt.day3;
/**
* 27. 移除元素
* 给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。
* 假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作:
* 更改 nums 数组,使 nums 的前 k 个元素包含不等于 val 的元素。nums 的其余元素和 nums 的大小并不重要。
* 返回 k。
* 用户评测:
* 评测机将使用以下代码测试您的解决方案:
* int[] nums = [...]; // 输入数组
* int val = ...; // 要移除的值
* int[] expectedNums = [...]; // 长度正确的预期答案。
* // 它以不等于 val 的值排序。
* int k = removeElement(nums, val); // 调用你的实现
* assert k == expectedNums.length;
* sort(nums, 0, k); // 排序 nums 的前 k 个元素
* for (int i = 0; i < actualLength; i++) {
* assert nums[i] == expectedNums[i];
* }
* 如果所有的断言都通过,你的解决方案将会 通过。
* 输入:nums = [0,1,2,2,3,0,4,2], val = 2
* 输出:5, nums = [0,1,4,0,3,_,_,_]
* 解释:你的函数应该返回 k = 5,并且 nums 中的前五个元素为 0,0,1,3,4。
* 注意这五个元素可以任意顺序返回。
* 你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。
* @author ygt
* @since 2024/8/13
*/
public class RemoveElement {
public static void main(String[] args) {
int[] nums = {0,1,2,2,3,0,4,2};
System.out.println("移除元素得到的答案:" + new RemoveElement().removeElement(nums, 2));
}
/*主要解题思路:需要掌握双指针*/
public int removeElement(int[] nums, int val) {
// 需要注意的一点是,"原地" 即在原数组上操作 --> 所以不能创建一个新数组
//
/*
首先,实现的关键还是双指针,其次这道题跟前面一道题的区别:
增加了一个val值,这道题跟顺序没大关系,只要是等于val的元素,就可以排除掉,而且返回的数组元素是无序。
移动过程中,只要不是等于val值,s的替换为f的值。
比较,f继续移动,直到遇到val值 f开始移动,直到遇到不是val值,s替换后并移动
一开始的数组 --> 移动后的数组 --> 移动后的数组
[ 0,1,2,2,3,0,4,2] [ 0,1,2,2,3,0,4,2] [0,1,3,2,3,0,4,2]
s s s f
f f
一直移动,s不断替换,直到遇到2 但是到数组末尾了,不需要再替换了,结束
--> 移动后的数组 --> 移动后的数组
[0,1,3,0,4,0,4,2] [0,1,3,0,4,0,4,2]
s f s f
*/
int slow = 0, fast = 0;
// fast只能移动到数组末尾处
while (fast < nums.length) {
if(nums[fast] != val) {
nums[slow++] = nums[fast];
}
fast++;
}
// 可以看一下当前数组 --> 可以看到前面正常改成功了,slow后面的是不管的。
//System.out.println(Arrays.toString(nums));
// 最终长度就是slow的大小 --> 这里不需要加一了,因为slow有自动加一。
return slow;
}
}
有序数组的平方
对于这道题的讲解呢,是双指针的对撞指针应用。
代码实现:
package com.ygt.day3;
import java.util.Arrays;
/**
* 977. 有序数组的平方
* https://leetcode.cn/problems/squares-of-a-sorted-array/description/
* 给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
* 输入:nums = [-4,-1,0,3,10]
* 输出:[0,1,9,16,100]
* 解释:平方后,数组变为 [16,1,0,9,100]
* 排序后,数组变为 [0,1,9,16,100]
* @author ygt
* @since 2024/8/13
*/
public class SortedSquares {
public static void main(String[] args) {
int[] nums = {-4,-1,0,3,10};
System.out.println("有序数组的平方的答案:" + Arrays.toString(new SortedSquares().sortedSquares(nums)));
}
/*主要解题思路:需要掌握双指针*/
public int[] sortedSquares(int[] nums) {
// 需要注意的一点是,原先为负数的值,平方后,可能反而变大。
// 实现的关键还是双指针, 实现思路:前后比较,从后不断填充大的值。
int left = 0, right = nums.length - 1;
// 需要一个新数组来实现,不断填充数据。
int[] result = new int[nums.length];
int resultIndex = right;
while (left <= right){
int leftValue = nums[left] * nums[left];
int rightValue = nums[right] * nums[right];
// 比较大小,谁大,填充谁的数据,并移动指针。
if(leftValue > rightValue) {
result[resultIndex] = leftValue;
left ++;
}else {
result[resultIndex] = rightValue;
right --;
}
resultIndex --;
}
return result;
}
}
最长连续递增序列
对于这道题的讲解呢,是双指针的滑动窗口应用。
滑动窗口的基本思想是用一个窗口(通常是一个固定大小的区间)在数组上滑动,通过维护这个窗口内的状态来解决问题。
滑动窗口的基本操作
- 初始化:设定窗口的初始位置,通常是数组的开始位置或数据流的开始位置;
- 扩展:逐步扩大窗口的范围,直到达到一定条件或者窗口的右边界;
- 收缩:当需要缩小窗口时,移动窗口的左边界,直到满足特定条件。
代码实现:
package com.ygt.day3;
/**
* 674. 最长连续递增序列
* https://leetcode.cn/problems/longest-continuous-increasing-subsequence/description/
* 给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
* 连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,
* 那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。
* 输入:nums = [1,3,5,4,7]
* 输出:3
* 解释:最长连续递增序列是 [1,3,5], 长度为3。
* 尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。
*
* @author ygt
* @since 2024/8/13
*/
public class FindLengthOfLCIS {
public static void main(String[] args) {
int[] nums = {1, 3, 5, 4, 7};
System.out.println("最长连续递增序列的答案:" + new FindLengthOfLCIS().findLengthOfLCIS(nums));
}
/*主要解题思路:需要掌握双指针和滑动窗口*/
public int findLengthOfLCIS(int[] nums) {
// 滑动窗口的思想就是双指针,根据需要不断扩大指针,从而达到一个窗口的目的。
/*
f一直移动到4,最大为3 s重新替换为4处,f一直移动到7,最大为2
[1,3,5,4,7] [1,3,5,4,7] [1,3,5,4,7]
s s s
f f f
所以最终最大为3
*/
int slow = 0 , fast = 1, max = 1;
while (fast < nums.length) {
// fast只要是比前面位置小或者相等,就不是递增序列,可以判断当前的最长连续递增序列
if(nums[fast-1] >= nums[fast]){
slow = fast;
}
fast++;
max = Math.max(max, (fast - slow));
}
return max;
}
}
最大连续 1 的个数
对于这道题的讲解呢,万变不离其宗,也是双指针的滑动窗口应用。
代码实现:
package com.ygt.day3;
/**
* 485. 最大连续 1 的个数
* https://leetcode.cn/problems/max-consecutive-ones/description/
* 给定一个二进制数组 nums , 计算其中最大连续 1 的个数。
* 输入:nums = [1,1,0,1,1,1]
* 输出:3
* 解释:开头的两位和最后的三位都是连续 1 ,所以最大连续 1 的个数是 3.
* @author ygt
* @since 2024/8/13
*/
public class FindMaxConsecutiveOnes {
public static void main(String[] args) {
int[] nums = {1,1,0,1,1,1};
System.out.println("最大连续 1 的个数的答案:" + new FindMaxConsecutiveOnes().findMaxConsecutiveOnes(nums));
}
/*主要解题思路:需要掌握双指针和滑动窗口*/
public int findMaxConsecutiveOnes(int[] nums) {
// 滑动窗口的思想就是双指针,根据需要不断扩大指针,从而达到一个窗口的目的。
// 这道题的思路,跟上一道一样类似,无非就是判断条件改了而已,判断不等于1而已。
int slow = 0, fast = 0, max = 0;
while (fast < nums.length) {
if(nums[fast] != 1) {
slow = fast + 1;
}
fast++;
max = Math.max(max, (fast - slow));
}
return max;
}
}
小结算法
今天的算法还是相对比较简单,很好刷,认真思考下,就可以完成的。
明日内容
基础面试题
下面的题目的答案是基于自己的理解和思考去编写出来的,也希望大家如果看到了,可以根据自己的理解去转换为自己的答案。
当然很多思考也有参考别人的成分,但是自己能讲述出来就是最棒的。
这里有一篇阿里的mysql面试题
明天给自己的目标是,二倍速过一下mysql的视频,能过多少算多少。
1. 一条SQL的执行过程是怎样的?
- 连接器:
- 客户端与数据库建立连接,并发送 SQL 语句给数据库服务;
- 连接器验证客户端的身份和权限,确保用户有足够的权限执行该 SQL 语句。
- 查询缓存:
- 连接器首先检查查询缓存,尝试找到与当前 SQL 语句完全相同的查询结果;
- 如果在缓存中找到匹配的结果,查询缓存直接返回结果,避免了后续的执行过程。
- 分析器:
- 若查询不命中缓存,连接器将 SQL 语句传递给分析器进行处理;
- 分析器对 SQL 语句进行语法分析,确保语句的结构和语法正确;
- 分析器还会进行语义分析,检查表、列、函数等对象的存在性和合法性,为其创建语法树,并根据数据字典丰富查询语法树,会验证该客户端是否具有执行该查询的权限。
- 优化器:
- 分析器将经过验证的 SQL 语句传递给优化器;
- 优化器对SQL 语句进行优化,包括不同的索引选择、连接顺序、筛选条件等,最后生成一条最优的执行计划。
- 执行器:
- 优化器选择一个最优的执行计划,并将其传递给执行器;
- 执行器根据执行计划执行具体的查询操作;
- 它负责调用存储引擎的接口,处理数据的存储、检索和修改;
- 执行器会根据执行计划从磁盘或内存中获取相关数据,并进行连接、过滤、排序等操作,生成最终的查询结果。
- 存储引擎:
- 执行器将查询请求发送给存储引擎组件;
- 存储引擎组件负责具体的数据存储、检索和修改操作;
- 存储引擎根据执行器的请求,从磁盘或内存中读取或写入相关数据。
- 返回结果:
- 存储引擎将查询结果返回给执行器。
- 执行器将结果返回给连接器。
- 最后,连接器将结果发送回客户端,完成整个执行过程。
最后,查询缓存在一些场景下可能不太适用,因为它有一定的缺陷和开销。MySQL 8.0 版本开始,默认情况下查询缓存已被废弃。因此,在实际应用中,需要权衡是否使用查询缓存。
2. 存储引擎Innodb结构
mysql的InnoDB存储引擎架构,包括了内存架构和磁盘架构两部分。
内存结构:
-
Buffer Pool:
- Buffer Pool(缓冲池)是主内存中的一部分空间,用来缓存已使用的表和索引数据;
- 当数据库操作数据的时候,把硬盘上的数据加载到buffer pool,不直接和硬盘打交道,操作的是buffer pool里面的数据;
- 数据库的增删改查都是在buffer pool上进行,和undo log、redo log、redo log buffer、binlog⼀起使用,后续会把数据刷到硬盘上,Buffer Pool的默认大小为128M。
-
Change Buffer:
- Change Buffer(修改缓冲区)是一个特殊的数据结构,用于缓存不在缓冲池中的那些二级索引页的变更。由insert, update或delete这些dml操作导致被缓存的变化,将在当这些页被其他读操作加载到缓冲池后合并。
-
Log Buffer:用来缓存redolog,Log Buffer会定期地将日志文件刷入磁盘。
-
Adaptive Hash Index:自适应哈希索引。
磁盘结构
-
系统表空间 系统表空间包括InnoDB数据字典、双写缓存、更新缓存和撤销日志,同时也包括表和索引数据。多表共享,系统表空间被视为共享表空间。
-
双写缓存 双写缓存位于系统表空间中,用于写入从缓存池刷新的数据页。只有在刷新并写入双写缓存后,InnoDB才会将数据页写入合适的位置。
-
撤销日志 撤销日志是一系列与事务相关的撤销记录的集合,包含如何撤销事务最近的更改。如果其他事务要查询原始数据,可以从撤销日志记录中追溯未更改的数据。撤销日志存在于撤销日志片段中,这些片段包含于回滚片段中。
-
每个表一个文件的表空间 每个表一个文件的表空间是指每个单独的表空间创建在自身的数据文件中,而不是系统表空间中。这个功能通过innodb_file_per_table配置项开启。每个表空间由一个单独的.ibd数据文件代表,该文件默认被创建在数据库目录中。
-
通用表空间 使用CREATE TABLESPACE语法创建共享的InnoDB表空间。通用表空间可以创建在MySQL数据目录之外能够管理多个表并支持所有行格式的表。
-
撤销表空间 撤销表空间由一个或多个包含撤销日志的文件组成。撤销表空间的数量由innodb_undo_tablespaces配置项配置
-
临时表空间 用户创建的临时表空间和基于磁盘的内部临时表都创建于临时表空间。innodb_temp_data_file_path配置项定义了相关的路径、名称、大小和属性。如果该值为空,默认会在innodb_data_home_dir变量指定的目录下创建一个自动扩展的数据文件
-
重做日志 重做日志是基于磁盘的数据结构,在崩溃恢复期间使用,用来纠正数据。正常操作期间,重做日志会将请求数据进行编码,这些请求会改变InnoDB表数据。遇到意外崩溃后,未完成的更改会自动在初始化期间重新进行。
3. MyISAM和InnoDB的区别?
- 存储方式:MyISAM使用非聚簇索引,索引文件和数据文件是分开的;而InnoDB使用聚簇索引,将索引和数据一起存储在同一个文件中。
- 外键约束:MyISAM不支持外键约束,而InnoDB支持外键约束,可以设置关联关系来保证数据的完整性。
- 锁机制:MyISAM采用表级锁定,意味着当对表进行写操作时,整个表都会被锁定,因此可能导致并发写操作的性能较差。而InnoDB采用行级锁定,只锁定需要修改的行,可以提供更好的并发性能和多用户写入的支持。
- 事务支持:MyISAM不支持事务处理,而InnoDB支持事务和ACID特性(原子性、一致性、隔离性和持久性),可以进行事务管理、回滚和恢复操作。
- 崩溃恢复:InnoDB具备崩溃恢复的能力,能够在数据库、服务器或系统发生故障时,恢复到崩溃前的状态。这是因为InnoDB引擎使用了redo日志和undo日志来记录数据的修改操作,确保数据的持久性。
- 性能特点:MyISAM在读取频繁、插入和更新较少的场景下性能较好,特别适合于读密集型应用;而InnoDB在并发写入和更新较多的情况下性能较好,适合于写入密集型应用或需要事务支持的场景。
以上就是MyISAM与InnoDB的区别,我们在实际使用时需要根据具体的应用需求和场景来选择适合的存储引擎和索引类型。
4. 索引的优缺点
索引是数据库中用于提高数据检索性能的排好序的数据结构。通过建立特定的数据结构将列或多个列的值与它们在数据表中对应的行关联起来,以加快查询速度。索引一般存储在磁盘的文件中,它是占用物理空间的。
✅ 索引的优点
- **提高查询性能:**索引可以加快数据库查找数据的速度,通过快速定位到符合查询条件的数据行,减少了数据库进行全表扫描的开销,从而显著提高查询效率;
- **唯一性约束:**通过在索引上设置唯一性约束,可以确保数据的唯一性,防止重复数据的插入;
- 加快表与表之间的连接:对于有依赖关系的子表和父表联合查询时,可以提高查询速度。
❌ 索引的缺点
- **占用存储空间:**索引通常需要占用一定的磁盘空间,过多的索引可能会增加存储成本;
- **索引维护的开销:**当对数据表进行插入、更新或删除操作时,索引也需要进行相应的维护操作,这可能导致数据写入的性能下降,更新缓慢。
5. Hash索引和B+树索引的区别?
索引的数据结构和具体的存储引擎实现有关,mysql默认存储引擎Innodb使用的默认索引是B+Tree索引,对于mysql使用较多也有hash索引。对于Hash索引来说,如果单条查询较多的话,可以使用hash索引,速度最快。而其他大部分场景就适合选择B+Tree索引。
-
B+Tree索引
B+Tree是一个平衡的多叉树,从根节点到每个叶子节点的高度差不会超过1,而且同层级的节点有指针相互连接,所以对于有一些范围查询的话,适合使用B+Tree索引,可以利用双向指针进行快速的左右移动,效率比较高,而对于常规查询的话,从根节点到叶子结点的搜索效率基本相同,不会相差太多。
-
Hash索引
哈希索引就是通过哈希算法计算成哈希值,然后存储在哈希表中,如果是单条等值查询的话,效率最高,因为只用计算一次哈希算法得到对应的哈希值就可以找到了,但是如果重复值较多的话,可能还需要在后续的链表中进行逐个查询,直到查询到为止。
对于范围查询的话,哈希算法非常不支持,因为它计算一次哈希算法,然后没办法快速的左右移动。
6. B 树和B+树的区别?
对于B树来说,非叶子结点同样存储数据,叶子结点中没有指向其他结点的指针;
对于B+树来说,非叶子结点不存储数据,只存储索引,叶子结点用指针相连,提高区间访问的性能。相比于B树,在相同深度下存放更多的索引,MySQL中千万级数据树高只需3层,并且支持范围查询。
B树适合随机读写操作,而B+树适合范围查询和顺序访问。在实际应用中,根据不同的场景和需求选择合适的树结构可以带来更高效的数据处理和索引操作。
7. 回表
回表操作发生于非聚簇索引查询时,需要查找完整的用户记录或者不包括当前索引的字段,就需要到聚簇索引中再查一遍,这个过程称为回表。
8. 覆盖索引
当一个查询需要返回的数据列都包含在一个或多个索引中时,就可以利用索引覆盖来避免额外的磁盘读取操作,从而提高查询性能。
所以如果索引能覆盖到查询的列,那么就可以避免对主键索引的二次查询。
9. 最左前缀法则(最左匹配)
最左前缀原则是数据库索引设计中的一个重要原则,当联合索引包含多个列时,查询语句可以利用从左到右的顺序使用索引,但只能连续使用最左侧的列来进行索引扫描。也就是说,如果一个查询只使用了联合索引中的部分列作为查询条件,那么只有从索引的最左侧列开始连续使用时,索引才会有效。
例如,假设有一个联合索引 (a, b, c),那么查询条件中包含 a 和 b 的查询可以利用该索引,而只包含 b 或者只包含 c 的查询则无法使用该索引。
遵循最左前缀原则的好处包括:
- **提高查询性能:**通过使用索引的最左前缀,可以最大限度地减少索引扫描的数据量,提高查询的效率和响应时间。
- **减少索引占用空间:**在某些情况下,使用最左前缀原则可以减少创建多个索引的需求,节省磁盘空间和索引维护的开销。
10. 索引失效情况
-
使用
like
并且是左边带%
, 右边可以带会走索引; -
隐式类型转换,索引字段与条件或关联字段的类型不一致;
-
在
where
条件里面对索引列使用运算或者使用函数; -
使用
OR
且存在非索引列; -
在
where
条件中两列做比较会导致索引失效; -
使用IN可能不会走索引;
-
使用非主键范围条件查询时,部分情况索引失效 ;
-
使用
order by
可能会导致索引失效; -
is null
is not null
≠
可能会导致索引失效。
11. Innodb的数据结构
InnoDB存储引擎将数据划分为若干个页,页的默认大小为16kb。以页
作为磁盘和内存之间交互的基本单位
,也就是一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。
各个数据页中通过双向链表管理,每个数据页中的记录会按照主键值从小到大的顺序组成一个单向链表
,每个数据页都会为存储在它里边的记录生成一个页目录
,在通过主键查找某条记录的时候可以在页目录中使用二分法
快速定位到对应的页,然后再遍历该页对应分组中的记录即可快速找到指定的记录。
另外在数据库中,还存在区(Extent)、段(Segment)和表空间(Tablespace):
-
区是比页大一级的存储结构,在InnoDB存储引擎中,一个区会分配
64个连续的页
。因为InnoDB中的页大小默认是16KB,所以一个区的大小是64*16KB= 1MB。 -
段由一个或多个区组成,区在文件系统是一个连续分配的空间(在InnoDB中是连续的64个页),不过在段中不要求区与区之间是相邻的。
段是数据库中的分配单位,不同类型的数据库对象以不同的段形式存在。
当创建数据表、索引的时候,就会相应创建对应的段,比如创建一张表时会创建一个表段,创建一个索引时会创建一个索引段。 -
表空间是一个逻辑容器,表空间存储的对象是段,在一个表空间中可以有一个或多个段,但是一个段只能属于一个表空间。数据库由一个或多个表空间组成,表空间从管理上可以划分为系统表空间,
用户表空间
、撤销表空间
、临时表空间
等。
页结构:
数据页的存储空间被划分为七个部分,分别是文件头(File Header)、页头(Page Header)、最大最小记录(Infimum+supremum)、用户记录(User Records)、空闲空间(Free Space)、页目录(Page Directory)和文件尾(File Tailer) 。
- 文件头:描述页的通用信息,主要包括有:页号、上一页、下一页、页的类型、校验和、日志序列号;
- 文件尾:校验页的完整性,主要包括有校验和和日志序列号;
- 空闲空间:页中还没有被使用的空间,随着记录不断插入,空闲空间不足时,就会申请新的页;
- 用户记录:按照指定的行格式一条一条存储,相互之间使用单链表关联;
- 最大最小记录:表示页中记录的最小和最大;
- 页目录:页内记录进行二分法分组,方便在一个页中二分查找进行检索,提升效率;
- 页头:主要包括有:页目录中槽数量、本页中的记录数量、第一个删除的记录地址、当前页中的层级等等。
行格式:
InnoDB存储引擎设计了4种不同类型的行格式,分别是Compact(紧密)、Redundant(冗余)、Dynamic(动态)和Compressed(压缩)行格式。
Compact:
- 变长字段长度列表:存储真实数据的实际长度大小,多个字段逆序存储;
- null指列表:把可以null的列统一管理,存在一个标记null值的列表中;
- 记录头信息:
- delete_mask:标记该记录是否被删除;
- min_rec_mask:B+树的每层非叶子结点的最小记录都会添加该标记;
- n_owned:表示当前记录拥有的记录数;
- heap_no:表示当前记录在本页的位置;
- record_type:表示当前记录的类型,0表示普通记录,1表示非叶子结点记录,2表示最小记录,3表示最小记录。
- next_record:表示下一条记录的偏移量。
- 隐藏列:
- db_row_id:行ID;
- db_trx_id:事务ID;
- db_roll_ptr:回滚指针。
算法
在有链表的基础上进行链表的算法题,可以事半功倍。
需要有链表以及双指针的基础。
🌸 完结
最后,相关算法的代码也上传到gitee或者github上了。
乘风破浪会有时 直挂云帆济沧海
希望从明天开始,一起加油努力吧,成就更好的自己。
🥂 虽然这篇文章完结了,但是我还在,永不完结。我会努力保持写文章。来日方长,何惧车遥马慢!✨✨✨
💟 感谢各位看到这里!愿你韶华不负,青春无悔!让我们一起加油吧! 🌼🌼🌼
💖 学到这里,今天的世界打烊了,晚安!🌙🌙🌙