[笔记]数据库内核杂谈系列笔记

知识点:数据库 --> 分布式数据库 --> 数据模型/数据湖 --> 其他数据库

  • 数据库的核心就在于一条query的执行流程 从这个开始,慢慢的拆解
  • 如何将一个普通数据库转化为分布式数据库?
  • 数据模型/数据库 -数据的组织形式
  • 向量数据库

1.一小时实现基本功能的数据库

数据库最基本功能的是从存储中取出数据。从直观上讲,构成这个核心功能只需要四个部分,存储,解析,执行器,sql客户端。

SQL是对数据进行操作的语言,解析器解析sql,执行器执行sql,从存储中取出数据。

一个小时实现基本功能的数据库肯定不可能真的实现所有部分。其只针对最核心的底层实现。即操作算子是如何实现的?

操作算子即select,group,max等操作称之为算子。

现在假设数据以csv形式存储在硬盘上。我们使用python来实现基本算子。

  • 一个sql语句即使再复杂,也可以分解成一个又一个原子算子的叠加
  • 将这些算子组建成一个算子树,然后自底向上的依次执行,就能得到最终的查询结果

2.存储演化论:不同的存储形式和优化方法来看存储

  • 数据库是基于关系模型,关系模型定义了所有的数据都以元组(tuple)的形式存在,每个tuple定义了多个属性的键对值,多个含有相同属性的tuple排列在一起形成表。

  • 表是关系型数据库最基本的存储对象

  • 用csv进行存储

    • 缺点:没有定义column的类型和命名。
    • 解决方案:将column的定义和数据分开存储。即数据和数据的属性信息(元数据)的分离。存储属性的形成header
    • 几乎所有的数据库都会有information schema,用来显示数据库的各种元数据。
    • 为了更好的对表的元数据进行管理和变更操作,从原有的csv文件进化出来第一个特定:元数据和存储的分离
    • 继续进化:
      • 为了提高操作效率:将数据进行了分离,数据中分离出来了slot_table, 存储csv文件的每一行的标注信息,这样当对行进行操作的时候,这实际上是一个延迟计算,惰性计算。将数据操作的日志在slot_table上记录,并不是实时的进行计算
      • 为了更高效地实现增删改数据,我们引入了第二个特性,slot_table 以及标注信息来纪录对数据的增删改,并且引入 vacuum 操作定期清理无用的行数据。

    数据模型:

    • 雪花模型:一张或者多张的实体数据表(entity table),配合一些辅助表(英文里称 dimension table)

在分析行数据库中(OLTP)引入列存。单独的文件存储一列的数据。行存转化为列存。

总结:

存储进行了四个优化:

  • 数据和数据类型的分离,数据类型单独存储为元数据
  • 延迟计算:将数据和数据中记录行的信息进行分离,部分操作只在行信息上进行操作,并不是实时修改数据
  • 对于OLTP数据库,使用列存;即一个文件只存储一列
  • csv转压缩。

总结之二:

  1. 为了更好得支持对表的元数据的管理和变更操作, 分离元数据和数据的存储
  2. 为了更高效地实现增删改数据,引入 slot_table 以及标注信息来纪录对数据的增删改,并且引入 vacuum 操作定期清理无用的行数据
  3. 为了更高效得存储数据,用 byte 来存储数据配合高效的编码和解码算法来加速读取和写入
  4. 为了应对数据仓库中复杂报表的查询语句和超大量的数据读取,引入列存概念,并且用压缩算法来进一步优化数据量

索引优化:从解决问题的角度来看不同类型的索引是为了解决哪些查询而演化而来的。

  • 索引通过引入冗余的数据存储(类比书籍最后的索引章节),以此来提高查询语句的速度

  • 构建hash索引,postgresql 语句如下:

    • CREATE INDEX index_name ON table_name USING HASH (column_name);
      
  • 修改为范围查询,则第一类hash失效

通过存储冗余信息来提高查询效率。范围查询引出来二分查询

