LOADING

加载过慢请开启缓存 浏览器默认开启

Okabe's LAB

Kmesh 环境配置

2024/7/9
  • 下载 openEuler 23.03

  • 安装 kernel headers:

    yum install kernel-headers
    
  • 安装 docker:

    参考:https://cloud.tencent.com/developer/article/2383890

    curl -fsSL https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/centos/docker-ce.repo -o /etc/yum.repos.d/docker-ce.repo
    sed -i 's#https://download.docker.com#https://mirrors.tuna.tsinghua.edu.cn/docker-ce#' /etc/yum.repos.d/docker-ce.repo
    sed -i 's#$releasever#7#g' /etc/yum.repos.d/docker-ce.repo
    yum install docker-ce-24.0.7
    
  • 修改 docker proxy:

    修改 /etc/docker/daemon.json 为如下格式:

    {
        "proxies": {
            "http-proxy": "http://ip:7890",
            "https-proxy": "http://ip:7890",
            "no-proxy": "127.0.0.1,localhost"
        }
    }
    

    重启 docker:

    systemctl restart docker
    systemctl status docker
    
  • 安装 kind

    wget https://github.com/kubernetes-sigs/kind/releases/download/v0.23.0/kind-linux-amd64
    chmod +x kind-linux-amd64
    mv kind-linux-amd64 /usr/bin/kind
    
  • 安装 istioctl

    curl -L https://istio.io/downloadIstio | sh -
    cd istio-1.22.2/bin
    chmod +x istioctl
    mv istioctl /usr/bin/
    
  • 创建 ambient 模式集群

    kind create cluster --image=kindest/node:v1.23.17 --config=- <<EOF
    kind: Cluster
    apiVersion: kind.x-k8s.io/v1alpha4
    name: ambient
    nodes:
    - role: control-plane
    - role: worker
    - role: worker
    EOF
    
    istioctl install --set profile=ambient --skip-confirmation
    
  • 安装 go

    https://golang.google.cn/doc/install

  • 安装 kubectl

    https://kubernetes.io/zh-cn/docs/tasks/tools/install-kubectl-linux/

  • 安装 kmesh

    参考 https://kmesh.net/en/docs/setup/quickstart/ 即可。

参考:

阅读全文

Istio Hello World

2024/6/4

部署应用

注意,如果用 kind 需要将 HTTP_PROXY 设置为本机的 ip(而非 127.0.0.1),否则访问不到。

按照 istio book info 教程一步步来:https://istio.io/latest/docs/tasks/traffic-management/ingress/ingress-control/

在安装 gateway 之后,由于我们用的是 kind 而不是在云服务商提供的环境,我们需要使用 metallb 来提供外部 ip:https://metallb.universe.tf/installation/

可视化

ssh -NL 20000:localhost:20001 root@10.119.14.80 -p 22
阅读全文

记一次使用 Docker 遇到的奇怪问题

2024/6/3

最近在 jcloud 的服务器上起了一些 docker 容器,发现在容器内部怎么都无法访问公网。最后发现问题是网卡的 MTU 和 docker0 的 MTU 对不上(Docker 默认是 1500,而网卡是 1450)。

类似的问题:https://www.zeng.dev/post/2022-the-docker-mtu-problem/

可以通过 ifconfig -s 查看 MTU 信息。

解决方案:

修改 /etc/docker/damon.json:

{
    "mtu": 1450
}

重启 docker

systemctl restart docker
阅读全文

C++ 笔记(3)—— Effective Modern C++ (3-6 章)

Language 2024/5/29

Item 18 —— std::unique_ptr

std::unique_ptr 支持使用自定义的析构函数,可以传入一个 lambda 函数:

auto delFunc = [](auto* v) {
    std::cout << "Deconstruct for value: " << *v;
};
std::unique_ptr<int, decltype(delFunc)> ptr(new int{123}, delFunc);

这里 decltype 将再次发挥作用。注意,与 std::shared_ptr 不同,这个自定义析构函数的类型是会绑定到 std::unique_ptr 上的(参见模板第二个参数)。

Item 19 —— std::shared_ptr

std::shared_ptr 会额外维护一个控制块:

这个控制块位于堆上。在执行 move 操作的时候,控制块会沿用。当一个 std::shared_ptr 移动构造一个新的 std::shared_ptr 的时候,ref_cnt 不会增加(会将原来的设置为 nullptr)。

