当前位置: 首页 > news >正文

MySQL图解索引篇(2)

从数据页的角度看B+树

B+树
InnoDB是如何存储数据的
数据页的结构
页目录与记录的关系
B+树是如何进行查询的
B+树的特点
B+树查询记录
聚集索引和二级索引
回表
索引覆盖
InnoDB是如何存储数据的?

InnoDB的数据是按「数据页」为单位来读写。也就是说,当需要读一条记录的时候,并不是将这个记录本身从磁盘读出来,而是以页为单位,将其整体读入内存。
数据库的I/O操作的最小单位是页,InnoDB数据页的默认大小是16KB,意味着数据库每次读写都是以16KB为单位的,一次最少从磁盘中读取16K的内容到内存中,一次最少把内存的16K内容刷新到磁盘中。
数据页入下图所示:
在这里插入图片描述

  • 文件头File Header:文件头,表示页的信息
  • 页头Page Header:页头,表示页的状态信息
  • 最小和最大记录Infimun+Supremum:两个虚拟的伪记录,分别表示页中的最小记录和最大记录
  • 用户记录User Records:存储行记录内容
  • 空闲空间Free Space:页中还没被使用的空间
  • 页目录Page Directory:存储用户记录的相对位置,对记录
  • 文件尾File Trailer:校验页是否完整

在File Header中有两个指针,分别指向上一个数据页和下一个数据页,连接起来的页相当于一个双向的链表:
在这里插入图片描述
数据页的主要作用是存储记录,也就是数据库的数据,数据页中的记录按照「主键」顺序组成单向链表,单向链表的特点就是插入、删除非常方便,但是检索效率不搞,最差的情况下需要遍历链表上的所有节点才能完成检索。
因此,数据页中有一个页目录,起到记录的索引作用。
在这里插入图片描述
页目录创建的过程如下:

  1. 将所有的记录划分成几个组,这些记录包括最小记录和最大记录,但不包括标记为“已删除”的记录;
  2. 每个记录组的最后一条记录就只组内最大的那条记录,并且最后一条记录的头信息中会存储该组一共有多少条记录,作为n_owned字段(上图中粉红色字段)
  3. 页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称为槽(slot),每个槽相当于指针指向了不同组的最后一个记录

从图可以看到,页目录就是由多个槽组成的,槽相当于分组记录的索引。然后,因为记录是按照「主键值」从小到大排序的,所以我们通过槽查找记录时,可以使用二分法快速定位要查询的记录在哪个槽(哪个记录分组),定位到槽后,再遍历槽内的所有记录,找到对应的记录,无需从最小记录开始遍历整个页中的记录链表。

B+树是如何进行查询的?

上述的存储结构中,当我们需要存储大量的记录时,就需要多个数据页,这时我们就需要考虑如何建立合适的索引,才能方便定位记录所在的页。
为了解决这个问题,InnoDB采用了B+数作为索引。磁盘的I/O操作次数对索引的使用效率至关重要,因此在构造索引的时候,我们更倾向于采用“矮胖”的B+树数据结构,这样所需要进行的磁盘I/O次数更少,而且B+数更适合进行关键字的范围查询。
InnoDB里的B+树中的每个节点都是一个数据页,结构示意图如下:
在这里插入图片描述

聚簇索引和二级索引

索引又可以分为聚簇索引和非聚簇索引(二级索引),它们的区别就在于叶子节点存放的是什么数据:

  • 聚簇索引的叶子节点存放的是实际数据,所有完整的用户记录都存放在聚簇索引的叶子节点;
  • 二级索引的叶子节点存放的是主键值,而不是实际数据。

如果某个查询语句使用了二级索引,但是查询的数据不是主键值,这时在二级索引找到主键值后,需要去聚簇索引中获得数据行,这个过程就叫作「回表」,也就是说要查两个B+树才能查到数据。不过,当查询的数据是主键值时,因为只在二级索引就能查询到,不用再去聚簇索引查,这个过程就叫作「索引覆盖」,也就是只需要查一个B+树就能找到数据。

总结

InnoDB的数据是按「数据页」为单位来读写的,默认数据页大小为16KB。每个数据页之间通过双向链表的形式组织起来,物理上不连续,但是逻辑上连续。