引出了低二类数据结构:B树和B+树

4. 执行模式

  1. 从宏观角度查看:一条sql的执行流程

image-20240722162307867

用户编写一条sql,那么数据内部要经过如下的处理:

  • 编译(parsing)

    • 通过编译器,将sql语句编译成抽象语法树(Abstracted Syntax Tree:AST)
    • 目的:语法层检测sql语句是否有问题
  • 绑定(Binding)

    • 将语法树和元数据(metadata)相结合,为AST附加上语义信息,形成Bound AST
    • 目的:从语义层面检测语SQL是否有问题
    • ex:某个表,某个字段是否存在
  • 优化(optimizing)

    • 优化器的输入是数据库的元数据以及语义绑定的语法树,输出是最终的物理算子的执行计划。
    • 优化器的工作大致包含如下三步:
      • 由 语法树 生成一个逻辑执行树(logical operator tree)
      • 由逻辑执行树 生成具体的 物理执行计划Physical query plan: 即:将sql分解了一个又一个的操作算子构成的计划
    • 由逻辑执行树(logical operator tree) 生成 物理执行数
      • 语法树节点 到 操作符节点一对一生成,此操作符节点称之为逻辑操作符(logical operator)
      • 由逻辑操作符(logical operator) 扩展出 物理操作符(physical operator),由逻辑执行树 ,扩展为 物理执行树,每一个逻辑操作节点对应多个物理节点
        • 这个过程涉及到选择问题:对于一个逻辑操作符,可能有多种实现,不同的实现对应不同的物理节点;不同顺序的逻辑操作符 之间因而形成了多个可以选择执行路径
        • ex:
          • 逻辑操作算子 TableScan,实际的实现方案有如下:
            • SequentialTableScan(全表扫描)
            • TreeIndexScan:通过读取该表的 BTree 索引来读取数据(建立在相应属性已建立 BTree 索引的前提下)
          • GroupByOperator,其具体的实现方案有如下几种:
            • HashGroupByOperator:通过 Hash 表来实现 GroupBy
            • SortGroupByOperator:通过对子节点的输入的 key 属性进行排序,然后对于相同 key 进行聚合操作再输出
        • 而最终的逻辑执行计划,可以选择 ashGroupBy 配 SequentialTableScan,也可以用 HashGroupBy 配 BTreeIndexScan
      • 从物理执行树中选择出一条路径,形成 物理执行计划(Physical Query Plan)
    • 执行(executing)
      • 加载物理执行计划中,执行操作符的代码。从执行树的底层,由读取表数据开始,依次向上执行。最后把执行得到的结果以 Rowset 的形式返回给用户

简单来说:

一个SQL自从输入到数据库中,到直接结束,完成一个生命周期,内部执行流程如下:

1、用户输入sql --> 编译 --> 抽象语法树

2、抽象语法树 --> 绑定 --> bounded AST : 绑定语义的抽象语法树

3、绑定语义的语法树 --> 优化器 --> 物理执行计划

4、物理执行计划- --> 执行器 --> 返回结果

执行模式:如何执行物理计划?物理执行计划有三种模式

1、materialization 模式:执行的过程自底向上,每个节点都一次性处理所有数据。优势是实现简单,但对于数据量很大的 OLAP 语句不太合适,但比较适合单次操作数据量较小的 OLTP(online transactional processing)语句。

2、迭代模式(或者叫 volcano model): 一种通用的执行模式。流式的执行过程,数据以一个一个 tuple 形式传递与操作符之间。有一些操作符会需要阻塞等待所有数据,需要 spill to disk 实现。缺点是实现复杂,由于操作符之间不断交互,所以效率相对较低。

3、向量模式:介于前两者之间,批量处理数据。更好地利用 SIMD 来提高执行速度。对于大量数据处理比迭代模式高效,所以也更适合 OLAP 语句。

5. 如何实现排序和聚合算子

