InterSystems corporate blog
High performance
Big Data
22 March 2013

Новое в СУБД Caché 2013.1: добавление и генерация индексов на «живых» классах

Tutorial
Предположим, что у вас есть таблица с большим количеством записей и в неё нужно добавить один или несколько индексов со следующими условиями:

  1. их генерация должна быть максимально быстрой
  2. чтобы генерацию можно было производить порциями.
    К примеру, если есть таблица на 300М записей и работы с ней можно производить только в нерабочее время, то чтобы можно было разбить весь процесс на три ночи по 100М записей
  3. появление новых индексов и сам процесс их генерации не должны мешать текущей работе с классом/таблицей

Для этого можно было бы воспользоваться уже известным методом %BuildIndices(), но в таком случае это не будет удовлетворять нашим условиям.

Каков же выход?

Теория


В версию СУБД Caché 2013.1 был добавлен новый класс %Library.IndexBuilder с одним, но мощным методом %ConstructIndicesParallel().
Из названия уже становится понятно, что генерация будет происходить параллельно с привлечением всех ядер процессоров.

Итак, рассмотрим параметры этого метода подробнее:

ClassMethod %ConstructIndicesParallel(pTaskId="", pStartId As %Integer = 0, pEndId As %Integer = -1, pSortBegin As %Integer = 1, pDroneCount As %Integer = 0, pLockFlag As %Integer = 1, pJournalFlag As %Boolean = 1) as %Status

  • pTaskId — ID фонового процесса. Оставьте пустым/неопределённым для интерактивного вызова
  • pStartId — ID, с которого нужно начать генерацию. По умолчанию 1
  • pEndId — ID, на котором нужно завершить генерацию. По умолчанию -1, означающее последний ID в таблице
  • pSortbegin — 1/0 флаг, определяющий использовать ли $SortBegin при генерации.
  • pDroneCount — количество фоновых процессов для генерации индексов.
    По умолчанию 0. В этом случае код будет самостоятельно определять оптимальное число процессов, основываясь на количестве доступных ядер/процессоров и количестве обрабатываемых записей.
  • pLockFlag — флаг, определяющий поведение блокировки во время выполнения генерации:
    • 0 = Нет блокировки
    • 1 = Extent locking — Получает исключительную блокировку на весь экстент в течение генерации
    • 2 = Row level locking — Получает разделяемую блокировку на каждую обрабатываемую строку и узел индекса для элемента. Когда генерация индекса для конкретной строки завершена, немедленно снимается блокировка этой строки.
  • pJournalFlag — 0/1 флаг, определяющий использование журналирования:
    1 — генерация индекса будет журналироваться, 0 — не будет.


Практика


Теперь рассмотрим пример применения нового класса.

Для начала создадим в области USER учебный класс, заполним его 1М записей строками переменной длины [1-100] и построим индекс с использованием классического %BuildIndices(), чтобы было с чем сравнивать:

Class demo.test Extends %Persistent
{

Index idxn On n As SQLUPPER(6);

Property As %String(MAXLEN 100);

ClassMethod Fill(As %Integer 10000000)
{
  
set data=$Replace($Justify("",100)," ","a")
  
set time=$ZHorolog
  do 
DISABLE^%NOJRN
  
do ..%KillExtent()
  
set ^demo.testD=n
  
set ^demo.testD(1)=$ListBuild("",$Extract(data,1,$Random(100)+1))
  
for i=2:1:set ^(i)=$ListBuild("",$Extract(data,1,$Random(100)+1))
  
do ENABLE^%NOJRN
  
write "вставка= ",$ZHorolog-time," сек.",!
}

ClassMethod BIndex()
{
  
set time=$ZHorolog
  do 
..%BuildIndices(,1,1)
  
write "переиндексация= ",$ZHorolog-time," сек.",!
}

}

Мои результаты:
USER>do ##class(demo.test).Fill()
вставка= 9.706935 сек.

USER>do ##class(demo.test).BIndex()
переиндексация= 71.966953 сек.

Теперь задействуем новый класс %IndexBuilder. Для этого выполним следующие действия:

  1. сперва очистим данные индекса от предыдущего теста методом %PurgeIndices() (необязательный шаг)
  2. унаследуем наш класс от %IndexBuilder
  3. пропишем список индексов через запятую в параметре INDEXBUILDERFILTER.
    Если этот параметр оставить пустым, то будут перегенерированы все индексы
  4. сделаем наш индекс невидимым для SQL, чтобы оптимизатор не использовал ещё не готовый к работе индекс.
    Для этого воспользуемся методом $SYSTEM.SQL.SetMapSelectability():

    ClassMethod SetMapSelectability(pTablename As %Library.String = "", pMapname As %Library.String = "", pValue As %Boolean = "") as %Library.String

    Описание аргументов:
    • pTablename — имя таблицы
    • pMapname — имя индекса
    • pValue — 0/1 флаг, определяющий видимость(1) или невидимость(0) индекса для SQL-оптимизатора
    Примечание: можно cделать индекс невидимым задолго до его добавления в класс.
  5. вызовем метод %ConstructIndicesParallel()
  6. сделаем наш индекс видимым для SQL
  7. Profit!

В итоге наш класс приобретёт следующий вид:

Class demo.test Extends (%Persistent%IndexBuilder)
{

Parameter INDEXBUILDERFILTER = "idxn";

Parameter BITMAPCHUNKINMEMORY = 0;

Index idxn On n As SQLUPPER(6);

Property As %String(MAXLEN 100);

ClassMethod FastBIndex()
{
  
do ..%PurgeIndices($ListBuild("idxn"))
  
do $SYSTEM.SQL.SetMapSelectability($classname(),"idxn",$$$NO)
  
do ..%ConstructIndicesParallel(,,,1,,2,0)
  
do $SYSTEM.SQL.SetMapSelectability($classname(),"idxn",$$$YES)
}
}

Мои результаты:
USER>do ##class(demo.test).FastBIndex()

Building 157 chunks and will use parallel build algorithm with 4 drone processes.
SortBegin is requested.
Started drone process: 3812
Started drone process: 4284
Started drone process: 7004
Started drone process: 7224
Expected time to complete is 43 secs to build 157 chunks of 64,000 objects using 4 processes.
Waiting for processes to complete....done.
Elapsed time using 4 processes was 34.906643.

Как видим, скорость возросла в два раза.

На вашем железе и на ваших данных результаты могут получиться ещё лучше.

Ещё быстрее?


Но есть ли возможность ещё больше ускорить перегенерацию индексов?
Если у вас есть в запасе много RAM, то да.

В процессе генерации индексов конструктором для внутренних нужд временно формируются так называемые bitmap-блоки. По умолчанию они записываются в приватные глобалы, но с помощью булева параметра BITMAPCHUNKINMEMORY можно указать, чтобы они формировались в оперативной памяти. Для этого нужно параметру присвоить 1.
Заметьте, что если RAM выделено мало, а индексы большие, то вы можете получить ошибку <STORE>.
По умолчанию BITMAPCHUNKINMEMORY равен 0.

+1
2.2k 9
Comments 9
Top of the day