Индекс Типы (виды) индексов

Утилиты

Важнейшим элементом любой системы управления базами данных является наличие средств ускоренного поиска данных, поскольку поиск — это самая распростарненная операция в системах обработки данных. Но простое сканирование исходной таблицы в поисках нужной записи — это относительно медленная операция. В предельном случае, необходимо будет просканировать все записи таблицы, чтобы найти нужную запись или убедится, что такой записи не существует. Чтобы уменьшить количество просматриваемых записей, как правило, создают специальный файл, который содержит в себе нечто вроде списка пар: значение записи — номер запись. Т.е. в этом файле, перечисленны все значения некоторого поля или функции полей таблицы и указаны идентификаторы соответсвующих записей Этот специальный файл называют индексным файлом, а имя поля или функцию полей, на основе которого вычисляется значение записи — индексным ключем

В FoxPro различают 2 вида индексных файлов

Простой (обычный) индексный файл — это файл с расширением IDX, который содержит в себе только один индексный ключ

Мультииндексный файл — это файл с расширением CDX, который может содержать в себе несколько индексных ключей. По существу, мультииндексный файл — это объединение в одном файле нескольких простых (обычных) индексных файлов. Каждый отдельный индекс в этом файле за неимением лучшего термина называют тегом (от английского TAG — этикетка, метка)

Кроме того, выделяют еще особый структурный индексный файл — это обычный мультииндексный файл, но его имя совпадает с именем файла DBF по полям которого и строятся индексные ключи

Структурный индексный файл отличается тем, что он автоматически открывается при открытии соответствующей таблицы и его невозможно закрыть, пока открыта таблица (хотя можно сделать не главным)

Главный индекс — это тот индексный ключ, под управлением которого в данный момент находится таблица. Таблица не может быть отсортирована по нескольким индексным ключам одновременно. Хотя, если одна и та же таблица открыта одновременно в нескольких рабочих областях, то у каждой копии таблицы может быть назначен свой главный индекс.

После всех этих определений наконец-то можно сказать, что же подразумевается в большинстве случаев под словом индекс в FoxPro
Индекс — это один тег структурного индексного файла

Типы (виды) индексов

В FoxPro в настоящий момент различают четыре типа (вида) индексовCandidat Данный тип индекса предполагает, что по указанному индексному ключу во всей таблице нет повторяющихся значений. Попытка ввода в таблицу значения, которое уже есть в какой-либо записи таблицы, приведет к сообщению об ошибке. При этом наложенные на таблицу фильтры игнорируются. Следует заметить, что записи, помеченные как удаленные, также учитываются при контроле уникальности значения. Попытка ввода значения типа NULL также вызовет сообщение об ошибке.

Очень много недоразумений возникает из-за контроля уникальности данных в индексе типа Candidat. Типичные ошибки, заключаются в следующем:
-) Пустое значение (одни пробелы) — это тоже значение, поэтому невозможно создать 2 записи с пустыми значениями. Это будет воспринято как попытка ввода 2 одинаковых значений.
-) Установка ограничения SET DELETED ON — не есть физическое удаление записей помеченных как удаленные. Это всего-лишь наложение специфичекого фильтра, который делает такие записи «невидимыми», но тем не менее они по прежнему существуют в таблице. Поэтому попытка ввода новой записи со значениями, которые есть в одной из записей помеченных как удаленная также будет рассматриваться как ввод дублирующего значения.
Primary Можно сказать, что данный тип — это частный случай индекса типа Candidat. Он обладает всеми свойствами индекса типа Candidat, но с дополнительными ограничениями. Данный тип индекса может быть только один в каждой таблице и данный тип индекса может быть только у таблиц включенных в базу данных. Если таблица с индексом типа Primary исключается из базы данных, то индекс типа Primary автоматически конвертируется в тип Candidat.

Строго говоря, никакой дополнительной функциональности, с точки зрения целостности данных индекс типа Primary по сравнению с индексом типа Candidat не дает. Однако факт его наличия облегчает процесс проектирования базы данных. Дело в том, что по умолчанию предполагается, что индекс типа Primary построен по ключевому полю таблицы. Соответственно в тех визуальных средствах программирования, где необходимо указание ключевого поля (например, View Designer на закладке Update Criteria) FoxPro автоматически предлагает считать ключевым то поле, по которому построен индекс типа Primary.
Regular Это самый распространенный тип индексов. Его использование не предполагает никаких особых ограничений на содержимое таблицы. Можно сказать, что это обычный (простой) индекс
Unique Данный тип индекса не запрещает ввод в таблицу одинаковых значений, однако учитывает (отображает) только одно (самое первое) из введенных одинаковых значений. Можно сказать, что это своеобразный фильтр, который отсекает повторяющиеся значения.

