dream

一个菜鸟程序员的成长历程

0%

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

MySQL零基础教程

本教程为零基础教程,零基础小白也可以直接学习。
基础篇和应用篇已经更新完成。
接下来是原理篇,原理篇的内容大致如下图所示。

原理学习

零基础MySQL教程原理篇之执行器原理,SQL如何执行

一天,小美突然肚子疼,原来是亲戚来了,小美只能躺在床上耍手机了。小美发了一个朋友圈。
张三看到了小美的朋友圈,立马自告奋勇过来照顾小美,要给小美做一个红糖姜茶。
张三到了小美的家里,张三找到了家里的红糖还有姜
第一步,倒出适量红糖,将姜洗净切丝
第二步,锅中放入水,开火,将水烧开
最后,将红糖和姜倒入锅中,等待片刻即可。
小美喝完以后发现浑身舒畅,当即对张三表示很满意。

执行器是什么?

执行器是数据库架构中的核心组件之一,负责具体的执行操作,就如何故事中的张三一样,负责具体煮红糖姜茶

大部分的八股文都会告诉你,当SQL进入到数据库中,要经历解析器、优化器、执行器等等,但是他们不会告诉你的是,执行器到底怎么执行的呢?

面试官:说一下数据库吧,你对数据库了解的如何?这是一个开放式的问题。
候选人A:按照八股文背了一遍
候选人B:详细的解释了数据库中每一步的执行流程,优化手段,MySQL和其他数据库的一些对比

看到这里,相信你也明白候选人B获得Offer的机会更大吧。

执行器就是一个苦力,真正负责干活的人,他会接受到一份执行计划。相当于厨师拿到了菜谱,只需要照着做就行了。

执行器模型

执行器模型大致可以分成下面三种:

  • Iterator Model: 大多数数据库使用的,也是MySQL数据库使用的一个模型,也叫 Volcano / pull-based
  • Materialization Model:Iterator Model的一个特定版本,用在内存型数据库
  • Vectorized/Batch Model:Iterator Model差不多,要传入一大堆东西, 分析型数据库用的多

Iterator Model

MySQL使用的这种执行模型来完成SQL的执行流程。这种模型像java的stream, 用流的方式执行。如果是做Java的同学应该很熟悉。

核心思想:通过每个执行节点暴露一个 Next()(或 getNext())接口,调用者从下游不断向上游拉取一条一条的行(tuple)。执行节点按需产生单行数据并返回,形成管道式逐行处理。

优点

  • 管道化,低内存(不必把中间结果全部物化)。
  • 支持早停(例如 LIMIT、EXISTS),能在满足条件后立即结束。
  • 实现简单、易于组合与优化(迭代器链)。

缺点

  • 每行的函数调用/控制开销比较高(上下文切换、分支预测负担)。
  • 对现代 CPU 的向量化 / 缓存利用不友好(逐行处理导致大量分散访问)。

通过一个示例进行讲解,假设执行如下SQL语句

SELECT R.id,S.cdate FROM R JOIN S ON R.id = S.id WHERE S.value > 100

首先,根据执行计划,我们会生成一些执行节点

  • 节点1:负责扫描R表数据
  • 节点2:负责扫描S表数据
  • 节点3: 负责执行WHERE S.value > 100,也就是对节点2的输出做过滤
  • 节点4: 负责执行R JOIN S,也就是执行JOIN算法,是对节点1和节点3的输出做JOIN
  • 节点5: 负责执行SELECT R.id,S.cdate也就是对节点4的输出做过滤,只取其中的R.id和S.date字段。

这样,我们还可以将这5个节点化成一个树的形式,如下图

Iterator Model

每个节点会暴露一个Next()接口,执行流程如下:

  1. 节点5会通过节点4的Next接口获取数据
  2. 节点4会通过节点1的Next接口和节点3的Next接口获取数据
  3. 节点1会扫描R表,获取到一条数据然后返回
  4. 节点3会通过节点2的Next接口获取数据
  5. 节点2会扫描S表,获取到一条数据然后返回
  6. 节点3拿到节点2的返回值,执行过滤,如果过滤以后有数据,就返回,没有数据,继续第4步
  7. 节点4获取到节点1和节点3的返回值,执行JOIN操作,并将结果返回给节点5
  8. 节点5拿到数据以后执行SELECT操作,并重复第1步,直到没有数据为止。

执行流程图如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
flowchart TD
N5[节点5: 投影 SELECT R.id, S.cdate]
N4[节点4: JOIN R.id = S.id]
N1[节点1: 扫描 R 表]
N3[节点3: 过滤 S.value > 100]
N2[节点2: 扫描 S 表]

N5 -->|Next| N4
N4 -->|Next_R| N1
N4 -->|Next_S| N3
N3 -->|Next| N2

N1 -.->|读取R表每行| N1
N2 -.->|读取S表每行| N2

时序图如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sequenceDiagram
participant P as 节点5<br>SELECT 投影
participant J as 节点4<br>JOIN
participant R as 节点1<br>扫描R
participant F as 节点3<br>过滤S.value>100
participant S as 节点2<br>扫描S

P->>J: Next()
J->>R: Next_R()
R-->>J: 返回一行R记录

J->>F: Next_S()
F->>S: Next()
S-->>F: 返回一行S记录
F-->>J: 若 S.value > 100 则返回,否则继续Next()

J-->>P: 返回JOIN配对结果

P-->>P: 投影 R.id, S.cdate 后输出一行

P->>J: Next()(重复流程直到结束)

生活故事:
想象一个传统小饭馆:服务员(下游)每来一个客人就去厨房(上游)拿一份菜。厨房现做现给,一份一份端出来。优点是不用提前占用很多桌子空间,也能马上停单;缺点是每次去拿都要来回跑,效率受人力往返影响。

Materalization Model

Iterator Model的特点是每次返回一行数据,而Materalization Model则是每次返回所有数据。因此,这种模型取消了Next方法,而是使用Output方法。

核心思想:某些执行节点将其输出完整“物化”为中间结构(数组、临时表、磁盘文件),下游执行节点再一次性读取/随机访问这个中间结果。常见于需要多次访问、排序、哈希构建等场景。

优点

  • 便于随机访问、多次读取(例如 hash join 的 build 阶段、聚合后多次扫描)。
  • 可以把中间数据放到磁盘/临时表以突破内存限制。
  • 有利于用批量算法对中间结果做集中优化(比如一次性排序、一次性写盘)。

缺点

  • 需要额外内存或 I/O(内存不足时会溢写到磁盘,开销大)。
  • 增加了延迟(必须等物化完成,下游才能消费)。

通过一个示例进行讲解,假设执行如下SQL语句

SELECT R.id,S.cdate FROM R JOIN S ON R.id = S.id WHERE S.value > 100

节点还是上面那些,不再说明。

每个节点会暴露一个Output()接口,执行流程如下:

  1. 节点5会通过节点4的Output接口获取所有数据
  2. 节点4会通过节点1的Output接口和节点3的Output接口获取所有数据
  3. 节点1会循环扫描R表,获取到R表的所有数据一次行返回
  4. 节点3会通过节点2的Output接口获取所有数据
  5. 节点2会循环扫描S表,获取到S表的所有数据一次行返回
  6. 节点3拿到节点2的返回值,循环执行过滤,如果循环过滤以后有数据,就返回所有过滤后的数据,没有数据,返回空。
  7. 节点4获取到节点1和节点3的返回值,循环执行JOIN操作,并将结果返回给节点5
  8. 节点5拿到数据以后循环执行SELECT操作,并将结果返回。

这个模型的流程图如下,注意其中的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
flowchart TD

P[节点5: 投影 SELECT<br/>Output → 返回所有结果] --> J
J[节点4: JOIN<br/>Output → JOIN所有数据] --> P

J --> R
J --> F

R[节点1: 扫描R表<br/>Output → 返回R表所有行] --> J

F[节点3: 过滤 S.value > 100<br/>Output → 返回过滤后的全部S行] --> J
F --> S

S[节点2: 扫描S表<br/>Output → 返回S表所有行] --> F

时序图如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sequenceDiagram
participant P as 节点5<br/>SELECT 投影 (Output)
participant J as 节点4<br/>JOIN (Output)
participant R as 节点1<br/>扫描R表 (Output)
participant F as 节点3<br/>过滤S (Output)
participant S as 节点2<br/>扫描S表 (Output)

P->>J: Output()
Note right of J: 需要 R 全量数据<br/>需要 S 过滤后全量数据

J->>R: Output()
R-->>J: 返回 R 全表数据列表

J->>F: Output()
F->>S: Output()
S-->>F: 返回 S 全表数据列表

F-->>J: 返回 过滤后的 S 列表

J-->>P: 返回 JOIN(R, S_filtered) 的全部结果

P-->>P: 投影 SELECT R.id, S.cdate<br/>返回最终结果集

生活故事:
把它想成自助餐(buffet)准备:厨房先把所有菜都做好、摆到台子上(物化)。顾客来时可以随意取,便于多个人同时取、重复取同一道菜。缺点是需要大的台面和提前准备时间,浪费食材或占地方。

Vectorized Model

这个模型同样进行了一些修改,不再是一行一行的获取数据,但是也不是一下获取所有数据,类似于中间的一个模型,一下子获取部分数据,同样使用Next方法,但是一次性返回一堆 tuples, 数量取决于 Buffer pool 大小

OLAP数据库基本上使用的是这个处理模型。

核心思想:执行节点按“批”(batch)或“向量”(columnar vectors)处理多行数据一次性做操作,而不是每次一行。批里通常是连续的列或行,能更好利用 CPU 缓存、分支预测、SIMD 指令、以及减少函数调用开销。

优点

  • 高吞吐:每批次 amortize 函数调用与调度开销。
  • 极佳的 CPU & 缓存利用(尤其是列式存储配合向量化运算)。
  • 更容易实现并行/向量化优化(SIMD、流水线)。

缺点

  • 批量带来一定的延迟(小查询或需要低延迟的 OLTP 不友好)。
  • 对非均匀数据/分支较多的操作优化有限(每批中仍需处理空值/分支)。
  • 较复杂的实现(需要设计批接口、内存布局)。

节点还是上面那些,不再说明。

每个节点会暴露一个Next()接口,执行流程如下:

  1. 节点5会通过节点4的Next接口获取N行数据
  2. 节点4会通过节点1的Next接口和节点3的Next接口获取N行数据
  3. 节点1会循环扫描R表,获取到R表的N行数据返回
  4. 节点3会通过节点2的Next接口获取N行数据
  5. 节点2会循环扫描S表,获取到S表的N行数据返回
  6. 节点3拿到节点2的N行返回值,循环执行过滤,如果循环过滤以后有数据,就返回所有过滤后的数据,没有数据,就继续重复第4步。
  7. 节点4获取到节点1和节点3的N行返回值,循环执行JOIN操作,并将结果返回给节点5
  8. 节点5拿到N行数据以后循环执行SELECT操作,并重复第1步,最后数据处理完成,将结果返回。

执行流程图如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
flowchart TD
A5[Node5 Projection SELECT<br/>NextN → 请求 N 行] --> A4
A4[Node4 JOIN<br/>NextN → 请求 N 行] --> A1
A4 --> A3

A1[Node1 Scan R<br/>一次返回 N 行] --> A4

A3[Node3 Filter value gt 100<br/>NextN → 请求 N 行] --> A2

A2[Node2 Scan S<br/>一次返回 N 行] --> A3

A3 --> A4
A4 --> A5

时序图如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
sequenceDiagram
participant P as 节点5<br/>SELECT 投影<br/>Next(N)
participant J as 节点4<br/>JOIN<br/>Next(N)
participant R as 节点1<br/>扫描R表<br/>Next(N)
participant F as 节点3<br/>过滤 S<br/>Next(N)
participant S as 节点2<br/>扫描S表<br/>Next(N)

P->>J: Next(N)
Note right of J: 需要 R_batch 和 S_filtered_batch

J->>R: Next(N)
R-->>J: 返回 R 的 N 行批次

J->>F: Next(N)
F->>S: Next(N)
S-->>F: 返回 S 的 N 行批次

F-->>J: 对 S_batch 过滤后返回<br/>S_filtered_batch(≤N 行)

J-->>P: JOIN(R_batch, S_filtered_batch)<br/>返回 Join_batch(≤N 行)

P-->>P: 对 Join_batch 执行投影<br/>输出 SELECT_batch(≤N 行)

P->>J: Next(N)(继续获取下一批…)

生活故事:
想象快餐连锁的生产线:厨房把同一种汉堡按批次一起做(比如一次做 20 个),装箱后一起送到窗口分发。这样每个环节都能流水作业,效率高,但如果只来一个客人,需要等一小批做完才拿到(稍有延迟)。

执行器模型的对比表格,一目了然,可以参照一下。

特性 Iterator(逐行/拉) Materialization(物化) Vectorized/Batch(向量/批)
典型接口 Next() 每次一行 完整生成中间结果(array/temp table) NextBatch() / 批量数组
内存占用 低(流式) 高(可能很大) 中等(批次大小决定)
延迟 低(单行) 高(等待物化) 中等 — 批大小影响延迟
吞吐量 中高(取决于IO) 高(最好)
适用场景 OLTP、早停、复合操作 需要随机访问/多次复用/排序、外部Join OLAP、大表扫描、列式引擎
优化方向 减少函数开销、内联 减少 I/O、压缩 SIMD、缓存局部性、批处理

节点类型

从上面可以发现,我们会按照节点去进行执行,那么每个节点其实都负责一个具体的任务。这也是软件设计原则中的单一职责

比如上面的示例中,有:

  • select节点
  • where节点
  • join节点
  • 扫描表节点

其实,SQL中的每一个操作都是一个不同的节点类型,除了上面那些,还有:

  • limit节点
  • order by节点
  • group by节点
  • sort 节点
  • 其他操作节点

当然了,每个节点其实都可以有不同的实现,每种实现可能会导致不同的执行速度。

在后续的文章中,我们会详细讲解每种节点的具体代码实现。本篇文章的重点还是聚焦在执行器上面。

进程线程模型

上面介绍了执行模型,那执行模型具体如何处理呢?单进程?单线程?多线程?多进程?

这里介绍一下进程线程模型,分为下面几种:

  • Process per DBMS Worker:每个 Worker 一个进程,相当于一个节点一个进程来执行。可以说是多进程 + 共享内存的方式执行。
  • Process Model:和上面那个差不多,但是增加了 worker pool,有多个worker进行调度处理。
  • Thread per DBMS Worker:每个 Worker 一个线程,相当于一个节点一个线程来执行。由数据库自己控制线程。
  • Embedded DBMS:嵌入式数据库,与应用在同一进程内

Process per DBMS Worker

每个进程是一个worker,负责执行任务。通过共享内存进行buffer pool的通信,要不然每个进程都会有一个buffer pool。老得数据库大部分使用的这个,因为当时没有统一的线程API,像DB2,oracle,postgraSQL等。

特点:

  • 稳定,功能隔离(进程级别隔离,不会相互崩)
  • 上下文切换昂贵
  • 增加连接数时压力大

Process Model

Process per DBMS Worker一样.但是增加了 worker pool,有多个worker,有一个主进程进行调度处理。像DB2,postgraSQL(2015)使用的是这种方式。相当于上面那种方式的一个优化版本吧,可以看到PostgraSQL一开始用上面的模型,后来切换到这个模型了。

Thread per DBMS Worker

一个进程,多个线程执行,由数据库自己控制线程。现在的数据库几乎都使用这种,像DB2, MSSQL, MySQL, Oracle(2014)。

线程的开销比进程小的多,因此,当线程出现以后,现代数据库基本都使用了这种方式。

特点:

  • 线程切换便宜(上下文切换轻量)
  • 所有线程共享进程内存
  • 一个线程崩掉可能拖垮整个 DBMS → 可靠性较低
  • 吞吐能力强,适合高并发

Embedded DBMS

数据库不是独立的服务器进程,而是嵌入到应用程序内部,比如一个链接库(.so / .dll)。应用程序与数据库运行在同一个进程中,没有网络开销。一些小型的嵌入式数据库就是这种的,比如著名的SQLite。

特点:

  • 极快(没有 IPC 或网络交互)
  • 轻量(没有独立服务器)
  • 通常不支持高并发连接(适合单用户/轻量多线程)
  • 适用于本地系统、移动端、嵌入式设备

进程线程模型的对比表格,可以参考一下

模型 单位 性能 稳定性 内存共享 典型数据库 类比
Process per Worker 每连接一个进程 较慢(进程切换重) 很高 需要共享内存 PostgreSQL 每客一个包间
Process Model 多进程协作 中等 有限共享 PostgreSQL/Oracle 多炒菜间+共享大冰箱
Thread per Worker 每连接一个线程 较低(线程崩会影响全局) 完全共享 MySQL / SQL Server 一个大厨房所有人共用
Embedded DBMS 与应用同一进程 极高 中等 全共享 SQLite / DuckDB 家里的咖啡机

并行化查询

并行化查询可以加速执行器的执行速度,并行化也是现代很多程序使用的一个方式。对于数据库而言,通常有下面三种并行化方式:

  • Intra operator(水平)
  • Inter operator(垂直)
  • Bushy(上面两种的组合)

Intra operator

通过水平拆分数据,由多个线程执行,比如3个线程,一个线程处理一个page,以此类推。处理完成以后通过exchange operator来进行合并,拆分也是通过它。

通过水平拆分来将一个任务分成多个子任务,每个子任务单独在一个CPU上执行,这样可以利用多核CPU来加速处理。

比如一个表有6个Page,将一个扫描表的任务分成3个子任务,CPU1扫描表的前面2个Page,CPU2扫描表的中间2个Page,CPU3扫描表的最后2个Page。

这里要注意的一点是,并行化是执行器级别的并行,而不是节点级别的,也就意味着3个子任务,是整个执行器的3个子任务。

通过一个示例进行讲解,假设执行如下SQL语句

SELECT R.id,S.cdate FROM R JOIN S ON R.id = S.id WHERE S.value > 100

我们在上面已经给出了执行图了,接下来会将整个执行图水平拆分成3个子任务。

如下图

Intra operator