不能用一个裸指针初始化多个 std::shared_ptr,这会导致多个控制块,执行多次析构,造成未定义结果

std::shared_ptr 还支持对于每个实例都使用不同的自定义析构函数函数。如果使用了自定义析构函数,就无法用 std::make_shared 生成对应 std::shared_ptr 对象。与 std::unique_ptr 不同,自定义函数不会体现到类型上。
例如:

std::shared_ptr<int> ptr(new int{123}, delFunc);

有时候我们希望,对于一个对象 std::shared_ptr<Object>,我们希望在 Object 的成员函数中创建一个新的,指向自己(this)的 std::shared_ptr<Object>

class Object : public std::enable_shared_from_this<Object> {
   public:
    static std::shared_ptr<Object> Create(int val) {
        return std::shared_ptr<Object>(new Object(val));
    }
    std::shared_ptr<Object> CloneInner() { return shared_from_this(); }
    inline int Val() const { return val; }

    ~Object() { std::cout << "Deconstruct called." << std::endl; }

   private:
    int val;

    // shared_from_this 必须基于已有的 std::shared_ptr 对象。
    // 设置为 private 以防止用户构造一个裸露对象并调用 shared_from_this。
    explicit Object(int val) : val(val) {}
};

int main() {
    auto p = Object::Create(233);
    std::cout << "Value 1: " << p->Val() << std::endl;
    std::cout << "Reference Count: " << p.use_count() << std::endl;

    auto p2 = p->CloneInner();
    std::cout << "Value 2: " << p2->Val() << std::endl;

    std::cout << "Reference Count: " << p2.use_count() << std::endl;
}
阅读全文

C++ 笔记(2)—— Effective Modern C++ (1-3 章)

Language 2024/5/25

以下笔记囊括了前三章我学习到的新知识。

Item 1 —— 模板参数

推导按值传递的模板形式参数类型 T 时,若传入的实参带有 constvolatile 时,将会去掉。
因为按值传递就相当于是对数据进行了拷贝,不应该有上述限制修饰。

若形式参数为 T,传入的数据为函数或者数组,将会退化成对应的指针。除非使用 T &

Item 2 —— auto

一般而言,使用 auto 和模板 T 是一样的。除了以下的情况:

// x 的类别为 std::initializer_list<int>
auto x = {1, 2, 3};

template <typename T>
void func(T x);
// 错误,无法推断 T 的类型
func({1, 2, 3})

// 错误,无法推断类型
// auto 用在函数类型推导时会被当作模板类型推导
auto func() {
    return {1, 2, 3}
}

auto 可以用来推断一些只有编译器清楚的类型,比如 lambda 表达式。

auto x = []() { return "test"; };
auto y = std::function{[]() { return "test"; }};

比起 ystd::function 包裹,使用 auto 的效率更高,内存占用更少。

Item 3 —— decltype

下面的代码将报错:

template <typename Container, typename Index>
auto func(Container& c, Index i) {
    return c[i];
}
std::vector<int> vec{1, 2};
func(vec, 1) = 10;

此处 auto 相当于 T,按值传递。这将会把 vector::operator[] 返回的 T& 的引用给去掉。我们无法给一个右值赋值。

而如下的代码将通过编译:

template <typename Container, typename Index>
decltype(auto) func(Container& c, Index i) {
    return c[i];
}
std::vector<int> vec{1, 2};
func(vec, 1) = 10;

一般而言,不同于 auto 和模板类型推导,decltype 只会鹦鹉学舌。
对于类型为 T 的表达式,除非该表达式仅有一个名字,否则 decltype 总是得出类型 T &

// 此处类型为 int &
decltype(auto) f() {
    int x = 0;
    return (x);
}

Item 5 —— 优先选用 auto 而非显式类别声明

  • 例子 1:vector<int>::size()

    vector<int>::size() 返回的类型是 vector<int>::size_type,用很多常见的显示类别去声明会产生隐式的类型转换,在不同平台可能会有一定问题。

  • 例子 2:遍历 map

    std::map<std::string, int> m;
    for (const std::pair<std::string, int>& p : m) {
    }
    

    这么写看似没有问题,但是实际上真正遍历的时候 p 的类型应该是 std::pair<const std::string, int> &。上述的遍历方式会新建临时变量。这是 map 中的源码:

    typedef pair<const key_type, mapped_type>        value_type;
    ...
    typedef value_type&                              reference;
    

    转换成 auto 之后:

    std::map<std::string, int> m;
    for (const auto& p : m) {
    }
    

    此时 p 的类型为 const std::map::value_type &,符合预期。

  • 例子 3:std::vector<bool>

    std::vector<bool>::operator[] 的返回值不是 bool &,而是 std::vector<bool>::reference。这是因为 bool 在 c++ 里面是用一个 bit 表示的,vector<bool> 不允许直接返回一个 bit 的引用。

    这里如果用 auto 就可能会产生问题,而用 bool 则可以实现隐式类型转换。

