> ## Documentation Index
> Fetch the complete documentation index at: https://villagesql.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 自定义索引

> index_type 和 index_profile 预览功能使扩展程序能够提供自己的索引结构，并通过配置文件将其绑定到自定义类型。

扩展程序可以注册自己的索引结构，并让服务器调用它。服务器拥有 SQL 语法、元数据和事务。扩展程序拥有索引的磁盘格式以及在其中的搜索过程。

有两个预览功能承载此能力。`vsql::preview::index_type` 注册索引结构本身。`vsql::preview::index_profile` 将该结构绑定到某个自定义类型，并指定索引进行计算所用的函数。两者都位于开发 ABI 头文件中的 `<villagesql/preview/index_builder.h>`，因此使用它们的扩展程序需要使用 `-DVSQL_USE_DEV_ABI=ON` 构建。

<Warning>
  目前支持：创建、修改和删除自定义索引的 DDL；`INSERT` 时的索引维护；以及一种查询形态，即由索引提供服务的最近邻排序。

  不支持：`DELETE`、修改被索引列的 `UPDATE`，以及最近邻搜索之外的所有访问模式。十二个必需钩子中的四个——`mark_delete`、`purge`、`save` 和 `restore`——必须存在，构建器才能编译，但服务器从不调用它们。

  除非 `vsql_allow_preview_extensions` 为 `ON`，否则指定自定义索引的语句会被拒绝；安装预览扩展程序本来就需要同一设置。
</Warning>

<h2 id="the-three-pieces">
  三个组成部分
</h2>

索引类型是指结构：条目如何存储、插入和扫描。每个扩展程序的每种结构注册一次，使用类似 `hnsw` 的名称。

索引函数是绑定到某个配置文件的确定性函数，例如距离函数。配置文件可绑定两类函数。使用 `with_function()` 绑定的函数是优化器可以为其规划索引扫描的函数。使用 `with_helper()` 绑定的函数仅由索引实现调用。

将函数绑定到配置文件并不会创建 SQL 函数。优化器按名称将 `ORDER BY` 中的调用与配置文件的绑定进行匹配，因此使用 `with_function()` 绑定的函数还必须用 `.func()` 以同一名称注册，否则任何语句都无法指定它，索引也永远不会被选中。辅助函数不需要 `.func()`，从 SQL 调用辅助函数会得到 `ERROR 1305 (42000): FUNCTION <database>.<name> does not exist`。

索引配置文件将自定义类型与索引类型关联起来，并为每个绑定的函数指定一个数字 id，索引通过该 id 调用它。一个索引类型可服务于多个配置文件：`vsql_vector` 注册了单个 `hnsw` 索引类型，并在其上注册了四个配置文件——`hnsw_l1`、`hnsw_l2`、`hnsw_cosine` 和 `hnsw_inner_product`——每种距离度量对应一个。

<h2 id="registering-an-index-type">
  注册索引类型
</h2>

`make_index_type<Name, Context>()` 接受索引名称和一个按索引划分的上下文结构体，该结构体保存扩展程序所需的任何运行时状态。全部十二个钩子都是必需的，分为三组，并按此顺序排列。如果缺少某个钩子，构建器将无法编译——失败的断言会指出该钩子的名称。

生命周期钩子创建索引存储、在打开表时加载索引，以及删除索引：

```cpp theme={null}
bool create(Ctx *, const Index &, Space::Ref, Segment::TrxRef, char *, uint32_t);
bool load(Ctx *, const Index &, Index::StorageRef, char *, uint32_t);
bool drop(Ctx *, const Index &, Segment::TrxRef, char *, uint32_t);
```

DML 钩子在行发生变化时维护索引。服务器会调用 `insert`。`mark_delete` 和 `purge` 必须以这些签名存在，构建器才能编译；请为它们提供桩实现：

```cpp theme={null}
bool insert(Ctx *, const Index &, Segment::TrxRef,
            IndexScanKey::KeyPartData *keys, IndexScanKey::KeyPartData *pkeys,
            IndexScanKey::KeyPartRef *key_ref, char *, uint32_t);
bool mark_delete(Ctx *, const Index &, Segment::TrxRef,
                 IndexScanKey::KeyPartRef *key_ref,
                 IndexScanKey::KeyPartData *keys,
                 IndexScanKey::KeyPartData *pkeys, bool delete_mark,
                 char *, uint32_t);
bool purge(Ctx *, const Index &, Segment::TrxRef,
           IndexScanKey::KeyPartRef *key_ref, IndexScanKey::KeyPartData *keys,
           IndexScanKey::KeyPartData *pkeys, char *, uint32_t);
```

扫描钩子负责响应查询。服务器会调用 `begin`、`position`、`fetch` 和 `end`，并且只向 `position` 传递 `Index::CursorOp::Next`。`save` 和 `restore` 必须存在且是桩实现，与上面的两个 DML 钩子一样：

```cpp theme={null}
bool begin(Ctx *, const Index &, MtrCtx::Ref, const IndexScanDesc &,
           Index::Cursor *, bool *, char *, uint32_t);
bool position(Index::Cursor, Index::CursorOp, bool *, char *, uint32_t);
bool fetch(Index::Cursor, IndexScanKey::KeyPartRef *,
           IndexScanKey::KeyPartData *keys, IndexScanKey::KeyPartData *pkeys,
           char *, uint32_t);
bool save(Index::Cursor, char *, uint32_t);
bool restore(Index::Cursor, MtrCtx::Ref, bool *, char *, uint32_t);
void end(Index::Cursor *);
```

每个可能失败的钩子都以消息缓冲区及其长度作为最后两个参数，并在将消息写入其中后返回 `true` 表示失败。

游标归扩展程序所有。请在 `begin` 中分配游标并将其赋给 `*cursor`，并让它已定位到第一条记录——服务器会先调用 `fetch`，再调用 `position`。在 `end` 中释放游标并将 `*cursor` 设置为 `nullptr`。C++ SDK 不封装游标状态。

`begin`、`position` 和 `restore` 的 `bool *` 参数是由钩子写入的扫描结束标志。成功时，即使 `begin` 设置了该标志，服务器也会调用 `end`；失败时，`begin` 必须让 `*cursor` 保持为空，并且不会调用 `end`。在服务器目前驱动的路径上，`begin` 收到的 `MtrCtx::Ref` 为空。

上下文不归扩展程序所有。C++ SDK 会在调用 `create` 或 `load` 之前，从 InnoDB 内存池中分配 `Index::StorageCtx<T>` 和您的 `T`，并在 `drop` 返回后销毁该内存池——这会运行 `~T`。请通过 `ctx->user()` 访问您自己的状态，通过 `ctx->arena()` 访问该内存池。切勿删除上下文、用户对象或内存池。

<h3 id="declaring-what-the-index-can-do">
  声明索引的能力
</h3>

`.capabilities()` 声明索引支持哪些访问模式，`.storage_props()` 声明每个索引条目如何引用它所索引的数据。两者都是必需的。

构建器接受五个能力值，而服务器只对其中一个采取行动：`Index::Support::KNN`，即用于 `ORDER BY <distance>(<column>, <reference>) LIMIT K` 的 k 最近邻扫描。`POINT_LOOKUP`、`RANGE_SCAN`、`REVERSE_SCAN` 和 `ORDER_BY` 可以编译和注册，但优化器不会构建其中任何一种路径。无论索引声明了什么，被索引列上的 `=` 条件仍然可能在自定义索引上规划出 `Index lookup`（例如在 `FORCE INDEX` 下），而该查询会以 `ERROR 1031 (HY000): Table storage engine for '<table>' doesn't have this option` 失败。

为最近邻扫描提供服务的存储声明是：在扩展程序自行存储的列上的单列索引中声明 `Index::Storage::HAS_COLUMN_REF | Index::Storage::HAS_ROW_REF`。`HAS_COLUMN_REF` 表示每个条目存储服务器提供的稳定列引用，而不是完整的主键，正是它使 `get_key_data()` 和 `get_key_ref()` 可用。`REF_LOOKUP` 表示索引可以根据该引用重新定位某个条目，并且它带来一项义务：每个条目都必须具有唯一的引用令牌。

`Index::Storage::HAS_ROW_REF` 表示每个条目存储它所索引的行的主键：服务器在 `pkeys` 数组中将其传递给 `insert()`，`fetch()` 必须在 `pkeys` 中返回它，服务器通过主键查找来解析每个命中结果。未声明它的索引永远不会被选中用于最近邻扫描。请将它与 `HAS_COLUMN_REF` 一起声明。

<h3 id="reading-and-writing-column-references">
  读取和写入列引用
</h3>

`get_key_data()` 将 `KeyPartRef`——DML 钩子和扫描钩子交给索引的那种引用——解析为键列的实际值，其大小由 `get_max_col_len(key_pos)` 决定。`get_key_ref()` 则反其道而行：它为已经位于列存储中的键列值（例如 `insert()` 收到的 `keys` 之一）派生引用。`get_key_data()` 在自己的迷你事务下读取，在扩展程序持有页面闩锁期间不得调用它。两者都接受 `key_pos`，即该值所属的从零开始计数的键列，并在失败时返回 `true`，消息位于 `index.get_error()` 中：

```cpp theme={null}
std::vector<unsigned char> buf(index.get_max_col_len(key_pos));
IndexScanKey::KeyPartData key_data{buf.data(), static_cast<uint32_t>(buf.size())};
// The hooks receive key_ref as a pointer; get_key_data() takes it by value.
if (index.get_key_data(key_pos, *key_ref, &key_data)) return true;  // index.get_error()
// key_data holds the value.

IndexScanKey::KeyPartRef derived_ref;
if (index.get_key_ref(key_pos, keys[key_pos], &derived_ref)) return true;
// derived_ref holds the reference.
```

除非索引声明了 `HAS_COLUMN_REF`，否则服务器会将两者都保留为空函数指针，而 C++ SDK 在调用它们时不做防护，因此在未声明的情况下调用任一函数会导致崩溃，而不是返回错误。

<h3 id="index-options">
  索引选项
</h3>

DDL 中的 `WITH (M = 8, ef_construction = 64)` 会作为索引选项传递给扩展程序。服务器会先将每个键和值转为小写，因此即使 DDL 写的是 `M`，`params[i].key` 读取到的也是 `m`。请声明一个带默认值和 `parse` 函数的结构体，并使用 `.options<T, &T::parse>()` 注册它：

```cpp theme={null}
struct HNSWOptions {
  int M = 16;
  int ef_construction = 200;
  static bool parse(const Index::Parameter *params, uint32_t count,
                    HNSWOptions *out, char *error_msg,
                    uint32_t error_msg_len);
};
```

请与转为小写后的键进行比较：

```cpp theme={null}
if (strcmp(params[i].key, "m") == 0) out->M = atoi(params[i].value);
```

当 DDL 不带 `WITH` 子句时，`params` 为空，因此应先检查 `count` 的值。返回失败的 `parse` 会中止 DDL，而它写入的消息会进入服务器错误日志——客户端只会看到 `ERROR 1030 (HY000): Got error 168 - 'Unknown (generic) error from engine' from storage engine`，因此请在消息中指明索引和出问题的键。

`index.options<HNSWOptions>()` 返回指向已解析结构体的指针。每个接收 `Index` 的钩子都可以访问它——生命周期钩子、DML 钩子和 `begin`。其余的扫描钩子只接收游标，因此在扫描过程中需要使用这些选项的索引类型必须在 `begin` 中将它们复制到游标中。

<h2 id="index-functions">
  索引函数
</h2>

索引函数必须是确定性的，并且必须返回 `REAL`。请构建一个索引函数并将其保存在 `static const` 中——配置文件存储的是对它的引用，因此其地址必须比注册过程存活得更久：

```cpp theme={null}
static const auto MY_INDEX_FN =
    make_index_function<&my_index_fn>("my_index_fn")
        .returns(vsql::REAL)
        .param(MY_TYPE)
        .param(MY_TYPE)
        .deterministic()
        .build();
```

一个函数最多可声明八个参数，即 `VEF_INDEX_PROFILE_FN_MAX_ARGS` 的值。服务器会在 `INSTALL EXTENSION` 时检查返回类型和参数个数，而不是在首次使用时检查。

<h2 id="index-profiles">
  索引配置文件
</h2>

配置文件指定自定义类型、索引类型，以及带有各自 id 的函数。`for_type()` 接受类型的*名称字符串*——即传给 `make_type` 的同一个常量——而不是类型描述符。`with_function()` 和 `with_helper()` 接受已构建的索引函数，而不是名称：

```cpp theme={null}
static const auto MY_PROFILE =
    make_index_profile("my_profile")
        .for_type(kMyType)
        .using_index(kMyIndex)
        .with_function(1, MY_DISTANCE_FN)
        .with_helper(1, MY_HELPER_FN)
        .ordering(Index::Ordering::ASC)
        .default_for_type(true)
        .build();
```

id 在每个列表内必须唯一，且这两个列表独立编号——`with_function(1, ...)` 和 `with_helper(1, ...)` 不会冲突。同一个列表内重复的 id 会在 `INSTALL EXTENSION` 时被拒绝。

`.default_for_type(true)` 将此配置文件标记为语句未指定配置文件时所使用的配置文件。它按类型和索引类型的组合生效，默认值为 false。`vsql_vector` 标记了 `hnsw_l2`，这就是为什么创建索引时未指定配置文件会记录为 `hnsw_l2`。

`.ordering()` 记录配置文件支持的扫描方向，取值为 `NONE`、`ASC`、`DESC` 或 `ASC | DESC`。服务器会存储该值，但没有任何代码读取它。无论配置文件声明什么，都只会规划升序排序：仅声明 `DESC` 的配置文件仍然为升序查询提供服务，而降序的 `ORDER BY` 会以表扫描方式运行。

钩子通过 `index.profile()` 调用绑定的函数，或通过 `index.helper()` 调用绑定的辅助函数。两者都接受键列位置、配置文件赋予该函数的 id、指向结果的指针，以及以 `vef_storage_col_data_t` 参数形式给出的列值。

`index.helper_fn_name()` 报告绑定的辅助函数的注册名称，而不是调用它，这使得内层循环可以一次性解析出自己的原生实现，而不必每次调用都进行分派：

```cpp theme={null}
std::array<char, 64> name{};
if (index.helper_fn_name(key_pos, fn_id, name.data(), name.size())) {
  // on failure, index.get_error()
}
```

<h2 id="putting-it-together">
  完整示例
</h2>

一个骨架，省略了钩子主体、选项结构体和上下文：

```cpp theme={null}
#include <villagesql/preview/index_builder.h>
#include <villagesql/vsql.h>

using namespace vsql::preview_index_builder;
using vsql::preview_storage::MtrCtx;
using vsql::preview_storage::Segment;
using vsql::preview_storage::Space;

static constexpr const char kMyType[] = "my_type";
static constexpr const char kMyIndex[] = "my_index";

constexpr auto MY_TYPE = vsql::make_type<kMyType>()/* ... */.build();

static constexpr auto MY_INDEX =
    make_index_type<kMyIndex, MyContext>()
        .lifecycle()
            .create<&my_create>()
            .load<&my_load>()
            .drop<&my_drop>()
        .dml()
            .insert<&my_insert>()
            .mark_delete<&my_mark_delete>()
            .purge<&my_purge>()
        .scan()
            .begin<&my_begin_scan>()
            .position<&my_position>()
            .fetch<&my_fetch>()
            .save<&my_save>()
            .restore<&my_restore>()
            .end<&my_end_scan>()
        .global()
            .capabilities(Index::Support::KNN)
            .storage_props(Index::Storage::HAS_COLUMN_REF |
                           Index::Storage::HAS_ROW_REF)
            .options<MyOptions, &MyOptions::parse>()
            .build();

static const auto MY_PROFILE =
    make_index_profile("my_profile")
        .for_type(kMyType)
        .using_index(kMyIndex)
        .with_function(1, MY_INDEX_FN)
        .ordering(Index::Ordering::ASC)
        .default_for_type(true)
        .build();

static auto INDEX_TYPE = IndexTypeCapability().index_type(MY_INDEX);
static auto INDEX_PROFILE = IndexProfileCapability().index_profile(MY_PROFILE);

VEF_GENERATE_ENTRY_POINTS(
    vsql::make_extension()
        .with(INDEX_TYPE)
        .with(INDEX_PROFILE)
        .type(MY_TYPE)
        // The bound function needs a SQL function of the same name, or no
        // statement can name it and the index is never chosen.
        .func(vsql::make_func<&my_index_fn>("my_index_fn")
                  .returns(vsql::REAL)
                  .param(MY_TYPE)
                  .param(MY_TYPE)
                  .deterministic()
                  .build()))
```

两个功能对象都必须是 `static`——C++ SDK 会持有指向它们的指针——并且每个对象都必须精确地传递给 `.with()` 一次。一个功能对象可以承载多个结构：为每个结构链式调用 `.index_type()` 或 `.index_profile()`。

<h2 id="the-sql-syntax">
  SQL 语法
</h2>

自定义索引通过 `USING EXTENDED` 选择，并且列可以指定要使用的配置文件：

