Предположим, что у вас есть таблица с большим количеством записей и в неё нужно добавить один или несколько индексов со следующими условиями:
- их генерация должна быть максимально быстрой
- чтобы генерацию можно было производить порциями.
К примеру, если есть таблица на 300М записей и работы с ней можно производить только в нерабочее время, то чтобы можно было разбить весь процесс на три ночи по 100М записей - появление новых индексов и сам процесс их генерации не должны мешать текущей работе с классом/таблицей
Для этого можно было бы воспользоваться уже известным методом %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 n As %String(MAXLEN = 100);
ClassMethod Fill(n 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:n 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. Для этого выполним следующие действия:
- сперва очистим данные индекса от предыдущего теста методом %PurgeIndices() (необязательный шаг)
- унаследуем наш класс от %IndexBuilder
- пропишем список индексов через запятую в параметре INDEXBUILDERFILTER.
Если этот параметр оставить пустым, то будут перегенерированы все индексы - сделаем наш индекс невидимым для 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делать индекс невидимым задолго до его добавления в класс.
- вызовем метод %ConstructIndicesParallel()
- сделаем наш индекс видимым для SQL
- Profit!
В итоге наш класс приобретёт следующий вид:
Class demo.test Extends (%Persistent, %IndexBuilder)
{Parameter INDEXBUILDERFILTER = "idxn";
Parameter BITMAPCHUNKINMEMORY = 0;
Index idxn On n As SQLUPPER(6);
Property n 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.
Автор: servitRM