Exchange operator

  • Gather:从多个线程的结果合并成一个输出流,PostgreSQL用这个
  • Repartition: 重新组织多个输入流到多个输出流的数据,像group by,BigQuery用这个
  • Distribute: 拆分一个输入流到多个输出流

很多数据库都使用了Exchange Operator的概念,其实不光数据库,很多数据处理中也使用了,如果你使用过其他数据库或者一些大数据处理经验的话,应该会感觉到熟悉。

这种水平拆分是非常常用的场景,很多数据库都是使用这个方式,比如PostgreSQL、Oracle、Snowflake、DuckDB 等。

Inter operator

这是一种垂直拆分的并行方式,通常在 流水线执行模型(pipelining) 内使用。重叠的操作从一个阶段到下一个阶段的pipeline数据,没有具体化。workers同时执行多个operators从一个查询计划的不同部分。也需要用到exchange operator。像Spark,Kafka常用这个方式。

这个就是多个节点并行的,比如扫描表的节点在CPU1执行,Where节点在CPU2执行,JOIN节点在CPU3执行,如此并行。

Inter operator

Bushy Parallelism

这种并行方式是结合了水平和垂直的方式,是并行度最高的一种,但也是复杂度最高的一种。

既可以水平拆分到多个CPU中,也将部分内容垂直拆分到多个CPU中。

假设我们有SQL

1
select * from A JOIN B JOIN C JOIN D

我们将A JOIN B拆分到CPU1中,C JOIN D拆分到CPU2中,这是水平拆分。

然后将JOIN操作进行垂直拆分。如图所示

039

许多数据库(Snowflake、Presto、Greenplum、Redshift)支持 bushy这种方式。

关于三种方式的对比可以参考下表

并行类型 含义 并行维度 类比
Intra-operator(水平) 单个算子内部并行 横向拆分数据 多人一起切西瓜
Inter-operator(垂直) pipeline 中的算子串行但并行执行 上下游算子同时工作 工厂流水线
Bushy 多分支、多算子、多层并行 多 pipeline + 多算子 + 多任务 多厨房同时做不同菜系

结论

本次分享了MySQL中重要的组件之一执行器,理解了执行器是如何进行工作的,它的执行模型以及一些节点的类型。最后还提到了并行化查询,通过并行化可以使用现代的多核CPU性能来加速数据处理。

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

ChatGPT正式开放成人模式

近日,OpenAI宣布正式推出ChatGPT的成人模式,这一消息在科技界和普通用户中引起了广泛关注和热议。作为全球最受欢迎的对话式AI助手,ChatGPT的这一新功能标志着人工智能在内容处理能力上的又一次重大突破。

什么是成人模式

ChatGPT成人模式是一种专门针对成年用户的增强功能模式,它允许AI处理和讨论更加成熟、复杂的话题。与标准模式相比,成人模式具有以下特点:

更高的内容理解深度

成人模式能够理解和讨论涉及成人生活的各种话题,包括职场挑战、人际关系、心理健康等更为复杂的现实问题。

增强的语境处理能力

在成人模式下,ChatGPT能够更好地理解隐含意思、讽刺幽默等复杂的语言表达,提供更加精准和贴切的回复。

更个性化的交互体验

系统会根据用户的使用习惯和偏好,调整对话风格和内容深度,提供更加个性化的服务。

技术实现背后的秘密

先进的内容过滤系统

成人模式的核心技术在于其精密的内容过滤系统。OpenAI采用了多层次的过滤机制:

  1. 预训练阶段的筛选:在模型训练过程中,通过精心设计的数据集确保模型学到的是健康、有益的知识。

  2. 实时内容监控:对话过程中,系统会实时分析生成内容的适当性,确保不会产生有害或不当信息。

  3. 用户身份验证:通过年龄验证机制,确保只有成年用户才能访问成人模式功能。

安全机制的升级

为了确保用户安全,OpenAI在成人模式中实施了多重保护措施:

  • 智能边界检测:AI能够自动识别并拒绝回答可能造成伤害的问题
  • 敏感话题预警:对于可能引起不适的话题,系统会提前提醒用户
  • 紧急支持连接:当检测到用户可能需要专业帮助时,系统会建议寻求专业支持

应用场景分析

职场发展咨询

成人模式下的ChatGPT能够为用户提供更加深入的职场建议,包括:

  • 职业规划指导
  • 人际关系处理
  • 领导力发展
  • 工作生活平衡

心理健康支持

在成人模式下,AI可以更好地理解成年人的心理压力来源,提供更加针对性的建议和支持。

教育进阶辅导

对于终身学习者来说,成人模式能够提供更加专业和深入的知识解答,满足成人教育的特殊需求。

争议与社会讨论

支持声音

支持者认为,成人模式是AI发展的必然趋势:

  • 满足用户真实需求:成年人确实有处理复杂问题的需求
  • 提升AI实用性:扩大了AI的应用范围和实用价值
  • 技术进步的体现:展示了AI技术在理解和处理复杂内容方面的进步

担忧声音

然而,也有不少专家和家长表示担忧:

  • 内容管控风险:如何确保不当内容不会泄露给未成年人
  • 道德边界模糊:AI参与成人话题讨论的伦理界限
  • 隐私安全问题:成人话题往往涉及更多个人隐私信息

行业监管呼吁

面对这一新功能,多个国家的监管机构表示将密切关注其发展,并考虑制定相应的监管政策。

对AI行业的影响

技术标准提升

ChatGPT成人模式的推出,实际上为整个AI行业设定了新的技术标准,特别是在内容安全处理方面。

产品设计趋势

预计其他AI公司也会跟进推出类似的分级功能,这将成为AI产品的一个新趋势。

用户体验重新定义

成人模式的出现,可能会重新定义用户对AI助手的期望和使用习惯。

未来展望

技术发展方向

  1. 更精准的年龄识别:通过生物识别等技术提高年龄验证的准确性
  2. 个性化程度提升:基于用户画像提供更加定制化的服务
  3. 多模态交互:结合语音、图像等多种交互方式

应用领域扩展

未来,成人模式可能会扩展到更多专业领域:

  • 专业咨询服务:法律、医疗等专业领域的初步咨询
  • 教育培训:成人职业教育和技能提升
  • 创意合作:在创意工作中扮演更加重要的角色

社会影响预期

随着技术的成熟和社会的适应,成人模式有望:

  • 提高AI在成年人生活中的实用性
  • 促进人机交互关系的深入发展
  • 为AI伦理和治理提供实践经验

总结

ChatGPT成人模式的正式开放,无疑是人工智能发展史上的一个重要里程碑。它不仅展示了AI技术在内容理解和处理方面的巨大进步,也为我们思考人机关系的未来提供了新的视角。

然而,技术的进步总是伴随着责任的增加。如何在发挥技术优势的同时确保安全和伦理,将是OpenAI和整个AI行业需要持续面对的挑战。

作为用户,我们也需要以理性和负责任的态度来对待这一新功能,合理利用其优势,同时保持必要的警惕和判断。

在未来,我们期待看到更多类似的技术创新,也希望这些创新能够真正为人类社会带来积极的影响和价值。

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

MySQL零基础教程

本教程为零基础教程,零基础小白也可以直接学习。
基础篇和应用篇已经更新完成。
接下来是原理篇,原理篇的内容大致如下图所示。

原理学习

零基础MySQL教程原理篇之B+树索引

在MySQL数据库中,索引是提升查询效率的核心工具。没有索引的查询,就像是在浩瀚的书海中翻书,效率低下。而良好的索引设计,则能帮助我们高效地找到我们需要的信息,避免无谓的资源浪费。因此,理解索引的原理、种类以及如何设计索引是每个开发者必须掌握的基础技能。

索引是什么?

索引是一种数据结构,它通过特定的方式排列数据,以便于快速查询。可以把索引想象成一本书的目录,通过目录可以快速找到你想要的章节,而不用从头到尾翻书。

在MySQL中,索引通常是通过B树或B+树等数据结构实现的,数据库查询引擎可以利用这些结构快速查找数据。

B+树索引(B+ Tree Index)

这里先搞清楚一些概念,什么是b树,b+树b*树还有b link树

  • b tree(1971):B-Tree (Balanced Tree) 是一种自平衡的多路搜索树,由 Rudolf Bayer 和 Edward M. McCreight 在 1971 年提出。
  • b+ tree (1973):B-Tree(Balanced Tree) 的变种,由 Rudolf Bayer 在 1973 年提出,主要改进了数据存储方式,因此大多数的数据库都使用这个作为索引。
  • b* tree (1977): 对 B+Tree 的进一步优化,主要的优化思路是提高空间利用率,同样的数据,可以使用更小的空间存储。
  • b link tree (1981): 由 LehmanYao 在 1981 年提出,在b+树的基础上,主要解决了并发控制问题。数据库中的b+树也做了一些优化,参考了b link tree的一些优点。

b tree

首先来看一下b树,这个是B+树的鼻祖了。

  • 多路结构:每个节点可以有多个子节点(不像二叉树最多2个)
  • 平衡性:所有叶子节点都在同一层
  • 动态调整:插入删除时自动保持平衡

规则:

  • 每个节点最多有 m 个子节点(m 阶)
  • 除根节点外,每个节点至少有 ⌈m/2⌉ 个子节点
  • 所有叶子节点在同一层
  • 节点中的键值有序排列

优点:

  • 减少 I/O 操作(宽而浅的结构)
  • 适合磁盘存储
  • 查询、插入、删除都是 O(log n)

示例图如下:
b树

b* tree

  • B*树是一种m阶的平衡多路搜索树
  • 除根节点外,所有节点的关键字个数至少为⌈(2m-1)/3⌉
  • 所有叶子节点都在同一层
  • 非叶子节点只存储索引信息,不存储实际数据
  • 所有数据都存储在叶子节点中
特性 B树 B+树 B*树
数据存储 内部节点和叶子节点都存数据 只有叶子节点存数据 只有叶子节点存数据
节点利用率 至少50% 至少50% 至少66.7%
叶子节点连接 有链表连接 有链表连接
空间效率 一般 较好 最好
范围查询 需要中序遍历 效率高 效率高

关键特性

  1. 高节点利用率
  • 每个节点至少2/3满(除根节点外)
  • 相比B+树的1/2要求,空间利用率提高33%
  1. 延迟分裂策略
  • 节点满时优先向兄弟节点转移关键字
  • 只有兄弟节点也满时才进行分裂操作
  • 减少了分裂操作的频率
  1. 更好的缓存性能
  • 更高的节点利用率意味着更少的节点数量
  • 减少了磁盘I/O操作
  • 提高了缓存命中率

主要优势

  1. 空间效率:相同数据量下,B*树的高度更低
  2. 查询性能:更少的磁盘访问次数
  3. 插入性能:延迟分裂减少了树结构的调整
  4. 存储成本:更高的空间利用率降低存储需求

示例图
b*树

B-link树(B-link tree)是B+树的一种变形,由Lehman和Yao在1981年提出。它是专门为支持高并发访问而设计的B+树变种。

基本定义:

  • B-link树是一种支持并发操作的平衡多路搜索树
  • 在传统B+树的基础上增加了链接指针(link pointer)
  • 每个内部节点都有一个右链接指针,指向其右兄弟节点
  • 支持无锁或低锁的并发读写操作

并发安全的设计理念
B-link树的核心设计思想是通过右链接指针和高水位标记来实现并发安全:

  1. Right-Link指针
  • 每个节点都有指向右兄弟的指针
  • 形成同一层级的有序链表
  • 允许在节点分裂过程中继续安全访问
  1. High Key(高水位键)
  • 每个节点维护一个最大键值
  • 用于判断搜索是否需要跳转到右兄弟
  • 解决并发分裂时的一致性问题

关键设计原则

设计原则:

  1. 结构不变性:树的基本结构在并发操作中保持稳定
  2. 单向链接:只需要右链接,简化了维护复杂度
  3. 原子操作:关键操作通过原子性保证一致性
  4. 乐观并发:减少锁的使用,提高并发性能

并发控制策略

  • 读操作:几乎无锁,可以并发执行
  • 写操作:最小化锁的范围和时间
  • 分裂操作:通过两阶段提交保证一致性