Отдельного упоминания заслуживает использование команды APPEND BLANK применительно к таблицам, по которым построены индексы типа Primary или Candidat.

По умолчанию, команда APPEND BLANK создает новую запись, где все поля имеют пустое значение. Но пустое значение — это тоже значение. Поэтому, если Вы дадите две команды APPEND BLANK подряд, то получите сообщение о нарушении уникальности индекса. Чтобы этого избежать необходимо создавать новую запись с не пустым и уже уникальным значением. Это можно сделать одним из следующих способов:
Вместо APPEND BLANK использовать INSERT-SQL указав заранее созданные уникальные значения
Использовать специальную функцию, генерирующую новые уникальные значения, вызов которой организовать из DEFAULT поля, по которому создан индекс типа Primary или Candidat.

Контроль уникальности в триггерах не сработает, поскольку проверка уникальности индекса выполняется ДО проверки триггеров. Т.е. проверять в триггерах в этом случае уже поздно.

Режим сортировки индексов (Collate)

Настройка SET COLLATE определяет режим сортировки символьных строк. Иными словами, какой символ надо поставить первым, а какой вторым, при упорядочивании.

По умолчанию, в FoxPro используется режим сортировки MACHINE. Это такой порядок сортировки, когда символы выстраиваются в соответствии с их ASCII-кодами. Чтобы понять что это означает (т.е. в каком порядке будут сортироваться символьные данные) посмотрите таблицу ASCII кодов. Это можно сделать так:
CREATE CURSOR curASCII (nASC I, cCHR C(1))
FOR m.lnI=1 TO 255
INSERT INTO curASCII (nASC,cCHR) VALUES (m.lnI, CHR(m.lnI))
ENDFOR
BROWSE NOWAIT

Обратите внимание, что сначала идут большие буквы и только потом маленькие. Кроме того, применительно к буквам русского алфавита, выпадает буква «ё» (ASCII-код 184) и «Ё» (ASCII-код 168). Именно в таком порядке и будут следовать символьные строки.

Если используется какой-нибудь национальный режим сортировки, например RUSSIAN, то различия между большими и маленькими буквами игнорируются и все буквы национального языка выстраиваются по алфавиту без разрывов. Чтобы понять как изменится порядок сортировки выполните следующий код, используя полученный ранее курсор
SET COLLATE TO ‘RUSSIAN’
select curASCII
INDEX ON cCHR TAG cCHR
BROWSE NOWAIT

Как видите, буквы русского языка выстроились строго по алфавиту игнорируя разделение на большие и маленькие буквы и буква «Ё» заняла надлежащее ей место после «Е». Можно заметить и другие отличия, но здесь я их рассматривать не буду.

Какое отношение ко всему этому имеют индексы? Самое непосредственное. Дело в том, что каждый индекс в отдельности запоминает тот режим сортировки при котором он был создан и при модификации данных автоматически упорядочивает записи именно в этом сохраненном режиме сортировки.

Таким образом внутри одного индексного файла могут быть индексы созданные в разных режимах сортировки. Проверить режим сортировки в котором был создан индекс можно при помощи функции IDXCOLLATE()

Однако я бы не советовал на первых порах экспериментировать с SET COLLATE. Оставьте эту настройку в режиме по умолчанию, т.е. SET COLLATE TO ‘MACHINE’. Этому есть причины.

В версиях FoxPro младше 6 ServicePack 5 запросы Select-SQL теряли некоторые числовые значения если была установлена сортировка отличная от MACHIN. Т.е. эта ошибка сохранялась вплоть до Visual FoxPro 6 Service Pack 4 и была исправлена только с выходом Service Pack 5 к 6 версии.
При использовании Private DataSession установка SET COLLATE — это одна из тех установок, которая сбрасывается в значение по умолчанию. Вследствии чего, ее надо устанавливать повторно каждый раз при открытии Private DataSession
Полный список настроек, сбрасываемых в значения по умолчанию при использовании Private DataSession можно почитать в описании к команде SET DataSession
При оптимизации используются только те индексы, которые созданы в том же режиме сортировки, что и текущий режим сортировки системы.
В подавляющем большинстве случаев, вместо использования режима сортировки SET COLLATE TO ‘RUSSIAN’ можно использовать режим сортировки MACHINE с индексным ключем UPPER(MyField) или LOWER(MyField). Применительно к русским буквам эффект будет тот же (за исключением букв «ё» и «Ё»)