Item 7 —— () 与 {}

std::vector<int>{1, 2};
std::vector<int>(1, 2);

两者完全不一样。

只要有机会,总是倾向于使用具有 std::initializer_list 类型的构造函数。

Item 9 —— using 与 typedef

using 支持模板,而 typedef 只能新建一个模板类:

template <typename T>
class Container {
    T x;
};

// 支持
template <typename T>
using ContainerList = std::vector<Container<T>>;
ContainerList<int> list;
// 不支持
template <typename T>
typedef std::vector<Container<T>> ContainerList;

// 支持
template <typename T>
class ContainerList {
   public:
    typedef std::vector<Container<T>> type;
};
typename ContainerList<int>::type list;

这里必须要有 typename,因为有些特例化实现可能会定义一个叫做 type 的成员对象,需要明确告诉编译器这里用的是 type 这个类型。

Item 10 —— 优先使用 enum class

enum class 会限制作用域。普通的 enum 中的枚举值可能会与其他的同名变量产生歧义。而使用 enum class 声明的枚举值只能通过 enum_class_name::value 的方式来访问。

同时,没有 class 限制的枚举值可以发生隐式类型转换,而加了限制的不行:

enum Color1 { red, white };
enum class Color2 { red, white };
// 可以
if (Color1::red > 1.5) {
}
// 不可以
if (Color2::red > 1.5) {
}

enum class 底层使用 int,也可以使用其他类型:

enum class Status: std::uint32_t;

Item 11 —— 使用 delete 而非未定义的私有函数

应该将声明为 delete 的函数声明为 public,这样便于生成更好的报错信息。
假如设置为 private,用户使用了这个函数,一些编译器会先检查函数的可访问性,说该函数是 private,但是更重要的信息显然是该函数被标记为了 delete

Item 12 —— 使用 override 关键字

使用 override 可以让编译器更好地识别函数是否实现了重写。例如 C++ 11 中有一个鲜为人知的特性:

class A {
    void doSomething() &;
    void doSomething() &&;
}

是两个函数,&& 对应右值调用该函数的情况,例如下面的例子:

class A {
   public:
    void doSomething() & { std::cout << "Called first." << std::endl; }
    void doSomething() && { std::cout << "Called second." << std::endl; }
};

A getRVal() {
    return A{};
}

int main() {
    A a{};
    a.doSomething();
    getRVal().doSomething();
}

将输出:

Called first.
Called second.

Item 14 —— noexcept

在保证一定不会产生异常的前提下,能用 noexcept 就用,因为编译器可以对具有该标识的函数进行更好的优化。同时,编译器不会强求 noexcept 一定要调用具有 noexcept 标识的函数。

Item 15 —— constexpr

  • constexpr 对象(包括函数)都具有 const 属性,且由编译期已知的值完成初始化。
  • constexpr 函数在调用的时候,如果传入的实参值是编译器已知的,则会产出编译期结果。否则退化成普通的函数,在运行时计算结果。

好处是可以在编译期就确定一些变量的值,坏处是可能增加编译时间。

Item 16 —— 保证 const 函数的线程安全性

对于被 const 修饰的函数,我们应该保证其线程安全性。例如:

class A {
   private:
    bool is_valid{};
    int val{};
    mutable std::mutex mtx;

   public:
    int getVal() const {
        std::lock_guard<std::mutex> lock_guard(mtx);
        if (is_valid) {
            return val;
        }
        return -1;
    }
};

为了这一点,我们通常会配合 std::mutex 或者 std::atomic 等同步原语。由于 mutex 在上锁的时候会修改其内部状态(该函数未被标记为 const),所以我们需要通过 mutable 告诉 const 函数该变量是会变化的,不应该在函数内限制其不可变。

阅读全文

Ulaanbuh

2024/5/24

I will always cherish my visit in Ulaanbuh desert.

My favourite photo.