两个算子:排序(Sort)和聚合(Aggregate)的实现。

排序和聚合算子的相同点:

  • 均是Blocking算子:即需要得到所有输入的tuple,才能完成计算后输出

一些算子需要局部信息,一些算子需要全局的信息。

排序

排序需要用到全部数据,容易out of memory, 这里用的是spill to disk技术排序。

  • 大部分数据库spill to disk,外排的大体思路都是归并排序

外部归并排序:

  • 时间复杂度\(Olog(n)\), 对于数据库而言,IO操作才是瓶颈(文件读写的速度和内存差了两个数量级别),故正确的衡量方法是大致需要读多少次数据

问题

  1. 为什么外部归并排序,可以对远远大于计算机内存的数据量进行排序?

假设 数据有n 页,一个page为最少读取单位。存储在disk上。一次只能读取3个page。

第一次,2页2页进行读取排完序之后。一页一页进行输出。有序的两页称为一个组。

第二次,再2个组,2个组进行排序。此时 4个组共8个page页面如何载入到内存中。

多路归并排序应该和多个有序链表直接排序一致。链表是指针指着每个链表第一个数据,依次比较。

上述可以这样:

  • 第一次,对一个page之内的数据进行排序,此时每一个page都是有序的
  • 第二次,2个page全部载入内存,进行归并排序(假设一次最多能载入1个page),将排序号的按照一个page,一个page写入disk。为了叙述方便,page编号1,2,3,4,5,6.... 此时1,2 有序,3,4有序以此类推
  • 第三次,2个page全部载入,载入1号page,载入3号page。将里面比较小的1个page数据写入1号page。载入2号page,依次类推.....
  1. 假设最少读取单位为1个page,那么一个算子的内存至少要分配多大?

    3个page,其中2个page是输入缓存,1个page是输出缓存。为什么需要一个page当做输出缓存呢?因为mergesort也需要内存来存储排好序的数据

外部归并排序的IO次数,用读取多少页来计算?

  • 一次归并排序是 2*n pages,一读一写。
  • 一共进行了\(log_2^n\)
  • 我认为是2*n*logn,但是作者似乎认为是 2*n(1+log2n)作者这样认为似乎 包含了 第一次读取并对每一个page进行排序的部分

聚合

单项聚合的实现

  • 算子只需要保存一个聚合的中间值,然后根据新的输入不断的更新即可

组队聚合

  • 先要把group by 键相同的输入放到一个组里,然后对每个组求聚合函数即可

实现方法:

  • 先根据group by的键进行排序,相同的键来计算聚合函数即可
  • hash方法:对 group by 键建立哈希表来维系键和中间值的状态

hash表的难点:

  • 如果数据量特别大,就需要维护一个超级大的哈希表。考虑到需要维护哈希表的性能,一般维持使用率在 50%左右,所以真正使用的内存空间应该会更大。

外部hash表算法:解决聚合算子的内存消耗问题

注意:

  • map reduce 不就是一种方法吗?mr是一种计算范式

综上可以知道排序算子在数据库中的作用:

  • order by

  • 实现group by

  • 可以实现join

    • 对于二元联合 table_a join table_b,我们只需要针对联合键分别对table_a和table_b进行排序,然后两个表分别维护一个指针,不断的往后迭代,当两边键值相同的时候,可以输出联合的结果

    6. join算子的实现

数据中台的必要性:

  • 和数据库系统提供 SQL 查询语句的封装是类似的概念:由于业务更加复杂,但迭代需求更快,如果每次实现新的业务或报表都需要写很复杂的查询语句,那岂不是效率很低。如何能够根据业务逻辑需求,把数据标准化,接口化,提供比 SQL 语句更高层次的 API 来方便上层开发。除了更进一步提高效率,还能提高安全性和对数据的控制性。未准就出现一套新的数据操作的标准,值得期待。
  • 数据中台本来就是和业务契合特别深的,甚至不同公司的同类型业务,做成数据中台,其接口和标准都会有差异。从抽象角度来看,中台就是把更多的业务相关的逻辑封装起来,方便更高效地重用和管理,以此来提高开发效率。 一些愚见。
  • 数据中台:构建数仓的必要性:提供更高层级的api