Метод половинного деления

Чтобы понять, как наличие индексного файла позволяет ускорить поиск рассмотрим примерную схему работы наиболее распространенного способа поиска методом половинного деления

Предположим, что у нас есть некая таблица, содержащая ряд числовых значений:
Запись Значение
1 500
2 300
3 200
4 400
5 100

Теперь предположим, что необходимо найти запись имеющую значение 200. В случае отсутствия индексного файла есть только один способ — сканирование всех записей таблицы, пока не будет найдено искомое значение или не будет достугнут конец файла. В данном случае можно считать повезло, поскольку сканирование закончится на 3 записи таблицы. Т.е. необходимо будет сделать как минимум 3 перехода.

Если будет создан индексный файл у которого в качестве индексного ключа будет указано просто имя поля, содержащего значение, то внутри этого индексного файла будет создано нечто вроде следующего списка
Значение Запись
100 5
200 3
300 2
400 4
500 1

А теперь посмотрим как поиск записи со значением 200 приблизительно будет выглядеть в индексе в случае использования метода половинного деления.

-) На первом этапе определяются начальные и конечные значения. В данном случае — это 100 и 500. Значение 200 находится между ними.

-) Вычисляется среднее арифметическое от граничных значений: (100+500)/2=300 и берется ближайшее значение из списка значений к вычисленному среднему. В данном случае это то же самое значение 300

-) Теперь смотрим в какой половине находится искомое значение 200. Оно находится в интервале от 100 до 300

-) Вычисляется среднее арифметическое этого нового интервала: (100+300)/2=200 и берется ближайшее значение из списка значений к вычисленному среднему. В данном случае это то же самое значение 200.

-) Поскольку одно из граничных значений равно искомому значению, то цикл половинного деления прерывается. Определяется, что данное значение имеет запись с номером 3 и осуществляем переход на найденную запись

А теперь сравним, сколько шагов необходимо сделать в случае простого сканирования и в случае использования метода половинного деления

Предельно допустимое количество записей в DBF-таблице — это 1 миллирад (1 billion) записей (это единица и девять нулей). Значит при прямом сканировании таблицы в предельном случае может понадобиться просканировать весь миллирад записей. Т.е. сделать миллиард операций.

А в случае половинного деления количество операций определяется значением степени в которую следует возвести число 2, чтобы превысить миллирад (логарифм от миллиарда по основанию 2). Это число 30, поскольку 2**30=1,073,741,824. Т.е. в предельном случае для поиска значения в индексе потребуется не более 30 шагов

Выигрыш в производительности очевиден: миллирад против тридцати.

Замечание: Подчеркну еще раз: приведенный алгоритм — это всего лишь общая схема. В реальности и способ хранения данных в индексном файле и способ поиска несколько отличаются

Использование индексов для отображения данных

До сих пор, я рассматривал индекс только как способ ускорения поиска нужной информации, но ведь индекс может быть еще использован и как способ отображения информации в нужной последовательности.

Проблема здесь в том, что как правило, пользователь не работает напрямую с исходной таблицей по причине ее больших размеров (слишком много записей, чтобы охватить их «одним взглядом»). Всегда делается некая выборка. Наложение фильтров на исходные таблицы не приветствуется, поскольку такая технология замедляет работу (переход от записи к записи происходит заметно медленнее)

Т.е. создание индексов для упорядочивания записей в структурном индексном файле исходной таблицы не имеет смысла, поскольку собственно исходная таблица и не отображается.

Однако это вовсе не означает, что индексы для упорядочивания записей не используются. Дело в том, что любая таблица используемая в FoxPro может быть проиндексирована. Это относится как к обычным таблицам, так и к результатам выполнения команд Select-SQL, Local View и Remote View.

Другое дело, что такие индексы не имеет смысла хранить в базе данных. Их надо будет создавать заново каждый раз при создании соответсвующих временных таблиц или курсоров. Поскольку временные таблицы для того и создаются, чтобы получить некую выборку из исходных таблиц, то, как правило, они имеют относительно небольшое количество записей и их индексация не требует много времени.