The galaxy.

阅读全文

C++ 笔记(1)—— 万能引用/完美转发/引用折叠

Language 2024/3/27

std::move

std::move 的源码长这样:

template <class _Tp>
_LIBCPP_NODISCARD_EXT inline _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR typename remove_reference<_Tp>::type&&
move(_Tp&& __t) _NOEXCEPT {
  typedef _LIBCPP_NODEBUG typename remove_reference<_Tp>::type _Up;
  return static_cast<_Up&&>(__t);
}

它本质上是将一个引用强制转换成了右值引用

万能引用(Universal Reference)

当我们使用 T && x 或者 auto && x 这种需要类型推断的右值形式的时候,就会被编译器作为万能引用。所谓万能引用,就是既能绑定左值引用,又能绑定右值引用。(所以说,在 C++ 中,并不是说两个 & 就代表是右值引用)。

引用折叠(Reference Collapse)

  • 左值-左值 T& &:函数定义的形参类型是左值引用,传入的实参是左值引用
  • 左值-右值 T& &&:函数定义的形参类型是左值引用,传入的实参是右值引用
  • 右值-左值 T&& &:函数定义的形参类型是右值引用,传入的实参是左值引用
  • 右值-右值 T&& &&:函数定义的形参类型是右值引用,传入的实参是右值引用
    但是C++中不允许对引用再进行引用,对于上述情况的处理有如下的规则:

所有的折叠引用最终都代表一个引用,要么是左值引用,要么是右值引用。规则是:如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果为右值引用。

#include <iostream>
template <typename T>
void PrintInner(T &x)
{
    std::cout << "I got a lvalue: " << x << std::endl;
}

template <typename T>
void PrintInner(T &&x)
{
    std::cout << "I got a rvalue: " << x << std::endl;
}

template <typename T>
void Print(T &&x)
{
    PrintInner(x);
    PrintInner(std::move(x));
    PrintInner(std::forward<T>(x));
}

int main()
{
    Print(5);
    std::cout << "-------------------" << std::endl;

    int x = 20;
    Print(x);
    std::cout << "-------------------" << std::endl;

    auto &&x2 = 30;
    std::cout << std::boolalpha << (typeid(x2) == typeid(int &&)) << std::endl;
    std::cout << "-------------------" << std::endl;

    auto &&x3 = x;
    std::cout << std::boolalpha << (typeid(x3) == typeid(int &)) << std::endl;
}

上面这个例子最终会输出:

I got a lvalue: 5
I got a rvalue: 5
I got a rvalue: 5
-------------------
I got a lvalue: 20
I got a rvalue: 20
I got a lvalue: 20
-------------------
true
-------------------
true

观察:

  • 虽然 5 是作为右值被传入函数 Print,但是这个右值被绑定到了形参 x 上,所以 PrintInner 最终调用的是左值引用的版本。
  • std::move 毋庸置疑,本质上就是都强制转换成右值引用。
  • std::forward 为了将 5 继续当作右值转发,我们可以使用 std::forward,也就是 完美转发
  • 事实上,可以将 T && x 和 T &x 合并成 T &&,因为我们有万能引用。

常左值引用可以绑定一个右值。

const std::string && func()  {
    return std::move(std::string("ok"));
}
const std::string &ok = func(); // ok
const std::string &ok = "ok";   // ok

std::string &fail = "fail";
error: non-const lvalue reference to type 'std::string' (aka 'basic_string<char, char_traits<char>, allocator<char>>') cannot bind to a value of unrelated type 'const char[5]'
    std::string &fail = "fail";
阅读全文

CMU-DB 15-445 学习笔记(3)

2024/3/27

对应课程

Buffer Pool

Buffer Pool 需要有一个页表记录哪些页被缓存在内存中,同时需要跟踪页的使用情况(例如是否被修改?被访问过几次?)。当一个页面被使用的时候,我们应该更新它的访问记录,增加它的 pin count(类似 reference cnt,当 pin count = 0 的时候我们才可以将一个页标记为 evictable)。当一个页被驱逐的时候,如果它是脏页,我们还需要将其 Flush 到磁盘。

Latch & Lock

  • Lock: 高级层次、逻辑上的锁,比如事务锁。
  • Latch: 数据库内部数据结构的锁。

Page Directory & Page Table

  • Page Director:持久化在磁盘中,将 page id 映射到磁盘中对应页的位置。
  • Page Table:存储在内存中,将 page id 映射到 frame id。