特性 传统B+树 B-link树
节点结构 [K1 P1
并发控制 需要复杂的锁机制 最小化锁使用
读操作 可能需要读锁 几乎无锁
写操作 需要写锁,可能阻塞读 短时间锁,不阻塞读
分裂操作 需要锁定多个节点 原子操作,影响最小
空间开销 较小 每个节点额外一个指针
实现复杂度 相对简单 较复杂

示例图
blink树

关于b link tree可以阅读论文:https://dl.acm.org/doi/pdf/10.1145/319628.319663

b+树

关于b+树的b,到底是什么意思,其实作者并没有一个明确的定义,有的人觉得是balance (平衡),有的人觉得是Bushy(茂密)

不过大多数人都更认同balance这个解释。

推荐一个paper:the ubiquitous B-tree

MySQL中的B+树其实和传统的还是有一点区别,比如它借鉴了b-link tree的兄弟节点的概念。

b+ tree 删除和插入的复杂度都是O(log n)

叶子节点的内容,通常key和value是分开存储的,因为搜索的时候并不需要加载value数据

  • 元数据
    • isleaf 是否是叶子节点
    • slots 有多少空闲的slot
    • prev 前一个叶子节点的指针
    • next 后一个叶子节点的指针
  • key数据
  • value数据

结构:

  • B+树是一种多路自平衡的树结构,所有的叶子节点都在同一层,并且叶子节点通过链表连接。
  • 它是MySQL中最常用的索引类型,支持范围查询、精确查找等操作。

优点:

  • 高效的范围查询:由于B+树是顺序存储的,支持范围查询,查询某个区间的数据时非常高效。
  • 高效的精确查询:B+树通过逐层查找,可以快速定位到数据行。
  • 支持排序:由于叶子节点按顺序排列,B+树非常适合用于ORDER BYGROUP BY等操作。
  • 空间利用高:每个非叶子节点仅包含键,而不包含数据,可以节省存储空间。

缺点:

  • 性能受页大小影响:B+树的性能与页的大小(每页存储的记录数)密切相关,过小的页会导致频繁的I/O操作,过大的页则会浪费内存。
  • 写入性能较低:对于频繁插入、删除操作的场景,B+树索引的维护成本较高,可能导致性能下降。

应用场景:

  • 最常用于主键索引、普通索引和唯一索引。尤其适用于需要进行范围查询、排序的场景。

特点:

  • b+ tree,保证每个节点都必须是半满的,对于存放在节点中的key数量来说,key数量至少为M/2 - 1个,M为树的高度,key的数量必须小于 M - 1,如果当删除数据以后导致key数量小于M/2 - 1个,就会进行平衡,使他满足M/2 - 1个。

    M/2 - 1 ≤ key数量 ≤ M - 1

  • 如果一个中间节点有k个key,那你就会有k+1个非空孩子节点,也就是k+1个指向下方节点的指针。每个节点的内容是一个指针和一个key
  • 叶子节点之间有连接叶子节点的兄弟指针,这个想法来源于b link tree。每个节点的内容是一个数据和一个key,数据可以是一个record id 也可以是一个 tuple
  • b+ tree 标准填充容量大概是67% - 69%,对于一个大小是8kb的page来说,如果高度为4,大约能记录30 0000个键值对。
  • b+ tree的节点大小,机械硬盘的大小最好在1M,ssd的大小在10KB

示例图
b+

B+树的叶子节点就是一个page,关于page的具体内容我们之前已经讲过了,不再赘述。我们将节点放大大概是这样的。

b+树叶子节点

b tree 和 b+ tree 的区别

  • b tree的中间节点也可以存数据,所以key是不重复的
  • b+ tree的中间节点没有数据,所有数据都在叶子节点,所以key有可能既存在中间节点也存在叶子节点。会重复
  • b tree的性能在并行处理上更差,因为修改以后需要向上传播也需要向下传播修改,这个时候两边都要增加锁
  • b+ tree的性能更好,因为只修改叶子节点,所以只需要向上传播,只需要增加一个锁
b+ tree的查找
  • 对于<a,b,c>,查找a=5 and b=3也是可以走索引的,但是hash索引就不行,有些数据库还支持b=3的搜索走索引,比如oracle和sql server

b+ tree还有一个方便的地方在于支持联合索引,我们可以设置A、B、C三个key的联合索引并支持查找。

假设我们查找A,B

假设我们查找A,*

假设我们查找*,A

可以看到,对于b+树索引来说,这些查找都可以满足,这也是数据库支持联合索引的重点。

b+ tree 插入
  1. 向下扫描,找到对应的叶子节点
  2. 如果可以插入就直接插入
  3. 如果不可以插入,那么从中间分开,变成两个叶子节点,并将中间的key传递给父节点,插入父节点。
  4. 如果父节点可以插入就直接插入并分出一个指针指向新的叶子节点
  5. 如果父节点不可以插入重复上述操作3

我们来看一个实例:依次插入1,2,3,4,5,6,7,假设我们的度是3,也就是一个节点最多能容纳3个数据。

插入1

插入2,这个时候2个节点是在一起的

插入3,这个时候不可以插入了,因为插入就满了,因此要从中间分开,变成两个叶子节点。并将中间的key传递给父节点,插入父节点。

插入4,因为4插入到2,3节点中,导致再次分裂,分裂后,如下图所示

插入5,同样再次分裂,但是,父级也到达了3个数据,因此父节点同样分裂。最后结果如下

插入6

插入7

b+ tree 删除
  1. 向下扫描,找到对应的叶子节点,这个时候就会增加latch,因为不知道需不需要合并,操作以后才会释放
  2. 如果可以删除就直接删除
  3. 如果删除后导致key数量 < M/2 - 1,那么就会出发合并,因为不满足key数量啦
  4. 进行合并的时候删除这个key,然后先查看左右的兄弟节点,是否能直接把数据插入过来,如果可以的话就掠夺一个key过来,然后向上传播
  5. 如果不能掠夺,那么就合并到兄弟节点,然后向上传播。

我们接着上面的实例:依次删除1,2,3,4,5,6,7

删除1,因为删除后 key数量 < M/2 - 1,因此触发了合并逻辑。

删除2,同样进行合并。

删除3

删除4

删除5

删除6

删除7以后,树就没了。

推荐书籍 Modern B-Tree Techniques

对于非唯一索引

  • 重复存储,需要注意两个相同的key存储在不同的page中
  • value list,key只存储一个,然后所有的value存储成value list

节点内部的搜索

  • 线性搜索
  • 二分搜索
  • interpolation
    • 通过数学计算出线性搜索的起点,提升搜索速度

优化方法

  • 前缀压缩
    • 比后缀截断用的更多
    • 存储在page中的key,如果前缀一样的可以提取出来存储一次,然后剩余的数据在存储在key里面
  • 后缀截断
    • 存储在中间节点的,用来寻路的key,可以只存储前面的部分,如果后面的不需要可以截断
    • 更新的时候需要进行维护
  • 批量插入
    • 如果已经有数据了再建立索引,这个时候不需要从头开始一个个建立,只需要先排序
    • 然后建立所有的叶子节点
    • 在一层层向上建立中间节点
    • 非常普遍的方法,主流数据库都支持
  • point willizeing
    • 将节点固定在内存中
    • 对于page来说,直接存储page指针而不是page id,就不需要请求buffer pool了

b+ tree的重复key,通常使用增加record id的方式,这种方式影响更小。

  • 增加record id,record idpage id + offset用来确定tuple的位置。
  • 垂直扩展叶子节点,将数据存在里面

部分索引

  • 在创建索引的时候添加where条件,只有符合条件的才会进入索引。
  • 查询的时候只有符合条件的才会走索引

覆盖索引

  • 在创建索引的时候添加联合索引
  • 查询的时候所需数据都在索引中,就不需要在找对应的tuple信息了。

函数索引

  • 创建索引的时候添加函数信息,比如 MONTH(date), 只对月份创建索引
  • 查询的时候 MONTH(date) 就会走索引了,而date就不会走索引了
  • 如果创建的时候只创建 date 索引,那么查询的时候 MONTH(date) 就不会走索引

trie index(前缀树)

  • 把每个单词建立成树,一层放一个字母

radix tree

  • trie index的升级版
  • 对于trie index进行了横向的压缩和纵向的压缩

结论

本文通过两个简单的实例,详细讲解了如何进行b+树的插入和删除操作,对于b+树了解以后,也知道是如何支持联合索引查询的了。

同时,还对比了其他几种数据结构。并提出了一些优化策略。

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

MySQL零基础教程

本教程为零基础教程,零基础小白也可以直接学习。
基础篇和应用篇已经更新完成。
接下来是原理篇,原理篇的内容大致如下图所示。

原理学习

零基础MySQL教程原理篇之hash索引原理

MySQL中的索引是一个非常重要的组件,面试中最常问的一个问题就是你是如何优化索引的?

一天,小美遇到了张三,小美说她老公嫌弃她们家衣柜衣服多又乱,早上出门找衣服要找半天!小美问张三有没有好男人介绍一下,哭诉生活太难了
张三当即表示我张三就是个好男人啊,能文能武,还会收拾衣柜!
张三告诉小美,要想保持衣柜整洁,一定要记得以下三点。。。

我们本次要讲解就是hash索引

索引是什么?

索引是一种数据结构,它通过特定的方式排列数据,以便于快速查询。可以把索引想象成一本书的目录,通过目录可以快速找到你想要的章节,而不用从头到尾翻书。

在MySQL中,索引通常是通过B树或B+树等数据结构实现的,数据库查询引擎可以利用这些结构快速查找数据。

我们先看一下索引都有哪些类型,最常用的当然是b+树索引啦

  • B+树索引:B+树是一种多路自平衡的树结构,所有的叶子节点都在同一层,并且叶子节点通过链表连接。它是MySQL中最常用的索引类型,支持范围查询、精确查找等操作。
  • 哈希索引:哈希索引基于哈希表的原理,通过哈希函数对索引列进行计算,得到一个哈希值,哈希值对应的桶中存储数据的指针。
  • 全文索引:全文索引是专门用于文本检索的索引结构,通常用于TEXT类型的数据。MySQL会将文本字段中的每个单词进行索引,创建倒排索引,以提高文本搜索的效率。
  • 空间索引: 空间索引是用于地理空间数据类型(如POINTLINESTRINGPOLYGON)的索引结构,通常使用R树(或改进版的R+树)来存储和查询空间数据。
  • 倒排索引:倒排索引主要用于文本搜索。它将文档中每个词的出现位置存储在一个列表中,从而支持高效的文本检索。
  • 位图索引:位图索引使用位图(bitmap)来表示数据列中各个值的存在情况。例如,假设某一列有3个不同的值,可以用3个位来表示每个值的出现情况。

hash索引

what什么是hash索引呢?

  • hash索引就是通过hash函数将数据库中存储的值a映射成一个值b,然后通过该值a进行搜索的时候,可以快速找出这条记录

why为什么要使用hash索引呢?

  • 很多人都知道b+树索引,那么为什么还要有hash索引呢?
  • 其实,hash索引的性能是要高于b+树索引的,但是hash索引的使用范围要小一些,这也是现在很多人使用b+树索引的原因
  • hash索引通过hash表来进行查找,时间复杂度为O(1),性能相当可以。

When什么时候使用hash索引呢?

  • 一些内存存储引擎会使用hash索引
  • InnoDB的自适应系统检测热点
  • 执行精确相等查询

注意,只有精确相等查询才适合使用hash索引,并且性能要比b+索引更好。

比如根据手机号、用户名、邮箱精确的查询用户信息。
下面列出一些使用场景:

  1. 幂等查询,有些操作是要求幂等的,因此会存储一些幂等键。
  2. session id进行会话管理。session id可以使用hash 索引。
  3. 配置信息获取,配置信息通常使用唯一的配置key获取,可以使用hash索引。
  4. 验证码存储及获取,验证码的校验一般都是精确查询,也可以使用hash索引。

大家还知道哪些场景适合hash索引呢?

性能对比示例
假设有100万条用户会话记录

1
2
3
4
5
6
7
-- 使用哈希索引的MEMORY表
SELECT * FROM user_sessions WHERE session_id = 'target_session';
-- 响应时间: ~0.001秒

-- 如果使用B+树索引的InnoDB表
SELECT * FROM user_sessions_innodb WHERE session_id = 'target_session';
-- 响应时间: ~0.01秒

为什么这些场景适合哈希索引?

  1. 都是精确匹配查询 (= 操作符)
  2. 数据量适中 (适合内存存储)
  3. 访问频率极高 (性能要求苛刻)
  4. 数据生命周期短 (临时性数据)
  5. 不需要范围查询 (没有 BETWEEN, >, < 操作)
  6. 不需要排序 (没有 ORDER BY 操作)

这些场景的共同点是:快速、精确、临时、高频 - 这正是哈希索引的优势所在!

实现原理

首先,需要通过一个hash函数来进行映射,因此,选择合适的hash函数也是比较重要的。

最快的hash函数应该是facebook的xxhash

常见的一些hash函数如下:

  1. 简单的hash函数
    这应该是最简单的了,直接取模就可以了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 简单取模
    unsigned int simple_hash(int key) {
    return key % TABLE_SIZE;
    }

    // 加法哈希
    unsigned int add_hash(char *str) {
    unsigned int hash = 0;
    while (*str) {
    hash += *str++;
    }
    return hash;
    }
  2. 经典的一些hash函数

  • DJB2 Hash
  • MurmurHash
  • Google开发的CityHash
MurmurHash

MurmurHash 是一种非加密型哈希函数,适用于一般的哈希检索操作。由Austin Appleby在2008年发明,并出现了多个变种,都已经发布到了公有领域(public domain)。与其它流行的哈希函数相比,对于规律性较强的key,MurmurHash的随机分布特征表现更良好.

当前的版本是MurmurHash3,能够产生出32-bit或128-bit哈希值。

较早的MurmurHash2能产生32-bit或64-bit哈希值。对于大端存储和强制对齐的硬件环境有一个较慢的MurmurHash2可以用。MurmurHash2A 变种增加了Merkle–Damgård 构造,所以能够以增量方式调用。 有两个变种产生64-bit哈希值:MurmurHash64A,为64位处理器做了优化;MurmurHash64B,为32位处理器做了优化。MurmurHash2-160用于产生160-bit 哈希值,而MurmurHash1已经不再使用。

最初的实现是基于C++的,不过现在主流的语言都支持了,包括Java、Python、Golang、Php、JS、Ruby等。

算法伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
Murmur3_32(key, len, seed)
c1

{\displaystyle \gets } 0xcc9e2d51
c2

{\displaystyle \gets } 0x1b873593
r1

{\displaystyle \gets } 15
r2

{\displaystyle \gets } 13
m

{\displaystyle \gets } 5
n

{\displaystyle \gets } 0xe6546b64

hash

{\displaystyle \gets } seed

for each fourByteChunk of key
k

{\displaystyle \gets } fourByteChunk

k

{\displaystyle \gets } k * c1
k

{\displaystyle \gets } (k << r1) OR (k >> (32-r1))
k

{\displaystyle \gets } k * c2

hash

{\displaystyle \gets } hash XOR k
hash

{\displaystyle \gets } (hash << r2) OR (hash >> (32-r2))
hash

{\displaystyle \gets } hash * m + n

with any remainingBytesInKey
remainingBytes

{\displaystyle \gets } SwapEndianOrderOf(remainingBytesInKey)
remainingBytes

{\displaystyle \gets } remainingBytes * c1
remainingBytes

{\displaystyle \gets } (remainingBytes << r1) OR (remainingBytes >> (32 - r1))
remainingBytes

{\displaystyle \gets } remainingBytes * c2

hash

{\displaystyle \gets } hash XOR remainingBytes

hash

{\displaystyle \gets } hash XOR len

hash

{\displaystyle \gets } hash XOR (hash >> 16)
hash

{\displaystyle \gets } hash * 0x85ebca6b
hash

{\displaystyle \gets } hash XOR (hash >> 13)
hash

{\displaystyle \gets } hash * 0xc2b2ae35
hash

{\displaystyle \gets } hash XOR (hash >> 16)
City Hash

简而言之,这是一种非加密哈希算法,比 MurmurHash 更快,但更复杂。可以参考Google的City Hash

Google发布了两个函数,CityHash64CityHash128。它们分别将字符串哈希为 64 位和 128 位的哈希码。这些函数不适合用于密码学,但根据google目前的经验,它们非常适合用于哈希表。

Google试图针对谷歌数据中心中常见的 CPU 进行优化,但结果表明大多数台式机和笔记本电脑也具备相关特性。其中重要的是 64 位寄存器、指令级并行和快速非对齐内存访问。

City Hash方法的关键优势在于大多数步骤都包含至少两个独立的数学运算。现代 CPU 通常在这种类型的代码上表现最佳。不利之处在于代码比大多数流行替代方案更复杂。

总的来说,Google认为 CityHash64CityHash128 是解决经典问题的新颖方法。在真实条件下,预计 CityHash64 的速度将至少比以往工作快 30%,甚至可能快一倍。此外,据我们所知,这些函数的统计特性是可靠的。请大胆尝试这个快速的新代码!

City Hash的Github仓库地址:City Hash

像Click House就使用了City Hash,并在这个基础上做了一些改变

xxhash

xxHash 是一种极其快速的非加密哈希算法,运行在内存速度极限。它有四种变体(XXH32、XXH64、XXH3_64bits 和 XXH3_128bits)。最新的变体 XXH3 在各方面都提供了性能提升,特别是在处理小数据时。

详细的可以查看xxhash

hash schema

静态Hash table

  • liner probe hashing
    • 如果要插入的位置有值了,就往下扫描,扫描到空的位置插入
    • 删除的时候可以增加一个墓碑标记,这样就知道这里是有数据的不是空,查找的时候就会继续往下扫描而不会是没找到
    • 删除的时候还可以把后面的数据往前移动,但是这样有的数据就不再原来的位置了,就找不到了。因为只会往下扫描不会往上扫描
  • robin hood hashing
    • 记录距离数,表示插入的位置和应该插入的位置的距离。从0开始。
    • 插入的时候判断距离数,进行劫富济贫,如果你向下扫描到距离数为3的地方插入,而在距离数为2的地方的数据x,x的距离数比你小,比如是0,1.那么你就占据这里,你插入距离数为2的地方,而将x插入你下面,x的距离数会+1.
    • 从整体来看,这个方法牺牲了插入的效率,将数据的距离数变得更加平均
  • cuckoo hashing
    • 该方法使用两个或多个hash table来记录数据,对A进行两次hash,得出两个hash table中的插入位置,随机选择一个进行插入
    • 如果选择的插入位置已经有数据了,就选择另一个插入
    • 如果两个都有数据了,就占据一个,然后对这个位置上之前的数据B再次hash选择其余位置。

动态hash table

  • chained hashing
    • 把所有相同hash的组成一个bucket链表,然后一直往后面增加
    • java的hash table默认就是这样的
  • extendible hashing
    • 对 chained hashing 的扩展
    • 有一个slot array,在slot array上有一个 counter, 如果counter = 2,代表看hash以后的数字的前两个bit,slot array就有4个位置,分别是00,01,10,11
    • 每个slot指向一个bucket
    • hash以后找到前两位对应的slot指向的bucket,将数据放进去,如果满了,放不下了就进行拆分
    • 将slot array的counter扩容为3,看前3个bit,slot array变成了8个位置
    • 只将这个满了的bucket拆分成2个,其余的不变,重新进行slot的映射
    • 再次hash这个值,看前3个bit找到对应的slot,在找到对应的bucket,然后插入进去
  • linear hashing
    • 对 extendible hashing 的扩展
    • 去掉了 conter,因为他每次加1,都会扩容一倍
    • 增加了split point,一开始指向0,然后每次overflow需要拆分的时候就拆分split point指向的那个bucket,然后slot array只扩容一个,这个时候出现第二个hash函数并将split point+1
    • 查询的时候如果slot array的位置小于split point,就使用第二个hash函数,因为被拆分了
    • 如果大于等于split point,就使用第一个hash函数

hash索引的一些优化手段

hash索引仅用于使用=或<=>运算符的相等比较(但非常快)。它们不用于查找值范围的比较运算符,如<。依赖于这种单值查找的系统被称为“键值存储”;要将MySQL用于此类应用程序,请尽可能使用散列索引。

优化器不能使用哈希索引来加速ORDER BY操作。

MySQL无法确定两个值之间大约有多少行(这是由范围优化器用来决定使用哪个索引)。如果您将MyISAM或InnoDB表更改为哈希索引的MEMORY表,这可能会影响某些查询。

只能使用整个键来搜索行。

结论

本次分享了MySQL中重要的概念Hash索引,接下来会出一个Hash索引具体实现的文章。

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

MySQL零基础教程

本教程为零基础教程,零基础小白也可以直接学习。
基础篇和应用篇已经更新完成。
接下来是原理篇,原理篇的内容大致如下图所示。

原理学习

零基础MySQL教程原理篇之缓冲池原理及实现

MySQL中的buffer pool(缓冲池)InnoDB存储引擎的重要组件,它负责在内存中管理数据库中的数据和索引的缓存。

它加速了数据库的运行速度,是数据库和磁盘之间的一个中间层。如果没有缓冲池,那么所有的数据库操作都需要进行磁盘IO,有了缓冲池,就不需要频繁的IO操作了。

缓冲池重点在于两个部分

  • 时间管理
  • 空间管理

时间管理

  • 将数据写入磁盘的何处
  • 目标是经常被一起使用的pages放在磁盘中也是一起的地方。

空间管理

  • 何时将pages读入内存,何时将pages写入磁盘
  • 目标是最小化的解决必须从磁盘读取数据这个事

同样的一个内存块在不同的地方,就有不同的叫法,比如在磁盘中,存储数据库中的数据,我们叫做page(页),而放在缓冲池中,就叫frame(帧)

一个frame其实就是一页数据。只不过这个数据是在缓冲池中的.

到这里有个问题了,那就是缓冲池里面都是一堆数据,可是MySQL怎么知道缓冲池的哪个frame里面有数据,哪个没有呢?frame里面的数据对应的是具体哪个page的数据呢?

因此,就需要另外一个组件了,叫做page table,本质上就是一个hash map。这个page table记录了页数据在当前缓冲池中的位置,通过page tablepage id可以知道在哪个frame中。

page table

我们还需要记录一些元数据,这些数据也有着重要的作用:

  • dirty flag: 记录是否被修改过,也就是常说的”脏数据标记”
  • 引用计数器: 记录有多少线程在使用这个数据
  • 访问追踪信息

缓冲池和mmap

如果你学过操作系统这个课,那么你看到这里,是不是觉得缓冲池很像一个东西?

没错,就是mmap。那为什么MySQL要使用自己实现的缓冲池呢?这是因为相比于mmap来说,自定义的缓冲池可以更加完美的控制,达到自己想要的效果。这也是很多大厂会自研很多组件的原因,更加适配自己的生态系统,并可以进行一些性能优化。

从下面几个点来看:

  • 事务安全:如果使用mmap,操作系统完全控制page的写入,刷新,有可能在一个事务没有完成的时候有些数据就已经写入磁盘了。
  • IO停顿:MySQL不知道哪些page在内存中,当读取不在内存中的时候触发page fault,操作系统才会从磁盘获取。
  • 错误处理:任何访问都可能触发操作系统的中断信号SIGBUS,而整个MySQL都需要处理它。
  • 性能问题: 没有办法实现独特的性能优化。

性能优化有两种考虑策略,分别是全局策略局部策略

全局策略

  • 针对所有的查询或者事务的策略

局部策略

  • 针对单个查询或者事务的策略
  • 可以对单个优化,虽然对全局可能不好

淘汰策略

既然MySQL自己实现了一个缓冲池作为磁盘数据的缓存,那么就像我们日常使用Redis作为缓存一样,也是需要有一个淘汰策略的,毕竟,缓冲池满了怎么办?总不能MySQL不工作了吧。

淘汰策略有几种算法

  • LRU:著名的LRU算法,这里不过多介绍了。
  • Clock:Linux使用的,把所有的page放成一个圈,每个page有一个标志位,如果为0表示没有被使用过,1被使用过,淘汰的时候淘汰0的,再把1改成0.
  • LRU-K:记录使用的次数k,达到次数才放到缓存里面,淘汰的时候比对两次的时间间隔,间隔长的认为是最近最少使用。这个基本上是MySQL使用的一个淘汰算法。属于LRU算法的变种。
  • PRIORITY HINTS(优先级提示):优先级提示是一种淘汰策略,用于根据缓存项的优先级或重要性做出更智能的决策。

LRU-K

LRU-K是标准LRU(最近最少使用)算法的变种,它的效果比传统的LRU算法更好,缓存的命中率更高,它考虑K个最近的访问,而不仅仅是最近的一次访问。

核心理念:

  • 跟踪每个缓存项的最近K次访问次数
  • 使用最近的第k个访问时间进行驱逐决策
  • K=1降为标准LRU

可以这么理解,LRU就是LRU-1算法。

LRU-K算法的实现比LRU更加复杂,不能单纯考虑最近一次,而是要考虑最近K次。

与LRU相比的优点:

  1. 更好的频率检测:
  • 区分“流行”和“暂时流行”的物品
  • 减少顺序扫描造成的缓存污染
  1. 自适应行为:
  • 不同的K值优化不同的访问模式
  • K=2通常提供良好的平衡
  1. 扫描阻力:
  • 大规模的连续扫描不会永久地删除流行的项目
  • 项目需要多次访问才能确定其重要性

LRU-K在频繁随机访问和偶尔顺序扫描的环境中特别有效。

在LRU-K中,首先需要定义一个LRUK的节点,这个节点存储了frame的访问次数,是否可以淘汰等信息,还需要存储所有LRUK节点的一个map,map的key是frame id,这样可以快速获取这个frame的LRUK节点信息。而不需要循环查找。

接下来可以通过两个队列来实现算法,第一个队列存储的是未满足K次的的frame信息,第二个队列存储的是满足K次的frame信息。

记录访问的大致代码如下:

1
2
3
4
5
6
7
8
9
10
// 访问次数决定队列分配
if (record_size == 1) {
lru_node_queue_.push_back(node.GetId()); // 首次访问 -> LRU队列
} else if (record_size == k_) {
lru_node_queue_.remove(node.GetId()); // 达到k次 -> 移到LRU-K队列
lru_k_queue_.push_back(node.GetId());
} else if (record_size > k_) {
lru_k_queue_.remove(node.GetId()); // k次后 -> 更新LRU-K队列位置
lru_k_queue_.push_back(node.GetId());
}

当缓冲池满了以后,我们需要淘汰一个page,给另外一个page让出空间。

这里会优先淘汰未满足K次的这些page,因此,直接从第一个LRU队列中进行LRU淘汰即可。

1
2
3
4
auto lru_it = std::find_if(lru_node_queue_.begin(), lru_node_queue_.end(),
[this](const auto &queue_frame_id) {
return node_store_.at(queue_frame_id).IsEvict();
});

如果大家都满足K次,就淘汰这些访问次数达到K次的page

1
2
3
4
auto lru_k_it = std::find_if(lru_k_queue_.begin(), lru_k_queue_.end(),
[this](const auto &queue_frame_id) {
return node_store_.at(queue_frame_id).IsEvict();
});

如果当有page更新的话,那么要记录这个page有更新,当淘汰的时候,需要将这个page写入磁盘中。

mysql 近似 LRU-k

MySQL使用的淘汰算法是一个近似LRU-K的算法。

相当于K=2。有一个LRU List,但是有两个指针,分别表示old listyoung list。当数据第一次被访问的时候放到old list中,再次被访问的时候放到young list中。

近似LRU-K

当访问 page1 的时候,需要淘汰掉old list中的page8,其实也是整个LRU中的最后一个元素。然后将page1插入old list

当再次访问 page1 的时候,将page1 插入young list。这个时候young list最后的元素也就进入了old list.

PRIORITY HINTS(优先级提示)

比如B+树的根节点具有最高的优先级,所以一直放在内存中。

B+树

核心概念:

  • 不像简单的LRU只考虑访问的近因性
  • 优先级提示包含关于项目重要性的额外元数据
  • 允许更复杂的驱逐策略

常见的实现方法:

  1. 基于优先级的LRU:
  • 物品有优先级(高,中,低)
  • 在每个优先级中,使用LRU
  • 从最低优先级优先驱逐
  1. 加权LRU:
  • 每个项目都有优先级权重
  • 将最近性与优先级分数结合起来
  • 驱逐最低综合分数的项目
  1. 多队列系统:
  • 为不同的优先级分隔队列
  • 后台进程管理每个队列的回收
  • 高优先级的项目留存时间更长

对于缓冲池的一些优化

多缓冲池

  • 通过使用多个缓冲池可以根据不同的table放入不同的缓冲池进行不同的优化。也可以通过其他的策略使用多个缓冲池
  • 由于有多个缓冲池,减少了锁争抢和锁等待的时间。
  • mysql中通过hash确定数据是否在缓冲池,然后通过取余确定在哪个缓冲池

预取数据

  • 顺序扫描的时候预先把后面的page取到缓冲池中。这一步mmap也可以实现
  • 索引扫描的时候预先把索引中需要用到的后面的page取到缓冲池中。这一步mmap实现不了,这也是数据库自己实现缓冲池的优势。

扫描共享

这个优化MySQL并没有实现。

  • 共享扫描到的page内容
  • 如果查询1需要扫描page1,page2,page3,page4的内容并且已经扫描到了page3,这个时候page1已经扫描完了被从缓冲池中丢弃了
  • 这时候有一个查询2也需要扫描所有的pages,如果从page1开始扫描,就会把page1再次读入缓冲池,但是这样是低效率的,所以可以先共享查询1的page数据,先扫描page3,然后page4,这时候查询1执行完毕,在回头扫描page1,page2。

buffer pool bypass

  • 单独开辟一个本地内存区域来用,而不是使用buffer pool
  • 可以避免操作page table带来的开销(latch锁住的开销)
  • 可以避免污染buffer pool
  • 适合数据量不大的情况
  • mysql5.7不支持

os page cache

  • 操作系统的文件缓存,当使用fopen,fread,fwrite的时候会先从操作系统缓存中读取文件内容。
  • 只有postgresql使用了这个。
  • 通过 direct IO可以不使用这个
  • 使用它会导致有两个缓存,buffer pool 和 os page cache。不好控制。
  • fsync如果失败以后再次调用也不会生效,因为它会将dirty设置为false

写入磁盘策略

两种写出方案需要做权衡,取舍

  • 如果写出dirty flag的数据然后读取新数据,就会产生2次IO。通常会有一个定时任务线程去将dirty flag的数据写入磁盘,写入之前必须要先将操作日志写入磁盘。
  • 如果直接读取新数据就只有1次IO,但是这样有可能把下次会用到的数据丢弃。

MySQL性能优化

buffer pool

建议设置内存的50%以上给buffer pool,设置的越多,就越像内存数据库

淘汰算法类似LRU-K,缓冲池分为Old和young两段,一开始插入old的头,如果再次访问则插入young。缓冲池的3/8专用于旧的子列表。

默认情况下,查询读取的页面会立即移动到新的子列表中,这意味着它们在缓冲池中停留的时间更长。例如,为mysqldump操作或不带WHERE子句的SELECT语句执行的表扫描可以将大量数据带入缓冲池并驱逐等量的旧数据,即使新数据永远不会再次使用。

类似地,由预读后台线程加载且仅访问过一次的页将被移动到新列表的头部。这些情况可能会将经常使用的页面推到旧的子列表中,在那里它们会被驱逐。

在具有足够内存的64位系统上,可以将缓冲池拆分为多个部分,以最小化并发操作之间对内存结构的争用。

您可以控制如何以及何时执行预读请求,以便在预期即将需要页时将页异步预取到缓冲池中。

您可以控制何时发生后台刷新,以及是否根据工作负载动态调整刷新速率。

您可以配置InnoDB如何保留当前缓冲池状态,以避免服务器重启后的漫长预热期。

可以使用SHOW ENGINE INNODB STATUS 来查看缓冲池状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 2198863872
Dictionary memory allocated 776332
Buffer pool size 131072
Free buffers 124908
Database pages 5720
Old database pages 2071
Modified db pages 910
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 4, not young 0
0.10 youngs/s, 0.00 non-youngs/s
Pages read 197, created 5523, written 5060
0.00 reads/s, 190.89 creates/s, 244.94 writes/s
Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not
0 / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read
ahead 0.00/s
LRU len: 5720, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]