Некоторые особенности создания индексов для временных таблиц
Если курсор, созданный командой Select-SQL имеет статус Read-Only, то для него можно построить только один индексный тэг.
По умолчанию, курсор созданный командой Select-SQL всегда имеет статус Read-Only, чтобы это изменить необходимо использовать опцию READWRITE (она появилась только в 7 версии). А для более младших версий курсор следует переоткрыть. Подробнее об этом читайте в разделе Курсор
Индексация View возможна только если он находится в 3 режиме буферизации (оптимистическая буферизация строк). Если View находится в 5 режиме буферизации (оптимистическая буферизация таблиц), то при попытке индексации Вы получите сообщение об ошибке.
Обновление содержимого View по команде Requery() также обновит и содержимое всех созданных для него индексов.
Если Вы создали структурный индексный файл к курсору или View, то он будет автоматически удален в момент закрытия этого курсора или View

Контроль уникальности данных при помощи индекса

Индекс типа Candidat по самой своей природе не допускает ввода повторяющихся значений. В связи с этим возникает искущение не писать специальных триггеров за контролем уникальности данных, а переложить эту работу на индекс. Посмотрим, какие преимущества и недостатки это дает

Прежде всего, следует заметить, что индекс типа Candidat в «чистом» виде нас не устаивает по той причине, что в большинстве случаев задача контроля уникальности формулируется так: В таблице должны быть уникальны значения, если они удовлетворяют некоторым дополнительным условиям

Ну например, Вы хотите контролировать уникальность только среди записей не помеченных как удаленные.

Это можно сделать добавив в индекс FOR-условие, которое в данном случае будет выглядеть так: FOR .NOT.Deleted()

Но добавление FOR-условия в выражение индекса автоматически исключает его из списка индексов, участвующих в оптимизации. Т.е. для ускорения выборок понадобиться создать еще один индекс с тем же выражением индексного ключа, но без FOR-условия. Чтобы этот новый индекс не контролировал уникальность, он должен быть типа Regular

Как следствие, получаем «раздувание» индексного файла на «лишний» индекс.

Кроме того, следует помнить, что проверка уникальности данных в индексах типа Candidat происходит сразу же при попытке внесения изменений, даже в буферизированных данных еще до сброса буфера. В то время, как триггер срабатывает только в момент сброса буфера. Эту особенность, следует учитывать при написании обработчика ошибок в приложении.

С другой стороны, если написать триггер для контроля уникальности, то при определенных условиях, можно написать один общий триггер на все таблицы базы данных куда в качестве параметра передается выражение, уникальность которого следует проконтролировать и дополнительное условие отбора контролируемых записей. Здесь выражение «при определенных условиях» не случайно. Написать такой универсальный триггер «на все случаи жизни» мне представляется затруднительным, но если придерживаться некоторых правил и ограничений при проектировании базы данных, то это становится возможным.

Есть еще одно соображение, не столь принципиального характера. В случае внесения изменений в правило контроля уникальности если речь идет об индексе, то потребуется удалить старый индекс и создать новый, а если речь идет о триггере, то надо будет просто заменить файлы базы данных (DBC, DCX, DCT) на новые (с новым триггером)

Итого, контроль уникальности данных при помощи индекса типа Candidat с FOR-условием по сравнению с триггером проще в реализации, но приводит к значительно большему увеличению объема базы данных (в байтах) и изменению логики обработчика ошибок при редактировании данных.

Название индексных тегов

Если Вы создаете индекс, где в качестве индексного ключа выступает только одно поле таблицы то желательно назвать индекс также как и поле таблицы, поскольку это значительно упростит процесс программирования.

Поскольку существует системное ограничение на количество символов в названии индексного тега (он не может превышать 10 символов), то и не следует давать полям, по которым будут построены такие индексы, названия, превышающие 10 символов. Это не так сложно, как может показаться на первый взгляд.

Сколько и каких индексов надо создавать

Вопрос крайне неоднозначный и сильно зависит от конкретной задачи, однако некоторые рекомендации все-таки можно сделать.
Прежде всего, Вам следует определиться в каком режиме сортировки (SET COLLATE) Вы будете работать. Дело в том, что индекс запоминает тот режим сортировки в котором он был создан и в оптимизации запросов участвуют только те индексы, которые были созданы в том же режиме сортировки, что и используемый на момент выполнения запроса.

