Власні типи індексів в СКБД Caché


В об'єктної і реляційної моделі даних СКБД Caché є три типи індексів — звичайні, bitmap і bitslice. Якщо з якихось причин цих індексів не вистачає, починаючи з версії 2013.1 програміст може визначити свій тип індексів і використовувати його в будь-яких класах.

Подробиці під катом (якщо вас не лякають слова типу метод-генератор).

«Свій тип індексів» — це клас, що реалізує методи інтерфейсу %Library.FunctionalIndex для вставки / видалення / зміни значень в індексі. Цей клас можна вказувати як тип індексу у визначенні індексу.

Наприклад:

Property A As %String;

Property B As %String;

Index someind On (A,B) As CustomPackage.CustomIndex;

Клас CustomPackage.CustomIndex як раз і є реалізація свого типи індексів.

В якості прикладу розглянемо невеликий прототип індексу-квадродерева для просторових даних, створений на хакатоне командою у складі Андрія ARechitsky Речитского, Олександра Погребникова і автора цих рядків. Хакатон проходив в рамках щорічної школи розробників InterSystems (окреме спасибі натхненнику хакатона tsafin). Матеріали школи, до речі, доступні на нашому сайті.

У даній статті ми не будемо торкатися того, що таке квадродерево і як з ним працювати.

Зупинимося на створенні класу, що реалізує інтерфейс %Library.FunctionalIndex для реалізації наявної квадродерева. Їй в нашій хакатонной команді займався Андрій. Андрій створив клас SpatialIndex.Indexer, який умів два методу — Insert(x, y, id) Delete(x, y, id). При створенні об'єкта класу SpatialIndex.Indexer потрібно було вказати вузол глобал, в подузлы якого писався індекс. Мені залишалося створити клас SpatialIndex.Index, що реалізує методи InsertIndex, UpdateIndex, DeleteIndex PurgeIndex. Перші три з цих методів беруть на вході Id змінною рядки та індексовані значення у тому ж порядку, як і у визначенні індексу в класі, де цей індекс використовується. У нашому прикладі, pArg(1) A, pArg(2) B.

Class SpatialIndex.Index Extends %Library.FunctionalIndex [ System = 3 ]
{

ClassMethod InsertIndex(pID As %CacheString, pArg... As %Binary) [ CodeMode = generator, ServerOnly = 1 ]
{
if %mode'="method" { //'
set IndexGlobal = ..IndexLocation(%class,%property)
$$$GENERATE($C(9)_"set indexer = ##class(SpatialIndex.Indexer).%New($Name("_IndexGlobal_"))")
$$$GENERATE($C(9)_"do indexer.Insert(pArg(1),pArg(2),pID)")
}
}

ClassMethod UpdateIndex(pID As %CacheString, pArg... As %Binary) [ CodeMode = generator, ServerOnly = 1 ]
{
if %mode'="method" { //'
set IndexGlobal = ..IndexLocation(%class,%property)
$$$GENERATE($C(9)_"set indexer = ##class(SpatialIndex.Indexer).%New($Name("_IndexGlobal_"))")
$$$GENERATE($C(9)_"do indexer.Delete(pArg(3),pArg(4),pID)")
$$$GENERATE($C(9)_"do indexer.Insert(pArg(1),pArg(2),pID)")
}
}

ClassMethod DeleteIndex(pID As %CacheString, pArg... As %Binary) [ CodeMode = generator, ServerOnly = 1 ]
{
if %mode'="method" { //'
set IndexGlobal = ..IndexLocation(%class,%property)
$$$GENERATE($C(9)_"set indexer = ##class(SpatialIndex.Indexer).%New($Name("_IndexGlobal_"))")
$$$GENERATE($C(9)_"do indexer.Delete(pArg(1),pArg(2),pID)")
}
}

ClassMethod PurgeIndex() [ CodeMode = generator, ServerOnly = 1 ]
{
if %mode'="method" { //'
set IndexGlobal = ..IndexLocation(%class,%property)
$$$GENERATE($C(9)_"kill " _ IndexGlobal)
}
}

ClassMethod IndexLocation(className As %String, indexName As %String) As %String
{
set storage = ##class(%Dictionary.ClassDefinition).%OpenId(className).Storages.GetAt(1).IndexLocation
quit $Name(@storage@(indexName))
}

}

Метод IndexLocation — допоміжний, по імені класу та індексу він повертає ім'я вузла глобал, у якому треба зберігати значення індексу.

Розглянемо тестовий клас з індексом типу SpatialIndex.Index:

Class SpatialIndex.Test Extends %Persistent
{
Property Name As %String(MAXLEN = 300);

Property Latitude As %String;

Property Longitude As %String;

Index coord On (Latitude, Longitude) As SpatialIndex.Index;
}

При компіляції класу SpatialIndex.Test для кожного індексу типу SpatialIndex.Index INT-коді генеруються методи:

zcoordInsertIndex(pID,pArg...) public {
set indexer = ##class(SpatialIndex.Indexer).%New($Name(^SpatialIndex.TestI("coord")))
do indexer.Insert(pArg(1),pArg(2),pID) }
zcoordPurgeIndex() public {
kill ^SpatialIndex.TestI("coord") }
zcoordSegmentInsert(pIndexBuffer,pID,pArg...) public {
do ..coordInsertIndex(pID, pArg...) }
zcoordUpdateIndex(pID,pArg...) public {
set indexer = ##class(SpatialIndex.Indexer).%New($Name(^SpatialIndex.TestI("coord")))
do indexer.Delete(pArg(3),pArg(4),pID)
do indexer.Insert(pArg(1),pArg(2),pID)
}

А методи %SaveData, %DeleteData, %SQLInsert, %SQLUpdate, %SQLDelete викликають методи індексу. Наприклад, %SaveData:

if insert {
// ...
do ..coordInsertIndex(id,i%Latitude,i%Longitude,"")
// ...
} else {
// ...
do ..coordUpdateIndex(id,i%Latitude,i%Longitude,zzc27v3,zzc27v2,"")
// ...
}

Веселіше дивитися на працюючий приклад — завантажте файли з репозиторію https://github.com/intersystems-ru/spatialindex/tree/no-web-interface. Це посилання на гілку без веб-інтерфейсу. Імпортуйте самі класи, розпакуйте RuCut.zip і завантажте дані:

do $system.OBJ.LoadDir("c:\temp\spatialindex","ck")
do ##class(SpatialIndex.Test).load("c:\temp\rucut.txt")

У файлі rucut.txt зберігаються дані про 100'000 населених пунктах Росії — назву та координати. Метод load читає кожну рядок з файлу і зберігає як об'єкт класу SpatialIndex.Test. Після його виконання в глобале ^SpatialIndex.TestI(«coord») буде зберігається квадродерево з координатами Latitude Longitude.

А тепер запити
Побудувати індекс — півсправи. Найцікавіше, коли запити можуть цей індекс використовувати. Для індексів нестандартних типів є стандартний синтаксис їх використання, який виглядає приблизно так:

SELECT *
FROM SpatialIndex.Test
WHERE %ID %FIND search_index(coord, 'window', 'minx=56,miny=56,maxx=57,maxy=57')

Тут %ID %FIND search_index — фіксована частина. Далі йде ім'я індексу, зверніть увагу, без лапок. Всі інші параметри ('window', 'minx=56,miny=56,maxx=57,maxy=57) передаються у метод Find, який теж потрібно визначити в класі, що описує тип індексу (в нашому випадку — SpatialIndex.Index):

ClassMethod Find(queryType As %Binary, queryParams As %String) As %Library.Binary [ CodeMode = generator, ServerOnly = 1, SqlProc ]
{
if %mode'="method" { //'
set IndexGlobal = ..IndexLocation(%class,%property)
set IndexGlobalQ = $$$QUOTE(IndexGlobal)
$$$GENERATE($C(9)_"result set = ##class(SpatialIndex.SQLResult).%New()")
$$$GENERATE($C(9)_"do result.PrepareFind($Name("_IndexGlobal_"), queryType, queryParams)")
$$$GENERATE($C(9)_"quit result")
}
}

Тут два параметра queryType queryParams, але це зовсім не обов'язково, їх може бути більше або менше.

Метод Find при компіляції класу, в якому використовується індекс SpatialIndex.Index, генерує допоміжний метод z<IndexName>Find, який викликається при виконанні SQL запитів:

zcoordFind(queryType,queryParams) public { Set:'$isobject($get(%sqlcontext)) %sqlcontext=##class(%Library.ProcedureContext).%New()
result set = ##class(SpatialIndex.SQLResult).%New()
do result.PrepareFind($Name(^SpatialIndex.TestI("coord")), queryType, queryParams)
quit result }

Метод Find повинен повертати екземпляр класу, що реалізує інтерфейс %SQL.AbstractFind. Методи цього інтерфейсу NextChunk, PreviousChunk повертають бітові рядки шматками по 64000 біт. Якщо запис з номером ID задовольняє умовам вибірки, то відповідний біт (номер_куска * 64000 + номер_позиции_внутри_куска) встановлений в 1.