```
INDEX <name> (<column> [[<extension>.]<profile>] [ASC | DESC])
  USING EXTENDED([<extension>.]<index_type>)
  [WITH (<option> = <value>[, ...])]
```

被索引列必须是某个已安装扩展程序注册的自定义类型，并且当 DDL 指定了配置文件时，该配置文件必须是为该列的类型声明的。普通列会被拒绝，错误为 `ERROR 3219 (HY000): No default index profile found: column '<name>' is not a custom type`。分区表会被拒绝，错误为 `ERROR 3219 (HY000): InnoDB: Custom index is not supported on partitioned tables`。

该子句可以内联用于 `CREATE TABLE`，也可以用于 `CREATE INDEX` 和 `ALTER TABLE ADD INDEX`。配置文件和索引类型都可以用扩展程序名称限定，您可以通过这种方式区分提供相同名称的两个扩展程序。如果未指定配置文件，服务器将使用针对该类型和索引类型声明了 `default_for_type(true)` 的配置文件。

本节中的每条语句都需要将 `vsql_allow_preview_extensions` 设置为 `ON`，可在启动时设置或使用 `SET PERSIST` 设置。没有它，语句会被拒绝：

```
ERROR 3219 (HY000): Extended Index feature not yet implemented
```

以 `vsql_vector` 为例，请先从 `vsql-vector` 仓库构建它，将 `vsql_vector.veb` 复制到 `veb_dir` 中，打开该设置，然后安装该扩展程序：

```sql theme={null}
SET PERSIST vsql_allow_preview_extensions = ON;
INSTALL EXTENSION vsql_vector;

CREATE TABLE t2 (
  id INT PRIMARY KEY,
  a SVECTOR(4) NOT NULL,
  b SVECTOR(8) NOT NULL,
  INDEX idx_a (a) USING EXTENDED(hnsw),
  INDEX idx_b (b hnsw_cosine) USING EXTENDED(hnsw) WITH (M = 8, ef_construction = 64)
) ENGINE=InnoDB;

CREATE INDEX idx_c ON t2 (a hnsw_l1) USING EXTENDED(hnsw);

ALTER TABLE t2 ADD INDEX idx_d (b vsql_vector.hnsw_inner_product)
  USING EXTENDED(vsql_vector.hnsw);
```

删除自定义索引与普通索引相同：使用 `DROP INDEX`、`ALTER TABLE ... DROP INDEX`，或者 `DROP TABLE`（会随表一起移除索引元数据）。自定义索引及其所保存的行在服务器重启后仍然存在。

<h2 id="reading-and-writing-through-a-custom-index">
  通过自定义索引读取和写入
</h2>

行通过 `INSERT` 写入，索引也随之得到维护。在已经包含行的表上创建索引同样可行，并且会基于这些行构建索引。该构建会阻塞对表的写入，而不是作为在线 DDL 运行；读取则继续进行。使用 `ALGORITHM=INPLACE`、`ALGORITHM=COPY` 或不带子句时，自定义索引都走离线构建路径，因为目前还没有任何机制能把并发写入重放到自定义索引中。`ALGORITHM=INSTANT` 和 `LOCK=NONE` 会被拒绝。

自定义索引只为一种查询形态提供服务：以 `ORDER BY <distance>(<column>, <reference>) LIMIT K` 作为唯一的排序项、升序，且 `<reference>` 为常量。索引必须是单列索引，其类型必须声明 `Index::Support::KNN` 和 `Index::Storage::HAS_ROW_REF`，并且其绑定的配置文件必须提供该距离函数。列和参考值可以按任一顺序给出。不满足其中任何一个条件的查询会以表扫描加排序的方式运行，且没有警告。`EXPLAIN` 会指明访问方式：

```sql theme={null}
CREATE TABLE docs (
  id INT PRIMARY KEY,
  vec SVECTOR(4) NOT NULL,
  payload VARCHAR(32),
  INDEX idx_vec (vec hnsw_l2) USING EXTENDED(hnsw)
) ENGINE=InnoDB;

INSERT INTO docs VALUES (1,'[1.0,2.0,3.0,4.0]','one'),
                        (2,'[5.0,6.0,7.0,8.0]','two'),
                        (3,'[9.0,1.0,2.0,3.0]','three');

EXPLAIN FORMAT=TREE
  SELECT id FROM docs ORDER BY L2_DISTANCE(vec,'[1.0,2.0,3.0,4.0]') LIMIT 2;
```