数据页内包含用户记录,每个记录之间用单向链表的方式组织起来,为了加快在数据页内高效查询记录,设计了一个页目录,页目录存储各个槽(分组),且主键值是有序的,于是可以通过二分查找法的方式进行检索从而提高效率。

为了高效查询记录所在的数据页,InnoDB采用b+树作为索引l,每个节点都是一个数据页。

如果叶子节点存储的是实际数据的就是聚簇索引,一个表只能有一个聚簇索引l;如果叶子节点存储的不是实际数据,而是主键值则就是二级索引l,一个表中可以有多个二级索引。

在使用二级索引进行查找数据时,如果查询的数据能在二级索引找到,那么就是「索引覆盖」操作,如果查询的数据不在二级索引里,就需要先在二级索引找到主键值,需要去聚簇索引中获得数据行,这个过程就叫作「回表」。

为什么 MySQL 采用 B+树作为索引?

怎样的索引的数据机构是最好的?

磁盘读写的最小单位是扇区,扇区的大小只有512B大小,操作系统一次会读多个扇区,所以操作系统的最小读写单位是块(Block).Linux中的块大小为4KB,也就是一次磁盘I/O操作会直接读写8个扇区。

要设计一个适合MySQL索引的数据机构,至少满足以下要求“:

  • 能在尽可能少的磁盘的I/O操作中完成查询工作;
  • 能在高效地查询某一个记录,也要能高效地执行范围查找。
什么是二分查找?

二分查找法每次都把查询的范围减半,这样时间复杂度就降到了 O(logn)

什么是二分查找树?

找到所有二分查找中用到的所有中间节点,把他们用指针连起来,并将最中间的节点作为根节点。这样就变成了二分查找树
二分查找树的特点是一个节点的左子树的所有节点都小于这个节点,右子树的所有节点都大于这个节点。
二分查找树的缺点:容易退化成链表。

什么是自平衡二叉树?

为了解决二分查找树会在极端情况下退化成链表的问题,引出了平衡二叉查找树(AVL树)

主要是在二叉查找树的基础上增加了一些条件约束:每个节点的左子树和右子树的高度差不能超过1。也就是说节点的左子树和右子树仍然为平衡二叉树,这样查询操作的时间复杂度就会一直维持在O(logn)。

红黑树也是通过一些约束条件来达到自平衡。不管平衡二叉查找树还是红黑树,都会随着插入的元素增多,而导致树的高度变高,这就意味着磁盘1/O操作次数多,会影响整体数据查询的效率。

什么是B树?

为了解决降低树的高度的问题,后面就出来了B树,它不再限制一个节点就只能有2个子节点,而是允许M个子节点(M>2),从而降低树的高度。

什么是B+树?

B+ 树就是对 B 树做了一个升级,B+树与B树主要有以下几点不同:

  • 叶子节点(最底部的节点)才会存放实际数据(索引+记录),非叶子节点只会存放索引;
  • 所有索引都会在叶子节点出现,叶子节点之间构成一个有序链表;
  • 非叶子节点的索引也会同时存在在子节点中,并且是在子节点中所有索引的最大(或最小)。
  • 非叶子节点中有多多少个子节点,就有多少个索引。
总结

MySQL默认的存储引擎innoDB采用的是B+作为索引的数据结构,原因有:

  • B+树的非叶子节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相比存储即存索引又存记录的B树,B+树的非叶子节点可以存放更多的索引,因此B+树可以比B树更「矮胖」,查询底层节点的磁盘I/O次数会更少。
  • B+树有大量的冗余节点(所有非叶子节点都是冗余索引),这些冗余索引让B+树在插入、删除的效率都更高,比如删除根节点的时候,不会向B树那样会发生复杂的树的变化;
  • B+树叶子节点之间用链表连接起来,有利于范围查询,而B树要实现范围查询,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘I/O操作,范围查询效率不如B+树。

MySQL单表不要超过2000W行,靠谱吗?

单表数量限制

id是主键,本身就是唯一的,也就是说主键的大小可以限制表的上限:

  • 如果主键声明int类型,也就是32位,那么支持2^32-1 ~ 21亿;
  • 如果主键声明bigint类型,那就是2^62-1,难以想象这个的多大了,一般还没有到这个限制之前,可能数据库已经爆满了!!