Class SpatialIndex.SQLResult Extends %SQL.AbstractFind
{

Property ResultBits [ MultiDimensional, Private ];

Method %OnNew() As %Status [ Private, ServerOnly = 1 ]
{
kill i%ResultBits
kill qHandle
quit $$$OK
}

Method PrepareFind(indexGlobal As %String, queryType As %String, queryParams As %Binary) As %Status
{
if queryType = "window" {
for i = 1:1:4 {
set item = $Piece(queryParams, ",", i)
set param = $Piece(item, "=", 1)
set value = $Piece(item, "=" ,2)
set arg(param) = value
}
set qHandle("indexGlobal") = indexGlobal
do ##class(SpatialIndex.QueryExecutor).InternalFindWindow(.qHandle,arg("minx"),arg("miny"),arg("maxx"),arg("maxy"))
set id = ""
for {
set id = $O(qHandle("data", id),1,idd)
quit:id=""
set tChunk = (idd\64000)+1, tPos=(idd#64000)+1
set $BIT(i%ResultBits(tChunk),tPos) = 1
}
}
quit $$$OK
}

Method ContainsItem(pItem As %String) As %Boolean
{
set tChunk = (pItem\64000)+1, tPos=(pItem#64000)+1
quit $bit($get(i%ResultBits(tChunk)),tPos)
}

Method GetChunk(pChunk As %Integer) As %Binary
{
quit $get(i%ResultBits(pChunk))
}

Method NextChunk(ByRef pChunk As %Integer = "") As %Binary
{
set pChunk = $order(i%ResultBits(pChunk),1,tBits)
quit:pChunk="" ""
quit tBits
}

Method PreviousChunk(ByRef pChunk As %Integer = "") As %Binary
{
set pChunk = $order(i%ResultBits(pChunk),-1,tBits)
quit:pChunk="" ""
quit tBits
}
}

Метод InternalFindWindow класу SpatialIndex.QueryExecutor у наведеному вище прикладі, це реалізація пошуку точок, що потрапляють в заданих прямокутник. Далі, в циклі FOR, ID відповідних рядків пишуться в бітові набори.

У нашому хакатонном проекті крім пошуку в прямокутнику Андрій реалізував пошук всередині овалу:

SELECT *
FROM SpatialIndex.Test
WHERE %ID %FIND search_index(coord,'radius','x=55,y=55,radiusX=2,radiusY=2')
and name %StartsWith 'Z'

Трохи про предикате %FIND
У цього предиката є додатковий параметр SIZE, який може підказати оптимізатору запиту примірний порядок кількості рядків, які будуть задовольняти предикату. На основі цього параметра оптимізатор зробить вибір використовувати індекс, до якого %FIND звертається.

Для прикладу, додамо наступний індекс до класу SpatialIndex.Test:

Index ByName on Name;

Перекомпилируем клас і побудуємо цей індекс:

write ##class(SpatialIndex.Test).%BuildIndices($LB("ByName"))

І, звичайно, запустимо TuneTable:

do $system.SQL.TuneTable("SpatialIndex.Test", 1)

Розглянемо план запиту:

SELECT *
FROM SpatialIndex.Test
WHERE name %startswith 'za'
and %ID %FIND search_index(coord,'radius','x=55,y=55,radiusX=2,radiusY=2') size ((10))



Індекс coord імовірно поверне мало рядків, тому в індекс поля Name оптимізатор звертатися не буде.

Інша картина для запиту:

SELECT *
FROM SpatialIndex.Test
WHERE name %startswith 'za'
and %ID %FIND search_index(coord,'radius','x=55,y=55,radiusX=2,radiusY=2') size ((1000))



При виконанні цього запиту будуть використовуватися обидва індексу.

В якості останнього прикладу, запит, який використовує тільки індекс поля Name — використовувати індекс coord, якщо очікується що він поверне близько 100'000 рядків, марно:

SELECT *
FROM SpatialIndex.Test
WHERE name %startswith 'za'
and %ID %FIND search_index(coord,'radius','x=55,y=55,radiusX=2,radiusY=2') size ((100000))



Спасибі всім, хто дочитав або хоча б переглянув цю статтю до кінця.

Великою підмогою крім документації, посилання на яку трохи нижче, будуть інші реалізації інтерфейсів %Library.FunctionalIndex %SQL.AbstractFind. Щоб ці реалізації подивитися — відкрийте в студії один з цих класів і в меню виберіть Клас -> Успадковані класи.

Посилання:


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.