Buffer Pool Optimization

Multiple Buffer Pools

  • 使用多个 Buffer Pool 以减少 latch 竞争。
  • 将 page id 映射到某个 Buffer Pool。

Pre-fetching

当执行某些扫描操作的时候,DBMS 可以进行预取操作,提前将未来可能读到的数据页加载到内存中。

数据库清楚自己的数据结构和语义,可以对 B+ 树上的范围查询进行预取。

Scan Sharing

当 DBMS 发现多个查询实际上是等价的时候,就可以将多个查询绑定到单个 cursor 上。查询的结果不一定要一样,还可以共享中间结果。

Buffer Pool Bypass

“Light Scan”。

顺序扫描之类的操作可能会污染 Buffer Pool,如果使用 Buffer Pool Bypass 将不会存储顺序扫描获取的页,而是保存到 worker 本地(不和其他 workers共享)。对于需要大量扫描的操作而言有效。

Bypass OS Page Cache

大多数的 DBMS 都使用 O_DIRECT 绕过 OS Page Cache,直接操作文件 I/O(这样 OS 就不会生成缓存,数据库自己来管理缓存)。

页面驱逐算法

LRU

Clock

LRU 的近似算法。

  • 访问一个页面:将该页面对应的位设置为 1;
  • 按照一定顺序(顺时针/逆时针)遍历,将位为 1 的设置为 0,将原先就是 0 的页面驱逐。

LRU-k

LRU 和 Clock 算法都存在的问题是它们只考虑了页面被访问的时间,但是没有考虑页面被访问的频率,很容易被 sequential flooding 污染。

  • 数据第一次被访问,加入到访问历史列表;
  • 如果数据在访问历史列表里后没有达到K次访问,则按照一定规则(FIFO,LRU)淘汰;
  • 当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列删除,将数据移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序;
  • 缓存数据队列中被再次访问后,重新排序;
  • 需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。

K-distance 的概念(LRU-k 是按照 K-distance 排序的):

MYSQL 使用了一种类似的算法,第一次访问的时候进入 Old List 链表头,再次访问则从 Old List 转移到 Young List 头部。说它是一种近似是因为它没有真正记录时间戳,只是将其直接放到头部(注意 LRU-k 是会计算 k-distance 的)。

Dirty Pages

Use background writing.

阅读全文

CMU-DB 15-445 学习笔记(2)

2024/3/25

对应课程

三种数据处理模式

  • OLTP: On-Line Transaction Processing
  • OLAP: On-Line Analytical Processing
  • HTAP: Hybrid Transaction + Analytical Processing

三种存储模型

N-ary Storage Modal(NSM) 行存储

适合 OLTP,一般 Page 大小都是 Hardware Page 的整数倍。

优点:

  • 快速增删改查;
  • 适合 OLTP;
  • 可以基于 index-oriented 实现存储。

缺点:

  • 不适用于扫描表的大范围内容;
  • 如果是想获取其中一列的值,会有很多随机写,内存 locality 很差;
  • 不适合压缩,因为单个页面里面有多种 value domains,重复的值出现的概率低。

Decomposition Storage Model(DSM) 列存储

  • 适合 OLAP。
  • 一些系统可能会有一个 Buffer Pool,通常是日志结构,缓存行修改,并且定期转换为列存储。
  • Oracle 保存以行格式存储的主表,但是同步按列存储的副本。

两种存储方式:

  • 定长存储,这样做的好处就是不用额外的表示符确定某个数据(index = offset / value size)。但是对于不定长的数据,则需要通过字典压缩之类的方式,将变长内容映射到固定大小(32 bit)。
  • 用额外的列记录

我们在做 OLAP 的时候,往往并不是只看其中一列,我们可能要在多列上获取值并且计算。基于此,我们需要 PAX。

Hybrid Storage Mode(PAX)混合存储

  • PAX 即 Partition Attributes Across
  • ParuqetOrc 格式使用的就是 PAX
  • 既要获得列存储快速处理的优点,又要获得行存储的优良空间 locality。

数据库压缩

目标

  • 必须产生定长的值
  • 解压缩应尽可能被延迟,需要的时候再进行(late materialization)
  • 必须是无损压缩

压缩粒度

