Sources
Delphi Russian Knowledge Base
DRKB - это самая большая и удобная в использовании база знаний по Дельфи в рунете, составленная Виталием Невзоровым

Поиск записи с помощью TQuery

01.01.2007

Компонент TQuery не предусматривает основанный на индексе поиск, подобный реализованному в компоненте TTable (FindKey, GotoKey и GotoNearest). Поэтому возникает следующий вопрос: как в данных, возвращаемых запросом TQuery, найти определенную запись?

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

Вот функция, выполняющая последовательный поиск в результатах запроса TQuery:

var
  pb: TProgressBar;
begin
...
 
function SeqSearch(AQuery: TQuery; AField, AValue: String): Boolean;
begin
  with AQuery do 
  begin
    First;
    while (not Eof) and (not (FieldByName(AField).AsString = AValue)) do
      Next;
    SeqSearch := not Eof;
  end;
end;

Данная функция требует три параметра:

AQuery: тип TQuery; компонент TQuery, в котором необходимо выполнить поиск.

AField: тип String; имя поля, с величиной которого проиходит сравнение значение поиска.

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

Возвращаемая логическая величина указывает на успешность выполнения функции (True) или отсутствие результата поиска (False).

Альтернативой служит использование метода заключения в скобки. На концептуальном уровне данный метод действует отчасти подобно индексу bb-дерева. Он основывается на методе сравнения значения текущей строки набора данных и искомой величины с последующей проверкой на выполнение одного из трех возможных условий:

Величина поля будет больше чем значение поиска, или...

Величина поля будет меньше чем значение поиска, или...

Величина поля равняется значению поиска.

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

Функционально данный процесс находит поля, удовлетворяющие условиям поиска, за количество итераций меньшее или равное числу записей. При этом возможно только два результата сравнения текущего поля и искомой величины: меньше чем/равняется/больше чем. Первоначально устанавливается диапазон чисел. Меньшая граница диапазона задается целым числом, начало процесса поиска устанавливается на 0 или величину меньшую, чем значение первой строки набора данных. Верхняя граница диапазона является также целым числом, содержащим значение свойства RecordCount экземпляра TQuery. Текущий указатель строки перемещается в в точку, лежащую посередине между нижней и верхней границей диапазона. Значение записи в этой точке сравнивается с искомой величиной. Если значение поля меньше или равно искомой величине, значит искомая величина находится в нижней части набора данных, поэтому верхняя граница диапазона перемещается к позиции текущей строки. Если значение поля больше величины поиска, то искомая величина находится в верхней части набора данных, поэтому к текущему указателю перемещается нижняя граница диапазона. Повторяя этот процесс, количество удовлетворяющих условиям поиска записей при каждой итерации уменьшается в два раза. В конечном счете должна остаться только одна строка.

Код модульной, транспортабельной функции должно выглядеть примерно так:

function Locate(AQuery: TQuery; AField, AValue: string): Boolean;
var
  Hi, Lo: Integer;
begin
  with AQuery do
  begin
    First;
    {Устанавливаем верхнюю границу диапазона строк}
    Hi := RecordCount;
    {Устанавливаем нижнюю границу диапазона строк}
    Lo := 0;
    {Текущий указатель перемещаем в в точку, лежащую посередине
    между нижней и верхней границей диапазона}
    MoveBy(RecordCount div 2);
    while (Hi - Lo) > 1 do
    begin
      {Значение поля больше искомой величины, величина в первой половине}
      if (FieldByName(AField).AsString > AValue) then
      begin
        {Вычисляем нижнюю границу верхнего поддиапазона общего диапазона}
        Hi := Hi - ((Hi - Lo) div 2);
        MoveBy(((Hi - Lo) div 2) * -1);
      end
        {Найденное поле меньше искомой величины, нужно искать в верхней половине}
      else
      begin
        {Перемещаем вверх нижнюю границу общего диапазона}
        Lo := Lo + ((Hi - Lo) div 2);
        MoveBy((Hi - Lo) div 2);
      end;
    end;
    {Обрабатываем нечетную нумерацию строк}
    if (FieldByName(AField).AsString > AValue) then
      Prior;
    Locate := (FieldByName(AField).AsString = AValue)
  end;