Например, если Вы создали индекс в режиме сортировки MACHINE, а запрос выполняется при настройке RUSSIAN, то индекс использоваться не будет, хотя казалось бы он есть. Проверить в каком режиме сортировки был создан тот или иной индекс можно используя функцию IDXCOLLATE(). При этом следует иметь в виду, что изменить режим сортировки индекса невозможно. Необходимо будет удалить «неправильный» индекс и создать новый в нужном режиме сортировки
По ключевому полю постройте индекс типа Primary. Индекс не должен содержать никаких FOR-условий, а выражение индексного ключа должно состоять только из имени ключевого поля. Очень желательно, чтобы имя этого индекса совпадало с именем ключевого поля, поскольку это серьезно упростит процесс программирования.
По всем внешним ключам (т.е. полям, содержащим ссылку на другую таблицу) следует построить простые индексы типа Regular. Индекс не должен содержать никаких FOR-условий, а выражение индексного ключа должно состоять только из имени поля. Очень желательно, чтобы имя этого индекса совпадало с именем поля, поскольку это серьезно упростит процесс программирования.

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

Индексы используются для ускорения получения выборок в так назваемой Rushmore-оптимизации. Что это такое и как оно работает я здесь объяснять не будут. С точки зрения создания индексов нас интересует следующая особенность работы Rushmore-оптимизации:

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

Следствием этой особенности работы является следующий вывод
Если объем информации (в байтах) НЕ удовлетворяющей условию выборки меньше, чем размер индекса (в байтах) по данному условию выборки, то в общем случае оптимизация приведет к замедлению выборки

В виде формулы это можно записать так. Оптимизация приведе к замедлению выборки если:

Объем информации НЕ удовлетворяющей условию < Объем индекса Объем информации НЕ удовлетворяющей условию — это произведение количества записей не попадающих в выборку на объем в байтах одной записи. Объем индекса — это сумма размеров индексных тэгов, участвующих в оптимизации. Проблема здесь именно в том, как определить размер одного индексного тэга. Очень грубо и приблизительно, его можно оценить как количество символов получающееся в результате расчета индексного ключа умноженное на общее количество записей в таблице. Это очень грубая оценка, поскольку способ хранения индекса сильно отличается от линейного списка пар: значение ключа — код записи. Однако даже при такой грубой оценке можно сделать вывод, что индекс по логическим полям в большинстве случаев приведет к замедлению, а не к ускорению выборки. Причина здесь в том, что логические поля в большинстве случаев работают как признак некой исключительной (т.е. достаточно редкой) ситуации, а запрос строится по принципу отобрать все, кроме этих заведомо редких случаев. Т.е. заранее можно сказать, что объем информации НЕ удовлетворяющий условию выборки будет очень небольшим (если вообще будет). Пожалуй, индекс по логическим полям имеет смысл, только в случае, если значения .T. и .F. распределены примерно равномерно по таблице. Предельный случай — две трети и одна треть. Частным случаем индекса по логическому выражению является индекс по выражению Deleted(). Этот индекс используется в случае настройки SET DELETED ON для того, чтобы оптимизировать выборку на предмет отсечения записей помеченных как удаленные. Как уже было сказано чуть выше, если количество удаленных записей относительно невелико, то данный индекс приведе не к ускорению, а к замедлению выборки. Значительно сложнее определить необходимость прочих индексов. Да, есть некоторые правила, которые говорят, что если построить такой и такой индекс, то запрос будет оптимизирован. Даже есть функция SYS(3054) при помощи которой можно установить используется ли данный индекс при оптимизации или нет (да и то не всегда из-за несовершенства функции SYS(3054)). Но как было замечено ранее: оптимизация далеко не всегда приводит к ускорению получения выборки. Поэтому фактически единственным критерием остается практика: если факт наличия индекса приводит к заметному (в разы) ускорению получения выборки, то его стоит создавать. Но даже факт ускорения получения выборки не может являтся единственным критерием необходимости создания индекса. Еще одним не маловажным критерием является вопрос о том, насколько часто используется эта выборка. Например, если речь идет о составлении квартального отчета, то особо гнаться за скоростью здесь не требуется. Пользователь вполне может подождать и воспримет это с пониманием. Но вот если в процессе открытия основной рабочей формы, программа будет задумывать на несколько минут, то ничего кроме раздражения со стороны пользователя это не вызовет. Замечу еще, что индексы содержащие FOR-условия в оптимизации не участвуют Собственно работа с индексами
В FoxPro не поддерживается значение индексного ключа переменной длины. Т.е. если в качестве выражения индексного ключа Вы напишите AllTrim(MyField), то хотя ошибки это и не вызовет, но фактически будут удалены только ведущие пробелы, а общая длина каждого значения будет дополнена концевыми пробелами до некоторой фиксированной величины. В данном случае до длины поля MyField. Поэтому, чтобы избежать различных недоразумений, лучше в таких сложных случаях самостоятельно явно указывать длину индексного ключа, например PADR(AllTrim(MyField),50)
По возможности, старайтесь избегать сложных выражений индексного ключа для тегов структурного индексного файла. Чем проще выражение индексного ключа — тем лучше. В идеале, лучше иметь индексный ключ состоящий из названия одного поля.
Индекс «запоминает» режим сортировки (SET COLLATE) при котором он был создан и при модификации исходных таблиц автоматически сортирует новые значения в «запомненном» режиме сортировки вне зависимости от текущей настройки SET COLLATE. Проверить в каком режиме сортировки был создан тот или другой тег можно функцией IDXCOLLATE()
На результат поиска по индексному выражению влияет настройка SET EXACT, а внутри команды SELECT-SQL аналогичная ей настройка SET ANSI
Эти настройки определяют как будет осуществлятся поиск в случае если заданный критерий поиска отличается по длине от длины индексного ключа. По умолчанию, если заданный критерий поиска меньше длины индексного ключа, то будут найдены записи, где заданный критерий — это первые символы индексного ключа. Фактически поиск ведется по первым символам, а не по всему ключу.
Если необходимо избежать влияния этих настроек на результат поиска (т.е. требуется строгое соответствие), то дополняйте критерий поиска нужным количеством пробелов справа используя функцию PADR(KeyValue,10)
Следует помнить, что изменения данных в исходной таблице приводит к немедленному изменению всех открытых в данный момент индексов и перестроению в соответствии с главным индексом. Это значит, что крайне нежелательно модифицировать те поля, которые входят в выражение индексного ключа главного индекса, поскольку это может привести к непредсказуемым последствиям. Предварительно надо или переглючится на другой индекс или вообще отключить главный индекс (SET ORDER TO 0)