change buffer

change buffer 是一种特殊的数据结构,当辅助索引页不在缓冲池中时,它会缓存这些页的更改。缓冲的更改(可能是由DELETE、UPDATE或DML操作引起的)在以后通过其他读取操作将页加载到缓冲池时合并。

与聚集索引不同,辅助索引通常是非唯一的,插入辅助索引的顺序相对随机。同样,删除和更新可能会影响索引树中不相邻的二级索引页。当其他操作将受影响的页读入缓冲池时,在以后合并缓存的更改可以避免将辅助索引页从磁盘读入缓冲池所需的大量随机访问I/O。

在系统大部分空闲时或缓慢关机期间运行的清除操作会定期将更新的索引页写入磁盘。与立即将每个值写入磁盘相比,清除操作可以更高效地写入一系列索引值的磁盘块。

当有许多受影响的行和许多要更新的辅助索引时,更改缓冲区合并可能需要几个小时。在此期间,磁盘I/O会增加,这可能会导致磁盘绑定查询的速度显著降低。更改缓冲区合并也可能在事务提交后继续发生,甚至在服务器关闭并重新启动后也会发生

在内存中,更改缓冲区占用缓冲池的一部分。在磁盘上,更改缓冲区是系统缓存的一部分,当数据库服务器关闭时,索引更改将在其中进行缓冲。

如果索引包含降序索引列,或者如果主键包含降序索引列,则不支持辅助索引的更改缓冲。

当对表执行INSERT, UPDATE和DELETE操作时,索引列的值(特别是辅助键的值)通常是无序的,需要大量的I/O来更新辅助索引。当相关页不在缓冲池中时,更改缓冲区将缓存对辅助索引条目的更改,从而避免了昂贵的I/O操作,因为它不会立即从磁盘阅读页。当页加载到缓冲池中时,将合并缓冲的更改,更新后的页稍后将刷新到磁盘。InnoDB主线程在服务器接近空闲时和缓慢关闭期间合并缓冲的更改。

由于更改缓冲可以减少磁盘读取和写入,因此它对于I/O受限的工作负载最有价值;例如,具有大量DML操作(如批量插入)的应用程序将受益于更改缓冲。

但是,更改缓冲区占用了缓冲池的一部分,从而减少了可用于缓存数据页的内存。如果工作集几乎适合缓冲池,或者表的辅助索引相对较少,则禁用更改缓冲可能很有用。如果工作数据集完全适合缓冲池,则更改缓冲不会产生额外的开销,因为它只应用于不在缓冲池中的页。

innodb_change_buffering变量控制InnoDB执行更改缓冲的程度。您可以为插入、删除操作(当索引记录最初标记为删除时)和清除操作(当索引记录被物理删除时)启用或禁用缓冲。更新操作是插入和删除的组合。默认的innodb_change_buffering值是none

innodb_change_buffer_max_size变量允许将更改缓冲区的最大大小配置为缓冲池总大小的百分比。默认情况下,innodb_change_buffer_max_size设置为25。最大设置为50。

考虑在具有大量插入、更新和删除活动的MySQL服务器上增加innodb_change_buffer_max_size,其中更改缓冲区合并无法跟上新的更改缓冲区条目,导致更改缓冲区达到其最大大小限制。

如果MySQL服务器上的静态数据用于报告,或者如果更改缓冲区占用了太多与缓冲池共享的内存空间,导致页面比预期更快地老化,请考虑减少innodb_change_buffer_max_size

使用具有代表性的工作负载测试不同的设置以确定最佳配置。innodb_change_buffer_max_size变量是动态的,允许在不重启服务器的情况下修改设置。

要查看监视器数据,请发出SHOW ENGINE INNODB STATUS语句。

1
2
3
4
5
6
7
8
9
10
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1, free list len 0, seg size 2, 0 merges
merged operations:
insert 0, delete mark 0, delete 0
discarded operations:
insert 0, delete mark 0, delete 0
Hash table size 4425293, used cells 32, node heap has 1 buffer(s)
13577.57 hash searches/s, 202.47 non-hash searches/s

双写缓冲区

双写缓冲区是一个存储区域,InnoDB在将页面写入InnoDB数据文件中的适当位置之前,将从缓冲池刷新的页面写入其中。如果在页面写入过程中出现操作系统、存储子系统或意外的mysqld进程退出,InnoDB可以在崩溃恢复期间从doublewrite缓冲区中找到页面的良好副本。

虽然数据被写入两次,但双写缓冲区不需要两倍的I/O开销或两倍的I/O操作。数据以一个大的顺序块的形式写入双写缓冲区,并对操作系统进行单个fsync()调用(除非innodb_flush_method设置为O_DIRECT_NO_FSYNC)。

双写缓冲存储区位于双写文件中。

为双写缓冲区配置提供了以下变量:

  • innodb_doublewrite变量控制是否启用双写缓冲区。它在大多数情况下默认启用。要禁用双写缓冲区,请将innodb_doublewrite设置为OFF。如果您更关心的是性能而不是数据完整性,那么可以考虑禁用双写缓冲区,例如,在执行基准测试时可能就是这种情况。
  • innodb_doublewrite支持DETECT_AND_RECOVER和DETECT_ONLY设置。
    • DETECT_AND_RECOVER设置与ON设置相同。通过此设置,双写缓冲区将完全启用,数据库页内容将写入双写缓冲区,在恢复期间将访问该缓冲区以修复不完整的页写入。
    • 使用DETECT_ONLY设置时,仅将元数据写入双写缓冲区。数据库页内容不写入双写缓冲区,恢复不使用双写缓冲区来修复不完整的页写入。此轻量级设置仅用于检测不完整的页写入。
  • innodb_doublewrite_dir变量定义了InnoDB创建doublewrite文件的目录。如果未指定目录,则在innodb_data_home_dir目录中创建双写文件,如果未指定,则默认为data目录。
  • innodb_doublewrite_files变量定义了双写文件的数量,默认值为2。默认情况下,为每个缓冲池实例创建两个双写文件:刷新列表双写文件和LRU列表双写文件。刷新列表双写文件用于从缓冲池刷新列表中刷新的页面。刷新列表双写文件的默认大小是InnoDB页面大小 * 双写页面字节。innodb_doublewrite_files变量用于高级性能调优。默认设置应该适合大多数用户。
  • innodb_doublewrite_pages变量控制每个线程的最大双写页面数。此变量用于高级性能调整。默认值应该适合大多数用户。

结论

本次分享了MySQL中重要的组件buffer pool的概念,以及设计理念,具体实现方案,采用的淘汰策略。

并根据这些内容提出了一些性能优化方案。明白了缓冲池的原理及作用以后,根据这些优化方案可以更好的进行数据库的性能优化。

但是,buffer pool也不是越大越好,根据需要来调整,调整以后可以进行一些测试,以测试出最适合自己业务的大小。

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

MySQL零基础教程

本教程为零基础教程,零基础小白也可以直接学习。
基础篇和应用篇已经更新完成。
接下来是原理篇,原理篇的内容大致如下图所示。

原理学习

零基础MySQL教程原理篇之缓冲池实现

MySQL中的buffer pool(缓冲池)InnoDB存储引擎的重要组件,它负责在内存中管理数据库中的数据和索引的缓存。

它加速了数据库的运行速度,是数据库和磁盘之间的一个中间层。如果没有缓冲池,那么所有的数据库操作都需要进行磁盘IO,有了缓冲池,就不需要频繁的IO操作了。

本次我们的目标是使用C++实现一个缓冲池,这也是CMU15445这门课的Project1

基于BusTub学术数据库管理系统构建一个面向磁盘的存储管理器。在这样的存储管理器中,数据库的主要存储位置在磁盘。

缓冲池负责在主内存和磁盘之间来回移动物理页面。它允许数据库管理系统支持比系统可用内存量更大的数据库。缓冲池的操作对系统其他部分是透明的。

例如,系统使用其唯一标识符 ( page_id_t )向缓冲池请求一个页面,它不知道这个页面是否已经在内存中,或者系统是否需要从磁盘中检索它。

实现需要是线程安全的。多个线程将并发访问内部数据结构,并且必须确保它们的临界区受到latches的保护(在操作系统中这些被称为“锁”)。

实现总共分为三个部分:

  1. 实现LRU-K淘汰策略
  2. 实现磁盘调度程序
  3. 实现缓冲池管理器

实现LRU-K淘汰策略

关于LRU-K淘汰策略的一些信息可以看上一篇文章零基础MySQL教程原理篇之缓冲池原理及实现

本次实现主要都在这两个文件中:

  • 头文件src/include/buffer/lru_k_replacer.h
  • 实现文件src/buffer/lru_k_replacer.cpp

LRU-K 算法会淘汰替换器中反向 k 距离最大的页帧。反向 k 距离是指当前时间戳与第 k 次先前访问的时间戳之间的时间差。少于 k 次历史访问的页帧其反向 k 距离被赋予+inf。当多个页帧具有+inf 反向 k 距离时,替换器会淘汰具有最早整体时间戳的页帧(即记录访问最久远的页帧,在所有页帧中整体上最不常被访问的页帧)。

LRUKReplacer 的最大大小与缓冲池的大小相同,因为它包含了 BufferPoolManager 中所有页帧的占位符。然而,在任意时刻,替换器中的所有页帧并非都被视为可被淘汰。 LRUKReplacer 的大小由可被淘汰页帧的数量表示。 LRUKReplacer 被初始化为空。只有当页帧被标记为可被淘汰时,替换器的大小才会增加。

总共要实现以下方法:

  • Evict(frame_id_t* frame_id):将与所有其他被跟踪的可淘汰帧相比具有最大向后k距离的帧驱逐。将帧id存储在输出参数中并返回 True 。如果没有可淘汰帧,则返回 False 。
  • RecordAccess(frame_id_t frame_id):记录给定帧 id 在当前时间戳被访问。此方法应在页面被固定在 BufferPoolManager 后调用。
  • Remove(frame_id_t frame_id):清除与帧关联的所有访问历史。此方法仅在页面被删除在 BufferPoolManager 时调用。
  • SetEvictable(frame_id_t frame_id, bool set_evictable): 此方法控制帧是否可被淘汰。它还控制 LRUKReplacer 的大小。当您实现 BufferPoolManager 时,您将知道何时调用此函数。具体来说,当页面的固定计数达到 0 时,其对应的帧被标记为可淘汰,并且替换器的大小增加。
  • Size():该方法返回当前在 LRUKReplacer 中的可淘汰帧数。

你可以假设不会内存耗尽,但你必须确保你的实现是线程安全的。

以上是实现要求.

接下来讨论一下实现的思路。关于LRU-K在数据库中的实现有一篇论文可以参考一下LRU-K

先来看RecordAccess方法吧,这个方法算是比较简单的了,也是整个LRU-K算法的入口方法,因为每次访问一个page,都需要调用该方法。

首先,我们可以看到在头文件中有LRUKNode的一个类型定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class LRUKNode {
public:
explicit LRUKNode(frame_id_t fid) : fid_(fid){};

~LRUKNode() = default;

auto Record(size_t timestamp) -> size_t;

auto SetEvictable(bool evictable) -> bool;

auto GetId() const -> frame_id_t { return fid_; }

auto GetK() const -> size_t { return k_; }

auto GetKDistance() const -> size_t { return k_distance_; }

auto IsEvict() const -> bool { return is_evictable_; }

// 定义比较操作符
auto operator==(const LRUKNode &other) const -> bool { return fid_ == other.fid_; }

void Print();

private:
/** History of last seen K timestamps of this page. Least recent timestamp stored in front. */
// Remove maybe_unused if you start using them. Feel free to change the member variables as you want.

std::list<size_t> history_; //记录访问历史
size_t k_distance_; //当前frame k distance
size_t k_{0};
const frame_id_t fid_; // frame id
bool is_evictable_{false}; // 是否可淘汰
};

在这里我们有一个List类型的history_用来记录这个Node的访问历史。k_distance_用来计算k距离。fid_表示这个Node的帧id,也就是frame idis_evictable_代表是否可以被淘汰掉。

因此,在我们的LRUK算法中,需要有一个地方存储这些Node,这里第一个想到的是用一个List来存储,但是,这样每次查找Node都不方便,因此,可以使用一个HashMap来存储,定义一个node_store_来进行存储,这个map的key是frame id,value是一个LRUKNode。我们还需要记录两个大小,一个是LRU淘汰器里面可淘汰的大小,一个是最大的大小。还有最关键的两个数据结构,我们采用两个queue来实现真个LRUK算法,一个lru_node_queue_来存储没有到达K次的节点,一个lru_k_queue_来存储已经到达K次的节点。

因此,在RecordAccess方法中,我们的实现思路就是:

  1. 给定一个frame id
  2. 从node_store_中获取这个frame id对应的节点信息
  3. 如果是第一次访问,继续往下。如果不是,跳到8处
  4. 首先,我们需要为这个帧创建一个LRUKNode
  5. 将这个节点插入node_store_中
  6. 记录本次对于这个新创建LRUKNode的访问历史
  7. 根据访问次数决定放入的队列,如果小于K次,放入lru_node_queue_,否则放入lru_k_queue_
  8. 如果node_store_中有这个帧,代表曾经访问过,不需要创建LRUKNode了
  9. 从node_store_中取出这个LRUKNode,并记录访问历史
  10. 根据访问次数决定放入的队列,如果小于K次,放入lru_node_queue_,否则放入lru_k_queue_

这里面有一个公共的点在于,6、7步骤和9、10步骤,因此可以把这两个步骤抽取出来一个公共方法,我们暂时命名为RecordHelper方法。

接下来看SetEvictable这个方法,这个方法比较简单,因为只是简单的设置一个LRUKNode的状态为可以被淘汰

同样的,给定一个frame id,我们需要先从node_store_中进行查询。

  1. 给定一个frame id
  2. 从node_store_中获取这个frame id对应的节点信息
  3. 如果没有查询到,说明不在淘汰器中,因此直接返回错误或者不处理即可。
  4. 如果查询到一个LRUKNode,从 node_store_ 中取出这个节点
  5. 修改这个LRUKNode的状态
  6. 更新可淘汰数量

对于Size方法是最简单的,直接返回可淘汰数量即可。

对于Remove方法,是要将一个节点从淘汰器中删除。

  1. 给定一个frame id
  2. 从node_store_中获取这个frame id对应的节点信息
  3. 如果没有查询到,说明不在淘汰器中,因此直接返回错误或者不处理即可。
  4. 如果查询到一个LRUKNode,从 node_store_ 中取出这个节点
  5. 从 node_store_ 中删除这个节点
  6. 因为我们维护了两个队列,因此这里要判断一下节点的K次,来决定从哪个队列中删除这个节点。

最重要的实现就是Evict方法了,这个是整个算法的核心实现。实现思路如下:

  1. 优先从 lru_node_queue_ 中淘汰
  2. 如果在 lru_node_queue_ 中,则将该节点从 lru_node_queue_ 中删除,并从 node_store_ 中删除
  3. 如果 lru_node_queue_ 中没有符合条件的,再从 lru_k_queue_ 中寻找
  4. 如果在 lru_k_queue_ 中,则将该节点从 lru_k_queue_ 中删除,并从 node_store_ 中删除。
  5. 如果两个队列中都没有可淘汰的页面,则返回 false

实现磁盘调度程序

磁盘调度程序负责读写磁盘IO,我们需要实现下面两个文件:

  • src/include/storage/disk/disk_scheduler.h
  • src/storage/disk/disk_scheduler.cpp