end;

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

Данная функция также требует три параметра, как и функция SeqSearch, описанная выше.

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

Чем этот метод лучше последовательного поиска? Во-первых, данный метод не производит сравнение всех строк, как это делает метод последовательного поиска, а опрашивает часть записей. Если искомая величина не располагается в числе первых 1,000 строк, то этот метод окажется быстрее чем метод последовательного поиска. Поскольку этот процесс всегда использует одинаковое количество записей, то время поиска будет одинаковым и когда искомая величина находится в записи с номером 1,000, и когда она находится в записи с номером 90,000. Это в корне отличается от последовательного поиска, когда время поиска напрямую зависит от местонахождения искомой величины.

Эти методы могут использоваться в любых результатах запроса TQuery? Нет. Все дело в технологии: описанные методы пользуются такими понятиями, как направление поиска, нижняя и верхняя границы диапазона. Это означает, что набор данных должен быть последователен и непрерывен, т.е. для получения результатов TQuery должен использовать SQL-запросы, содержащие ключевую фразу ORDER BY. Размер полученного набора данных также является показателем для выбора метода. Метод заключения в скобки выгоднее использовать при большом наборе данных. В случае, когда число записей невелико (1,000 и менее строк), метод последовательного поиска все же будет быстрее.


Как мне найти запись (осуществить 'Find') в TQuery?

Я привел ниже код, который я использую в своей работе, правда, в нем еще необходимо организовать обработку исключительных ситуаций, но это дело времени. Когда пользователь нажимает кнопку "Найти", обработчик события OnClick вызывает процедуру SearchName.

Объявляем: FindSearch : Boolean и инициализируем значением True.

function LookForString(target, source: string): boolean;
{ в случае игнорирования перед вызовом pos необходимо
преобразовать source и target в верхний регистр }
begin
  LookForString := pos(target, source);
end;
 
procedure SearchName(searchtype: string; stringtofind: string);
var
  OldCursor: TCursor;
  CurrentPos: TBookmark;
  found: boolean;
begin
  if Form1.Query1.State = dsEdit then
    Form1.Query1.Post;
  if StringToFind = '' then
    exit;
  OldCursor := Screen.Cursor;
  Screen.Cursor := crHourGlass;
  with Form1 do
  begin
    CurrentPos := Query1.GetBookmark;
    Query1.DisableControls;
    found := false;
    if searchtype <> 'prev' then { первый или следующий }
    begin
      if searchtype = 'first' then
        Query1.First
      else if not Query1.EOF then
        Query1.Next;
      while (not Query1.EOF) and (not found) do
      begin
        if LookForString(StringToFind, MemberName) <> 0 then
          found := true;
        if not found then
          Query1.Next;
      end;
    end
    else
    begin { prev }
      if not Query1.BOF then
        Query1.Prior;
      while (not Query1.BOF) and (not found) do
      begin
        if LookForString(StringToFind, MemberName) <> 0 then
          found := true;
        if not found then
          Query1.Prior;
      end;
    end;
    Screen.Cursor := OldCursor;
    if found then
    begin
      FindSearch := false;
      ChangeFindCaption;
      UpdateStatusLabel;
    end
    else
    begin
      MessageDlg('Больше ничего не найдено.', mtInformation,
        [mbOK], 0);
      Query1.GotoBookmark(CurrentPos);
    end;
    Query1.EnableControls;
    Query1.FreeBookmark(CurrentPos);
  end; { конец работы с Form1 }
end;
 
procedure TForm1.FindButtonClick(Sender: TObject);
begin
  if FindSearch then
    SearchName('first', Page0Edit.Text)
  else
    SearchName('next', Page0Edit.Text);
end;

Взято с https://delphiworld.narod.ru