Block Level(Naive Compression)

  • 使用通用压缩算法,Snappy, Zstd 等
  • MySQL 使用的是通用的压缩,因为它是行存储,没法针对数据进行很好的压缩。
  • 使用通用算法的弊端在于,虽然他们都是基于字典压缩算法的变形,但是由于 MYSQL 无法知道字典数据结构,所以 MYSQL 不得不做整体解压(无法根据字典拿到某个局部值)。

我们真正想要获得的效果应该如下图所示。我们希望能够对某些局部值进行解压缩,而不用解压缩整个页。

我们可以在列式存储中实现这一点。

Column Level

RLE(Run-Length Encoding)

(value, index, number) 三元组,即(数据,索引,数量)。

Bit Packing

通常数据都不会撑满设定的数值上限,例如 32 字节,我可能最大的数值只有 4 字节不到,那么我们就可以用 4 字节存储一个页中的任一数据(而不是原先的 32 字节)。

Mostly Encoding

对于 9999… 这么大的数,可以维护一个查询表来存储,其他的数保持 Bit Packing。

Bitmap Encoding

适用于有限值域,这里 M,F 两种值占两个字节 2 * 8 = 16 bits。

Incremental Encoding

Delta Encoding

利用差值进行压,甚至可以基于 RLE 进一步压缩。

Dictionary Compression

  • 将原始值映射到一个新值,同时维护一个字典记录这种映射关系。

  • 这里查询的时候也需要将查询值进行压缩,不然的话每一行查询都需要将压缩值进行解压,这样就失去了压缩带来的好处。

  • 有时候我们希望对压缩值进行范围查询,所以我们也会希望映射值也能满足顺序。

  • 如果满足字典顺序没有发生变化,那么 DBMS 就可以把 LIKE ‘And%’ 重写成 BETWEEN min AND max 这样的语句(因为满足顺序)。

    SELECT name FROM users
    WHERE name LIKE 'And%';
    
  • 对于 DISTINCT 这样的语句,甚至可以直接在字典上查询(DISTINCT -> 无重复值 -> 字典无重复值)。

    SELECT DISTINCT name
        FROM users
    WHERE name LIKE 'And%';
    

阅读全文

CMU-DB 15-445 学习笔记(1)

2024/3/24

对应课程

聚合操作

  • AVG(col)
  • MIN(col)
  • MAX(col)
  • SUM(col)

以上都是 SQL 中的聚合函数。

  • 支持 DICTINCT

    SELECT COUNT(DISTINCT login) 
    FROM student WHERE login LIKE '%@cs'
    
  • 将聚合结果与非聚合的列混合在标准 SQL 下是未定义行为

    应该使用 GROUP BY

    SELECT AVG(s.gpa), e.cid, s.name
    FROM enrolled AS e, student AS s
    WHERE e.sid = s.sid
    GROUP BY e.cid
    
  • 使用 HAVING 语句来筛选聚合结果

    SELECT AVG(s.gpa) AS avg_gpa, e.cid
    FROM enrolled AS e, student AS s
    WHERE e.sid = s.sid
    AND avg_gpa > 3.9
    GROUP BY e.cid
    

    以上方式是错误的(AND avg_gpa > 3.9),因为平均值只有在聚合之后才知道。

    SELECT AVG(s.gpa) AS avg_gpa, e.cid
    FROM enrolled AS e, student AS s
    WHERE e.sid = s.sid
    GROUP BY e.cid
    HAVING AVG(s.gpa) > 3.9;
    

    SQL 是声明式的,AVG 函数不会执行两遍。

字符串

MySQL 默认情况下不区分字符串大小写,使用 CAST 将其转成二进制字符串:

SELECT * FROM student WHERE CAST(name AS BINARY) = 'TupaC';
  • 模糊查询

    • % 类似正则表达式的 *
    • _ 类似正则表达式的 .

窗口函数

窗口函数是一种类似聚合函数的函数,但它们不会将数据集分组为单个值,而是在给定窗口范围内对每一行进行计算。

ROW_NUMBER 是窗口函数,OVER 括号里面声明了如何切片,如果为空则代表不切分表。

例如,如果我希望计算每门课第二高成绩的学生:

SELECT * FROM (
 SELECT *, RANK() OVER (PARTITION BY cid
 ORDER BY grade ASC) AS rank
 FROM enrolled) AS ranking
WHERE ranking.rank = 2

嵌套查询

SELECT * FROM student AS s 
WHERE s.sid IN (
    SELECT sid FROM enrolled where cid = '15-445'
)

LATERAL JOIN

LATERAL JOIN 允许嵌套查询中的表能够引用另一张表的属性

以下的查询会报错,因为 t2t1 是并行执行的,t2 无法知道 t1 的存在。

SELECT * from (SELECT 1 AS x) AS t1, (SELECT t1.x + 1 AS y) as t2; 

使用 LATERAL 关键字可以实现这一点:

SELECT * from (SELECT 1 AS x) AS t1, LATERAL (SELECT t1.x + 1 AS y) as t2; 

CTE(Common Table Expression)

类似于临时表,或者说是 MYSQL Derived Table(嵌套查询的子查询生成的表叫做派生表) 的增强版。

例子(找到 sid 最大的学生):

WITH cteSource (maxId) AS (
 SELECT MAX(sid) FROM enrolled
)
SELECT name FROM student, cteSource
WHERE student.sid = cteSource.maxId

面向磁盘的 DBMS

  • Buffer Pool 作为中转,缓存数据库页面,页面目录标识数据页在磁盘的位置。
  • 获取页面 2 的时候,先加载目录到内存,然后通过操作系统调用加载数据页到内存,返回给执行引擎一个内存地址。
  • Buffer Pool 还需要负责写回脏页,保持一致性。

MMAP 的问题

虽然我们可以使用 mmap 调用将文件映射到内存(操作系统会帮我们进行页面换入和换出),但是操作系统执行页面驱逐的逻辑可能和数据库的驱逐逻辑冲突(换出数据库不想被换出的页面)。

  • 无法满足事务安全,操作系统可能随时 flash 脏页。假如事务要求将多个页按照一定顺序写入,无法保证 page A 在 page B 写入之后再被写入。
  • DBMS 无法知道某个页面在不在内存中,操作系统会触发 page fault 阻塞线程。
  • 难以处理错误,任何访问都可能触发一个 SIGBUS 中断,DBMS 必须处理它,即使正在执行一些 critical section。
  • 与操作系统产生数据竞争。

Storage Manager

  • 负责维护数据库文件
  • 调度读写以获取更好的时空 locality。
  • 以页的形式组织数据库文件
  • 记录元数据(可用页面)

Page

  • 固定大小的数据块

  • 数据 + 元数据、日志

  • Oracle 中 Page 是 Self-contained(包含了用于理解这个页的元数据,比如所属 Table 的信息,这种冗余可以避免数据库文件损坏导致无法读取数据)。

  • 每个页都有一个唯一标识符(由此映射到物理内存位置)。

  • Hardware Page(4KB)是能够保证原子性写入的数据块最大大小。数据库页默认大小通常大于 4KB(非原子性)。

Heap File

页的无序集合,每个数据元祖随机存储(关系型数据库并不要求按照顺序排列)。对于单个文件而言,找到对应的页很容易,但是对于多个文件而言则比较棘手,需要存储额外的元数据记录哪一个页存在哪一个文件里面,也就是 Page Directory。Page Directory 需要和数据页保持同步,同时需要维护和记录空闲页的数量和位置。

  • 对 Page Directory 的更新需要立即写入磁盘。
  • 像 BLOB 这种字段可能是分另一个文件存储。

存储诸如页大小,校验和之类的元数据。

Tuple-Oriented Storage

File -> Page -> Tuple

  • 使用 Slotted Pages,Slot Array 从头部往尾部增长,Tuple 从尾部往头部增长。
  • 使用 Record ID 唯一标识一个 Page(Postgresql 中的 ctid)。

存在的问题:

  • 一个 page 中可能有大量未被使用的空间(内存碎片);
  • 更新一个 tuple 需要读写整个 page;
  • 随机磁盘读写,极端情况是修改 N 个 tuple,每个都属于不同的 page;
  • 由于需要写回更新,在一些云存储系统可能不支持这么做,例如 HDFS

Log-Structured Storage

  • 追加对 tuple 的 PUT/DELETE 操作
  • 定期合并 page(compaction)

存在的问题:

  • 写放大:Compaction 非常昂贵,从磁盘读回内存再写回磁盘。
  • 读放大

NULL

  • 通过一个 bitmap 标识属性是否为空,这通常不仅仅占一个 bit,因为还要解决内存问题(填充多个 0)。
  • 标记为非 NULL 就不需要存 bitmap。

Large Values & External Value Storage

阅读全文
1 2
avatar
Zihong Lin

What I can’t create, I don’t understand.