Рассмотрим следующий пример
CREATE CURSOR test (testID I)
INSERT INTO test (testID) VALUES (2)
INSERT INTO test (testID) VALUES (1)
INDEX ON testID TAG testID

Предположим, необходимо увеличить значение поля TetsID в каждой записи таблицы на 10. Для чего используем цикл SCAN
SELECT test
SCAN
REPLACE testID WITH testID+10
ENDSCAN

Так вот, в данном случае произойдет модификация только одной записи. Механизм здесь следующий:

-) На первом шаге цикла SCAN указатель записей будет установлен на первую запись в соответсвии с главным индексом. Это запись со значением поля testID=1

-) По команде REPLACE значение поля TestID первой записи будет увеличено на 10 и примет значение 11. Немедленно произойдет модификация индексного файла и его перестроение. Теперь эта же самая запись оказывается не первой, а самой последней

-) На команде ENDSCAN будет предпринята попытка перейти на следующую запись, но в соответствии с новым значением индекса текущая запись уже последняя. Поэтому цикл будет завершен и запись со значением testID=2 так и останется не измененной.

Для исправления этой ситуации необходимо перед началом цикла сбросить главный индекс
SELECT test
SET ORDER TO 0
SCAN
REPLACE testID WITH testID+10
ENDSCAN

Индексный файл — это все-таки отдельный (другой) файл. Поэтому если индексный файл не открыт (командой SET INDEX или опцией ORDER в команде USE), то изменения сделанные в исходной таблице не отразятся в индексе. Это может привести к серьезным недоразумениям — запись введена, но ее не видно.

Исключением в этом смысле является структурный индексный файл, который открыт всегда, пока открыта таблица, но для не структурных индексов за этим надо следить отдельно. Фактически, если используются не структурные индексные файлы, то необходимо либо сразу после их подключения давать команду REINDEX, либо вообще их не хранить и создавать каждый раз заново.
Курсор, созданный командами CREATE CURSOR или SELECT … INTO CURSOR можно индексировать. Однако если курсор созданный командой SELECT … INTO CURSOR имеет статус Read-Only (т.е. не была указана опция ReadWrite или он не был переоткрыт), то для такого курсора можно будет создать только один индексный тег. Если Вы создадите структурный индексный файл для таких курсоров, то этот файл будет автоматически удален в момент закрытия курсора
Local View и Remote View также можно индексировать если они находятся в режиме оптимистической буферизации строк (3). В режиме оптимистической буферизации таблиц (5) индексирование невозможно. Обновление содержимого View по команде Requery() также обновит и структурный индексный файл созданный для данного View. Закрытие View автоматически уничтожит и структурный индексный файл.