磁盘调度器可以被其他组件(在此情况下,任务#3 中的 缓冲池管理器 )用来排队磁盘请求,这些请求由 DiskRequest 结构体表示(已在 src/include/storage/disk/disk_scheduler.h 中定义)。磁盘调度器将维护一个后台工作线程,负责处理已排队的请求。

磁盘调度器将使用一个共享队列来调度和处理 DiskRequests。一个线程会将请求添加到队列中,而磁盘调度器的工作线程会处理队列中的请求。我们在 src/include/common/channel.h 中提供了一个 Channel 类来促进线程间数据的安全共享,但如果你认为有必要,也可以使用自己的实现。

DiskScheduler 的构造函数和析构函数已经实现,它们负责创建和加入后台工作线程。你只需要实现以下方法:

  • Schedule(DiskRequest r) : 安排 DiskManager 执行请求。 DiskRequest 结构体指定请求是读/写操作,数据应写入/读取的位置,以及操作的page IDDiskRequest 还包括一个 std::promise ,其值应在请求处理完成后设置为 true。
  • StartWorkerThread() : 启动后台工作线程的方法,该方法处理计划中的请求。工作线程在 DiskScheduler 构造函数中创建,并调用此方法。此方法负责获取排队中的请求并将它们分派到 DiskManager 。记得在 DiskRequest 的回调中设置值,以通知请求发起人请求已完成。此方法不应在 DiskScheduler 的析构函数被调用之前返回。

最后, DiskRequest 中的一个字段是一个 std::promise 。如果你不熟悉 C++ 的 promisefuture,可以查看它们的文档。在这个项目中,它们基本上为线程提供了一个回调机制,以便线程知道它们的计划请求何时完成。要查看它们可能的使用示例,可以查看 disk_scheduler_test.cpp

磁盘管理器类( src/include/storage/disk/disk_manager.h )负责从磁盘读取和写入页面数据。当磁盘调度器处理读或写请求时,会使用 DiskManager::ReadPage() 和 DiskManager::WritePage() 。

在头文件中,已经给我们定义了一个request_queue_属性,这个队列是一个Channel类型的,是一个并发安全的类型,可以简单理解为go中的Channel或者是一个并发安全的队列。

因此,对于Schedule(DiskRequest r)方法,最简单的一个做法就是我们接收到请求以后直接放入这个Channel中。

  1. 直接放入request_queue_

主要的处理逻辑全部放在StartWorkerThread这个方法里面。这里我们首先定一个变量stop_pool_来表示磁盘调度程序是否可以结束,如果不结束,我们就一直循环处理即可。

  1. 判断stop_pool_是否结束
  2. 不结束,我们从request_queue_中获取一个要处理的请求。
  3. 判断该请求是否有效
  4. 判断请求是写请求还是读请求
  5. 如果是写请求,调用disk manager进行写入
  6. 如果是读请求,调用disk manager进行读取
  7. 设置返回值true

实现缓冲池管理器

这是最核心的一个功能了,那就是缓冲池的实现。

缓冲池会调用磁盘调度程序来完成磁盘读写请求。当被明确指示执行或需要淘汰Page以腾出空间给新Page时, 缓冲池 还可以安排脏页写入磁盘。

系统中的所有内存页都由 Page 对象表示。 BufferPoolManager 不需要理解这些页面的内容。

但对于作为系统开发者的你来说,重要的是要理解 Page 对象只是缓冲池中内存的容器,因此它们并不特定于某个唯一页面。也就是说,每个 Page 对象包含一个内存块, DiskManager 将使用这个内存块作为位置来复制它从磁盘读取的物理页面的内容。当 BufferPoolManager 在磁盘之间来回移动时,它会重用同一个 Page 对象来存储数据。这意味着在系统的整个生命周期中,同一个 Page 对象可能包含不同的物理页面。 Page 对象的标识符( page_id )会跟踪它包含的物理页面;如果 Page 对象不包含物理页面,那么它的 page_id 必须设置为 INVALID_PAGE_ID 。

每个 Page 对象还维护一个计数器,用于记录有多少线程“固定”了该页。你的 BufferPoolManager 不允许释放被固定的 Page 。每个 Page 对象还跟踪它是否已变脏。在页被取消固定之前,你的工作是要记录该页是否已被修改。你的 BufferPoolManager 必须在对象被重用之前,将脏 Page 的内容写回磁盘。

你的 BufferPoolManager 实现将使用在本次作业前几步中创建的 LRUKReplacer 和 DiskScheduler 类。 LRUKReplacer 将跟踪 Page 对象何时被访问,以便在必须释放一个帧以从磁盘复制新的物理页时决定要驱逐哪一个。在 BufferPoolManager 中将 page_id 映射到 frame_id 时,再次警告:STL 容器不是线程安全的。 DiskScheduler 将在 DiskManager 上调度对磁盘的读写。

该实现涉及到两个文件:

  • src/include/buffer/buffer_pool_manager.h
  • src/buffer/buffer_pool_manager.cpp

我们需要实现下面的几个方法:

  • FetchPage(page_id_t page_id):如果没有空闲页且所有其他页都被固定,应返回 nullptr。 FlushPage 应无条件刷新页面,无论其固定状态如何。
  • UnpinPage(page_id_t page_id, bool is_dirty): is_dirty 参数用于跟踪页面在固定期间是否被修改。
  • FlushPage(page_id_t page_id)
  • NewPage(page_id_t* page_id)
  • DeletePage(page_id_t page_id)
  • FlushAllPages()

这些方法的实现依赖上面的方法,建议上面的方法首先通过测试,再继续这几个方法的实现

初始化

在之前讲过了,我们缓冲池的查找是根据page table来查找的,因此,我们的缓冲池管理器中,需要定义一个page table变量,类型是一个map,key是一个page id,value是一个frame id,这样,我们就可以通过page id来找到对应的帧信息。

同样的,在缓冲池管理中,我们还需要定义一个page list来存放所有内存中的page信息,在c++中,这通常是一个指针类型,代码中已经给出了这个变量Page *pages_

我们还需要使用一个list来告诉我们有哪些空闲空间。我们使用变量free_list来表示。

在初始化的时候,假设我们有5个page的内存可以进行缓冲。那么free_list里面有5个大小,代表有5个page的空间,pages为空,代表这些空间没有page,page table为空。

FetchPage

这个方法的入参是一个page id,因此,我们的第一步就是通过page id来找到frame id

如果找到了,代表这个page已经在我们内存中,不需要进行磁盘IO,直接返回即可。这里如何取到Page呢,通过Pages这个数组就可以拿到了,下标是我们frame id。即pages_[frame id]就可以获取到内存中的page信息了。

注意:在Page信息中,有一个Pin的概念,pin住也就是固定住的意思是,当前这个page有一个上层的应用在使用,也就意味着我们不能将这个page页面淘汰出内存。

因此,在返回page之前,我们需要pin住这个page,可以通过Page里面的pin_count来完成。

pin住这个page以后,需要调用第一个LRU-K淘汰策略,这里需要调用两个方法SetEvictable告诉淘汰器我们pin住了这个页面,你不能淘汰它了。RecordAccess告诉淘汰器,我们使用了这个页面,请你更新它的淘汰距离。

如果没有从page table中查找到对应的信息,说明这个page还在磁盘中,我们需要将它从磁盘中,读取到内存。

首先,我们需要知道现在内存中,是否还有空间来存放page,如果没有空间了,就需要淘汰出一个空间。
通过free_list可以看到是否有空间,如果不为空,代表还有空闲的空间。那么就好办了。我们取出free list中第一个空闲空间来进行存放,取出来是一个frame id

如果没有空闲的空间了,首先,要通过page table来检查,是否所有的页面都pin住了,如果是,那么则只能返回nullptr,代表没有空间可以使用了,相当于内存耗尽。

如果有页面没有被pin住,那么我们可以淘汰出一个页面,通过LRU-K算法的Evict方法,会告诉我们有哪个页面可以被淘汰,如果没有可以淘汰的,同样返回nullptr。

接下来,我们已经拿到了一个可以使用的frame id,不管是从空闲空间,还是淘汰出来的。

我们通过pages_[frame id]拿到对应的page页面,这个页面可能是一个空的,也可能是一个淘汰出来的有信息的页面。

我们首先,重置page table,因为所有的信息都需要经过page table获取,我们要告诉page table,当前这个page id对应的frame id已经改变了。

接下来,pin住这个页面,因为上层会使用,然后判断这个page是否有被更新,如果有,那么需要写入磁盘。

接下来,更新内存中page的一些元信息,比如清空数据,设置page id,并从磁盘读取新的数据放到这个page中。

最后,返回page信息。

总结一下:

  1. 通过page table使用page id来找到frame id
  • 找到以后pin住,返回。
  1. 通过free list找到空闲的空间,如果有,转到5
  2. 没有,检查是否所有页面都pin住,如果是,那么返回nullptr
  3. 通过LRU-K算法淘汰出一个page以使用。
  4. 修改page table
  5. pin住页面
  6. 是否脏page,是的话写入磁盘
  7. 更新元数据并从磁盘读取新的数据放入page
  8. 返回page

NewPage

这个方法和上面的方法几乎一样,不同之出在于这个方法是创建一个page空间供使用,因此不需要从磁盘加载数据,也不需要查看page table,因为page table一定没有。

因此步骤如下:

  1. 通过free list找到空闲的空间,如果有,转到5
  2. 没有,检查是否所有页面都pin住,如果是,那么返回nullptr
  3. 通过LRU-K算法淘汰出一个page以使用。
  4. 修改page table
  5. pin住页面
  6. 是否脏page,是的话写入磁盘
  7. 更新元数据
  8. 返回page

UnpinPage

这个方法主要是为了解除固定,告诉缓冲池,这个页面上层不再使用了。以后你可以淘汰它了。

这个方法的入参是一个page id,因此,我们的第一步就是通过page id来找到frame id

和上面不同的在于,如果没有找到,那么直接返回false就可以了,毕竟这个页面不在内存中,没有必要继续了。

如果找到了,通过pages来获取具体的page信息,然后检查,是否pin住,如果没有被pin住,也没有必要继续了。

接下来,对page的pin_count减1,如果只有1个上层使用它,那么减1后会变成0,代表可以淘汰,通知LRU-K算法可以淘汰它。

如果有更新的话,修改page的is_dirty,这样的话,淘汰的时候就会写入磁盘了。

总结步骤如下:

  1. 通过page table查询,如果没有,返回false
  2. 获取page具体信息
  3. 查看page是否pin住,如果没有,返回false
  4. pin_count–
  5. 如果pin count是0代表没有上层应用使用,通知LRU-K算法可以淘汰
  6. 如果有更新,更新page的is_dirty

FlushPage

FlushPage方法的主要作用在于将当前内存中的数据,强制输入磁盘,和淘汰的时候写入磁盘不同,这个是强制写入磁盘。或者说主动写入磁盘。

同样的,如果在page table中没有查到,说明不存在内存中,直接返回false。

如果查到了,那么加载page信息,调用磁盘调度来写入磁盘

FlushAllPages

这个方法不多说了,很简单,批量写入磁盘,你想的没错,你太聪明了,就是批量调用FlushPage即可。

DeletePage

DeletePage方法的主要作用在于删除这个内存中的页面。

首先,同样是查找page table,如果不在内存,返回false

找到了以后,需要加载page信息,查看是否pin住,如果有程序在使用这个page,那么不能删除。

如果没有pin住,那么可以进行删除了。

删除分成几步:

  • 从page table删除
  • 放入free list
  • 从LRU-K淘汰器删除
  • 重置page内存信息
  • 释放内存空间

结论

本次我们完成了CMU15445课程的Lab1,实现了一个数据库的缓冲管理器,其实这个管理器真正实现起来非常难,我这里没有说详细的锁信息,如何防止并发,如何细粒度的加锁,可以给出一些我的锁参考。

最简单的当然是一个大锁锁天下,但是这样会影响性能。

我划分了下面几个锁:

  • PageTableLock 用来锁住page table的所有操作
  • FreeLock 用来锁住free list的所有操作
  • PageLock 用来锁住所有page内存的操作

这三个锁之间是有一些嵌套关系的,整体通过两阶段锁协议来实现,来保证锁的安全性。以防止死锁发生。

使用细粒度的锁进行性能优化以后,我在这个项目的排行榜中获得了53的名次。

因为教授说过不要看别人的代码,因此我这里给出的是比较详细的实现思路。如果有同学在实现,有一些问题的话可以留言或者私信我一起讨论哦~

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

什么,你说微积分之类的太难?那是你不会用chatGPT学习模式!

openAI一向是AI届的风向标,就如同手机都对标苹果,汽车都对标特斯拉,AI也都在对标chatGPT。

2025年7月29日,chatGPT发布了一项新功能,学习模式功能。

在 ChatGPT 中学习的一种新方式,它通过逐步指导而非快速回答来辅助学习。

这项功能类似一个导师,你可以理解为聘请了一个全能家教、你的24小时专属导师等等。

ChatGPT 正迅速成为全球最广泛使用的学习工具之一。学生们借助它来解决难题、备考以及探索新概念。但其在教育中的应用也引发了一个重要问题:如何确保它被用于支持真正的学习,而不仅仅是提供解决方案却不帮助学生理解这些解决方案?

相信大家在使用AI的过程中,都有一个问题,那就是AI确实能快速的给你答案,但是这个答案没有过程。就像你解数学题一样,光写个答案上去,老师也是会扣你分的。

这也是现在网上很多人不推荐AI的原因,因为他们认为这会让人丧失思考能力。

现在,GPT上线了学习模式,再也不用担心只给你答案了。

当学生使用学习模式时,系统会提供引导性问题,根据他们的学习目标和技能水平调整回答内容,从而帮助他们建立更深入的理解。学习模式生动有趣且互动性强,旨在帮助学生真正掌握知识——而不仅仅是完成任务。

学习模式的构建过程

在技术层面,学习模式由openAI与教师、科学家及教育学专家合作开发的定制系统指令支持,旨在体现支持深度学习的核心行为,包括:

  • 鼓励积极参与
  • 管理认知负荷
  • 主动培养元认知和自我反思能力
  • 激发好奇心
  • 提供具有实际操作性和支持性的反馈

这些行为基于学习科学领域长期以来的研究成果,并决定了学习模式如何对学生做出响应。

“与其替他们完成作业,学习模式更鼓励学生对学习过程进行批判性思考。此类功能是推动 AI 在学习领域有效应用的积极一步。即便在 AI 时代,最有效的学习仍发生在学生对课程内容充满热情并积极参与其中时。”——Common Sense Media AI 项目高级总监 Robbie Torney。

主要功能

  • 交互式提示:结合苏格拉底式提问、提示和自我反思提示,引导理解,促进主动学习,而非直接提供答案。
  • 支架式回复:信息被整理成易于理解的章节,突出各主题之间的关键联系,通过恰到好处的背景信息保持内容的吸引力,并减少复杂主题带来的信息过载。
  • 个性化支持:课程会根据用户在之前聊天中回答的问题,评估其技能水平和记忆力,从而为用户量身定制适合其水平的课程。
  • 知识检查:测验和开放式问题,结合个性化反馈以跟踪学习进展,有助于巩固知识并提升在不同情境中应用知识的能力。
  • 灵活性:在对话过程中,您可以轻松开启和关闭学习模式,从而在每次对话中灵活适应您的学习目标。

学生如何使用学习模式

学生应该是这个模式的最大受益者了,中小学生不需要请家教了,大学生也可以获得顶级导师7*24小时服务了。

学习模式专为大学生设计。在早期测试中,我们收到了学生们的一些反馈:

  • “我认为最贴切的描述是:一个实时、全天候、无所不知的‘办公时间’。”——Noah Campbell,大学生
  • “学习模式在将复杂内容分解为清晰、节奏适中的解释方面做得非常出色。”——Caleb Masi,大学生
  • “我用学习模式来辅导我学习一个我之前尝试过多次但始终未能掌握的概念:正弦位置编码。它就像一位不会对我的问题感到厌倦的辅导员。经过三个小时的学习,我终于对这个概念有了足够的理解,感到信心十足。”——Maggie Wang,大学生

什么,你说微积分太难?那是你不会用chatGPT学习模式!

一些使用例子

用户提问:我是计算机科学的大四学生,但我忘记了这门课上的一切。这是教学大纲,是离散数学。我是大一的时候上的,我记得我完全迷路了。你能帮我重新学习吗?

GPT回答:

1
2
3
4
5
6
7
8
9
10
11
太好了,谢谢你上传教学大纲!在我们深入研究之前,让我们快速校准一下。

一个简单的问题:

1. 你的目标是什么?(例如,“为技术面试做准备”,“在内容相似的课程中取得好成绩”,“填补研究生入学前的空白”)

2. 那时候这门课的哪些部分最让人困惑?(例如,证明、逻辑、归纳法、图论?)

3. 你喜欢怎样学习?(你喜欢大量的练习题、视觉解释、教学等吗?)

让我们一起重新建立起来——但这次要更聪明一些。

继续提问:

1
2
3
4
5
1. 我只想再学一遍。我想看看这种学习模式是怎么运作的!

2. 我不记得了,全部都记得吗?递归代码非常困难。

3. 我认为以上都是

GPT回答:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
太棒了,从头开始。

让我们重新建立起你的基础,一点一点。我们将这样做:

1. 从小事开始:一次一个主题(我们将按照课程顺序进行,除非你想跳过)。

2. 混合使用:我会在问题、教学、短练习和复习之间切换。

3. 经常查看:如果有任何不清楚的地方,我们会暂停并深入了解。

🔹第一站:鸽子洞原理

让我们慢慢来——它在你们的教学大纲的前面,是数学推理的经典入门。

Q1:在我定义它之前,你认为鸽子洞原理可能意味着什么,仅从名字上看?

(不要有压力——鼓励猜测。)

提问:

1
2
3
好吧。我记得如果鸽子比洞多,那么就会有重叠。所以如果你有5个人,4种颜色的衬衫,你可以肯定地说不止一个人穿着其中一种颜色。

我记得我发现很难识别分类原理问题的证明等。就像期末考试有一个非常难的问题完全难倒了我,我的朋友说“哦,是PHP”。我至今还记得我对这个问题有多恼火

GPT回答:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
🔹重申你说过的话(你说对了)

基本思想如下:

如果鸽子数量多于鸽笼数量,则至少有一个鸽笼数量大于2只。

衬衫的例子你说得很好。这就是基本的鸽子洞原理。

我们稍后会讲到一个更强大的版本。

让我们做一个快速的热身:

假设房间里有13个人。我们能保证至少两个孩子是在同一个月出生的吗?
为什么或者为什么不?

尝试一下——暂时不要担心形式证明语言。解释你的理由。

可以看到,AI在引导你学习,引导你思考。这是一个非常好的学习方式。

如何使用?

学习模式特别适合作业辅导、考试准备和学习新知识。要尝试学习模式,请在 ChatGPT 的工具中选择“研究与学习”,然后提出问题。

在对话框中点击+号,然后选择研究与学习就可以开始使用了!非常棒!

如何使用

总结

功能刚出没多久,大家快快体验吧,当然了,功能还是有一些局限性的。

这是提升 ChatGPT 学习体验的漫长旅程中的第一步。

目前,学习模式由自定义系统指令支持。选择这种方法是因为它能让我们快速从真实学生的反馈中学习并优化体验——即使这可能导致对话中出现一些不一致的行为和错误。openAI计划在通过迭代和学生反馈确定最佳方案后,将这一行为直接融入核心模型中。

后续的一些探索:

  • 对复杂或文字密集型概念的更清晰可视化展示
  • 跨对话的目标设定与进度跟踪
  • 根据每位学生的技能水平和目标进行更深入的个性化定制

免费用户也是可以使用这个功能的,如果你有小孩,那么你不需要再去话大价钱报名培训班,请家教。

如果你是学生,那么你也可以通过这个快速的学习,不需要在吐槽老师的水平了。

文末福利

创作不易,感谢大家支持。希望对大家有帮助。

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

学习资料1

学习资料2

学习资料3

学习资料4

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

MySQL零基础教程

本教程为零基础教程,零基础小白也可以直接学习。
基础篇和应用篇已经更新完成。
接下来是原理篇,原理篇的内容大致如下图所示。

原理学习

零基础MySQL教程原理篇之SQL数据存储

我们知道数据库只是管理数据的,数据最终存储在磁盘上还是一些文件,那么这些文件是如何存储的呢?文件内容是什么?加载到内存以后,内存布局是什么样的?老说page,page是什么?

page(页)

页的三个概念

  1. 硬件上的页面(通常是4KB)
  2. 操作系统上的页面(4KB,x64 2MB/1GB)
  3. 数据库页面(512B-32KB)

磁盘和内存通信是一页一页的,如果数据都在一页里,后续的访问请求就可以走内存了,要不然还的从磁盘获取。内存中可以获取bit数据。

这样做的好处就是加快获取速度,对于MySQL中的一张表来说,相邻的10行数据通常都在一页里,因此我们只产生一次IO,就可以获取到10行数据。

流程:

  • 查询执行器:向内存中的buffer pool请求查询内容。
  • buffer pool: 如果数据所在的页已经在buffer pool中,就直接返回。如果数据所在的页不在buffer pool中,就向磁盘中的database file请求。
  • database file: 有页目录,还有具体的页,数据存在页中,查询页目录找到对应的页返回给buffer pool

数据存储

上面的步骤操作系统本身就可以实现,比如使用mmap,但是操作系统是统一的动作,遇到一些问题不知道该如何处理,而DBMS则可以根据不同的情况做不同的处理,进行优化。像主流的mysql,SqlServer,Oracle都没有用mmapmongoDB早期使用的mmap,后面也是用WiredTiger替换掉了mmap

MySQL自己实现的话,主要关心的两个问题:

  1. 如何表示磁盘上文件的数据
  2. 如何管理内存以及在硬盘间移动数据

如何表示磁盘上文件的数据

数据库的数据最终以文件的形式放在磁盘中。通过文件读写将数据读写到文件中。文件有特定的格式,具体的内容有数据库进行解析然后展示在数据库中。这就是storage manager or storage engine。或者换一个名字,大家就知道了,就是存储引擎。例如InnoDB或者Myisam

storage manager负责文件的读写工作。所有的文件(不管是一个或者多个)以 page 的形式存储,管理多个 page 组成的集合。

一个page就是一个固定大小的数据块。page 可以保存任何东西,tupe(表中的一行数据), metadata(表的元数据), indexes(索引), log等等。每个page有唯一的ID,是page ID

有些page要求是独立的,自包含的(self-contained)。比如mysqlInnoDB。因为这样的话一个表的元数据和本身的数据内容在一起,如果发生问题的话,可以找回元数据和数据。如果元数据和数据在不同的page中,如果发生问题导致元数据的page丢失,那么数据则恢复不了了。

indirection layer记录page ID的相对位置,方便找到对应的偏移量。这样page目录就能找到对应的page。

不同的DBMS对于文件在磁盘上的存储方式不一样,有下面几种

  • 堆存储
  • 树存储
  • 有序文件存储(ISAM)
  • hashing文件存储

我们这里主要介绍堆存储

堆存储

  • 无序的,保存的顺序和存储的顺序无关。
  • 需要读写page
  • 遍历所有的page
  • 需要元数据记录哪些是空闲的page,哪些是已经使用的page。
  • 使用 page directory 方式来记录文件位置。

page directory

  • 存储page ID和所在位置的关系
  • 存储page的空闲空间信息

堆存储

page header(页的元数据)

  • page 大小
  • checksum 校验和
  • DBMS版本信息
  • 事务可见性
  • 压缩信息

页布局

面向元组的存储

一般想法,直接存储,并在后面追加,比如存储张三的用户信息,然后紧跟着后面存储李四的用户信息,这就像我们平时写txt文件一样,不断追加。但是对于可变数据长度很难管理。

  • 记录page数,也就是page内部可插入的偏移量
  • 一个一个tupe按照顺序存储

页布局

所以,page内部,通常不使用上面那种,而使用的是slotted pages(插槽布局)

  • slotted pages
    • slot array 存储插槽信息的偏移量,通过他找到对应的tuple
    • 支持可变长度的tuple
    • 但是会产生一些碎片空间,因为太小,tuple放不下。
    • 压缩可以去除碎片空间,但是压缩的时候这个page就不能读写了。

slotted pages的样式如下,通过这样的改造,我们就可以支持可变长度的管理了。

slotted pages

record ID表示一个tuple的物理位置,不同的DBMS有不同的名称,来表示数据的唯一位置,比如postgresqlctid,oraclerowidctidpage idslot number组成。

009

这里要注意一点,record ID并不是我们表里的主键ID,主键ID代表一行数据的唯一标识,而record ID表示的是这一行数据存储的物理位置,这是两个东西。在MySQL中,record ID是不可见的。

接下来,看一下增删改查的操作。

插入新的tuple的时候,也就是执行insert语句的时候。

  1. 检查page direactory,找到一个page里面有空的可用的slot(插槽位置)
  2. 如果该page不在内存,就从磁盘上获取它,将它加载到内存,这需要一次读IO
  3. 在page里面检查slot array,找到一个空的空间,将tuple插入

更新tuple的时候

  1. 检查page direactory,找到tuple对应的page
  2. 如果该page不在内存,就从磁盘上获取它,将它加载到内存
  3. 在page里面通过slot array获取tuple的偏移量
  4. 如果数据空间合适,那么直接覆盖该tuple,否则,将原来的tuple标记为已删除,并将新tuple插入其他page。

因此更新的时候有一些问题

  • page会产生碎片空间,如果有一个空闲空间是1KB的,但是每次插入的数据都放不下,那么这1KB空间就一直都没办法使用了。
  • 更新的时候需要从磁盘获取整个page,也就是说,哪怕你就更新一行数据,他也会读取一页数据出来。
  • 更新多条数据的时候,可能多个tuple在多个page中,产生随机IO,如果你更新的数据不在一起,就会产生多次IO加载多个page。

所以有些DBMS不能更新数据,只能增加数据,比如HDFS

MySQL使用的就是面向元组的存储,除了这个,其实还有日志结构化存储面向索引的存储

行布局

tuple就是一行数据,也就是一堆bitDBMS解释他们的作用。里面包含

  • header
  • data

首先看看行数据是怎么表示的

数据布局

假设我们有一个数据表foo,有id和value字段如下:

  • id int类型 primary key
  • value bigint类型

数据布局

我们每一行的数据和上面的图一样,最前面是这一行的header,然后就是一些字段了,比如id、value,然后后面在跟着下一行数据的header,以此类推。

数据对齐

现代CPU是64位对齐,创建表以后,DBMS会自动的将数据进行对齐存储,不过,如果在创建表的时候考虑对齐,可以优化速度和存储空间。

数据对齐是可以直接对MySQL起到优化作用的,尤其是数据量大的时候,但是很多人都忽视了这一点,比如同样一行数据,如果有4个字段,分别是32位的id,64位的cdate,16位的color和32位的zipcode

那么占用空间如下:

  • id: 32位,但是会占用64位,因为下一个是64位,所以会插入一个32位的空白进行64位对齐
  • cdate: 64位
  • zipcode:32位,但是会占用64位因为要64位对齐

上面的实际占用空间达到了 64 + 64 + 64 = 192位空间

而如果我们稍微改变一下顺序呢?

  • id: 32位
  • zipcode:32位
  • cdate:64位

这样以来,我们就只占用了 64 + 64 = 128位空间,节省了64位空间,在数据量大的情况下,就意味着一页可以放更多的行数据,因此查询的IO次数就会减少。

数据表示

可变长度的数据varchar,varbinary,text,blob,他们的长度存在header里面。

日期时间类型存储的是时间戳。

float/real/double: 是浮点数,cpu支持浮点数运算,优点是速度快,但是会精度缺失
decimal: 是定点数,运算速度慢,但是精度高。

对于一些很大的数据,比如text,blob,应该避免使用这种类型,有可能一行就占用了一半的page空间,因此,不会在page里直接存储这些数据,而是采用了overflow page的方式。

  • tuple中存储另外一个page页的指针,将具体数据存放到另外一个page页中。
  • postgresql中叫toast,如果数据大于2KB,就会放到toast中,tuple中只存储指针。
  • mysql中叫overflow page,如果数据大于1/2的page大小,就会放进去,tuple中只存储指针。

我们应该避免使用这些类型,因为维护overflow page很麻烦。

外部存储

  • tuple中存储指向外部文件的指针或者文件地址。

NULL存储

  • 行数据库通常是在Header里面增加bit map来判断是否是null
  • 列数据库通常使用占位符来标识NULL
  • 在每个属性前面增加bit来标识是否是NULL,这么做会破坏对齐,或增加存储空间,MySQL曾使用这个方法,后来抛弃了这个方法。
  • NULL == NULL 是 NULL, NULL is NULL 是 true

所以大家知道为什么 NULL != NULL 了吗?

catalogs 用来存储数据库元信息,大多数数据库将这些信息存到一张表里面,下面的都是元数据。

  • 表,字段,索引,视图等
  • 用户,权限,安全等
  • 内部数据统计等
  • infomation schemal api 通过这个来获取catalogs信息
    • mysql
      • show tables 获取所有的表
      • describe table_name 获取表的信息
    • postgresql
      • \d or \d+ 获取所有的表
      • \d table_name 获取表信息

数据库压缩

目标1:必须产生固定长度的值
目标2:在查询期间尽可能推迟解压缩,你不希望先解压缩在查询,这样很占空间且影响速度
目标3:必须是无损方案

压缩粒度

  • Block-level: 压缩同一张表的tuple
  • Tuple-level: 压缩整个tuple的内容(仅限行存储)
  • Attribute-level:压缩同一个tuple的多个属性或单个属性
  • Column-level:压缩存储于多个tuple中的一个或多个属性的多个值(仅限列存储)

mysql innodb 压缩

innodb 在写入的时候可以不解压,但是读取的时候会先在buffer pool中解压在读取。因此Mysql innodb的压缩的好处是提升空间利用率,减少了磁盘IO,缺点是读取的时候需要解压,因此增加了这部分的时间和CPU功耗以及解压以后会占用更多的内存空间。
innodb 默认page 是 16KB,可以压缩到1/2/4/8KB。

mysql innodb 压缩

Column-level 压缩算法

run length encoding

将单个column中的相同值压缩成三元组,需要对列进行智能排序,以最大限度地提高压缩机会。

  • 属性的值
  • column segment的起始位置
  • 值的数量

比如下面的数据,将压缩成右边的数据,(Y,0,3),代表值是Y,起始位置0,值的数量有3个。后面的压缩数据是一样的。这种压缩方法可以快速计算count的数量等。

如果你的值类型很少,且有序,那么将大大减少空间占用。

run length encoding

bit packing

如果字段里面的值都比较小,但是column type很大,可以忽略掉不需要的bit,比如int是32 bit,但是里面的值都很小,用不了这么多,就可以忽略他们。

bit packing

bit map encoding

使用bit map来标识数据值,仅仅适用于值的类型比较少的。

bit map encoding

delta encoding

找到一个基本的数据,以它为基础,进行压缩,+1,-1这种。再将其按照run length encoding的方式压缩,可以再次节省空间。

delta encoding

DICTIONARY COMPRESSION

按照字典将数据进行映射,并存储,这样可以节省空间,如果在字典映射的时候还能先排序,那么还可以完成将where like 'and%'转成where between 10 and 20

DICTIONARY COMPRESSION

结论

本次讲解了数据库是如何存储数据的,数据布局是什么样子的,更是讲解了一些优化方法,比如数据压缩数据对齐。如果你不理解原理的话,面试的时候你就只能背八股答出来网上千篇一律的回答了。

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

MySQL零基础教程

本教程为零基础教程,零基础小白也可以直接学习。
基础篇和应用篇已经更新完成。

接下来是原理篇,原理篇的内容大致如下图所示。

原理学习

零基础MySQL教程原理篇之优化器原理解析

MySQL优化应该是一个面试必考的问题了,也是一个老生常谈的问题。

10个面试里面至少有9个面试会问你,你怎么优化SQL性能?

本文将从MySQL原理层面出发,告诉你如何优化,为何要这么做。

查询计划

假设有以下sql,其中Emp表有10000个records, 1000个pages, Dept表有500个records, 50个pages

  • 对于Emp表来说,代表一个page里面有10行记录
  • 对于Dept表来说,也同样一个page里面有10行记录
1
select distinct ename from Emp E join Dept D on E.did = D.did where D.dname = 'Toy'

这个SQL的功能相信都知道,就是一个简单的连表查询,那么想要优化,我们首先要知道,这个SQL会怎么执行呢?

数据库实际上会构建出一个执行树,按照这个执行树来执行。他还有另一个名字,也就是查询计划

查询计划实际上就是会输出这个执行树。

执行树

每个DBMS的输出都不太一样,比如MySQL的查询计划其实和树很难联系起来。

但是如果你使用PostgreSQL的话,就能明显看出来了。

如果按照上述的执行树来执行,步骤如下:

  1. 先关联Emp表和Dept表进行查询,这一步对于两个表都是全表扫描,大约产生(50 + 50000)个读IO和1000000个写IO
  2. 执行on E.did = D.did,这一步会筛选出符合这个条件的数据,大约产生1000000个读IO和2000个写入IO
  3. 执行where D.dname = 'Toy',这一步会筛选出Dept表中dname字段是Toy的数据,大约产生2000个读IO和4个写入IO
  4. 最后执行select distinct ename,取出每一行中的ename字段并且去重,产生4个读IO和1个写入IO

好了,到这里,MySQL就执行结束了,这时候就会输出最终结果了。

可以看到,IO次数非常多,大家都知道,磁盘IO是一个非常慢的操作,这么多次IO,这个SQL怎么会快呢?

上面使用的JOIN方式比较简单粗暴,因此,我们可以将这个替换成另外一个执行方式.

上面是笛卡尔积的执行方式,我们可以换成JOIN代数的方式。

对于我们的SQL来说,是没有变化的,但是对于MySQL的执行方式来说,是不一样的。

为什么JOIN代数比笛卡尔积快呢?因为JOIN是把第1-2两步合并到一起执行了,因此执行速度要快的多。

JOIN代数执行

对于JOIN来说,其实是有不同的执行算法在里面的,具体的执行算法,在原理篇中也会讲到。

结论

掌握MySQL崩溃恢复不仅能提高您的数据库管理技能,还能为您的职业发展提供坚实的基础。希望这篇文章能帮助您在面对数据库崩溃时保持冷静,并迅速恢复数据。记住,数据安全是数据库管理的核心,定期备份和监控是确保数据安全的重要手段。

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

概念学习

概念学习

概念学习

概念学习

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

chatGPT深入研究功能,不愧是AI届的扛把子

openAI一向是AI届的风向标,就如同手机都对标苹果,汽车都对标特斯拉,AI也都在对标chatGPT。

2025年2月2日,chatGPT发布了一项新功能,深入研究功能。当时是给Pro用户开放的,而现在,普通用户也有每个月5次的使用机会,大家可以体验一下。

效果确实很好,比思考要好用的多。

深入研究功能是一款能通过推理整合海量在线信息,并为您完成多步骤研究任务的智能代理。

这是一项新的代理功能,可在互联网上针对复杂任务进行多步骤研究。它可以在数十分钟内完成人工需要数小时才能完成的任务。

深入研究是OpenAI下一代能够自主为您完成任务的智能代理——只需要提供一个提示,ChatGPT 就会查找、分析并整合数百个在线来源,生成具有研究分析师水平的全面报告。

整合知识的能力是创造新知识的先决条件。因此,深入研究标志着向开发 AGI 这一更广泛的目标迈出了重要一步,我们很早就设想 AGI 能够产生新的科学研究成果。

下面有一个视频。

为什么要关注“深入研究”这类代理?

很多团队碰到的痛点是:信息太分散,人工查资料耗时。像我以前做竞品分析,常常需要翻几十篇白皮书,像剥洋葱一样慢。深入研究型智能代理能跨网页、PDF、图片检索并做推理。结果是什么?效率提升,但也伴随成本和风险。

举个生活例子:你叫外卖,速度快了,但偶尔会送错菜;同样,代理能快,但会“自信地撒谎”(hallucination)。这篇文章要帮你判断在哪些场景值得用,怎么安全地把它丢进工作流里。

该功能在查找需要浏览大量网站的细分、非直观信息方面尤为有效。深入研究只需一次查询,即可代您快速完成复杂耗时的网络调研,从而节省宝贵时间。深入研究可以独立发现、推理和整合来自整个网络的见解。

这项功能可以大大提升我们的效率,如果说第一代搜索引擎是百度、谷歌这种传统搜索引擎,第二代搜索引擎是chatGPT、Cluade、DeepSeek、Kimi这样的AI大模型,可以直接给出我们想要的答案,并且可以替我们搜索网页获得答案。那么深入研究就可以称得上是2.5代搜索引擎,它比单纯的AI大模型提供了更加优秀的搜索、整合、推理功能。

比如,你要分析一些竞品公司,那么可以直接用这个功能,整合这些竞品公司的各种数据,直接生成报表等,方便你进行分析查看,并且会给出分析建议。

核心能力与现状

  • 能力焦点:多步骤网页浏览 + PDF/图片解析 + Python 数据处理 + 引用回溯。就像请了个会翻书还会写脚注的助理。例子:把 200 页技术白皮书压缩成 3 页要点,像把大米煮成粥——省事但要把火候掌握好。
  • 可用度与配额(示例):Pro 用户优先开放,最初版本对 Pro 每月配额更高(示例:250 次轻量/100 次完整版等,具体以官方为准)。想象成健身房会员等级,高级卡能先用器械。
  • 准确率(内部评估示例):在某些基准上,深入研究模型表现显著优于传统模型(示例数值:模型在“人类终极测试”上得分 26.6%),但这并不等于“百分百可信”。就像高考分高不代表专业合格。

3 个适合交给“深入研究”的实战场景

初步情报收集与文献综述

场景:团队需要 24 小时内得到一份覆盖面广的背景报告。

  • 为什么合适:代理能并行抓取多个来源,做出结构化摘要。例子:要评估竞争对手 10 项专利时,代理能先做出专利矩阵,像把书架上的书按颜色分类一样快。
  • 风险与对策:引用格式可能有问题。对策:要求代理输出“原文句子 + 源链接 + 抓取时间”。

数据整合与可视化原型

场景:你需要把不同来源的表格合并并绘图支持决策。

  • 为什么合适:代理带 Python 工具可以帮你清洗、合并、画图。生活例子:像把冰箱里剩菜拼成一顿正餐,省时但口味需调。
  • 风险与对策:数据缺失或单位不一。对策:在代理任务里明确“校验步骤”与“异常报告”。

商业可行性与快速市场筛查

场景:想要短时间判断市场机会(如你刚才提供的 iOS/Android + 语言学习场景)。

  • 为什么合适:代理能产出候选市场清单并排序。例子:像速配 APP 的“初筛”,把 50 个市场筛成 5 个。
  • 风险与对策:法律或本地可用性(如某些地区无法访问服务)。对策:把“法律/可用性检查”作为单独子任务。

如何使用深入研究

在 ChatGPT 中,在消息编辑器中选择“深入研究”,然后输入您的查询。告诉 ChatGPT 您的需求——无论是关于流媒体平台的竞争分析,还是关于最佳通勤自行车的个性化报告。您可以附加文件或电子表格,为您的问题添加背景信息。一旦开始运行,侧边栏就会显示所采取步骤和所用资料来源的摘要。

深入研究可能需要 5 到 30 分钟才能完成工作,该时长可确保对网络信息进行充分深入的挖掘。在此期间,您可以离开或处理其他任务,研究完成后您会收到通知。最终输出会以报告形式显示在聊天工具中。在接下来的几周内,我们还将在这些报告中添加嵌入式图片、数据可视化和其他分析输出,以提供更清晰的解读视角和背景信息。

这里,可以明显发现深入研究的时长比较长,正常来说,我们直接使用AI工具是最快的,如果加上思考功能,会慢一些,而深入研究功能会更慢,当然了,效果也会更好。

与深入研究相比,GPT‑4o 是实时、多模态对话的理想选择。对于需要深度和细节的多方面专业领域查询,深入研究能够进行广泛探索,并对每项主张进行引证,这就是快速摘要与有据可查、经过验证并可作为工作成果使用的答案之间的区别。

接下来看两个例子:

  1. 商业应用

给出一个问题:

请帮我找到过去10年iOS和android的采用率,想学习另一门语言的百分比,以及移动渗透率的变化,按GDP排名前10的发达国家和发展中国家。将这些信息放在表格中,并将统计数据分成不同的列,并包括ChatGPT针对新iOS翻译应用的市场推荐,重点关注ChatGPT目前活跃的市场。

我们来看一下普通的回答:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
由于数据的限制,编纂关于iOS和Android采用率、有兴趣学习另一种语言的人口百分比以及过去十年移动渗透率变化的综合数据对于GDP排名前10的发达国家和发展中国家来说是具有挑战性的。然而,基于现有的信息,我们可以通过一个由ChatGPT驱动的新的iOS翻译应用程序为目标市场提供见解和建议。

Top 10 Developed Countries by GDP:

1.United States
2.Japan
3.Germany
4.United Kingdom
5.France
6.Italy
7.Canada
8.South Korea
9.Australia
10.Spain

Top 10 Developing Countries by GDP:

1.China
2.India
3.Brazil
4.Russia
5.Indonesia
6.Mexico
7.Turkey
8.Saudi Arabia
9.Argentina
10.South Africa

Insights:

- iOS和Android的采用率:在全球范围内,Android占据着大约70.93%的主导市场份额,而iOS在2024年占了大约28.32%。在美国,iOS以58.81%的市场份额领先。日本、加拿大、澳大利亚和英国等其他发达国家也对iOS设备表现出更高的偏好。相比之下,印度和巴西等发展中国家的Android设备普及率更高。
-对语言学习的兴趣:虽然对学习另一门语言感兴趣的人口的具体比例还不清楚,但像Duolingo这样的语言学习应用程序在全球的流行,在2024年7月大约有1430万次下载,表明人们的兴趣很大。在语言环境多样或英语不是主要语言的国家,这种兴趣可能更高。
-过去十年的手机渗透率:全球手机拥有量大幅增长。在美国,智能手机的拥有率从2011年的35%上升到2024年的91%。发展中国家也经历了移动普及率的显著增长,有助于更多地获得数字服务。

建议:

根据现有数据和ChatGPT的活跃市场,以下发达国家有望成为iOS翻译新应用的目标市场:
-美国:iOS市场占有率高,人们对语言学习的兴趣浓厚。
-英国:大量的iOS用户基础和具有不同语言需求的多元文化人口。
-加拿大:iOS的广泛应用和双语人口(英语和法语)增加了对翻译服务的需求。

在发展中国家中,下列情况值得注意:

- 中国:尽管安卓占据主导地位,但中国人口众多,人们对英语和其他语言的兴趣也在不断增长,这是一个重要的机会。
- 印度:安卓的使用率很高,但中产阶级也在不断壮大,iOS的普及率也在不断提高,对语言学习的兴趣也很强,尤其是英语。
- 巴西:主要是Android用户,但有相当大的语言学习应用市场,尤其是英语和西班牙语。

专注于这些市场符合ChatGPT目前的可用性,并利用了现有的移动应用趋势和语言学习兴趣。

可以看到上面的结果中规中矩,当然了,要比我们自己去搜索更加省事。

我们再看看深入研究的回答。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
十大发达经济体(GDP排名)——智能手机操作系统份额、语言学习兴趣和移动渗透率增长

[这里是一个巨大的表格数据,表格比较详细,比较大,我就不放这里了]

十大发展中/新兴经济体(按GDP计算)-(考虑ChatGPT可用性)

[同上,一个大表格]

资料来源:智能手机操作系统份额来自StatCounter/Statista data(2023-24)。语言兴趣指标来自Preply, Eurobarometer, Babbel等的调查(例如,英国73%的人感兴趣;美国70%的人后悔没有语言;日本>的50%想要英语;培生全球调查)。移动渗透率是通过智能手机拥有率来衡量的——在过去的十年中,发展中市场(如巴西+40个百分点)显著增长,发达市场(如英国~82%)的饱和度也很高。

基于以上,新的ChatGPT支持的iOS翻译应用的主要目标是那些拥有庞大的iOS用户群和强大的语言学习需求,并且可以使用ChatGPT的国家。值得注意的是,日本、美国、英国、加拿大和澳大利亚成为首选。这些市场将iOS的高使用率(50-60%以上的份额)与对语言学习的巨大热情相结合——例如,日本的iPhone主导地位和学习英语的普遍愿望使其成为理想选择。像美国、英国、加拿大和澳大利亚这样的英语国家,尽管是单语社会,但对学习其他语言(如西班牙语或法语)表现出了浓厚的兴趣,这些国家都有超过80%的智能手机普及率和数百万的iPhone用户。

在新兴经济体中,墨西哥和巴西是次要目标:尽管Android盛行,但其庞大的人口规模意味着存在数千万iPhone用户,而且出于职业和教育原因,人们对英语的兴趣非常高。这些拉丁美洲市场(ChatGPT活跃地区)可能会为ios版应用带来巨大的市场份额,特别是考虑到人们学习英语的动机。相反,中国和俄罗斯则不太可行——中国的操作系统分裂为Android, ChatGPT在那里没有正式可用,俄罗斯面临准入和监管方面的不确定性。

总而言之,专注于ios密集型、支持学习的市场:日本和西方国家(美国、英国、加拿大、澳大利亚,以及德国和法国等高收入欧洲国家)应该是第一梯级目标。他们将iOS的稳健使用与良好的语言学习态度结合起来。第二梯队包括人口众多的发展中市场(墨西哥、巴西,也许还有沙特阿拉伯,因为其iPhone的历史使用率很高),这些市场对英语的兴趣很强——一旦应用程序在主要市场获得吸引力,这些市场就可以接近。这样的策略可以确保应用程序在发布时最大程度地符合平台(iOS)存在和用户对翻译/语言工具的需求,从而最大限度地提高采用和增长的可能性。

可以看到上面的一些区别,对于普通回答来说,深入研究有着更丰富的信息来源,更强大的信息查找能力,基于这些信息,会给出更加优秀的推理结果。

  1. 用户体验设计

给出一个问题:

找到证据,证明带有图标和标签的按钮比没有标签的按钮或没有图标的按钮更有用。我知道有很多关于它的用户研究,希望看到一份详细的报告以及一个关于有效性的高水平的、一旦确定的答案。

普通回答:

1
2
3
4
5
6
7
研究表明,组合了图标和文本标签的按钮通常比只包含其中一种元素的按钮更有效。由于不同上下文的不同解释,图标本身通常是模棱两可的。例如,像红心或星星这样的符号可能代表收藏夹、书签或评分,这会导致用户困惑。附带文本标签的图标明确了它们的预期功能,增强了可用性。

史蒂夫·麦卡锡的一项研究表明,当用户看到没有标签的常见图标时,他们很难准确识别它们的含义。这就强调了文本标签在确保用户理解图标目的方面的重要性。

然而,值得注意的是,在用户已经非常熟悉特定图标的界面中,没有标签可能不会影响可用性,甚至可以简化界面。然而,对于更广泛的受众,特别是在不同的背景下,将图标与文本标签相结合是可取的,可以减少歧义并增强用户体验。

总之,虽然图标可以增强界面的美感和效率,但将它们与文本标签配对可以确保清晰度并提高整体可用性。

深入研究回答:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
带有图标和标签的按钮的有效性:可用性比较分析

总结
用户界面中的按钮可以用图标和文本标签来显示,也可以只用图标,也可以只用文本标签。可用性研究一致表明,将图标和文本标签结合在一起往往会为大多数用户提供最佳的整体效果。总的来说,图标+标签按钮减少了歧义性,帮助用户以更少的错误执行任务,是大多数用户的首选。如果图标非常熟悉,那么只有图标的按钮有时可以让用户快速识别,但它们通常不够清晰,当用户误解图标时,任务完成速度会变慢,甚至会出错。纯文本按钮通常很清晰,但缺乏图标所能提供的快速视觉线索和美学吸引力。总的来说,包含图标和标签的按钮达到了最佳平衡——提供了视觉锚点和明确的含义——从而在不同用户群体中带来了更好的效率、准确性、满意度和可访问性。以下详细的发现,来自同行评审的研究和权威的用户体验来源,支持这一结论。

任务完成时间和效率
用户找到并点击正确的按钮有多快?关于任务时间和效率的研究探索了图标是否加快了识别速度,标签是否提高了可查找性。主要发现包括:
- 图标可以快速-如果立即识别。在某些用户可以快速识别符号的情况下,只有图标的按钮会导致选择时间稍微快一些。例如,一项眼球追踪研究发现,没有文本标签的图标网格被选择得最快,这表明当图标易于识别并有效排列时(以熟悉的矩阵布局),它们可以最小化搜索时间。这表明图标具有促进快速视觉扫描的潜力。
- 模棱两可的图标拖慢了用户的速度。当一个图标的含义不明显时,用户会犹豫或花费额外的时间来弄清楚。在这种情况下,文本标签通过立即传达功能大大减少了认知负荷。Nielsen Norman Group强调,几乎所有的图标在没有上下文的情况下都是模棱两可的——即使是一个微小的变化或一个不太常见的符号也会让用户感到困惑。在可用性测试中,用户经常不得不“思考”未标记图标的含义,增加了交互时间。因此,如果用户不得不解读图标,那么图标的速度优势就会消失。
- 图标+文本组合设计的效果和单独设计一样好,甚至更好。经验测试通常表明,为图标添加标签不会显著损害效率——有时还可以提高效率。在一项比较纯文本UI、纯图标UI和图标+文本UI的研究中,三者的总体任务完成时间非常相似。完成一组任务的中位数时间,纯文本按钮约为78秒,纯图标按钮约为76秒,组合按钮约为93.5秒——差异没有统计学意义。这表明,平均而言,用户在使用组合格式时并不会变慢。事实上,阅读文本的任何轻微延迟往往会被选择正确选项的信心增加所抵消。
- icon+标签在复杂或新颖任务中的效率提高。当任务更复杂或用户不熟悉界面时,同时使用图标和标签可以简化流程。用户不会浪费时间猜测按钮的用途——图标吸引了注意力,标签也证实了这一点。特别是在导航菜单中,从一开始就在图标旁边显示文本(而不是在工具提示中隐藏标签或悬停时)可以防止额外的交互,并使导航更加高效。
- 年龄较大的用户受益于标签的速度。对老年人的研究表明,当图标伴随文本时,他们定位功能的速度要快得多。一项针对老年人(包括一些有轻度认知障碍的人)的实验发现,他们在使用带有文本的图标时表现最好,而在使用只有图标的按钮时表现困难且速度较慢。这意味着对于某些用户群体(较老的用户)来说,icon+label并不仅仅是同样有效——它比单独的图标更有效。

最重要的是——时间/效率:对于熟悉的符号和有经验的用户来说,图标可以很快地被识别,但在大多数情况下,添加一个文本标签并不会减慢用户的速度,而且通常可以防止由混淆引起的延迟。图标+标签按钮支持快速发现,通过结合直观的清晰提示,帮助用户快速自信地完成任务。

用户错误率
本节将讨论每种按钮样式对错误的影响——例如,点击了错误的按钮,遗漏了某个功能,或者因为误解了按钮的含义而误用了某个功能。
- 只有图标的界面通常会因为误解而产生更多的错误。没有标签,用户只能猜测图标的含义,这可能会导致错误的选择。研究表明,人们经常会错误识别未标记的图标。例如,一项经典的研究发现,用户在经常使用Microsoft Word后,只能说出大约6个工具栏图标的名称,这常常使许多人感到困惑。另一个有趣的发现是:一个用户认为标准的“保存”图标(软盘)是一台电视机,这说明符号是多么容易被误读。这种误解可能会导致错误(点击了一个意想不到的操作),或者导致用户完全避免使用图标(担心出现未知的结果)。在实际使用中,关键功能一直被忽视,直到添加了标签——例如,早期版本的MS Outlook有一个只有图标的工具栏,大多数非专业用户完全忽略了(实际上是遗漏错误),因为他们不理解这些图标。
- 纯文本按钮的解释错误很低,但也可能发生其他类型的错误。有了清晰的文本标签,用户很少会误解这个功能——“没有什么比‘保存’这个词更能表达‘保存’了。”由于歧义导致的错误被最小化。然而,纯文本界面的浏览速度可能会慢一些,如果用户浏览速度过快,可能会不小心跳过某个选项。一般来说,标签本身对于沟通功能是非常可靠的,在大多数情况下比单独的图标更少导致选择错误。
- 带有标签的图标错误率最低。这种组合提供了冗余:如果图标本身可能会误导,则标签会纠正它。相反,如果用户不阅读完整的标签,图标熟悉的形状或颜色可以吸引他们的眼球作为提示。许多可用性专家认为图标+文本是减少错误的最佳实践。为之前被忽略的Outlook工具栏图标添加标签导致了使用上的彻底转变——突然间“所有技能水平的人都开始正确使用它们”。这表明标签消除了混淆(用户现在知道每个按钮的作用),有效地消除了之前的使用错误。在另一个例子中,一项UI改进研究报告称,在图标旁边引入文本标签“立即解决了可用性问题”,人们不再点击错误的工具栏按钮。总之,图标+标签的设计往往是不言自明的,因此用户很少会选择错误的控件或忽略功能,与只使用图标的设计相比,大大降低了错误率。
- 早期的研究表明,在学习后,图标的准确性可以等同于或超过文本。值得注意的是,在受控的实验室条件下,用户拥有一组预先定义好的精心设计的符号,单独的图标有时会在错误率上匹配甚至超过文本。例如,一个早期的实验(Remington & Williams, 1986)发现,在视觉搜索任务中,参与者使用图标比使用文本标签犯的错误更少。然而,这个场景假定用户已经有效地了解了每个图标的含义,或者这些图标非常直观。在实际的日常软件使用中,这样的理想条件很少存在。在实验室之外,用户面临着许多抽象的图标,而无需训练——在这种情况下,文本标签要安全得多,可以防止错误。现在的共识是,除非你的图标像“垃圾桶=删除”或“打印机=打印”那样显而易见,否则仅仅依靠图标可能会让用户出错。
- 工具提示(悬停文字)是一个备用方案,而不是一个修复。界面有时试图通过在鼠标悬停(或长按)时显示文本提示来减少图标的模糊性。虽然这可以通过最终揭示标签来帮助减少错误,但它不能替代可见标签。首先,用户必须将鼠标悬停在每个图标上才能知道它是做什么的,本质上是反复试验。这会减慢它们的速度,并且仍然会导致初始错误。其次,对于触控设备或不太懂技术的用户来说,鼠标悬停提示可能根本就找不到。专家警告说“你的用户不应该到处寻找工具提示”——文本标签从来不需要这样的悬停帮助,这一事实有力地表明文本本身就更清晰。因此,为了尽量减少错误,最好直接显示标签,而不是隐藏它。

底线-错误:依赖只有图标的按钮通常会因为模糊性而增加用户错误(错误点击或错过选项)的风险。文本标签大大减少了解释错误,当与图标搭配时,它们几乎消除了识别按钮功能的混淆。图标+标签的格式是最抗错误的,特别是对于关键功能,错误会付出代价。

用户满意度与偏好
用户满意度是指用户有多喜欢这个界面以及他们使用它的主观舒适度/信心。用户偏好通常会在可用性研究中通过调查、访谈或可用性评分得出。以下是研究表明的用户对图标和文本的看法:

- 清晰驱动满足感。当用户能够轻松地理解界面时,他们往往是最开心的。不出所料,导致混乱的界面(通常只有图标的设置)导致沮丧。许多用户表示,他们不喜欢必须猜测符号含义的界面。“好的UI应该总是清晰的,”正如一位用户所说——当面对使用没有标签的图标的应用程序时,人们通常会求助于随机点击,并且感觉不太满意。另一方面,当按钮被明确标记(图标+文本或纯文本)时,用户会有更多的掌控感和自信,这提高了满意度。
- 图标和文本相结合的界面通常是评分最高的。当被问及偏好时,用户通常会选择同时具有图标和标签的设计,认为这是最友好的。在一项可用性比较研究中,参与者尝试了所有三种风格,图标+标签界面获得了最高的系统可用性量表(SUS)分数(标准化的可用性满意度指标)。具体来说,组合设计的得分约为91/100(这是一个“优秀”的分数),相比之下,只有文本的得分约为85,只有图标的得分要低得多(~75或更低)。这表明用户认为图标+标签的版本是最简单和最有效的。尽管纯文本版本的可用性也很好,但人们对添加的图标更满意,而纯图标版本在可用性方面远远落后。
- 图标增强了主观吸引力(当与文本搭配时)。从视觉上看,用户通常喜欢图标的存在——它们增加了色彩和趣味性,打破了原本单调的文本。带有图标的界面通常被描述为更“吸引人”和更容易浏览,因为图形将眼球吸引到关键选项上。然而,这种对审美满意度的积极影响只有在图标不引入混淆的情况下才会起作用。有了标签,用户就可以从图标的视觉吸引力中获得好处,而不会产生歧义,从而给人一种积极的印象。研究发现,用户有时会认为有图标的应用程序更容易使用,即使他们的实际任务性能并没有提高——这是Wiedenbeck(1999)观察到的现象。在该研究中,参与者认为图标+文本界面学习起来更“舒适”,可能是因为它看起来友好且信息丰富,尽管它们的成功率与纯文本界面相似。这表明了一种心理上的好处:用户只是喜欢使用有用的视觉效果的界面,只要它们具有解释性。
- 首选项因上下文而异,但icon+text是安全的默认值。用户的偏好取决于他们的目标。对于快速重复的操作,一些专业用户说他们更喜欢只有图标的工具栏(一旦他们学会了),因为它很简洁。但对于任何新的或不常见的任务,大多数用户表示希望有一个可见的标签。他们经常引用这样的理由:“我不想记住图标的意思”或“文本让它很明显”。在设计调查和A/B测试中,大多数用户选择标签图标作为他们最喜欢的设计,认为它“清晰且视觉上令人愉悦”。它达到了一种平衡——纯粹的文本可能会让人觉得无聊或难以浏览,而纯粹的图标则让人感觉神秘;这种结合既吸引人又可以理解。
- 使用图标+标签降低认知负荷。用户满意度与使用界面所需的心理投入密切相关。一项测量NASA-TLX(任务负载指数)的研究发现,严重依赖图标的界面具有最高的心理负荷,而同时具有图标和文本的界面具有最低的心理负荷(用户发现它的要求最低)。当用户不需要理解含义时,它会减少压力。参与者还评论说,当他们通过阅读标题来确认图标的含义时,他们感觉更放松,而只有图标的情况下,他们感觉必须记住或猜测,这可能会让他们感到压力。这种减少的精神努力转化为更高的主观满意度。

底线——满意度:绝大多数用户更喜欢易于理解和视觉上有帮助的界面。同时带有图标和标签的按钮正好满足了这一点——它们通常因清晰、自信和美观而受欢迎。只有图标的按钮通常会让用户感到沮丧(除非他们是专家),而只有文本的按钮虽然清晰,但在视觉上可能并不赏心悦目。将文本与图标组合在一起的满意度最高,这也是用户在问及他们喜欢的样式时的最常见选择。

可访问性的考虑
- 无障碍确保界面对残疾人或有残疾的人可用,包括使用辅助技术(如屏幕阅读器)的人、有视觉障碍、认知障碍的人等。当比较图标、文本和两者时,可访问性有一些重要的影响:
- 屏幕阅读器的兼容性:按钮上的文本标签可以立即被屏幕阅读器读取(为盲人或弱视用户读取UI元素的软件)。图标本身本质上是一个图像;如果没有文本标签,开发人员必须提供一个替代文本(“aria-label”或类似的文本),以便屏幕阅读器可以指出图标的作用。这是可访问性标准的要求(WCAG成功标准1.1.1:非文本内容必须有文本替代)。在实践中,当图标旁边有一个可见标签时,该标签通常用作按钮的可访问名称——这是一个健壮的解决方案。对于只有图标的按钮,如果缺少替代文本或写得不好,按钮将被宣布为“按钮”或其他模糊的东西,使界面对盲人用户不可用。因此,图标+文本是满足可访问性指导原则的最安全选择,因为文本是内置的,并且总是可用于辅助技术。
- 视力障碍和视力低下:视力低下的用户经常放大或使用屏幕放大镜。文本标签通常可以很好地缩放(特别是当UI使用响应式文本时)。图标缩放可能不那么清晰,或者可能像素化,如果在高缩放时人们只能看到屏幕的一部分,那么不熟悉的图标可能无法识别。此外,一些用户很难感知细节或颜色——一个小图标可能无法区分(例如,一个细线条的图标可能看起来只是一个污迹)。对于这些用户来说,文本标签至关重要。色盲用户也可以从标签中获益——例如,一个依赖于颜色编码的图标(如红色和绿色符号)对他们来说是无法区分的,但标签可以明确任何颜色的操作。同时提供两种模态(图标形状和文本)可以提高在错过另一种模态时,至少一种模态可以清晰显示的机会。
- 认知和学习障碍:图标在认知上具有挑战性——它们本质上是一种必须学习的视觉语言。有认知障碍、阅读困难或记忆问题的用户可能会发现很难记住给定的图标代表什么,特别是当它是抽象的。另一方面,文本标签可以用用户自己的语言阅读(或通过辅助技术听到),减少认知解码的工作量。对轻度认知障碍用户的研究表明,在图标中添加文本可以显著提高他们识别正确功能的能力。同样,自闭症患者或有注意力障碍的人往往更喜欢明确的指令(文本),而不是符号指令;标签可以通过消除歧义帮助他们专注于任务。总的来说,icon+text被认为更具有认知可及性,因为它不仅仅依赖于符号解释。
- 运动和灵巧性的考虑:如果用户有运动障碍,并且正在使用开关控制或语音命令进行导航,为目标设置清晰的标签可能很重要。语音控制软件通常让用户说出按钮的名称。如果按钮只是一个没有可访问标签的图标,用户可能不知道要说什么词(或者可能没有合适的名称)。对于可见文本,他们可以简单地说出该文本。对于只使用开关或键盘的用户来说,轻按控制键,听到有意义的标签被大声朗读,这对他们知道何时聚焦到正确的按钮至关重要。如果没有正确标记,只有图标的按钮可能会减慢或阻碍这些导航方法。
- 为了可访问性避免依赖于悬停的标签:一些设计隐藏了文本标签,只在悬停时显示它(工具提示)。这在多个方面都是可访问性的问题。首先,hover对于触屏用户(手机或平板电脑)来说是不存在的,所以这些用户可能根本看不到标签。其次,灵巧度有限的用户可能会发现很难在小图标上准确地悬停。第三,如前所述,屏幕阅读器默认不会触发鼠标悬停提示;它们依赖于底层的可访问名称。Nielsen Norman Group明确建议图标标签应该在任何时候都是可见的,而不仅仅是在鼠标悬停时,因为隐藏它们会“降低效果”,并且在触控设备上失败。简而言之,永久可见的文本比只在鼠标悬停时显示的文本更容易访问。
- 用户控制和自定义:作为辅助功能增强,一些应用程序提供了一种设置,可以在纯图标、纯文本或两者之间进行切换。这允许用户选择最适合他们的方式——例如,如果应用程序以某种方式默认只使用图标,视障用户可能会打开“图标上的文本标签”模式。专家建议,如果界面必须使用只有图标的按钮(可能是由于空间限制),最好至少允许文本标签的选项(或“可访问模式”)。然而,只需简单地为每个人默认使用带有标签的图标,就可以完全避免对这些选项的需求。

深入研究的回答其实后面还有很多内容,强烈建议大家去试一试,非常好用。

这个例子的对比更加明显,能看出来深入研究的回答更像一位专业的助手,你吩咐下去一个任务,他会去自己找资料,自己总结,并给你一份文档,你只需要看这个文档就可以了。

像不像老板和秘书?而且是有专业知识的秘书?

如果你是老板,你会选择雇佣谁呢?评论区一起讨论下~

工作原理

深入研究采用端到端强化学习方法,针对跨领域复杂浏览与推理任务进行训练。通过这种训练,它学会了规划和执行多步骤轨迹来查找所需的数据,并在必要时进行回溯和对实时信息做出反应。该模型还能浏览用户上传的文件,使用 Python 工具绘制和迭代图形,在回复中嵌入生成的图形和网站图片,并引用来源中的特定句子或段落。经此训练,该系统在多项面向现实问题的公开评估中达到全新性能峰值。

人类终极测试

在最近发布的人类终极测试⁠(在新窗口中打开) (Humanity’s Last Exam) 评估中,深入研究模型的准确率达到 26.6%,创下新高。这项测试涵盖多学科专家级难题,包括 3,000 多道选择题和简答题,涉及 100 多个学科,从语言学到火箭科学,从古典文学到生态学。与 OpenAI o1 相比,该模型在化学、人文和社会科学以及数学学科的进步最大。为深入研究提供技术支持的模型能够按需高效获取专业化信息,展示了类似于人类的方法。

模型 准确率 (%)
GPT-4o 3.3
Grok-2 3.8
Claude 3.5 Sonnet 4.3
Gemini Thinking 6.2
OpenAI o1 9.1
DeepSeek-R1 9.4
OpenAI o3-mini(均衡版) 10.5
OpenAI o3-mini(高性能版) 13.0
OpenAI 深入研究 26.6

可以看到准确率遥遥领先。。。

GAIA

在 GAIA⁠(针对真实世界问题评估人工智能的公共基准)上,为深入研究提供技术支持的模型达到了新的技术水平 (SOTA),在外部排行榜⁠(在新窗口中打开)上名列前茅。这些任务包括三个难度级别的问题,成功完成这些任务需要具备推理、多模式流畅性、网络浏览和工具使用熟练程度等能力。

GAIA 1 级 2 级 3 级 平均值
上一个 SOTA 67.92 67.44 42.31 63.64
深入研究 (pass@1) 74.29 69.06 47.6 67.36
深入研究 (cons@64) 78.66 73.21 58.03 72.57

专家级任务

在跨领域专家级任务的内部评估中,深入研究功能经领域专家评定,可自动化完成需数小时人工完成的复杂调研工作。

最高工具调用次数

总结

这个功能现在可能还有一些问题,但是相比于其他模型来说,准确率已经算不错的了。因此还是很推荐大家使用的。

我觉得唯一的缺点就是使用次数太少了,普通用户每个月5次。

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。