Собственные типы индексов в СУБД Caché

imageВ объектной и реляционной моделях данных СУБД 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)_"set result = ##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()
    set result = ##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))

image

Индекс 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))

image

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

В качестве последнего примера, запрос, который использует только индекс по полю 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))

image

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

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

Ссылки:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *