Skip to main content
확장 프로그램은 자체 인덱스 구조를 등록하고 서버가 이를 호출하도록 할 수 있습니다. 서버는 SQL 구문, 메타데이터, 트랜잭션을 소유합니다. 확장 프로그램은 인덱스의 디스크 상 형식과 그 안에서의 검색을 소유합니다. 두 개의 미리보기 기능이 이를 담당합니다. vsql::preview::index_type은 인덱스 구조 자체를 등록합니다. vsql::preview::index_profile은 그 구조를 사용자 정의 유형에 바인딩하고 인덱스가 계산에 사용하는 함수의 이름을 지정합니다. 둘 다 dev ABI 헤더의 <villagesql/preview/index_builder.h>에 있으므로, 이를 사용하는 확장 프로그램은 -DVSQL_USE_DEV_ABI=ON으로 빌드합니다.
현재 지원되는 항목: 사용자 정의 인덱스를 생성, 변경, 삭제하는 DDL, INSERT 시의 인덱스 유지 관리, 그리고 인덱스가 제공하는 최근접 이웃 정렬이라는 하나의 쿼리 형태.지원되지 않는 항목: DELETE, 인덱싱된 열을 변경하는 UPDATE, 그리고 최근접 이웃 검색 이외의 모든 접근 패턴. 열두 개의 필수 훅 중 네 개 — mark_delete, purge, save, restore — 는 빌더가 컴파일되도록 존재해야 하지만, 서버는 이를 호출하지 않습니다.사용자 정의 인덱스를 명시하는 문장은 vsql_allow_preview_extensions가 ON이 아니면 거부됩니다. 이는 미리보기 확장 프로그램을 설치할 때 이미 필요한 것과 동일한 설정입니다.

세 가지 구성 요소

인덱스 유형은 구조입니다. 즉, 항목을 저장하고, 삽입하고, 스캔하는 방식입니다. 이는 확장 프로그램당 구조당 한 번, 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 — 을 거리 지표마다 하나씩 등록합니다.

인덱스 유형 등록

make_index_type<Name, Context>()는 인덱스 이름과, 확장 프로그램에 필요한 런타임 상태를 담는 인덱스별 컨텍스트 구조체를 받습니다. 열두 개의 훅이 모두 필수이며, 세 그룹으로, 다음 순서로 나뉩니다. 하나라도 누락되면 빌더가 컴파일되지 않으며, 실패한 어설션이 해당 훅의 이름을 알려줍니다. 라이프사이클 훅은 인덱스 저장소를 생성하고, 테이블이 열릴 때 이를 로드하고, 삭제합니다:
DML 훅은 행이 변경될 때 인덱스를 유지 관리합니다. 서버는 insert를 호출합니다. mark_delete와 purge는 빌더가 컴파일되도록 다음 서명으로 존재해야 합니다. 스텁으로 작성하세요:
스캔 훅은 쿼리에 응답합니다. 서버는 begin, position, fetch, end를 호출하며, position에는 Index::CursorOp::Next만 전달합니다. save와 restore는 위의 두 DML 훅과 마찬가지로 존재해야 하며 스텁입니다:
실패할 수 있는 모든 훅은 마지막 두 인자로 메시지 버퍼와 그 길이를 받으며, 그곳에 메시지를 기록한 후 실패 시 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()로 접근하세요. 컨텍스트, 사용자 객체, 아레나를 절대 삭제하지 마세요.

인덱스가 할 수 있는 작업 선언

.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와 함께 선언하세요.

열 참조 읽기 및 쓰기

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()에 담깁니다:
서버는 인덱스가 HAS_COLUMN_REF를 선언하지 않으면 둘 다 null 함수 포인터로 남겨 두고, C++ SDK는 이를 보호 없이 호출하므로, 선언 없이 둘 중 하나를 호출하면 오류를 반환하는 대신 크래시가 발생합니다.

인덱스 옵션

DDL의 WITH (M = 8, ef_construction = 64)는 인덱스 옵션으로 확장 프로그램에 전달됩니다. 서버가 먼저 모든 키와 값을 소문자로 변환하므로, DDL에 M이라고 작성해도 params[i].key는 m으로 읽힙니다. 기본값과 parse 함수를 갖는 구조체를 선언하고, .options<T, &T::parse>()로 등록하세요:
소문자로 변환된 키와 비교하세요:
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에서 옵션을 커서로 복사해야 합니다.

인덱스 함수

인덱스 함수는 결정적이어야 하며 REAL을 반환해야 합니다. 함수를 빌드하여 static const에 보관하세요. 프로필이 함수에 대한 참조를 저장하므로, 그 주소가 등록보다 오래 유지되어야 합니다:
함수는 VEF_INDEX_PROFILE_FN_MAX_ARGS 값인 최대 여덟 개의 매개변수를 선언할 수 있습니다. 서버는 반환 유형과 인자 개수를 첫 사용 시점이 아니라 INSTALL EXTENSION에서 확인합니다.

인덱스 프로필

프로필은 사용자 정의 유형, 인덱스 유형, 그리고 id가 부여된 함수들의 이름을 지정합니다. for_type()은 유형 이름 문자열 — make_type에 전달한 것과 동일한 상수 — 을 받으며, 유형 디스크립터를 받지 않습니다. with_function()과 with_helper()는 이름이 아니라 빌드된 인덱스 함수를 받습니다:
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()은 바인딩된 헬퍼를 호출하는 대신 그 등록된 이름을 알려주므로, 내부 루프가 호출마다 디스패치하는 대신 자체 네이티브 구현을 한 번만 해석할 수 있습니다:

종합하기

훅 본문, 옵션 구조체, 컨텍스트를 생략한 뼈대입니다:
두 기능 객체는 모두 static이어야 하며 — C++ SDK가 그 내부를 가리키는 포인터를 보관합니다 — 각각 정확히 한 번 .with()에 전달되어야 합니다. 하나의 기능 객체가 여러 구조를 담을 수 있습니다. 구조마다 .index_type() 또는 .index_profile()을 연결하세요.

SQL 구문

사용자 정의 인덱스는 USING EXTENDED로 선택하며, 열은 사용할 프로필을 명시할 수 있습니다:
인덱싱되는 열은 설치된 어떤 확장 프로그램이 등록한 사용자 정의 유형이어야 하며, 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으로 설정해야 합니다. 그렇지 않으면 문장이 거부됩니다:
vsql_vector를 예로 들면, vsql-vector 저장소에서 빌드하고, vsql_vector.veb를 veb_dir에 복사하고, 설정을 켠 다음, 먼저 확장 프로그램을 설치하세요:
인덱스 삭제는 일반적인 방식과 같습니다. DROP INDEX, ALTER TABLE ... DROP INDEX, 또는 테이블과 함께 인덱스 메타데이터를 제거하는 DROP TABLE을 사용합니다. 사용자 정의 인덱스와 그것이 담고 있는 행은 서버 재시작 후에도 유지됩니다.

사용자 정의 인덱스를 통한 읽기 및 쓰기

행은 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이 접근 방식의 이름을 보여줍니다:
서버는 인덱스 자체의 적중 항목을 읽는 트랜잭션의 뷰로 필터링하지 않습니다. 각 적중 항목을 행으로 해석하고, 그 뷰가 볼 수 없는 행을 건너뛰고, 다음 후보를 취합니다. 따라서 begin은 정확히 limit개의 항목이 아니라 검색 풀 전체를 넘겨주어야 하며, 그렇지 않으면 동시 쓰기로 인해 쿼리가 K개의 행에 미치지 못하게 됩니다. 인덱싱된 열을 변경하는 UPDATE는 거부됩니다:
기본 키를 변경하는 UPDATE도 동일한 오류를 일으키며, 기존 행에 적중하는 REPLACE나 인덱싱된 열 또는 기본 키에 값을 대입하는 INSERT ... ON DUPLICATE KEY UPDATE도 마찬가지입니다. 인덱싱되지 않은 열에만 값을 대입하는 UPDATE 또는 INSERT ... ON DUPLICATE KEY UPDATE는 성공합니다. 사용자 정의 인덱스가 있는 테이블의 기본 키를 변경하는 ALTER TABLE은 ALGORITHM과 무관하게 거부됩니다:
열을 추가하거나 기본 키 이외의 열을 수정하면 테이블의 기존 사용자 정의 인덱스는 그대로 유지됩니다. ALGORITHM=COPY 또는 ALGORITHM=INPLACE로 테이블 재구축을 강제해도 인덱스는 그대로 유지되며, 재구축된 인덱스는 재시작 후에도 유지됩니다.

서버가 보고하는 내용

SHOW CREATE TABLE은 사용자 정의 인덱스에 바인딩된 프로필, 인덱스 유형, 그리고 모든 WITH 매개변수를 표시합니다:
프로필과 인덱스 유형은 항상 백틱으로 인용된 extension.name 식별자이며, 결코 문자열이 아닙니다. WITH 매개변수의 키와 값은 모두 소문자로 변환되고 키 기준으로 알파벳순 정렬되므로 — 위의 인덱스 옵션에서 설명한 것과 동일한 정규화 — M으로 작성한 키는 m으로 표시됩니다. 프로필은 DDL이 전혀 명시하지 않아 서버가 유형의 기본값으로 해석한 경우에도 항상 표시됩니다. 저장된 메타데이터는 해석된 이름만 기록하기 때문입니다. 이 표시는 왕복이 가능하므로, 캡처한 SHOW CREATE TABLE 문자열을 재생하면 동일하게 표시되는 인덱스가 재현됩니다. INFORMATION_SCHEMA.STATISTICS.INDEX_TYPE과 SHOW INDEX는 스토리지 엔진 자체의 알고리즘 이름 대신 USING EXTENDED(...)에 전달된 인덱스 유형 이름을 대문자로 보고합니다. 이 값에는 확장 프로그램 한정자가 없으므로, 동일한 인덱스 유형 이름을 제공하는 두 확장 프로그램은 이 열에서 구분할 수 없습니다. 같은 테이블의 일반 B-트리 인덱스는 여전히 BTREE를 보고합니다:
DROP INDEX idx_a ON t2 후에는 이 쿼리에서도 그 행이 사라집니다.

메타데이터가 저장되는 위치

서버는 각 사용자 정의 인덱스를 villagesql.custom_indexes에 기록하고, 각 키 열을 — 각각에 바인딩된 프로필과 함께 — villagesql.custom_index_columns에 기록합니다. 둘 다 재시작 후에도 유지됩니다. 둘 다 root를 포함하여 SQL에서 읽을 수 없습니다:
이들은 서버 내부용으로 취급하세요. SHOW CREATE TABLE과 INFORMATION_SCHEMA.STATISTICS가 클라이언트에서 사용자 정의 인덱스를 검사하는 지원되는 방법입니다.