# TiDB 8.1 和 8.5 的新功能

## 概述

在本实验中，我们将探讨 TiDB 8.x 中引入的一些关键新功能。我们的重点将集中在六个关键模块上：TiProxy、外键、分区表的全局唯一索引、监控索引使用情况统计、向量搜索和 TiKV MVCC 内存引擎。有关 TiDB v8 中引入的新功能的完整列表，请访问 [TiDB 发行说明](https://docs.pingcap.com/tidb/stable/release-notes)。

### 实验时长

+ 本实验大约需 90 分钟完成。

### 先决条件

+ 对 SQL 和数据库管理有基本的了解。
+ 具有 MySQL 或类似关系数据库以及任何分布式 NoSQL 数据库的使用经验。
+ 熟悉 TiDB 的 [核心组件](https://docs.pingcap.com/tidb/stable/tidb-architecture/) 和 [MVCC](https://docs.pingcap.com/tidb/stable/tidb-storage/#mvcc)。
+ 熟悉 [TiUP](https://docs.pingcap.com/tidb/stable/tiup-documentation-guide/) 实用工具。

> [!TIP]
> 点击 **开始** 启动实验环境。
> 环境准备就绪后，点击底部工具栏中的 **前进** 进入 **模块 1**。
> 然后，按照实验指南的步骤连接到实践环境并开始你的任务。

## 模块 1: 负载均衡和会话管理器 - TiProxy

TiProxy 是 TiDB 的官方代理组件，通过提供负载均衡、连接持久性和服务发现来增强数据库的功能。它作为客户端和 TiDB 服务器之间的中介，确保在各种维护操作（如扩容、滚动升级和重启）期间客户端连接保持稳定和不中断。这在需要保持客户端连接持续性的场景中特别有用，例如在计划服务器停机期间或在多个 TiDB 服务器实例之间平衡工作负载时。

在本模块中，您将部署一个新的 TiDB 集群以及 TiProxy 组件，并验证其负载均衡和会话迁移功能。

### 步骤 1. 连接到 TiUP 控制机

+ 点击这里 - [终端 1](ssh://monitor1-ssh#1) 打开一个新的终端，连接到为你的实践实验分配的虚拟机。

+ 登录后显示的横幅：

    ```output
     ,     #_
    ~\_  ####_        Amazon Linux 2
    ~~  \_#####\
    ~~     \###|      
    ~~       \#/ ___
    ~~       V~' '->
    ~~~         /    A newer version of Amazon Linux is available!
     ~~._.   _/
        _/ _/       Amazon Linux 2023, GA and supported until 2028-03-15.
      _/m/'           https://aws.amazon.com/linux/amazon-linux-2023/
    ```

+ 获取分配给您用于本实验的所有服务器实例 IP 地址的概览。环境变量前缀分别代表它们在 TiDB 集群中的角色。请注意，`sample_output` 中显示的 IP 地址会与您的不同。

  + `HOST_MONITOR` will host Prometheus and Grafana.
  + `HOST_CM` will host the TiUP Control Machine.
  + `HOST_PD` for Placement Driver server.
  + `HOST_DB` for TiDB server.
  + `HOST_KV` for TiKV server.
  + `HOST_TIFALSH` for TiFlash server.
  + `HOST_TIPROXY` for TiProxy server.

  ```bash
  cat hosts-env.sh
  ```
  ```output
  export HOST_MONITOR1_PRIVATE_IP=10.90.4.40
  export HOST_MONITOR1_PUBLIC_IP=35.166.47.245
  export HOST_CM_PRIVATE_IP=10.90.4.40
  export HOST_CM_PUBLIC_IP=35.166.47.245
  export HOST_PD1_PRIVATE_IP=10.90.2.51
  export HOST_PD1_PUBLIC_IP=52.27.45.10
  export HOST_DB1_PRIVATE_IP=10.90.2.14
  export HOST_DB1_PUBLIC_IP=44.244.238.105
  export HOST_DB2_PRIVATE_IP=10.90.2.43
  export HOST_DB2_PUBLIC_IP=34.219.136.55
  export HOST_KV1_PRIVATE_IP=10.90.1.93
  export HOST_KV1_PUBLIC_IP=52.12.133.66
  export HOST_KV2_PRIVATE_IP=10.90.1.30
  export HOST_KV2_PUBLIC_IP=18.236.119.16
  export HOST_KV3_PRIVATE_IP=10.90.1.208
  export HOST_KV3_PUBLIC_IP=35.90.161.51
  export HOST_TIFLASH1_PRIVATE_IP=10.90.4.243
  export HOST_TIFLASH1_PUBLIC_IP=34.210.114.26
  export HOST_TIPROXY1_PRIVATE_IP=10.90.4.154
  ```

### 步骤 2. 创建一个配备 TiProxy 的新 TiDB 集群

+ 检查提供的拓扑文件 `topology-with-tiproxy.yaml`，该文件用于部署 TiDB 集群。请注意拓扑文件中的 TiProxy 设置（TiDB 服务器配置下的 `security.auto-tls: true`、`graceful-wait-before-shutdown: 10` 以及 `tiproxy_servers`）。请注意，`sample_output` 中显示的 IP 地址会与您的不同。

  ```bash
  cat topology-with-tiproxy.yaml
  ```
  ```output
  ...
  server_configs:
  ...
    tidb:
      security.auto-tls: true
      graceful-wait-before-shutdown: 10
  ...
  tiproxy_servers:
    - host: 10.90.4.113
  ```

+ 以下代码块中的命令将按顺序执行一系列操作：1. 更改 [TiUP mirror](https://docs.pingcap.com/tidb/stable/production-deployment-using-tiup) 设置，2. 检查系统设置，3. 修复失败的检查，4. 部署 TiDB 集群，最后 5. 启动 TiDB 集群。大约需要 3 分钟完成。
   
  ```bash
  tiup mirror set tidb-community-server-tidb_version-linux-amd64
  tiup cluster check ./topology-with-tiproxy.yaml \
   --user ec2-user \
   -i /home/ec2-user/.ssh/pe-class-key-oregon.pem \
   --apply
  tiup cluster deploy tidb-demo tidb_version ./topology-with-tiproxy.yaml \
   --user ec2-user \
   -i /home/ec2-user/.ssh/pe-class-key-oregon.pem \
   --yes
  tiup cluster start tidb-demo
  sleep 5;
  tiup cluster display tidb-demo
  ```
  ```output
  Successfully set mirror to /home/ec2-user/tidb-community-server-tidb_version-linux-amd64
  /home/ec2-user/tidb-community-server-tidb_version-linux-amd64
  ...
  Cluster type:       tidb
  Cluster name:       tidb-demo
  Cluster version:    v8.5.1
  Deploy user:        ec2-user
  SSH type:           builtin
  Dashboard URL:      http://52.27.45.10:2379/dashboard
  Grafana URL:        http://10.90.4.40:3000
  ID                 Role          Host         Ports                            OS/Arch       Status  Data Dir                      Deploy Dir
  --                 ----          ----         -----                            -------       ------  --------                      ----------
  10.90.4.40:9093    alertmanager  10.90.4.40   9093/9094                        linux/x86_64  Up      /tidb-data/alertmanager-9093  /tidb-deploy/alertmanager-9093
  10.90.4.40:3000    grafana       10.90.4.40   3000                             linux/x86_64  Up      -                             /tidb-deploy/grafana-3000
  10.90.2.51:2379    pd            10.90.2.51   2379/2380                        linux/x86_64  Up|L    /tidb-data/pd-2379            /tidb-deploy/pd-2379
  10.90.4.40:9090    prometheus    10.90.4.40   9090/12020                       linux/x86_64  Up      /tidb-data/prometheus-9090    /tidb-deploy/prometheus-9090
  10.90.2.14:4000    tidb          10.90.2.14   4000/10080                       linux/x86_64  Up      -                             /tidb-deploy/tidb-4000
  10.90.2.43:4000    tidb          10.90.2.43   4000/10080                       linux/x86_64  Up      -                             /tidb-deploy/tidb-4000
  10.90.4.243:9000   tiflash       10.90.4.243  9000/3930/20170/20292/8234/8123  linux/x86_64  Down    /tidb-data/tiflash-9000       /tidb-deploy/tiflash-9000
  10.90.1.208:20160  tikv          10.90.1.208  20160/20180                      linux/x86_64  Up      /tidb-data/tikv-20160         /tidb-deploy/tikv-20160
  10.90.1.30:20160   tikv          10.90.1.30   20160/20180                      linux/x86_64  Up      /tidb-data/tikv-20160         /tidb-deploy/tikv-20160
  10.90.1.93:20160   tikv          10.90.1.93   20160/20180                      linux/x86_64  Up      /tidb-data/tikv-20160         /tidb-deploy/tikv-20160
  10.90.4.154:6000   tiproxy       10.90.4.154  6000/3080                        linux/x86_64  Up      -                             /tidb-deploy/tiproxy-6000
  Total nodes: 11
  ```

+ 请注意，`tiproxy` 组件也列在集群显示输出中。自管理集群由一个 TiProxy 实例和两个 TiDB 服务器实例组成。此外，虽然我们通常使用至少三个 PD 服务器，但这里只使用了一个 PD 服务器，因为本动手实验不涉及 PD 相关主题。

### 步骤 3. 验证负载均衡功能

+ 默认情况下，TiProxy 实例监听端口 6000 而不是端口 4000。让我们通过 TiProxy 连接到 TiDB 服务器实例。

  ```bash
  mysql -h ${HOST_TIPROXY1_PRIVATE_IP} --port 6000 -u root
  ```
  ```output
  mysql> 
  ```

+ 创建一个名为 `test.tiproxy_test` 的表并退出连接。我们将使用它来证明 TiProxy 的连接负载均衡能力。

  ```sql
  DROP TABLE IF EXISTS test.tiproxy_test; 
  CREATE TABLE test.tiproxy_test (id INT AUTO_INCREMENT PRIMARY KEY, tidb_server_instance VARCHAR(30));
  EXIT
  ```
  ```output
  Query OK, 0 rows affected, 1 warning (0.00 sec)
  Query OK, 0 rows affected (0.06 sec)
  Bye
  ```

+ 通过 TiProxy 生成 50 个数据库连接，在每个连接中，它将插入一个新行，然后关闭连接。请注意，`test.tiproxy_test` 表中的 `tidb_server_instance` 列将显示接受连接并处理插入 DML 的 TiDB 服务器实例的身份（格式为 **TIDB_SERVER_IP_ADDRESS:10080**）。

  ```bash
  cat > insert_one_row.sql <<EOF
  INSERT INTO test.tiproxy_test (tidb_server_instance)
  VALUES (
      (SELECT instance FROM information_schema.cluster_processlist
       WHERE host=(SELECT host FROM information_schema.processlist WHERE id=CONNECTION_ID())
      )
  );
  EOF
  cat > insert_one_row.sh <<EOF
  mysql -h ${HOST_TIPROXY1_PRIVATE_IP} --port 6000 -u root < insert_one_row.sql
  EOF
  chmod +x insert_one_row.sh
  for c in {1..50}; do
  ./insert_one_row.sh &
  done;
  sleep 1;
  jobs
  ```
  ```output
  ...
  [43]   Done                    ./insert_one_row.sh
  [44]   Done                    ./insert_one_row.sh
  [45]   Done                    ./insert_one_row.sh
  [46]   Done                    ./insert_one_row.sh
  [47]   Done                    ./insert_one_row.sh
  [48]   Done                    ./insert_one_row.sh
  [49]-  Done                    ./insert_one_row.sh
  [50]+  Done                    ./insert_one_row.sh
  ```

+ 连接到 TiDB 服务器实例。

  ```bash
  mysql -h ${HOST_TIPROXY1_PRIVATE_IP} --port 6000 -u root
  ```

+ 检查 `test.tiproxy_test` 表，您会发现连接和插入操作已被两个 TiDB 服务器实例均匀处理。请注意，如果您注意到连接计数有轻微差异，例如 28 对 22，这是正常且预期的。

  ```sql
  SELECT tidb_server_instance, COUNT(1)
  FROM test.tiproxy_test
  GROUP BY tidb_server_instance;
  EXIT
  ```
  ```output
  +----------------------+----------+
  | tidb_server_instance | count(1) |
  +----------------------+----------+
  | 10.90.1.14:10080     |       25 |
  | 10.90.1.43:10080     |       25 |
  +----------------------+----------+
  2 rows in set (0.00 sec)
  Bye
  ```

### 步骤 4. 验证会话迁移功能

+ 在计划维护操作期间，如 TiDB 扩容、缩容或集群升级，TiProxy 通过在活动的 TiDB 服务器实例之间无缝迁移现有会话，确保客户端应用程序的连接不中断。TiProxy 的主要目标是在维护 TiDB 服务器之间的负载均衡的同时，为您的应用程序提供零停机时间。

+ 查看提供的迷你 Java 程序代码，它将初始化 4 个线程来对表 `test.tiproxy_test` 运行插入工作负载。

  ```bash
  cat InsertJobNoExceptionHandling.java | grep -A1 -B1 'new Thread'
  ```
  ```output
          for (int i = 0; i < 4; i++) {
              new Thread(new Worker(tidbHost, port, dbUsername, dbPassword, (i + 1))).start();
          }
  ```

+ 如果线程遇到任何异常，它将打印错误堆栈并退出。

  ```bash
  cat InsertJobNoExceptionHandling.java | grep -A3 catch
  ```
  ```output
          } catch (Exception e) {
              e.printStackTrace();
              System.exit(1);
          }
  ```

+ 在端口 6000 上启动针对 TiProxy 的工作负载。请注意，它将使用 4 个线程连接到 TiDB 集群，从而产生 4 个数据库连接。

  ```bash
  rm -f InsertJobNoExceptionHandling.class
  javac -cp .:mysql-connector-java-5.1.36-bin.jar InsertJobNoExceptionHandling.java
  sleep 1
  java -cp .:mysql-connector-java-5.1.36-bin.jar InsertJobNoExceptionHandling ${HOST_TIPROXY1_PRIVATE_IP} 6000 root
  ```
  ```output
  Inserting 0 by worker 3
  Inserting 0 by worker 1
  Inserting 0 by worker 4
  Inserting 0 by worker 2
  Inserting 1 by worker 1
  Inserting 1 by worker 2
  Inserting 1 by worker 3
  Inserting 1 by worker 4
  Inserting 2 by worker 1
  Inserting 2 by worker 3
  Inserting 2 by worker 2
  Inserting 2 by worker 4
  ...
  ```

+ 点击 [Grafana Prometheus Dashboard](http://${LAB:HOST_MONITOR1_PUBLIC_IP}:3000) 并使用用户名和密码（都是"admin"）登录。导航到 `Home` > `tidb-demo` > `tidb-demo-TiProxy-Summary` > `Balance`。观察 `Backend Connections` 面板，每个 TiDB 服务器应该有 2 个连接。

    ![90%](https://lab-static.pingcap.com/quick-demo/TiProxyBalance_2x2.png)

+ 将 `Refresh time` 更改为 `10s`。保持浏览器中的 `Balance` 面板打开，并保持 Java 程序运行。

+ 点击这里 - [终端 2](ssh://monitor1-ssh#2) 打开一个新的终端，连接到为你的实践实验分配的虚拟机。

+ 登录后显示的横幅：

    ```output
     ,     #_
    ~\_  ####_        Amazon Linux 2
    ~~  \_#####\
    ~~     \###|      
    ~~       \#/ ___
    ~~       V~' '->
    ~~~         /    A newer version of Amazon Linux is available!
     ~~._.   _/
        _/ _/       Amazon Linux 2023, GA and supported until 2028-03-15.
      _/m/'           https://aws.amazon.com/linux/amazon-linux-2023/
    ```

+ 通过从集群中移除 IP 地址为 ${HOST_DB2_PRIVATE_IP} 的 TiDB 服务器来缩减 SQL 层。完成后，集群中只有 1 个 TiDB 服务器。

  ```bash
  tiup cluster scale-in tidb-demo --node ${HOST_DB2_PRIVATE_IP}:4000 -y
  sleep 2
  tiup cluster display tidb-demo
  ```
  ```output
  ...
  Stopping component tidb
  ...
  Scaled cluster `tidb-demo` in successfully
  ...
  Cluster type:       tidb
  Cluster name:       tidb-demo
  Cluster version:    v8.5.1
  Deploy user:        ec2-user
  SSH type:           builtin
  Dashboard URL:      http://13.115.186.238:2379/dashboard
  Grafana URL:        http://10.90.4.53:3000
  ID                 Role          Host         Ports                            OS/Arch       Status  Data Dir                      Deploy Dir
  --                 ----          ----         -----                            -------       ------  --------                      ----------
  10.90.4.53:9093    alertmanager  10.90.4.53   9093/9094                        linux/x86_64  Up      /tidb-data/alertmanager-9093  /tidb-deploy/alertmanager-9093
  10.90.4.53:3000    grafana       10.90.4.53   3000                             linux/x86_64  Down    -                             /tidb-deploy/grafana-3000
  10.90.3.19:2379    pd            10.90.3.19   2379/2380                        linux/x86_64  Up|L    /tidb-data/pd-2379            /tidb-deploy/pd-2379
  10.90.4.53:9090    prometheus    10.90.4.53   9090/12020                       linux/x86_64  Up      /tidb-data/prometheus-9090    /tidb-deploy/prometheus-9090
  10.90.1.202:4000   tidb          10.90.1.202  4000/10080                       linux/x86_64  Up      -                             /tidb-deploy/tidb-4000
  10.90.4.75:9000    tiflash       10.90.4.75   9000/3930/20170/20292/8234/8123  linux/x86_64  Up      /tidb-data/tiflash-9000       /tidb-deploy/tiflash-9000
  10.90.3.10:20160   tikv          10.90.3.10   20160/20180                      linux/x86_64  Up      /tidb-data/tikv-20160         /tidb-deploy/tikv-20160
  10.90.3.207:20160  tikv          10.90.3.207  20160/20180                      linux/x86_64  Up      /tidb-data/tikv-20160         /tidb-deploy/tikv-20160
  10.90.3.208:20160  tikv          10.90.3.208  20160/20180                      linux/x86_64  Up      /tidb-data/tikv-20160         /tidb-deploy/tikv-20160
  10.90.4.56:6000    tiproxy       10.90.4.56   6000/3080                        linux/x86_64  Up      -                             /tidb-deploy/tiproxy-6000
  Total nodes: 10
  ```

+ 在[终端 1](ssh://monitor1-ssh#1) 上，迷你 Java 程序继续运行而不中断。

  ```output
  ...
  Inserting 1295 by worker 4
  Inserting 1295 by worker 3
  Inserting 1295 by worker 1
  Inserting 1295 by worker 2
  Inserting 1296 by worker 4
  Inserting 1296 by worker 3
  ...
  ```

+ 从浏览器中检查 Grafana TiProxy `Balance` 面板。4 个连接现在都在单个 TiDB 服务器上。2 个连接已被迁移。

![90%](https://lab-static.pingcap.com/quick-demo/TiProxyBalance_4x0.png)

+ 切换到[终端 2](ssh://monitor1-ssh#2) ，通过向集群添加一个 TiDB 服务器来扩展 SQL 层。完成后，集群中有 2 个 TiDB 服务器。

  ```bash
  tiup cluster scale-out tidb-demo scale-out-tidb.yaml \
   --user ec2-user \
   -i ~/.ssh/pe-class-key-oregon.pem \
   -y
  tiup cluster display tidb-demo
  ```
  ```output
  ...
  Deploy TiDB instance
  ...
  Scaled cluster `tidb-demo` out successfully
  ...
  Cluster type:       tidb
  Cluster name:       tidb-demo
  Cluster version:    v8.5.1
  Deploy user:        ec2-user
  SSH type:           builtin
  Dashboard URL:      http://13.115.186.238:2379/dashboard
  Grafana URL:        http://10.90.4.53:3000
  ID                 Role          Host         Ports                            OS/Arch       Status  Data Dir                      Deploy Dir
  --                 ----          ----         -----                            -------       ------  --------                      ----------
  10.90.4.53:9093    alertmanager  10.90.4.53   9093/9094                        linux/x86_64  Up      /tidb-data/alertmanager-9093  /tidb-deploy/alertmanager-9093
  10.90.4.53:3000    grafana       10.90.4.53   3000                             linux/x86_64  Up      -                             /tidb-deploy/grafana-3000
  10.90.3.19:2379    pd            10.90.3.19   2379/2380                        linux/x86_64  Up|L    /tidb-data/pd-2379            /tidb-deploy/pd-2379
  10.90.4.53:9090    prometheus    10.90.4.53   9090/12020                       linux/x86_64  Up      /tidb-data/prometheus-9090    /tidb-deploy/prometheus-9090
  10.90.1.202:4000   tidb          10.90.1.202  4000/10080                       linux/x86_64  Up      -                             /tidb-deploy/tidb-4000
  10.90.1.233:4000   tidb          10.90.1.233  4000/10080                       linux/x86_64  Up      -                             /tidb-deploy/tidb-4000
  10.90.4.75:9000    tiflash       10.90.4.75   9000/3930/20170/20292/8234/8123  linux/x86_64  Up      /tidb-data/tiflash-9000       /tidb-deploy/tiflash-9000
  10.90.3.10:20160   tikv          10.90.3.10   20160/20180                      linux/x86_64  Up      /tidb-data/tikv-20160         /tidb-deploy/tikv-20160
  10.90.3.207:20160  tikv          10.90.3.207  20160/20180                      linux/x86_64  Up      /tidb-data/tikv-20160         /tidb-deploy/tikv-20160
  10.90.3.208:20160  tikv          10.90.3.208  20160/20180                      linux/x86_64  Up      /tidb-data/tikv-20160         /tidb-deploy/tikv-20160
  10.90.4.56:6000    tiproxy       10.90.4.56   6000/3080                        linux/x86_64  Up      -                             /tidb-deploy/tiproxy-6000
  Total nodes: 11
  ```

+ 再次返回到[终端 1](ssh://monitor1-ssh#1) ，您会发现迷你 Java 程序继续运行而不中断。

  ```output
  ...
  Inserting 1707 by worker 1
  Inserting 1707 by worker 3
  Inserting 1706 by worker 4
  Inserting 1708 by worker 2
  Inserting 1708 by worker 1
  Inserting 1708 by worker 3
  Inserting 1707 by worker 4
  ...
  ```

+ 再次从浏览器中检查 Grafana TiProxy `Balance` 面板。每个 TiDB 服务器有 2 个连接。2 个连接已被迁移。

![90%](https://lab-static.pingcap.com/quick-demo/TiProxyBalance_2x2_2.png)

+ 这就结束了 TiProxy 的会话/连接迁移能力演示。关于使用 TiProxy 支持计划的数据库在线滚动升级，我们有另一个独立的动手实验来演示这个功能，请访问 [使用 TiProxy 实现数据库零停机在线升级](https://labs.tidb.io/labs/demo_005) 了解更多详情。

+ 转到[终端 1](ssh://monitor1-ssh#1) ，按**CTRL-C**停止 Java 工作负载。然后，继续下一个模块。

## 模块 2: 外键（v8.5.0）

外键在传统的 RDBMS 中很常见，但在分布式数据库中很少见。许多分布式数据库未能实现外键，这给维护数据库内数据完整性带来了挑战。然而，作为一个分布式数据库，TiDB 支持外键。

TiDB 中的外键约束允许您定义表之间的关系，利用这些关系来强制执行数据完整性。在基本层面上，外键约束确保子表中的一组列对应于父表中的一组列，禁止可能破坏链接的操作。考虑一个订单表和一个客户表；订单表上的外键约束可以确保每个订单对应一个现有客户，从而维护数据准确性和完整性。

### 步骤 1. 创建一个 `bookshop` schema 并通过 TiDB SQL 层加载数据

+ 在[终端 1](ssh://monitor1-ssh#1) 上，连接到 TiDB 服务器实例。

  ```bash
  mysql -h ${HOST_TIPROXY1_PRIVATE_IP} --port 6000 -u root
  ```
  ```output
  mysql> 
  ```

+ 在 `mysql> ` 提示符下运行以下命令来创建数据库对象并为我们的书店 schema 加载数据。该脚本创建一个名为 `bookshop` 的 schema。

  ```sql
  SOURCE bookshop_demo.sql
  SHOW TABLES;
  ```
  ```output
  Query OK, 0 rows affected (0.08 sec)
  ...
  +--------------------+
  | Tables_in_bookshop |
  +--------------------+
  | authors            |
  | book_authors       |
  | books              |
  | orders             |
  | ratings            |
  | users              |
  +--------------------+
  6 rows in set (0.00 sec)
  ```

+ 以下是上一步中 `bookshop_demo.sql` 脚本内容的摘录，显示了我们书店应用程序的表和索引的创建。您现在不需要运行这些语句，因为它们已经执行过了，但您可以看到我们如何为应用程序建模数据。值得注意的是，您会发现除了 `AUTO_RANDOM` 之外，所有声明都与 MySQL 中使用的相同。

  ```output
  CREATE TABLE `authors` (
   `id` bigint(20) AUTO_RANDOM,
   `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
   `gender` tinyint(1) DEFAULT NULL,
   `birth_year` smallint(6) DEFAULT NULL,
   `death_year` smallint(6) DEFAULT NULL,
   PRIMARY KEY (`id`)
  );

  CREATE TABLE `book_authors` (
   `book_id` bigint(20) NOT NULL,
   `author_id` bigint(20) NOT NULL,
   PRIMARY KEY (`book_id`, `author_id`)
  );

  CREATE TABLE `books` (
  `id` bigint AUTO_RANDOM,
  `title` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
  `type` enum(
      'Magazine',
      'Novel',
      'Life',
      'Arts',
      'Comics',
      'Education & Reference',
      'Humanities & Social Sciences',
      'Science & Technology',
      'Kids',
      'Sports'
  ) COLLATE utf8mb4_unicode_ci NOT NULL,
  `published_at` datetime NOT NULL,
  `stock` int DEFAULT '0',
  `price` decimal(15, 2) DEFAULT '0.0',
  PRIMARY KEY (`id`)
  ) PARTITION BY HASH (`id`) PARTITIONS 8;

  CREATE TABLE `orders` (
   `id` bigint(20) NOT NULL,
   `book_id` bigint(20) NOT NULL,
   `user_id` bigint(20) NOT NULL,
   `quality` tinyint(4) NOT NULL,
   `ordered_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
   PRIMARY KEY (`id`),
   KEY `orders_book_id_idx` (`book_id`)
  );

  CREATE TABLE `ratings` (
   `book_id` bigint(20) NOT NULL,
   `user_id` bigint(20) NOT NULL,
   `score` tinyint(4) NOT NULL,
   `rated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
   UNIQUE KEY `uniq_book_user_idx` (`book_id`, `user_id`),
   PRIMARY KEY (`book_id`, `user_id`)
  );
  
  CREATE TABLE `users` (
   `id` bigint(20) AUTO_RANDOM,
   `balance` decimal(15, 2) DEFAULT '0.0',
   `nickname` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
   PRIMARY KEY (`id`),
   UNIQUE KEY `nickname` (`nickname`)
  );
  ```

+ 您可以使用 mysql 命令探索 schema 并描述数据库中的对象，就像使用任何 MySQL 数据库一样。

  ```sql
  DESC bookshop.books;
  ```
  ```output
  +--------------+-----------------------------------------------------------------------------------------------------------------------------------------------+------+------+---------+-------+
  | Field        | Type                                                                                                                                          | Null | Key  | Default | Extra |
  +--------------+-----------------------------------------------------------------------------------------------------------------------------------------------+------+------+---------+-------+
  | id           | bigint(20)                                                                                                                                    | NO   | PRI  | NULL    |       |
  | title        | varchar(100)                                                                                                                                  | NO   |      | NULL    |       |
  | type         | enum('Magazine','Novel','Life','Arts','Comics','Education & Reference','Humanities & Social Sciences','Science & Technology','Kids','Sports') | NO   |      | NULL    |       |
  | published_at | datetime                                                                                                                                      | NO   |      | NULL    |       |
  | stock        | int(11)                                                                                                                                       | YES  |      | 0       |       |
  | price        | decimal(15,2)                                                                                                                                 | YES  |      | 0.0     |       |
  +--------------+-----------------------------------------------------------------------------------------------------------------------------------------------+------+------+---------+-------+
  6 rows in set (0.00 sec)
  ```

+ 在 `mysql>` 提示符下，运行以下查询以查看一些书籍记录。
   
  ```sql
  SELECT * FROM bookshop.books ORDER BY title LIMIT 3;
  ```
  ```output
  +----+-------------------------------+------------------------------+---------------------+--------+-------+
  | id | title                         | type                         | published_at        | stock  | price |
  +----+-------------------------------+------------------------------+---------------------+--------+-------+
  | 43 | (I've Got a Gal In) Kalamazoo | Sports                       | 2006-05-22 05:19:49 | 231321 | 85.72 |
  | 88 | (Let Me Be Your) Teddy Bear   | Sports                       | 2003-04-08 08:37:52 |    140 | 22.14 |
  | 40 | (You Keep Me) Hangin' On      | Humanities & Social Sciences | 2005-02-12 05:29:04 |    163 | 32.72 |
  +----+-------------------------------+------------------------------+---------------------+--------+-------+
  3 rows in set (0.05 sec)
  ```

### 步骤 2. 使用外键强制引用完整性

+ 在我们的示例 bookshop schema 中，我们在模块 1 创建 schema 时没有添加外键。现在，我们需要强制执行下图所示的实体关系。

  ```mermaid
  ---
  title: Book Authors Example
  ---
  erDiagram
    AUTHORS ||--|{ BOOK_AUTHORS : references
        AUTHORS {
            BIGINT id PK
            VARCHAR name
            TINYINT gender
            SMALLINT birth_year
            SMALLINT death_year
        }
    BOOK_AUTHORS {
        BIGINT book_id   PK 
        BIGINT author_id PK,FK
    }
  ```

+ 执行以下命令在 `book_authors` 和 `authors` 表之间建立引用约束，约束 `book_authors.auth_id` 和 `authors.id` 字段。让我们在[终端 1](ssh://monitor1-ssh#1) 的 `mysql>` 提示符下执行此命令。

  ```sql
  ALTER TABLE bookshop.book_authors
      ADD CONSTRAINT book_author_fk 
      FOREIGN KEY (author_id) REFERENCES bookshop.authors (id); 
  ```
  ```output
  Query OK, 0 rows affected (0.10 sec)
  ```

+ 尝试向 `book_authors` 添加一条违反外键约束的记录。由于 `auth_id` 为 99999999 的作者不存在，预期会出现 **ERROR**。

  ```sql
  INSERT INTO bookshop.book_authors (book_id, author_id) VALUES (100, 99999999);
  ```
  ```output
  ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`bookshop`.`book_authors`, CONSTRAINT `book_author_fk` FOREIGN KEY (`author_id`) REFERENCES `authors` (`id`))
  ```

+ 尝试向 `book_authors` 添加一条符合外键约束的记录。预期会插入成功。例如，声明 Dr. Lindsey Welch II（一个已存在的作者，作者 ID 为 1）也是《Till The End of Time》（一本已存在的书，书籍 ID 为 100）的作者。

  ```sql
  INSERT INTO bookshop.book_authors (book_id, author_id) VALUES (100, 1);
  ```
  ```output
  Query OK, 1 row affected (0.00 sec)
  ```

+ 至此，您已经体验了分布式数据库 TiDB 的外键约束功能。请保持 mysql 客户端连接打开，然后继续进入下一个模块。

## 模块 3: 分区表的全局唯一索引 (v8.5.0)

与索引类似，数据库中的表分区是一种用于加速数据检索操作的数据结构。当执行查询语句时，表分区使数据库能够快速找到满足查询条件的数据。与顺序扫描整个表（`TableFullScan`）相比，这可以节省时间，从而提高查询效率。

在引入全局索引之前，TiDB 为每个分区支持本地索引，这导致主键和唯一键必须包含分区键以确保数据唯一性。此外，在跨多个分区查询数据时，TiDB 需要扫描每个分区的数据才能返回结果。

为了解决上述问题，TiDB 引入了全局索引功能。全局索引使用单个索引覆盖整个表的数据，允许主键和唯一键在不包含所有分区键的情况下保持全局唯一性。此外，全局索引可以在单个操作中访问跨多个分区的索引数据，显著提高非分区键的查询性能，而不是在每个分区的本地索引中查找。

### 步骤 1. 查看分区表的特征

+ 在模块 1 中，我们已经创建了一个分区表 - `books`。它在 `id` 列上使用 8 个哈希分区。

+ 通过执行以下 SQL 命令验证表分区。注意输出中的 `PARTITION BY` 子句。

  ```sql
  SHOW CREATE TABLE books\G
  ```
  ```output
  *************************** 1. row ***************************
         Table: books
  Create Table: CREATE TABLE `books` (
    `id` bigint NOT NULL /*T![auto_rand] AUTO_RANDOM(5) */,
    `title` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
    `type` enum('Magazine','Novel','Life','Arts','Comics','Education & Reference','Humanities & Social Sciences','Science & Technology','Kids','Sports') COLLATE utf8mb4_unicode_ci NOT NULL,
    `published_at` datetime NOT NULL,
    `stock` int DEFAULT '0',
    `price` decimal(15,2) DEFAULT '0.0',
    PRIMARY KEY (`id`) /*T![clustered_index] CLUSTERED */
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin /*T![auto_rand_base] AUTO_RANDOM_BASE=30002 */
  PARTITION BY HASH (`id`) PARTITIONS 8
  1 row in set (0.00 sec)
  ```
  ```sql
  SELECT table_name, partition_name, partition_method
  FROM information_schema.partitions
  WHERE table_name='books';
  ```
  ```output
  +------------+----------------+------------------+
  | table_name | partition_name | partition_method |
  +------------+----------------+------------------+
  | books      | p0             | HASH             |
  | books      | p1             | HASH             |
  | books      | p2             | HASH             |
  | books      | p3             | HASH             |
  | books      | p4             | HASH             |
  | books      | p5             | HASH             |
  | books      | p6             | HASH             |
  | books      | p7             | HASH             |
  +------------+----------------+------------------+
  8 rows in set (0.01 sec)
  ```

+ 由于 `books` 表被分成 8 个分区，它至少有 8 个 TiKV regions 用于存储数据行。

  ```sql
  SHOW TABLE books REGIONS;
  ```
  ```output
  +-----------+-----------+---------+-----------+-----------------+---------------+------------+---------------+------------+----------------------+------------------+------------------------+------------------+
  | REGION_ID | START_KEY | END_KEY | LEADER_ID | LEADER_STORE_ID | PEERS         | SCATTERING | WRITTEN_BYTES | READ_BYTES | APPROXIMATE_SIZE(MB) | APPROXIMATE_KEYS | SCHEDULING_CONSTRAINTS | SCHEDULING_STATE |
  +-----------+-----------+---------+-----------+-----------------+---------------+------------+---------------+------------+----------------------+------------------+------------------------+------------------+
  |       286 | t_119_    | t_120_  |       287 |               1 | 287, 288, 289 |          0 |             0 |       1301 |                    1 |                8 |                        |                  |
  |       290 | t_120_    | t_121_  |       291 |               1 | 291, 292, 293 |          0 |            39 |       1321 |                    1 |                0 |                        |                  |
  |       294 | t_121_    | t_122_  |       295 |               1 | 295, 296, 297 |          0 |             0 |       1329 |                    1 |               44 |                        |                  |
  |       298 | t_122_    | t_123_  |       299 |               1 | 299, 300, 301 |          0 |            39 |       1332 |                    1 |                8 |                        |                  |
  |       302 | t_123_    | t_124_  |       305 |               2 | 303, 304, 305 |          0 |            39 |       1374 |                    1 |              125 |                        |                  |
  |       306 | t_124_    | t_125_  |       307 |               1 | 307, 308, 309 |          0 |             0 |       1278 |                    1 |                2 |                        |                  |
  |       310 | t_125_    | t_126_  |       313 |               2 | 311, 312, 313 |          0 |             0 |       1231 |                    1 |               14 |                        |                  |
  |       314 | t_126_    | t_128_  |       315 |               1 | 315, 316, 317 |          0 |            39 |       1248 |                    1 |                9 |                        |                  |
  +-----------+-----------+---------+-----------+-----------------+---------------+------------+---------------+------------+----------------------+------------------+------------------------+------------------+
  8 rows in set (0.02 sec)
  ```

+ 由于 `books` 表是基于主键 `id` 进行哈希分区的，数据行应该大致均匀地分布在 8 个分区中。执行以下查询来确认这一点。

  ```sql
  SELECT 'P0_COUNT', COUNT(1) FROM books PARTITION (p0)
  UNION ALL
  SELECT 'P1_COUNT', COUNT(1) FROM books PARTITION (p1)
  UNION ALL
  SELECT 'P2_COUNT', COUNT(1) FROM books PARTITION (p2)
  UNION ALL
  SELECT 'P3_COUNT', COUNT(1) FROM books PARTITION (p3)
  UNION ALL
  SELECT 'P4_COUNT', COUNT(1) FROM books PARTITION (p4)
  UNION ALL
  SELECT 'P5_COUNT', COUNT(1) FROM books PARTITION (p5)
  UNION ALL
  SELECT 'P6_COUNT', COUNT(1) FROM books PARTITION (p6)
  UNION ALL
  SELECT 'P7_COUNT', COUNT(1) FROM books PARTITION (p7)
  ORDER BY 1;
  ```
  ```output
  +----------+----------+
  | P0_COUNT | COUNT(1) |
  +----------+----------+
  | P0_COUNT |       12 |
  | P1_COUNT |       13 |
  | P2_COUNT |       13 |
  | P3_COUNT |       13 |
  | P4_COUNT |       13 |
  | P5_COUNT |       12 |
  | P6_COUNT |       12 |
  | P7_COUNT |       12 |
  +----------+----------+
  8 rows in set (0.00 sec)
  ```

### 步骤 2. 在分区表上添加全局二级索引

+ 使用以下命令尝试在 `books` 表上创建全局唯一索引。例如，您想要确保书籍标题和类型的组合是唯一的。预期会出现**错误**。

  ```sql
  CREATE UNIQUE INDEX book_title_uk ON bookshop.books (title, type);
  ```
  ```output
  ERROR 8264 (HY000): Global Index is needed for index 'book_title_uk', since the unique index is not including all partitioning columns, and GLOBAL is not given as IndexOption
  ```

+ 错误信息显示，您想要创建一个本地唯一索引（没有给出 `GLOBAL` 关键字），并且没有包含分区列（本地唯一索引必须包含分区列，在我们的例子中是 `id`），TiDB 无法执行。要解决此问题，您可以执行以下操作之一：

    > 在 `title` 和 `type` 上创建一个**本地唯一索引**，由于它是一个本地索引，您必须额外包含 `id` 列，尽管实际上您不需要在索引中包含 `id` 列。
    
    或者
    
    > 在 `title` 和 `type` 上创建一个**全局唯一索引**，不需要包含 `id`，它是一个单列索引。**这正是我们在本模块中讨论的新功能。**

+ 让我们再次尝试通过添加 `GLOBAL` 关键字来创建全局唯一索引，这次不应出现错误。

  ```sql
  CREATE UNIQUE INDEX book_title_uk ON bookshop.books (title, type) GLOBAL;
  ```
  ```output
  Query OK, 0 rows affected (3.06 sec)
  ```

+ 通过查询 `information_schema.tidb_indexes` 表并检查输出中的 `is_global` 列，验证您的索引是否为 `GLOBAL`。

  ```sql
  SELECT key_name, seq_in_index, column_name, clustered, is_global
  FROM information_schema.tidb_indexes
  WHERE table_name='books';
  ```
  ```output
  +---------------+--------------+-------------+-----------+-----------+
  | key_name      | seq_in_index | column_name | clustered | is_global |
  +---------------+--------------+-------------+-----------+-----------+
  | PRIMARY       |            1 | id          | YES       |         0 |
  | book_title_uk |            1 | title       | NO        |         1 |
  | book_title_uk |            2 | type        | NO        |         1 |
  +---------------+--------------+-------------+-----------+-----------+
  3 rows in set (0.00 sec)
  ```

### 步骤 3. 全局唯一索引的典型场景

+ 请记住，在 `books` 表中，`title` 和 `type` 的组合是唯一的。

+ 让我们删除全局唯一索引 `book_title_uk` 并运行查询解释。请注意，所有分区都被扫描以获取结果。注意解释输出中 `access object` 列下的 `partition:all`。如果我们的数据集很大，这个操作在 IO 方面是昂贵的。

  ```sql
  DROP INDEX book_title_uk ON bookshop.books;
  EXPLAIN SELECT stock, price FROM bookshop.books
  WHERE type = 'Science & Technology' AND title = 'Play That Funky Music';
  ```
  ```output
  Query OK, 0 rows affected (0.14 sec)

  +---------------------------+---------+-----------+---------------+----------------------------------------------------------------------------------------------------+
  | id                        | estRows | task      | access object | operator info                                                                                      |
  +---------------------------+---------+-----------+---------------+----------------------------------------------------------------------------------------------------+
  | TableReader_11            | 0.00    | root      | partition:all | data:Projection_5                                                                                  |
  | └─Projection_5            | 0.00    | cop[tikv] |               | bookshop.books.stock, bookshop.books.price                                                         |
  |   └─Selection_10          | 0.00    | cop[tikv] |               | eq(bookshop.books.title, "Play That Funky Music"), eq(bookshop.books.type, "Science & Technology") |
  |     └─TableFullScan_9     | 100.00  | cop[tikv] | table:books   | keep order:false, stats:pseudo                                                                     |
  +---------------------------+---------+-----------+---------------+----------------------------------------------------------------------------------------------------+
  4 rows in set (0.00 sec)
  ```

+ 让我们在 `(type, title)` 上添加一个**本地**唯一索引，并再次运行查询解释。这次我们发现 `IndexRangeScan` 被用于所有分区。好消息是索引被利用来加速查询，坏消息是所有 8 个分区上的 8 个本地索引都被范围扫描以获取结果。对于 `type` 和 `title` 的唯一组合，这是不必要的。请注意解释输出中 `IndexLookUp` 行的 `access object` 列下的 `partition:all`。这比之前好，但仍不是我们案例的最佳解决方案。

  ```sql
  CREATE UNIQUE INDEX book_title_idx ON bookshop.books (type, title, id);
  EXPLAIN SELECT stock, price FROM bookshop.books
  WHERE type = 'Science & Technology' AND title = 'Play That Funky Music';
  ```
  ```output
  Query OK, 0 rows affected (2.98 sec)

  +---------------------------------+---------+-----------+----------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
  | id                              | estRows | task      | access object                                      | operator info                                                                                                                                                                                                                                                                                                                                                             |
  +---------------------------------+---------+-----------+----------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
  | Projection_4                    | 0.00    | root      |                                                    | bookshop.books.stock, bookshop.books.price                                                                                                                                                                                                                                                                                                                                |
  | └─IndexLookUp_8                 | 0.00    | root      | partition:all                                      |                                                                                                                                                                                                                                                                                                                                                                           |
  |   ├─IndexRangeScan_6(Build)     | 0.00    | cop[tikv] | table:books, index:book_title_idx(type, title, id) | range:["Science & Technology" "\x0f\xa7\x0f.\x0e3\x10^\x02\t\x10\x02\x0e\xe1\x0e3\x10\x02\x02\t\x0e\xb9\x10\x1f\x0fd\x0f!\x10^\x02\t\x0f[\x10\x1f\x0f\xea\x0e\xfb\x0e`","Science & Technology" "\x0f\xa7\x0f.\x0e3\x10^\x02\t\x10\x02\x0e\xe1\x0e3\x10\x02\x02\t\x0e\xb9\x10\x1f\x0fd\x0f!\x10^\x02\t\x0f[\x10\x1f\x0f\xea\x0e\xfb\x0e`"], keep order:false, stats:pseudo |
  |   └─TableRowIDScan_7(Probe)     | 0.00    | cop[tikv] | table:books                                        | keep order:false, stats:pseudo                                                                                                                                                                                                                                                                                                                                            |
  +---------------------------------+---------+-----------+----------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
  4 rows in set (0.00 sec)
  ```
+ 让我们将**本地**索引更改为**全局**唯一索引。

  ```sql
  DROP INDEX book_title_idx ON bookshop.books;
  CREATE UNIQUE INDEX book_title_uk ON bookshop.books (title, type) GLOBAL;
  ```
  ```output
  Query OK, 0 rows affected (0.17 sec)
  Query OK, 0 rows affected (2.38 sec)
  ```

+ 运行以下查询以验证全局索引是否正常工作。您将看到在解释输出的 `access object` 列下访问了 `index:book_title_uk` 对象。操作是 `Point_Get`。没有在所有分区上进行 `IndexLookUp` 扫描，也没有在所有分区上进行 `TableFullScan`，只有一个 `Point_Get`，这是在数据集很大时该模块中最有效的查询。

  ```sql
  EXPLAIN SELECT stock, price FROM bookshop.books
  WHERE type = 'Science & Technology' AND title = 'Play That Funky Music';
  ```
  ```output
  +-------------+---------+------+-----------------------------------------------+---------------+
  | id          | estRows | task | access object                                 | operator info |
  +-------------+---------+------+-----------------------------------------------+---------------+
  | Point_Get_1 | 1.00    | root | table:books, index:book_title_uk(title, type) |               |
  +-------------+---------+------+-----------------------------------------------+---------------+
  1 row in set (0.00 sec)
  ```

+ 到此为止，您已经了解了使用全局唯一索引的典型场景。保持 mysql 客户端连接打开，并继续下一个模块。

## 模块 4: 监控索引使用情况统计

适当的索引设计是维护数据库性能的关键前提。TiDB v8.0.0 引入了 `information_schema.tidb_index_usage` 表和 `sys.schema_unused_indexes` 视图，以提供索引的使用情况统计信息。此功能有助于您评估数据库中索引的效率并优化索引设计。

以下来自 [TiDB SQL Tuning](https://www.pingcap.com/education/course-tidb-sql-tuning/) 培训的补充信息是使用此模块的前提知识。为方便起见，在此显示。

  ![70%](https://lab-static.pingcap.com/quick-demo/SelCarCost.png)

### 步骤 1. 通过查询 `tidb_index_usage` 验证索引效率

+ 通过查询 `tidb_index_usage` 表获取 `book_title_uk` 索引使用基线。请注意，统计信息全为零，因为前一个模块中的 `EXPLAIN` 命令实际上并未执行查询。
  
  ```sql
  SELECT table_schema, table_name, index_name, query_total, rows_access_total, last_access_time,
  percentage_access_0, percentage_access_0_1, percentage_access_1_10, percentage_access_10_20,
  percentage_access_20_50, percentage_access_50_100, percentage_access_100
  FROM information_schema.tidb_index_usage
  WHERE index_name='book_title_uk'\G
  ```
  ```output
  *************************** 1. row ***************************
              table_schema: bookshop
                table_name: books
                index_name: book_title_uk
               query_total: 0
         rows_access_total: 0
          last_access_time: NULL
       percentage_access_0: 0
     percentage_access_0_1: 0
    percentage_access_1_10: 0
   percentage_access_10_20: 0
   percentage_access_20_50: 0
  percentage_access_50_100: 0
     percentage_access_100: 0
  1 row in set (0.01 sec)
  ```

+ 收集 CBO 统计信息。

  ```sql
  ANALYZE TABLE bookshop.books;
  ```
  ```output
  Query OK, 0 rows affected, 8 warnings (0.69 sec)
  ```

  + 执行可能使用 `book_title_uk` 索引的查询。

  ```sql
  SELECT stock, price FROM bookshop.books
  WHERE type = 'Science & Technology' AND title = 'Play That Funky Music';
  ```
  ```output
  +-------+--------+
  | stock | price  |
  +-------+--------+
  |   147 | 185.76 |
  +-------+--------+
  1 row in set (0.00 sec)
  ```

+ 再次检查使用统计信息。请注意 `query_total`、`rows_access_total` 和 `percentage_access_1_10` 将增加。

  ```sql
  SELECT table_schema, table_name, index_name, query_total, rows_access_total, last_access_time,
  percentage_access_0, percentage_access_0_1, percentage_access_1_10, percentage_access_10_20,
  percentage_access_20_50, percentage_access_50_100, percentage_access_100
  FROM information_schema.tidb_index_usage
  WHERE index_name='book_title_uk'\G
  ```
  ```output
  *************************** 1. row ***************************
              table_schema: bookshop
                table_name: books
                index_name: book_title_uk
               query_total: 1
         rows_access_total: 1
          last_access_time: 2025-03-09 05:33:47
       percentage_access_0: 0
     percentage_access_0_1: 0
    percentage_access_1_10: 1
   percentage_access_10_20: 0
   percentage_access_20_50: 0
  percentage_access_50_100: 0
     percentage_access_100: 0
  1 row in set (0.01 sec)
  ```

+ 执行另一个不太可能使用 `book_title_uk` 索引的查询。在这种情况下，`type` 是谓词，而 `title` 被省略。由于 `book_title_uk` 索引的前导列是 `title`，因此 CBO 不会选择 `book_title_uk` 索引访问路径。

  ```sql
  SELECT stock, price FROM bookshop.books
  WHERE type = 'Science & Technology';
  ```
  ```output
  +------------+--------+
  | stock      | price  |
  +------------+--------+
  | 2147483647 | 101.02 |
  |         30 | 195.32 |
  |        141 |  13.20 |
  |        138 |  94.34 |
  |         38 | 138.97 |
  |         66 |  13.00 |
  |        147 | 185.76 |
  |          6 | 109.95 |
  |         20 |  75.24 |
  |        990 | 197.15 |
  |       4981 | 133.59 |
  |    9888998 | 106.51 |
  |        288 | 197.48 |
  |      30000 |  56.56 |
  |    5555555 | 104.12 |
  |          6 | 187.77 |
  +------------+--------+
  16 rows in set (0.02 sec)
  ```

+ 然而，仅根据结果集，我们无法知道前一个查询是通过索引访问路径执行的还是通过全表扫描访问路径执行的。让我们从 `tidb_index_usage` 表中获取答案。您会发现没有计数器增加，这意味着前一个查询未使用索引。

  ```sql
  SELECT table_schema, table_name, index_name, query_total, rows_access_total, last_access_time,
  percentage_access_0, percentage_access_0_1, percentage_access_1_10, percentage_access_10_20,
  percentage_access_20_50, percentage_access_50_100, percentage_access_100
  FROM information_schema.tidb_index_usage
  WHERE index_name='book_title_uk'\G
  ```
  ```output
  *************************** 1. row ***************************
              table_schema: bookshop
                table_name: books
                index_name: book_title_uk
               query_total: 1
         rows_access_total: 1
          last_access_time: 2025-03-09 05:33:47
       percentage_access_0: 0
     percentage_access_0_1: 0
    percentage_access_1_10: 1
   percentage_access_10_20: 0
   percentage_access_20_50: 0
  percentage_access_50_100: 0
     percentage_access_100: 0
  1 row in set (0.01 sec)
  ```

+ 确认 `type = 'Science & Technology'` 谓词无法触发索引使用。

  ```sql
  EXPLAIN ANALYZE SELECT stock, price FROM bookshop.books
  WHERE type = 'Science & Technology';
  ```
  ```output
  +---------------------------+---------+---------+-----------+---------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------+-----------+------+
  | id                        | estRows | actRows | task      | access object | execution info                                                                                                                                                                                                                                                                                                                            | operator info                                   | memory    | disk |
  +---------------------------+---------+---------+-----------+---------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------+-----------+------+
  | TableReader_11            | 16.00   | 16      | root      | partition:all | time:1.5ms, loops:2, RU:4.149196, cop_task: {num: 8, max: 1.33ms, min: 955.8µs, avg: 1.17ms, p95: 1.33ms, max_proc_keys: 13, p95_proc_keys: 13, tot_proc: 662.4µs, tot_wait: 4.26ms, copr_cache_hit_ratio: 0.00, build_task_duration: 23.7µs, max_distsql_concurrency: 8}, rpc_info:{Cop:{num_rpc:8, total_time:9.31ms}}                  | data:Projection_5                               | 994 Bytes | N/A  |
  | └─Projection_5            | 16.00   | 16      | cop[tikv] |               | tikv_task:{proc max:0s, min:0s, avg: 0s, p80:0s, p95:0s, iters:8, tasks:8}, scan_detail: {total_process_keys: 100, total_process_keys_size: 8414, total_keys: 108, get_snapshot_time: 4.02ms, rocksdb: {key_skipped_count: 100, block: {}}}, time_detail: {total_process_time: 662.4µs, total_wait_time: 4.26ms, tikv_wall_time: 6.22ms}  | bookshop.books.stock, bookshop.books.price      | N/A       | N/A  |
  |   └─Selection_10          | 16.00   | 16      | cop[tikv] |               | tikv_task:{proc max:0s, min:0s, avg: 0s, p80:0s, p95:0s, iters:8, tasks:8}                                                                                                                                                                                                                                                                | eq(bookshop.books.type, "Science & Technology") | N/A       | N/A  |
  |     └─TableFullScan_9     | 100.00  | 100     | cop[tikv] | table:books   | tikv_task:{proc max:0s, min:0s, avg: 0s, p80:0s, p95:0s, iters:8, tasks:8}                                                                                                                                                                                                                                                                | keep order:false                                | N/A       | N/A  |
  +---------------------------+---------+---------+-----------+---------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------+-----------+------+
  4 rows in set (0.00 sec)
  ```

+ 此时，您可能会想，CBO 选择 `TableFullScan` 的另一个原因是因为我们的数据集太小（表中只有 100 行）。您是对的，因为当数据集不够大时，选择 `IndexLookUp` 和 `TableFullScan` 之间不会有明显的性能差异。但执行计划确实反映了 CBO 的决策。让我们做另一个实验来测试 CBO 的决策。

+ 创建一个新的全局唯一索引，这次是在列组合 `(type, title)` 上，`type` 是前导列。

  ```sql
  CREATE UNIQUE INDEX book_type_uk ON bookshop.books (type, title) GLOBAL;
  ANALYZE TABLE bookshop.books;
  ```
  ```output
  Query OK, 0 rows affected (6.02 sec)

  Query OK, 0 rows affected, 8 warnings (0.82 sec)
  ```

+ 再次执行 `type = 'Science & Technology'` 谓词查询。这次我们有两个索引，`book_type_uk` 和 `book_title_uk`，CBO 会选择哪种访问方法？`book_title_uk` 不太可能，选择将在 `book_type_uk` 和 `TableFullScan` 之间做出。正如我们已经知道的，结果集中的行占总行数的 16%。查询选择性可能不足以使 CBO 偏向索引访问路径。

  ```sql
  SELECT stock, price FROM bookshop.books
  WHERE type = 'Science & Technology';
  ```
  ```output
  +------------+--------+
  | stock      | price  |
  +------------+--------+
  |      30000 |  56.56 |
  |    5555555 | 104.12 |
  |          6 | 187.77 |
  | 2147483647 | 101.02 |
  |         30 | 195.32 |
  |        141 |  13.20 |
  |        138 |  94.34 |
  |         38 | 138.97 |
  |        990 | 197.15 |
  |       4981 | 133.59 |
  |    9888998 | 106.51 |
  |         66 |  13.00 |
  |        147 | 185.76 |
  |          6 | 109.95 |
  |         20 |  75.24 |
  |        288 | 197.48 |
  +------------+--------+
  16 rows in set (0.00 sec)
  ```

+ 再次查询 `tidb_index_usage` 表。您会发现 `book_type_uk` 没有计数器增加，特别是 `percentage_access_10_20` 计数器为 0，这意味着前一个查询未使用索引。然而，如果 `books` 表中有很多行，并且选择性保持为 16%，CBO 可能会为相同的查询选择索引访问路径。

  ```sql
  SELECT table_schema, table_name, index_name, query_total, rows_access_total, last_access_time,
  percentage_access_0, percentage_access_0_1, percentage_access_1_10, percentage_access_10_20,
  percentage_access_20_50, percentage_access_50_100, percentage_access_100
  FROM information_schema.tidb_index_usage
  WHERE index_name in ('book_title_uk','book_type_uk')\G
  ```
  ```output
  *************************** 1. row ***************************
              table_schema: bookshop
                table_name: books
                index_name: book_title_uk
               query_total: 1
         rows_access_total: 1
          last_access_time: 2025-03-09 05:33:47
       percentage_access_0: 0
     percentage_access_0_1: 0
    percentage_access_1_10: 1
   percentage_access_10_20: 0
   percentage_access_20_50: 0
  percentage_access_50_100: 0
     percentage_access_100: 0
  *************************** 2. row ***************************
              table_schema: bookshop
                table_name: books
                index_name: book_type_uk
               query_total: 0
         rows_access_total: 0
          last_access_time: NULL
       percentage_access_0: 0
     percentage_access_0_1: 0
    percentage_access_1_10: 0
   percentage_access_10_20: 0
   percentage_access_20_50: 0
  percentage_access_50_100: 0
     percentage_access_100: 0
  2 rows in set (0.01 sec)
  ```

+ 在 TiDB 中，一般规则是选择性低于 10% 被认为是索引访问路径的最佳选择，但这并不是每个边缘案例的绝对规则。

### 步骤 2. 根据索引使用情况统计删除未使用的索引

+ 无用索引的唯一目的是减慢 DML 操作（INSERT、DELETE、UPDATE 等）和某些 DDL 操作。删除它是正确的做法。

+ 由于 `tidb_index_usage` 表可以告诉哪些索引正在使用或未使用，TiDB 提供了另一个有趣的内存表 `sys.schema_unused_indexes`，它可以告诉 DBA 和 DevOps 工程师那些无用的索引，并且在集群重启时会被清除。在依赖 `sys.schema_unused_indexes` 之前，必须确保至少有一个 TiDB 节点已运行足够长的时间。

+ 查找 `books` 表上到目前为止未使用的索引。

  ```sql
  SELECT * FROM sys.schema_unused_indexes
  WHERE object_schema='bookshop' AND object_name='books';
  ```
  ```output
  +---------------+-------------+--------------+
  | object_schema | object_name | index_name   |
  +---------------+-------------+--------------+
  | bookshop      | books       | book_type_uk |
  +---------------+-------------+--------------+
  1 row in set (0.01 sec)
  ```

+ 使未使用的索引对 CBO 不可见。然后 CBO 不会在执行计划中使用此类索引。在实际场景中，您应该监控系统是否存在任何潜在的性能回归。

  ```sql
  ALTER TABLE bookshop.books ALTER INDEX book_type_uk INVISIBLE;
  ```
  ```output
  Query OK, 0 rows affected (0.51 sec)
  ```

+ 完成某些业务周期后，如果未观察到与不可见索引相关的性能回归，则应将其删除。

  ```sql
  ALTER TABLE bookshop.books DROP INDEX book_type_uk;
  ```
  ```output
  Query OK, 0 rows affected (0.51 sec)
  ```

+ 现在您了解了如何从索引使用统计信息中获得帮助，以帮助您做出操作决策。请继续下一个模块。

## 模块 5: 向量搜索 (v8.5.0)

受 AI 进步和在高维空间中高效处理数据需求的驱动，TiDB 支持向量数据类型和数据库内向量搜索。

在本模块中，您将创建一个具有向量数据类型的表，并插入带有向量数据的记录，使用 HNSW 索引来提高向量搜索性能。

+ 您可以将 TiDB 用作向量存储，提供即时优势，包括：
  + 高效的近似最近邻搜索。
  + 无计划和非计划停机时间，具有无限扩展性。
  + 统一存储关系数据和向量数据，扩展到极大规模。

### 步骤 1. 创建一个具有向量数据类型的测试表

+ 创建一个具有向量数据类型的表，并向其中填充随机数据（32768 行）。表名为 `vs_example.vector_table`。请注意，输出中显示的向量数据类型列为 `embedding`。
   
  ```sql
  CREATE DATABASE vs_example;
  USE vs_example;
  DROP SEQUENCE IF EXISTS vs_seq1;
  CREATE SEQUENCE vs_seq1;
  DROP TABLE IF EXISTS vector_table;
  CREATE TABLE vector_table (
      id BIGINT AUTO_RANDOM PRIMARY KEY,
      doc TEXT,
      embedding VECTOR(3)
  );
  SOURCE populate_vector_table.sql
  DESC vector_table;
  SELECT COUNT(1) FROM vector_table;
  SHOW CREATE TABLE vector_table\G
  ```
  ```output
  ...
  Query OK, 16384 rows affected (0.06 sec)
  Records: 16384 Duplicates: 0  Warnings: 0
  Query OK, 0 rows affected (0.10 sec)

  +-----------+-----------+------+------+---------+-------+
  | Field     | Type      | Null | Key  | Default | Extra |
  +-----------+-----------+------+------+---------+-------+
  | id        | bigint    | NO   | PRI  | NULL    |       |
  | doc       | text      | YES  |      | NULL    |       |
  | embedding | vector(3) | YES  |      | NULL    |       |
  +-----------+-----------+------+------+---------+-------+
  3 rows in set (0.00 sec)

  +----------+
  | COUNT(1) |
  +----------+
  |    32768 |
  +----------+
  1 row in set (0.02 sec)

  *************************** 1. row ***************************
         Table: vector_table
  Create Table: CREATE TABLE `vector_table` (
    `id` bigint NOT NULL /*T![auto_rand] AUTO_RANDOM(5) */,
    `doc` text DEFAULT NULL,
    `embedding` vector(3) DEFAULT NULL,
    PRIMARY KEY (`id`) /*T![clustered_index] CLUSTERED */
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin /*T![auto_rand_base] AUTO_RANDOM_BASE=625585 */
  1 row in set (0.01 sec)
  ```

### 步骤 2. 运行 K-最近邻搜索并检查执行计划

+ 查看以下查询。它显示了使用余弦距离的典型 K-最近邻搜索。
   
+ 该算法在数据集 - 表 `vector_table` 中查找参考节点 `[0.10752534,0.3524283,0.46134815]` 的最近节点（前 3 个）。由于在步骤 1 中使用了随机数据，您的结果将与示例输出不同。

  ```sql
  SELECT doc, embedding
  FROM vector_table
  ORDER BY VEC_COSINE_DISTANCE(embedding, '[0.10752534,0.3524283,0.46134815]')
  LIMIT 3;
  ```
  ```output
  +-------+-----------------------------------+
  | doc   | embedding                         |
  +-------+-----------------------------------+
  | 3343  | [0.10953534,0.3534283,0.46134815] |
  | 20717 | [0.18479975,0.59234893,0.7714548] |
  | 19597 | [0.20609,0.6679932,0.8865328]     |
  +-------+-----------------------------------+
  3 rows in set (0.02 sec)
  ```
  
+ 查看执行计划以验证查询中是否使用了 `FullTableScan`。考虑到表 `vector_table` 仅包含 32768 行，响应时间显著迅速，但在处理数十亿行时，潜在的性能影响是显著的，不能忽视。请注意解释输出底部的操作，其中估计行数 (`estRows`) 远大于 `3.00`。
  
  ```sql
  EXPLAIN
  SELECT doc, embedding
  FROM vector_table
  ORDER BY VEC_COSINE_DISTANCE(embedding, '[0.10752534,0.3524283,0.46134815]')
  LIMIT 3;
  ```
  ```output
  +--------------------------------+----------+-----------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------+
  | id                             | estRows  | task      | access object      | operator info                                                                                                                                      |
  +--------------------------------+----------+-----------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------+
  | Projection_14                  | 3.00     | root      |                    | vs_example.vector_table.doc, vs_example.vector_table.embedding                                                                                     |
  | └─TopN_7                       | 3.00     | root      |                    | Column#4, offset:0, count:3                                                                                                                        |
  |   └─Projection_15              | 3.00     | root      |                    | vs_example.vector_table.doc, vs_example.vector_table.embedding, vec_cosine_distance(vs_example.vector_table.embedding, [0.11,0.35,0.46])->Column#4 |
  |     └─TableReader_13           | 3.00     | root      |                    | data:TopN_12                                                                                                                                       |
  |       └─TopN_12                | 3.00     | cop[tikv] |                    | vec_cosine_distance(vs_example.vector_table.embedding, [0.11,0.35,0.46]), offset:0, count:3                                                        |
  |         └─TableFullScan_11     | 30179.00 | cop[tikv] | table:vector_table | keep order:false                                                                                                                                   |
  +--------------------------------+----------+-----------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------+
  6 rows in set (0.00 sec)
  ```

### 步骤 3. 在向量列上添加 HNSW 索引

+ 启用 TiFlash 副本。

  ```sql
  ALTER TABLE vector_table SET TIFLASH REPLICA 1;
  ```

+ 在列 `embedding` 上创建 HNSW 向量索引。HNSW 索引在近似精度（大多数情况下大于 98%）方面提供了良好的性能。

  ```sql
  CREATE VECTOR INDEX embedding_idx ON vector_table ((VEC_COSINE_DISTANCE(embedding))) USING HNSW;
  ```

+ 确认列已完全索引。验证 `ROWS_STABLE_INDEXED` 为 `32768`。

  ```sql
  SELECT * FROM information_schema.tiflash_indexes
  WHERE tidb_table='vector_table'\G
  ```
  ```output
  *************************** 1. row ***************************
            TIDB_DATABASE: vs_example
               TIDB_TABLE: vector_table
                 TABLE_ID: 114
              COLUMN_NAME: embedding
               INDEX_NAME: embedding_idx
                COLUMN_ID: 3
                 INDEX_ID: 1
               INDEX_KIND: HNSW
      ROWS_STABLE_INDEXED: 32768
  ROWS_STABLE_NOT_INDEXED: 0
       ROWS_DELTA_INDEXED: 0
   ROWS_DELTA_NOT_INDEXED: 0
            ERROR_MESSAGE: 
         TIFLASH_INSTANCE: 10.90.4.131:3930
  1 row in set (0.00 sec)
  ```

### 步骤 4. 针对索引表运行 K-最近邻查询

+ 针对表 `vector_table` 运行相同的 k-最近邻搜索。

  ```sql
  SELECT doc, embedding
  FROM vector_table
  ORDER BY VEC_COSINE_DISTANCE(embedding, '[0.10752534,0.3524283,0.46134815]')
  LIMIT 3;
  ```
  ```output
  +-------+------------------------------------+
  | doc   | embedding                          |
  +-------+------------------------------------+
  | 3343  | [0.10953534,0.3534283,0.46134815]  |
  | 20717 | [0.18479975,0.59234893,0.7714548]  |
  | 24641 | [0.21949558,0.73159087,0.94375384] |
  +-------+------------------------------------+
  3 rows in set (0.03 sec)
  ```

### 步骤 5. 查看执行计划

+ 查看执行计划以验证查询中是否使用了近似最近邻索引（解释输出中 `access object` 列下的 `index:embedding_idx(embedding)` 对象和 `operator info` 列下的 `annIndex:COSINE` 操作符）。在大规模生产环境中，表 `vector_table` 预计在服务此查询时将优于 `vector_table_noidx`。请注意解释输出底部的操作，其中估计行数 (`estRows`) 仅为 `3.00`，这比步骤 2 中的 `TableFullScan` 高效得多。除了索引优化外，您还可以在输出的 `task` 列中找到 `mpp[tiflash]` 任务，这是 TiDB 实现的列存储查询优化。

  ```sql
  EXPLAIN
  SELECT doc, embedding
  FROM vector_table
  ORDER BY VEC_COSINE_DISTANCE(embedding, '[0.10752534,0.3524283,0.46134815]')
  LIMIT 3;
  ```
  ```output
  +--------------------------------------+---------+--------------+----------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------+
  | id                                   | estRows | task         | access object                                      | operator info                                                                                                                                      |
  +--------------------------------------+---------+--------------+----------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------+
  | Projection_28                        | 3.00    | root         |                                                    | vs_example.vector_table.doc, vs_example.vector_table.embedding                                                                                     |
  | └─TopN_10                            | 3.00    | root         |                                                    | Column#5, offset:0, count:3                                                                                                                        |
  |   └─Projection_29                    | 3.00    | root         |                                                    | vs_example.vector_table.doc, vs_example.vector_table.embedding, vec_cosine_distance(vs_example.vector_table.embedding, [0.11,0.35,0.46])->Column#5 |
  |     └─TableReader_25                 | 3.00    | root         |                                                    | MppVersion: 2, data:ExchangeSender_24                                                                                                              |
  |       └─ExchangeSender_24            | 3.00    | mpp[tiflash] |                                                    | ExchangeType: PassThrough                                                                                                                          |
  |         └─Projection_26              | 3.00    | mpp[tiflash] |                                                    | vs_example.vector_table.doc, vs_example.vector_table.embedding                                                                                     |
  |           └─TopN_23                  | 3.00    | mpp[tiflash] |                                                    | Column#4, offset:0, count:3                                                                                                                        |
  |             └─Projection_27          | 3.00    | mpp[tiflash] |                                                    | vs_example.vector_table.doc, vs_example.vector_table.embedding, vec_cosine_distance(vs_example.vector_table.embedding, [0.11,0.35,0.46])->Column#4 |
  |               └─TableFullScan_22     | 3.00    | mpp[tiflash] | table:vector_table, index:embedding_idx(embedding) | keep order:false, annIndex:COSINE(embedding..[0.11,0.35,0.46], limit:3)                                                                            |
  +--------------------------------------+---------+--------------+----------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------+
  9 rows in set (0.00 sec)
  ```

+ 如前所述，HNSW 索引依赖于 TiFlash。如果您是 TiDB 列存储 TiFlash 的新手，请查看其他动手实验 - TiDB Cloud Serverless 入门或注册自学课程 [TiDB 简介](https://www.pingcap.com/education/course-introduction-to-tidb/)。继续下一个模块。

+ 与本模块中的 3 维向量不同，现实世界中的场景通常涉及更多维度的向量，例如由嵌入模型生成的 1536 维向量。我们还提供了专注于这些主题的动手实验 - [TiDB 的生成式 AI](https://labs.tidb.io/labs?category=Generative+AI+at+TiDB)。

+ 请继续下一个模块。

## 模块 6: TiKV MVCC 内存引擎 (v8.5.0)

多版本并发控制 (MVCC) 是现代数据库的关键方面，允许并发访问而不影响完整性。TiKV MVCC 内存引擎主要用于加速需要扫描大量 MVCC 历史版本的查询。一个优化良好的查询可能会因为扫描大量 MVCC 版本而变成慢查询。

TiKV MVCC 内存引擎适用于以下场景以提高查询性能：

+ 需要查询频繁更新或删除记录的应用程序。
+ 需要调整 `tidb_gc_life_time` 以在 TiDB 中保留历史版本更长时间的应用程序。

### 步骤 1. 检查默认的 `in-memory-engine` 设置

+ 连接到 TiDB。

  ```bash
  mysql -h ${HOST_TIPROXY1_PRIVATE_IP} --port 6000 -u root
  ```
  ```output
  mysql> 
  ```

+ 验证 TiKV MVCC `in-memory-engine` 默认是关闭的。

  ```sql
  SHOW CONFIG WHERE type='tikv' AND name='in-memory-engine.enable';
  ```
  ```output
  +------+-------------------+-------------------------+-------+
  | Type | Instance          | Name                    | Value |
  +------+-------------------+-------------------------+-------+
  | tikv | 10.90.1.41:20160  | in-memory-engine.enable | false |
  | tikv | 10.90.1.223:20160 | in-memory-engine.enable | false |
  | tikv | 10.90.1.231:20160 | in-memory-engine.enable | false |
  +------+-------------------+-------------------------+-------+
  3 rows in set (0.01 sec)
  ```

+ 增加 `tidb_gc_life_time` 以便在本次研讨会期间保留以前的版本。

  ```sql
  SET GLOBAL tidb_gc_life_time='1440m';
  ```

### 步骤 2. 验证 `in-memory-engine` 设置为 `OFF` 时的行为

+ 设置两个名为 `ime_test1` 和 `ime_test2` 的测试表，具有相同的模式。请注意，每个表的 `content` 列上都有一个索引。

  ```sql
  DROP TABLE IF EXISTS test.ime_test1;
  DROP TABLE IF EXISTS test.ime_test2;
  CREATE TABLE test.ime_test1 (
      `id` BIGINT AUTO_RANDOM PRIMARY KEY,
      `content` INT,
      `created_at` DATETIME,
      `updated_at` DATETIME,
      `version` INT,
      KEY content_idx (`content`)
  );
  CREATE TABLE test.ime_test2 (
      `id` BIGINT AUTO_RANDOM PRIMARY KEY,
      `content` INT,
      `created_at` DATETIME,
      `updated_at` DATETIME,
      `version` INT,
      KEY content_idx (`content`)
  );
  ```

+ 向两个表中插入一行。

  ```sql
  INSERT INTO test.ime_test1 (
      content, created_at, updated_at, version
  ) VALUES (
      FLOOR(1 + (RAND() * 99999999)), NOW(), NOW(), 1
  );
  INSERT INTO test.ime_test2 (
      content, created_at, updated_at, version
  ) VALUES (
      FLOOR(1 + (RAND() * 99999999)), NOW(), NOW(), 1
  );
  SELECT * FROM test.ime_test1;
  SELECT * FROM test.ime_test2;
  EXIT
  ```

+ 扩展两个表到 32768 行。

  ```bash
  cat > double.sql <<EOF
  INSERT INTO test.ime_test1 (
      content, created_at, updated_at, version
  )
  SELECT FLOOR(1 + (RAND() * 99999999)), NOW(), NOW(), 1 FROM test.ime_test1;
  INSERT INTO test.ime_test2 (
      content, created_at, updated_at, version
  )
  SELECT FLOOR(1 + (RAND() * 99999999)), NOW(), NOW(), 1 FROM test.ime_test2;
  EOF
  cat > double.sh <<EOF
  mysql -h ${HOST_TIPROXY1_PRIVATE_IP} --port 6000 -u root < double.sql
  EOF
  chmod +x double.sh
  for c in {1..15}; do
  ./double.sh
  done;
  ```

+ 通过对 `content` 列执行 100 次更新操作，为表 `test.ime_test2` 中的每一行生成 100 个版本。在我们的实验环境中（未优化性能），大约需要 2 分钟。

  ```bash
  cat > update_hog.sql <<EOF
  UPDATE test.ime_test2
  SET content = FLOOR(1 + (RAND() * 99999999)), version = version + 1;
  EOF
  cat > update_hog.sh <<EOF
  mysql -h ${HOST_TIPROXY1_PRIVATE_IP} --port 6000 -u root < update_hog.sql
  EOF
  chmod +x update_hog.sh
  for c in {1..100}; do
  ./update_hog.sh
  done;
  ```

+ 再次连接到 TiDB。

  ```bash
  mysql -h ${HOST_TIPROXY1_PRIVATE_IP} --port 6000 -u root
  ```
  ```output
  mysql> 
  ```

+ 运行两个对表 `test.ime_test1` 和 `test.ime_test2` 进行全表扫描的 SQL 语句。

  ```sql
  SELECT COUNT(*) FROM test.ime_test1;
  SELECT COUNT(*) FROM test.ime_test2;
  EXIT
  ```
  ```output
  +----------+
  | COUNT(*) |
  +----------+
  |    32768 |
  +----------+
  1 row in set (0.03 sec)

  +----------+
  | COUNT(*) |
  +----------+
  |    32768 |
  +----------+
  1 row in set (1.09 sec)

  Bye
  ```

+ 转到 [TiDB PD Dashboard](http://${LAB:HOST_PD1_PUBLIC_IP}:2379/dashboard) 并使用用户名 `root` 登录，目前不需要密码。导航到 `SQL Statements` 面板，将显示时间更改为 `Recent 5 min`。

  ![60%](https://lab-static.pingcap.com/quick-demo/ime-off-dashboard-time-1.png)

+ 选择数据库为 `test`。

  ![60%](https://lab-static.pingcap.com/quick-demo/ime-off-dashboard-db-2.png)

+ 将显示的操作设置为 `Select` 和 `Update`。

  ![60%](https://lab-static.pingcap.com/quick-demo/ime-off-dashboard-kind-3.png)

+ 在 Dashboard 的右上角，选择 `Columns` 并勾选菜单项 `Mean Visible Version Per Query` 和 `Mean Meet Versions Per Query`，然后点击 `Query` 按钮。

  ![60%](https://lab-static.pingcap.com/quick-demo/ime-off-dashboard-columns-4.png)

+ 从查询结果中，对于表 `test.ime_test1` 的扫描，`Mean Visible Version Per Query` 等于 `Mean Meet Versions Per Query`，表明没有 MVCC 快照需要处理。
相比之下，对于表 `test.ime_test2` 的扫描，`Mean Meet Versions Per Query` 显著高于 `Mean Visible Version Per Query`，表明在扫描过程中有大量的 MVCC 快照处理。这也从 `Total Latency` 和 `Mean Latency` 列中得以体现，显示对于相同数量的扫描行，执行时间不同，表 `test.ime_test2` 的扫描消耗了大量时间处理 MVCC 快照。

  ![95%](https://lab-static.pingcap.com/quick-demo/ime-off-dashboard-sql-5.png)

+ 点击 Grafana Dashboard 并使用用户名和密码（均为 "admin"）登录。导航到 `Home` > `tidb-demo` > `tidb-demo-TikV-Details` > `In Memory Engine`，向下滚动到关于 `Region Cache Hit`，`Region Cache Hit Rate`，`Region Cache Miss Reason`，`Memory Usage` 和 `Region Count` 的面板。

  ![90%](https://lab-static.pingcap.com/quick-demo/ime-off-grafana-no-data-1.png)

+ 目前，它们都显示 `No data`，因为当前数据库集群中未启用 TiKV MVCC 内存引擎。将 `Absolute time range` 更改为 `Last 5 minutes`。

  ![60%](https://lab-static.pingcap.com/quick-demo/ime-off-grafana-time-range-2.png)

+ 将 `Refresh time` 更改为 `10s`。

  ![60%](https://lab-static.pingcap.com/quick-demo/ime-off-grafana-refresh-time-3.png)

### 步骤 3. 启用 TiKV MVCC 内存引擎

+ 返回到 [终端 1](ssh://monitor1-ssh#1) ，使用 `tiup cluster edit-config` 打开 `vi` 中的 TiDB 集群配置。

  ```bash
  tiup cluster edit-config tidb-demo
  ```

+ 要启用 TiKV MVCC 内存引擎，请为所有三个 `tikv_servers` 设置 `in-memory-engine.capacity` 为 `1GiB`，`in-memory-engine.enable` 为 `true`，以及 `in-memory-engine.mvcc-amplification-threshold` 为 `100`。

+ 将以下部分复制到每个 TiKV 的 `os: linux` 条目下，然后使用 `:wq` 保存并退出。

  ```yaml
    config:
      in-memory-engine.capacity: 1GiB
      in-memory-engine.enable: true
      in-memory-engine.mvcc-amplification-threshold: 50
  ``` 
  ```output
  ...
  tikv_servers:
  - host: 10.90.1.41
    ssh_port: 22
    port: 20160
    status_port: 20180
    deploy_dir: /tidb-deploy/tikv-20160
    data_dir: /tidb-data/tikv-20160
    log_dir: /tidb-deploy/tikv-20160/log
    arch: amd64
    os: linux
    config:
      in-memory-engine.capacity: 1GiB
      in-memory-engine.enable: true
      in-memory-engine.mvcc-amplification-threshold: 50
  - host: 10.90.1.223
    ssh_port: 22
    port: 20160
    status_port: 20180
    deploy_dir: /tidb-deploy/tikv-20160
    data_dir: /tidb-data/tikv-20160
    log_dir: /tidb-deploy/tikv-20160/log
    arch: amd64
    os: linux
    config:
      in-memory-engine.capacity: 1GiB
      in-memory-engine.enable: true
      in-memory-engine.mvcc-amplification-threshold: 50
  - host: 10.90.1.231
    ssh_port: 22
    port: 20160
    status_port: 20180
    deploy_dir: /tidb-deploy/tikv-20160
    data_dir: /tidb-data/tikv-20160
    log_dir: /tidb-deploy/tikv-20160/log
    arch: amd64
    os: linux
    config:
      in-memory-engine.capacity: 1GiB
      in-memory-engine.enable: true
      in-memory-engine.mvcc-amplification-threshold: 50
  ...
  ```

+ 回答提示 `y`。

  ```output
  Please check change highlight above, do you want to apply the change? [y/N]:(default=N) y
  Applying changes...
  Applied successfully, please use `tiup cluster reload tidb-demo [-N <nodes>] [-R <roles>]` to reload config.
  ```

+ 运行 `tiup cluster reload` 重新加载配置。

  ```bash
  tiup cluster reload tidb-demo --yes
  ```
  ```output
  ...
  Reloaded cluster `tidb-demo` successfully
  ```

+ 配置重新加载后，连接到 TiDB。

  ```bash
  mysql -h ${HOST_TIPROXY1_PRIVATE_IP} --port 6000 -u root
  ```
  ```output
  mysql> 
  ```

+ 验证 TiKV MVCC `in-memory-engine` 已开启。

  ```sql
  SHOW CONFIG WHERE type='tikv' AND name='in-memory-engine.enable';
  EXIT
  ```
  ```output
  +------+-------------------+-------------------------+-------+
  | Type | Instance          | Name                    | Value |
  +------+-------------------+-------------------------+-------+
  | tikv | 10.90.1.41:20160  | in-memory-engine.enable | true  |
  | tikv | 10.90.1.223:20160 | in-memory-engine.enable | true  |
  | tikv | 10.90.1.231:20160 | in-memory-engine.enable | true  |
  +------+-------------------+-------------------------+-------+
  3 rows in set (0.02 sec)

  Bye
  ```

### 步骤 4. 验证 `in-memory-engine` 设置为 `ON` 时的行为

+ 点击 [Grafana Prometheus Dashboard](http://${LAB:HOST_MONITOR1_PUBLIC_IP}:3000) 并使用用户名和密码（均为 "admin"）登录。导航到 `Home` > `tidb-demo` > `tidb-demo-TikV-Details` > `In Memory Engine`，向下滚动到关于 `Region Cache Hit`，`Region Cache Hit Rate`，`Region Cache Miss Reason`，`Memory Usage` 和 `Region Count` 的面板。现在，这些面板不再显示 `No data`，但是 `Region Cache Hit`，`Region Cache Hit Rate`，`Region Cache Miss Reason`，`Memory Usage` 和 `Region Count` 的值非常低，几乎为零，因为目前没有查询 MVCC 快照。

  ![90%](https://lab-static.pingcap.com/quick-demo/ime-on-grafana-no-query-1.png)

+ 返回到[终端 1](ssh://monitor1-ssh#1) ，通过命令行对 `test.ime_test1` 表执行 10,000 次扫描，同时在 [Grafana Prometheus Dashboard](http://${LAB:HOST_MONITOR1_PUBLIC_IP}:3000) 中监控 `Home` > `tidb-demo` > `tidb-demo-TikV-Details` > `In Memory Engine` 面板。

  ```bash
  for i in `seq 10000`;
  do mysql -uroot -h ${HOST_TIPROXY1_PRIVATE_IP} -P 6000 -e "SELECT COUNT(*) FROM test.ime_test1;";
  done;
  ```

+ 在 `test.ime_test1` 表的扫描过程中，可以从 `tidb-demo-TikV-Details` > `In Memory Engine` 面板中观察到 `Region Cache Hit` 和 `Region Cache Hit Rate` 变化不大。（`Region Cache Hit` 表示成功从 Region 缓存中检索数据的次数，而 `Region Cache Hit Rate` 表示 Region 缓存的命中率。）变化不大的原因是 `test.ime_test1` 表本身没有 MVCC 历史快照。

  ![90%](https://lab-static.pingcap.com/quick-demo/ime-on-grafana-ime-test1.png)

+ 在 `test.ime_test1` 表的 10,000 次扫描完成后，返回到[终端 1](ssh://monitor1-ssh#1) ，通过命令行对 `test.ime_test2` 表执行另外 50,000 次扫描，同时再次在 [Grafana Prometheus Dashboard](http://${LAB:HOST_MONITOR1_PUBLIC_IP}:3000) 中监控 `Home` > `tidb-demo` > `tidb-demo-TikV-Details` > `In Memory Engine` 面板。

  ```bash
  for i in `seq 50000`;
  do mysql -uroot -h ${HOST_TIPROXY1_PRIVATE_IP} -P 6000 -e "SELECT COUNT(*) FROM test.ime_test2;";
  done;
  ```

+ 在 `test.ime_test2` 表的扫描过程中，可以从 `tidb-demo-TikV-Details` > `In Memory Engine` 面板中观察到 `Region Cache Hit` 和 `Region Cache Hit Rate` 发生了显著变化。原因是 `test.ime_test2` 表本身包含大量的 MVCC 历史快照。TiKV MVCC 内存引擎将最新写入的 MVCC 版本缓存到内存中，并实现了独立于 TiDB 的 MVCC GC 机制。因此，在扫描过程中，从 TiKV MVCC 内存引擎的缓存中读取了大量数据。

  ![90%](https://lab-static.pingcap.com/quick-demo/ime-on-grafana-ime-test2.png)

+ TiKV MVCC 内存引擎有时可能无法及时缓存数据；在这种情况下，您可以尝试重复上述步骤，再次扫描 `ime_test2` 表并观察监控面板。

## 结论

+ TiProxy 是 TiDB 的网关，充当 TiDB 集群和客户端之间的中介。它提供了各种功能，增强了数据库交互的性能和管理。TiProxy 的核心设计是管理流量，确保 TiDB 集群不同节点之间的负载均衡。TiProxy 的创新之处在于其提供无缝流量管理的能力。请参见 [数据库零停机在线升级与 TiProxy](https://labs.tidb.io/labs/demo_005) 了解更多 TiProxy 的实际应用。

+ TiDB 不仅是一个分布式 SQL 数据库；它还是一个完全成熟的分布式 RDBMS。外键支持的增加巩固了其在 RDBMS 中的 **R**（关系），使其成为现代分布式架构中关系数据管理的全面解决方案。

+ 全局唯一索引通过单个索引覆盖整个分区表的数据，允许主键和唯一键在不包括所有分区键的情况下保持全局唯一性。它提供了额外的灵活性来优化查询。

+ 索引使用统计提供了一种清晰直观的方法来确定哪些索引被积极使用，哪些未被使用，从而为数据库优化和维护提供了更明智和高效的决策。

+ 大规模存储和搜索向量是支持 GenAI 应用程序的关键需求。TiDB 是一个理想的全能存储平台，能够在单一统一平台上无缝托管关系数据、文档数据和向量数据。有几个动手实验可以引导您了解 TiDB 如何支持 GenAI 应用程序，例如：[构建一个基于图的 RAG 应用程序](https://labs.tidb.io/labs/demo_202)，[使用 OpenAI 部署您自己的知识库聊天机器人](https://labs.tidb.io/labs/demo_401)。

+ TiKV MVCC 内存引擎 (IME) 用于加速需要扫描大量 MVCC 历史版本的查询。TiKV MVCC 内存引擎将最新写入的 MVCC 版本缓存到内存中，并实现了独立于 TiDB 的 MVCC GC 机制。这使得它能够快速在内存中执行 GC 操作，减少查询过程中扫描的版本数量，从而降低请求延迟并减少 CPU 开销。如果您的应用程序涉及查询频繁更新或删除的记录，MVCC 内存引擎是您的理想解决方案。