表空间

在这里插入图片描述
这张表数据,在硬盘上存储也是类似如此的,它实际是放在一个叫person.ibd(innodb data)的文件中,也叫做表空间;虽然数据表中,他们看起来是一条连着一条,但是实际上在文件中它被分成很多小份的数据页,而且每一份都是16K。
在这里插入图片描述

页的数据结构

在这里插入图片描述
在页的7个组成部分中,我们自己存储的记录会按照我们指定的行格式存储到User Records部分。
但是在一开始生成页的时候,其实并没有UserRecords 这个部分,每当我们插入一条记录,都会从FreeSpace部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到UserRecords 部分。
当Free Space 部分的空间全部被User Records 部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。

索引的数据结构

在MySQL中索引l的数据结构和刚刚描述的页几乎是一模一样的,而且大小也是16K。

单表建议值

B+树的叶子节点才能存放数据,而非叶子及诶单是用来存放索引数据的。
所以,同样一个16K的页,非叶子节点里的每条数据都指向新的页,而新的页有两种可能:

  • 如果是叶子节点,那么里面就是一行行的数据
  • 如果是非叶子节点的话,那么就会继续指向新的页

假设:

  • 非叶子节点内指向其他页的数量为x
  • 叶子节点内能容纳的数据行数为y
  • B+树的层级为z

则:total = x^(z-1)*y也就是说总数会等于x的z-1次方与Y的乘积。

X=?
在文章的开头已经介绍了页的结构,索引也也不例外,都会有 File Header(38 byte)、Page Header(56Byte)、Infimum +Supermum(26 byte)、File Trailer(8byte),再加上页目录,大概 1k 左右。
我们就当做它就是1K,那整个页的大小是16K,剩下 15k 用于存数据,在索引页中主要记录的是主键与页号,主键我们假设是Bigint(8 byte),而页号也是固定的(4Byte),那么索引页中的一条数据也就是12byteo

所以x=15*1024/12~1280 行。

Y=?
叶子节点和非叶子节点的结构是一样的,同理,能放数据的空间也是15K。
但是叶子节点中存放的是真正的行数据,这个影响的因素就会多很多,比如,字段的类型,字段的数量。
每行数据占用空间越大,页中所放的行数量就会越少。
这边我们暂时按一条行数据1k来算,那一页就能存下15条,Y=15*1024/1000~15。
算到这边了,是不是心里已经有谱了啊。
根据上述的公式,Total=x~(z-1)*y,已知x=1280,y=15:

  • 假设B+树是两层,那就是z=2,Total=(1280^1)*15=19200
  • 假设B+树是三层,那就是z=3,Total=(1280~2)*15=24576000(约 2.45kw)

哎呀,妈呀!这不是正好就是文章开头说的最大行数建议值2000W嘛!对的,一般B+数的层级最多也就是 3 层。

MySQL为了提高性能,会将表的索引装载到内存中,在InnoDB buffer size足够的情况下,其能完成全部加载进内存,查询不会有问题。

总结
  • MySQL的表数据是以页的形式存放的,页在磁盘中不一定是连续的。
  • 页的空间是16K,并不是所有的空间都是用来存放数据的,会有一些固定的信息,如,页头、页尾、页码、校验码等等。
  • 在B+树中,叶子节点和非叶子节点的数据结构是一样的,区分在于,叶子节点存放的是实际的行数据,而非叶子节点存放的是主键和页号。
  • 索引结构不会影响单表最大行数,2000W也只是推荐值,超过了这个值会导致B+树层级更高,影响查询性能。

索引失效有哪些?

索引失效
对索引使用左或者左右模糊匹配
对索引使用函数
对索引进行表达式计算
对索引隐式类型转换
联合索引非最左匹配
WHERE子句中的OR

InnoDB和MyISAM都支持B+树索引,但是它们数据的存储结构实现方式不同。不同之处在于:

  • InnoDB存储引擎:B+树索引的叶子节点保存数据本身;
  • MyISAM存储引擎:B+树索引的叶子节点保存数据的物理地址。
为什么对索引使用函数,就无法走索引了呢?