```
-> Limit: 2 row(s)  (cost=0.55 rows=2)
    -> Custom index distance scan on idx_vec  (cost=0.55 rows=3)
```

服务器不会按读取事务的视图过滤索引自身的命中结果。它将每个命中解析为对应的行，跳过该视图看不到的行，然后取下一个候选——因此 `begin` 应当返回其完整的搜索池，而不是恰好 `limit` 个条目，否则并发写入会使查询返回的行少于 `K` 行。

修改被索引列的 `UPDATE` 会被拒绝：

```sql theme={null}
UPDATE docs SET vec = '[0.0,0.0,0.0,0.0]' WHERE id = 1;
```

```
ERROR 3219 (HY000): UPDATE that changes an indexed column on a table with a custom index (USING EXTENDED) is not supported yet.
```

修改主键的 `UPDATE` 会得到同样的错误；命中已有行的 `REPLACE`，以及为被索引列或主键赋值的 `INSERT ... ON DUPLICATE KEY UPDATE` 也是如此。只为未被索引的列赋值的 `UPDATE` 或 `INSERT ... ON DUPLICATE KEY UPDATE` 会成功。

对带有自定义索引的表修改主键的 `ALTER TABLE` 会被拒绝，无论 `ALGORITHM` 为何：

```sql theme={null}
ALTER TABLE docs MODIFY id BIGINT NOT NULL;
```

```
ERROR 3219 (HY000): VillageSQL: Changing the primary key of a table with a USING EXTENDED index is not supported
```

添加列，或修改主键以外的列，会使表现有的自定义索引保持原样。使用 `ALGORITHM=COPY` 或 `ALGORITHM=INPLACE` 强制重建表同样会保留该索引，并且重建后的索引在重启后仍然存在。

<h2 id="what-the-server-reports">
  服务器报告的内容
</h2>

`SHOW CREATE TABLE` 会呈现自定义索引绑定的配置文件、索引类型以及任何 `WITH` 参数：

```
  KEY `idx_b` (`b` `vsql_vector`.`hnsw_cosine`) USING EXTENDED(`vsql_vector`.`hnsw`) WITH (`ef_construction` = 64, `m` = 8)
```

配置文件和索引类型始终是以反引号引起的 `extension.name` 标识符，而绝不是字符串。`WITH` 参数的键和值都会转为小写，并按键的字母顺序排序——与上文“索引选项”中描述的规范化相同——因此写作 `M` 的键会呈现为 `m`。配置文件始终会显示，即使 DDL 从未指定过配置文件而是由服务器将其解析为该类型的默认值：持久化的元数据只记录解析后的名称。该呈现可以往返还原，因此重放捕获到的 `SHOW CREATE TABLE` 字符串会重现一个呈现完全相同的索引。

`INFORMATION_SCHEMA.STATISTICS.INDEX_TYPE` 和 `SHOW INDEX` 报告传递给 `USING EXTENDED(...)` 的索引类型名称（转为大写），以取代存储引擎自身的算法名称。该值不带扩展程序限定符，因此提供相同索引类型名称的两个扩展程序在此列中无法区分。同一个表上的普通 B 树索引仍然报告 `BTREE`：

```sql theme={null}
SELECT INDEX_NAME, INDEX_TYPE
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='t2'
ORDER BY INDEX_NAME;
```

```
INDEX_NAME	INDEX_TYPE
idx_a	HNSW
idx_b	HNSW
idx_c	HNSW
idx_d	HNSW
PRIMARY	BTREE
```

在 `DROP INDEX idx_a ON t2` 之后，它对应的行也会从此查询中消失。

<h2 id="where-the-metadata-lives">
  元数据的存放位置
</h2>

服务器将每个自定义索引记录在 `villagesql.custom_indexes` 中，并将其键列——以及绑定到每个键列的配置文件——记录在 `villagesql.custom_index_columns` 中。两者在重启后均保持有效。

即使以 `root` 身份，也无法从 SQL 读取这两个表：

```sql theme={null}
SELECT * FROM villagesql.custom_indexes;
```

```
ERROR 3554 (HY000): Access to table 'villagesql.custom_indexes' is rejected.
```

请将它们视为服务器内部内容。`SHOW CREATE TABLE` 和 `INFORMATION_SCHEMA.STATISTICS` 是从客户端检查自定义索引的受支持方式。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.