6.事务、隔离、并发-1

为什么要支持事务?

事务(transaction) 和 ACID

事务:一个事务 是一组对数据库中数据操作的集合。

一个事务的所有操作要么被全部执行,要么一个都不执行。如果在执行事务的过程中,因为任何原因导致事务失败,已经执行的操作要么回滚(rollback)。 这就是事务的原子性。

当一个事务执行成功之后,即代表这个事务的操作被数据库持久化了。即使数据库在此时发生了崩溃,那么这个事务依然有效。这就是事务的持久性。

假定数据库的初始状态是稳定的或者对用户来说是一致的,由于事务执行的原子性,即执行失败就回滚到执行前的状态,执行成功就变成了一个新的稳定状态。因此,事务的执行会保持数据库状态的一致性。

多个用户可能在同一时间执行不同的事务。

数据库是多用户系统。同一时间,即使多个事务都在执行,从微观尺度上看,它们之间也有先来后到,必须等到一个事务完成后,另一个事务才开始。这种并发事务之间的不感知就是事务的隔离性。

隔离级别(Isolation Level)

微观级别,事务的隔离性,

事务:

因为业务的原因,使得事务必须要具备4种特性。

最简单的实现方案:增加全局锁

主要通过两门技术:并发控制技术日志恢复技术

常见的并发异常:脏写,丢失更新,脏读,不可重复读,幻读

并发控制

数据库系统只能推出最保守的隔离机制,serializable(可有序化),即所有的事务必须按照一定顺序执行,直接避免了不同事务并发带来的各种问题。

我惊叹于数据库系统的设计。它是人们思想的集中体现。

脏读:一个事务中访问到了另外一个事务未提交的数据。

  • 如何避免脏读呢?数据库引入了第二层的隔离级别,read committed(读提交)。读提交就是指在一个事务中,只能够读取到其他事务已经提交的数据。

不可重复读:一个事务读取同一条记录2次,得到的结果不一致。

  • 数据库引入了第三层隔离级别,根据上面的经验,你可能已经猜出来了,名称就叫做 repeatable read(可重复读)。可重复读指的是在一个事务中,只能读取已经提交的数据,且可以重复查询这些数据,并且,在重复查询之间,不允许其他事务对这些数据进行写操作。
  • 实现:对数据库进行加锁是一种方法

幻读:一个事务读取2次,得到的记录条数不一致。

  • 如何才能避免幻读呢?数据库系统只能推出最保守的隔离机制,serializable(可有序化),即所有的事务必须按照一定顺序执行,直接避免了不同事务并发带来的各种问题。

用户无感知。

不是隔离性是针对并发的,是并发是针对隔离性,并发控制是保证txn并发的前提下,保证个理性,提高并发度

7. 事务 -2

二阶段锁

  • 1)共享锁(share-mode lock; S-lock),即当事务获得了某个数据的共享锁,它仅能对该数据进行读操作,但不能写,共享锁有时候也被称为读锁。
  • 2)独占锁(exclusive-mode lock; X-lock),即当事务获得了某个数据的独占锁,它可以对数据进行读和写操作,独占锁也常被叫做写锁。

解决补录奥 连锁回滚的问题(cascading rollback)问题,死锁问题

4)两阶段加锁的两个升级版本:(更)严格的两阶段加锁(rigorous/strict two-phase locking)通过规定把释放锁等到事务结束来避免连锁回滚(cascading rollback)s

ps:二阶所能否解决之前的提到的三种冲突

我们可以通过比较事务的时间戳来保证事务之间的有序性。假定 Ti 和 Tj 的时间戳分别为 TS(Ti)及 TS(Tj)。如果 TS(Ti)<TS(Tj),数据库系统就需要保证无论如何调度实现,都和序列化执行 Ti 然后 Tj 一致。