因为索引保存的是索引字段的原始值,而不是经过函数计算后的值,自然就没办法走索引了。
不过,从MySQL 8.0开始,索引特性增加了函数索引,即可以针对函数计算后的值建立一个索引,也就是说该索引的值是函数计算后的值,所以就可以通过扫描索引来查询数据。

为什么对索引进行表达式计算,就无法走索引了呢?

原因跟对索引使用函数差不多。
因为索引保存的是索引字段的原始值,而不是表达式计算后的值,所以无法走索引,只能通过把索引字段的取值都取出来,然后依次进行表达式的计算来进行条件判断,因此采用的就是全表扫扫描的方式。
其实MySQL完全可以计算出表达式的值在进行搜索,但是MySQL还是偷了这个懒,没有实现。可能表达式计算的情况多种多样,每种都要考虑的话,代码可能会很臃肿,所以干脆表示表达式计算会导致索引失效。

MySQL在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。

从MySQL 5.6之后·,有一个索引下推功能,可以在存储引擎层进行索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,再返回给Server层,从而减少回表次数。

为什么联合索引不遵循最左匹配原则就会失效?

原因是,在联合索引的情况下,数据是按照索引第一列排序,第一列数据相同时才会按照第二列排序。
也就是说,如果我们想使用联合索引中尽可能多的列,查询条件中的各个列必须是联合索引中从最左边开始连续的列。如果我们仅仅按照第二列搜索,肯定无法走索引。

WHERE子句中的OR

在WHERE子句中,如果在OR前的条件列是索引列,而在OR后的条件列不是索引列,那么索引会失效。(解决办法:将age字段设置Wie索引即可)

总结

6种会发生索引失效的情况:

  • 当我们使用左或者左右模糊匹配的时候,也就是like%xx或者like%xx%这两种方式都会造成索引失效;
  • 当我们在查询条件中对索引|列使用函数,就会导致索引失效。
  • 当我们在查询条件中对索引列进行表达式计算,也是无法走索引的。
  • MySQL在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。如果字符串是索引列,而条件语句中的输入参数是数字的话,那么索引列会发生隐式类型转换,由于隐式类型转换是通过CAST函数实现的,等同于对索引I列使用了函数,所以就会导致索引失效。
  • 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引引的匹配,否则就会导致索引失效。
  • 在WHERE子句中,如果在OR 前的条件列是索引I列,而在OR后的条件列不是索引I列,那么索引会失效。

count(*)和count(1)有什么区别?哪个性能最好?

在这里插入图片描述

哪种count性能最好

按照性能排序:count(*) = count(1) > count(主键字段) > count(字段)

count()是什么?

count()是一个聚合函数,函数的参数不仅可以是字段名,也可以是其他任意表达式,该函数作用是统计符合查询条件的记录中,函数指定的参数不为NULL的记录有多少
假设count()函数的参数是字段名,如下:

select count(name) from t_order;
sleect count(1) from t_order;	// 统计1这个表达式不为null的记录,实际在统计t_order表中有多少记录。
count(主键字段)执行过程是怎样的?

在通过count函数统计有多少个记录时,MySQL的server层会维护一个名为count的变量。
server层会循环向InnoDB读取一条记录,如果count函数指定的参数不为NULL,那么就会将遍历count加1,直到符合查询的全部·记录被读完,就退出循环。最后将count变量的值发送给客户端。

// id为主键值
select count(id) from t_order;

如果表里只有主键索引,没有二级索引时,那么,InnoDB循环遍历聚簇索引,将读取到的记录返回给Server层,然后读取记录中的id值,就会id值判断是否为NULL,如果不为NULL,就将count变量加1。
但是,如果表里有二级索引时,InnoDB循环遍历的对象就不是聚簇索引,而是二级索引。(I/O成本小)

count(1)执行过程是怎样的?

InnoDB循环遍历聚簇索引(主键索引),将读取到的记录返回给server层,但是不会读取记录中的任何字段的值,因为count函数的参数是1,不是字段,所以不需要读取记录中的字段值。参数1很明显并不是NULL,因此server层每次从InnoDB读取一条记录,就将count变量加1。如果表里有二级索引,InnoDB循环遍历的对象就二级索引。

可以看到,count(1)相比count(主键字段)少一个步骤,就是不需要读取记录中的字段值,所以通常会说count(1)执行效率会比count(主键字段)高一些。

count(*)执行过程是怎样的?

count()其实等于count(0),count()执行过程跟count(1)执行过程基本一样的。

count(字段)执行过程是怎样的?
// name不是索引,普通字段
select count(name) from t_order;

对于这种查询来说,会采用全表扫描的方式来计数,所以它的执行效率是比较差的。

为什么要通过遍历的方式来计数?

上述的遍历方式都是基于InnoDB存储引擎来说明的,但是在MyISAM存储引擎里,执行count函数的方式是不一样的,通常在没有任何查询条件下的count(*),MyISAM的查询速度要明显快于InnoDB。

使用 MylSAM引擎时,执行count 函数只需要O(1)复杂度,这是因为每张 MyISAM 的数据表都有一个meta信息有存储了row_count值,由表级锁保证一致性,所以直接读取row_count值就是count函数的执行结果。
而InnoDB 存储引擎是支持事务的,同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB表"应该返回多少行"也是不确定的,所以无法像MyISAM一样,只维护一个row_count变量。

在使用InnoDB存储引擎时,为了保证查询的一致性,就需要通过扫描表来统计具体的记录。
而带上where条件语句之后,MyISAM跟InnoDB就没有区别了,它们都需要扫描表来进行记录个数的统计。

如何优化count(*)?
  • 近似值:可以使用show table status 或者explain命令来对表进行估算。
    执行explain命令效率是很高的,因为它并不会真正的去查询。
    在这里插入图片描述
    在这里插入图片描述

  • 额外表保持计数值:如果是精确的获取表的记录总数,我们可以将这个计数值保存到单独的一张计数表中。
    当我们在数据表插入一条记录的同时,将计数表中的计数字段+1。也就是说,在新增和删除操作时,我们需要额外维护这个计数表。

MySQL分页有什么性能问题?怎么优化?

在这里插入图片描述
为了实现分页,我们很容易联想到下面这样的sql语句。

select * from page order by id limit offset, size;

查询第一页:select * from page order by id limit 0, 10;
查询第100页:select * from page order by id limit 990, 10;
同样都是拿10条数据,查第一页和第一百页的查询速度是一样的吗?为什么?

两种limit的执行过程
基于主键索引的limit执行过程
select * from page order by id limit 0, 10;

上面select后面带的是星号*,也就是要求获得行数据的所有字段信息
server层会调用innoDB的接口,在innoDB里的主键索引中获取到第0到的10条完整行数据,依次返回给server层,并放到server层的结果集中,返回给客户端。
mysql查询中limit 1000,10会比limit 10更慢。原因是limit 1000,10会取出1000+10条数据,并抛弃前1000条,这部分耗时更大。
那这种case有办法优化吗?

select * from page where id >= (select id from page order by id limit 6000000, 1) order by id limit 10;

上面这条sql语句,里面先执行子查询select id from page order by id limit 6000000,1,这个操作,其实也是将在innodb中的主键索引l中获取到6000000+1条数据,然后server层会抛弃前6000000条只保留最后一条数据的id。
但不同的地方在于,在返回server层的过程中,只会拷贝数据行内的id这一列,而不会拷贝数据行的所有列,当数据量较大时,这部分的耗时还是比较明显的。
在拿到了上面的id之后,假设这个id正好等于6000000,那sql就变成了:

select * from page where id >= (6000000) order by id limit 10;

这样innodb再走一次主键索引,通过B+树快速定位到id=6000000的行数据,时间复杂度是lg(n),然后向后取10条数据。
这样性能确实是提升了,亲测能快一倍左右,属于那种耗时从3s变成1.5s的操作。

基于非主键索引的limit执行过程
select * from page order by user_name limit 0, 10;

server层会调用innodb的接口,在innodb里的非主键索引l中获取到第0条数据对应的主键id后,回表到主键索引中找到对应的完整行数据,然后返回给server层,server层将其放到结果集中,返回给客户端。
而当offset>O时,且offset的值较小时,逻辑也类似,区别在于,offset>O时会丢弃前面的offset条数据。
也就是说非主键索引的limit过程,比主键索引的limit过程,多了个回表的消耗。
但当offset变得非常大时,比如600万,此时执行explain。

