> ## 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`은 그 구조를 사용자 정의
유형에 바인딩하고 인덱스가 계산에 사용하는 함수의 이름을 지정합니다. 둘 다
dev 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를 부여합니다. 하나의 인덱스 유형은 여러
프로필을 지원합니다. `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`에 대입하세요. 서버는 `position`을 호출하기 전에 `fetch`를
호출합니다. `end`에서 커서를 해제하고 `*cursor`를 `nullptr`로 설정하세요. C++
SDK는 커서 상태를 감싸지 않습니다.

`begin`, `position`, `restore`의 `bool *` 인자는 훅이 기록하는 스캔 종료
플래그입니다. 성공 시 서버는 `begin`이 그 플래그를 설정했더라도 `end`를
호출합니다. 실패 시 `begin`은 `*cursor`를 null로 남겨야 하며, `end`는 호출되지
않습니다. `begin`이 받는 `MtrCtx::Ref`는 현재 서버가 구동하는 경로에서는
null입니다.

컨텍스트는 확장 프로그램에 속하지 않습니다. 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()`는
각 인덱스 항목이 인덱싱하는 데이터를 어떻게 다시 참조하는지 선언합니다. 둘 다
필수입니다.

빌더는 다섯 가지 기능 값을 허용하며, 서버는 그중 하나에 대해 동작합니다:
`ORDER BY <distance>(<column>, <reference>) LIMIT K`를 위한 k 최근접 이웃 스캔인
`Index::Support::KNN`입니다. `POINT_LOOKUP`, `RANGE_SCAN`, `REVERSE_SCAN`,
`ORDER_BY`는 컴파일되고 등록되지만, 옵티마이저는 그 경로 중 어느 것도 구성하지
않습니다. 인덱싱된 열에 대한 `=` 조건은 인덱스가 무엇을 선언했든, 예를 들어
`FORCE INDEX` 아래에서 여전히 사용자 정의 인덱스에 대한 `Index lookup`을 계획할 수
있으며, 그 쿼리는 `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()`는 DML 및 스캔 훅이 인덱스에 건네는 종류의 `KeyPartRef`를 키 열의
실제 값으로 해석하며, 그 크기는 `get_max_col_len(key_pos)`로 정합니다.
`get_key_ref()`는 그 반대 방향입니다. 이미 열 저장소에 있는 키 열 값, 예를 들어
`insert()`가 받는 `keys` 중 하나에 대한 참조를 도출합니다. `get_key_data()`는
자체 미니 트랜잭션 아래에서 읽으므로, 확장 프로그램이 페이지 래치를 보유한
동안에는 호출해서는 안 됩니다. 둘 다 값이 속한 0 기반 키 열인 `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`를 선언하지 않으면 둘 다 null 함수 포인터로 남겨
두고, 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`는 null이므로, `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)`를 선언한 프로필을 사용합니다.

이 섹션의 모든 문장은 시작 시 또는 `SET PERSIST`로
`vsql_allow_preview_extensions`를 `ON`으로 설정해야 합니다. 그렇지 않으면 문장이
거부됩니다:

```
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`은 거부됩니다.

사용자 정의 인덱스는 하나의 쿼리 형태를 제공합니다. 유일한 정렬 항으로서
오름차순이고 상수 `<reference>`를 갖는
`ORDER BY <distance>(<column>, <reference>) LIMIT K`입니다. 인덱스는 단일
열이어야 하고, 그 유형은 `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.