日志还可以audit,也就是查旧账
还可以提高性能,对于I/O操作不行的时候,可以选择先写日志,然后累加到一定时候,异步写到磁盘

数据库系统是可以发生死锁的。

二阶段锁能否解决上面的几种问题

目前得到的线索如下: 进行不下去了,全局上有逻辑 >> 局部上逻辑

  1. 并发和日志的基础是事务,事务的标准是ACID,事务之所以要满足ACID是业务的要求
  2. 最简单的策略实现事务
    1. 串行
    2. 复制
  3. 上述方案的缺陷
  4. 解决方案
    1. 原子性的实现
      1. 日志
      2. shallow page 复制法,只复制需要改变的page
    2. 一致性:如果每个事务一致,并且数据库的状态在开始时与最终时一致
      1. 一致性包括:
        1. 数据库一致性
        2. 业务一致性
      2. 没有介绍实现方案
    3. 隔离性:对数据库而言,可以并发多个事务,但是对用户,就好像串行一样
      1. 方案:
        1. 乐观、悲观2派
      2. 并发引起的问题:三种冲突。对并发进行分类,有的类别可以并发,有的不可以
      3. 三种基本冲突
        1. 读写冲突:多次读取同一对象时,事务无法获得相同的值
        2. 写读冲突:事务在写之后放弃了更改,但是放弃之前就有事务读取了更改后的值
        3. 写写冲突:一个事务覆盖另一个并发事务的未提交数据
      4. 一些冲突可以是可以序列化的
      5. 基准:顺序执行。
      6. 依赖图
      7. 二阶段锁能否解决上述三个冲突问题,什么是二阶段锁?因为解决不了,所以又引出了严格二阶段锁

意向锁分为三种:意向共享 (IS)、意向排他 (IX) 和意向排他共享 (SIX)。 意向锁可以提高性能,因为数据库引擎仅在表级检查意向锁来确定事务是否可以安全地获取该表上的锁,而不需要检查表中的每行或每页上的锁以确定事务是否可以锁定整个表.

 T1:select * from table (xlock) where id=10

 T2:select * from table (tablock)

分析:T1线程执行该语句时,会对该表id=10的这一行加排他锁,同时会对整个表加上意向排它锁(IX),当T2执行的时候,不需要逐条去检查资源,只需要看到该表已经存在【意向排它锁】,就直接等待。

PS: update table set xx=xx where id=1, 不光会对id=1的这条记录加排它锁,还会对整张表加意向排它锁。

13:45 重新看一遍

问题

1、什么是数据库下推?

2、如果是data skew引起的bucket过大的问题怎么处理,无论怎么取hash函数还是在一个桶里

  • multi-level bucketization, 然后用不同的hash 函数。

3、构建一个事务

4、存储过程是否支持事物?

顺序

1、将这个看完,前16节是重点。查看一定要记录笔记。

2、常见算子专题

3、如何将一个单机数据库 扩展到 分布式?其面临的问题是什么?

  • 之前看的一个小时的介绍,重新搞一遍

4、gaussdb是怎样的数据库?

5、gaussdb之间进行的操作

6、cs-15-445 进行补充

  • PPT

7、OLAP和OLTP的区别?

9、向量数据库常用的算法

  • 王树森说的算法
  • K-means算法

日志

20240723 如何实现排序和聚合、

https://xie.infoq.cn/article/abc01e15060867eebd8822cdc?utm_campaign=geek_search&utm_content=geek_search&utm_medium=geek_search&utm_source=geek_search&utm_term=geek_search

条例

介绍了条例的订正历史。

2023 12个版 3编 11章,158条

强化组织意识

学党纪,强

我现在应该当一名苦修士

posted @ 2024-10-10 20:17  金字塔下的蜗牛  阅读(6)  评论(0编辑  收藏  举报