当limit offset过大是,非主键索引查询非常容易变成全表扫描。是真性能杀手。

这种情况也能通过一些方式去优化。比如

select * from page t1, (select id from page order by user_name limit 6000000, 100) t2 where t1.id = t2.id;
深度分页问题

当offset变得超大时,比如到了百万千万的量级,问题就突然变得严肃了。
这里就产生了专门的术语,叫深度分页

如果你是想取出全表的数据

我们可以将所有的数据根据id主键进行排序,然后分批次取,将当前批次的最大id作为下次筛选的条件进行查询。
在这里插入图片描述

如果是给用户做分页展示

如果我们要做搜索或筛选类的页面的话,就别用mysql了,用es,并且也需要控制展示的结果数,比如一万以内,这样不至于让分页过深。

如果因为各种原因,必须使用mysql。那同样,也需要控制下返回结果数量,比如数量1k以内。

这样就能勉强支持各种翻页,跳页(比如突然跳到第6页然后再跳到第106页)。

但如果能从产品的形式上就做成不支持跳页会更好,比如只支持上一页或下一页
在这里插入图片描述

这样我们就可以使用上面提到的start_id方式,采用分批获取,每批数据以start_id为起始位置。这个解法
最大的好处是不管翻到多少页,查询速度永远稳定。

变成像抖音那样只能上划或下划,专业点,叫瀑布流

在这里插入图片描述

总结

  • limit offset,size比Llimit size要慢,且offset的值越大,sql的执行速度越慢。
  • 当offset过大,会引发深度分页问题,目前不管是mysql还是es都没有很好的方法去解决这个问题。只能通过限制查询数量或分批获取的方式进行规避。
  • 遇到深度分页的问题,多思考其原始需求,大部分时候是不应该出现深度分页的场景的,必要时多去影响产品经理。
  • 如果数据量很少,比如1k的量级,且长期不太可能有巨大的增长,还是用 limit offset,size的方案吧,整挺好,能用就行。
http://www.dtcms.com/a/306996.html

相关文章:

  • 斯皮尔曼spearman相关系数
  • 25年新算法!基于猛禽的优化算法(BPBO):一种元启发式优化算法,附完整免费MATLAB代码
  • Java反射-动态代理
  • cmake_parse_arguments()构建清晰灵活的 CMake 函数接口
  • 智汇AI,应用领航 | 华宇万象问数入选2025全景赋能典型案例
  • 36、spark-measure 源码修改用于数据质量监控
  • Linux零基础Shell教学全集(可用于日常查询语句,目录清晰,内容详细)(自学尚硅谷B站shell课程后的万字学习笔记,附课程链接)
  • 「Spring Boot + MyBatis-Plus + MySQL 一主两从」读写分离实战教程
  • Linux 中,命令查看系统版本和内核信息
  • Linux 系统原理深度剖析与技术实践:从内核架构到前沿应用
  • 【选型】HK32L088 与 STM32F0/L0 系列 MCU 参数对比与选型建议(ST 原厂 vs 国产芯片)(单片机选型主要考虑的参数与因素)
  • 【python】列表“*”方式与推导式方式初始化区别
  • 数据结构——单链表1
  • 【WRF-Chem】EDGAR 排放数据处理:分部门合并转化为二进制(Python全代码)
  • RAG实战指南 Day 27:端到端评估框架实现
  • CSS-in-JS 动态主题切换与首屏渲染优化
  • 1.5.Vue v-for 和 指令修饰符
  • COZE 开源,新一代 AI Agent 本地部署一条龙
  • Excel文件解析
  • OpenWrt Network configuration
  • 百度统计在哪里添加网站?
  • Linux系统启动不受未挂载硬盘影响的解决方案
  • Windows系统使用命令生成文件夹下项目目录树(文件结构树)的两种高效方法
  • 深度学习-丢弃法 Dropout
  • C语言基础11——结构体1
  • Qt Quick 动画与过渡效果
  • QT中QTableView+Model+Delegate实现一个demo
  • TikTok 视频审核模型:用逻辑回归找出特殊类型的视频
  • 全栈:SSH和SSM和Springboot mybatisplus有什么区别?
  • 以ros的docker镜像为例,探讨docker镜像的使用