BDE против ADO в Дельфи

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

Недавно мы модифицировали большое приложение Delphi для использования соединений и запросов ADO вместо соединений и запросов BDE. После этого изменения производительность стала ужасной.

Я профилировал приложение, и узкое место, кажется, при фактическом вызове TADOQuery.Open, Другими словами, я мало что могу сделать с точки зрения кода, чтобы улучшить это, кроме реструктуризации приложения, чтобы фактически использовать базу данных меньше.

У кого-нибудь есть предложения о том, как улучшить производительность приложения Delphi, подключенного к ADO? Я испробовал оба предложения, приведенные здесь, практически безрезультатно.

Чтобы дать представление о разнице в производительности, я протестировал ту же самую крупную операцию:

  • Под BDE: 11 секунд

  • Под ADO: 73 секунды

  • Под ADO после изменений, на которые ссылается эта статья: 72 секунды

Мы используем серверную часть Oracle в среде клиент-сервер. Каждый из локальных компьютеров поддерживает отдельное соединение с базой данных.

Для записи строка подключения выглядит так:

const
  c_ADOConnString = 'Provider=OraOLEDB.Oracle.1;Persist Security Info=True;' +
                    'Extended Properties="plsqlrset=1";' +
                    'Data Source=DATABASE.DOMAIN.COM;OPTION=35;' +
                    'User ID=******;Password=*******';

Чтобы ответить на вопросы, заданные Зендаром:

Я использую Delphi 2007 на Windows Vista и XP.

Серверная часть - это база данных Oracle 10g.

Как указано в строке подключения, мы используем драйвер OraOLEDB.

Версия MDAC на моем тестовом компьютере - 6.0.

Редактировать:

Под BDE у нас было много кода, который выглядел так:

procedure MyBDEProc;
var
  qry: TQuery;
begin
  //fast under BDE, but slow under ADO!!
  qry := TQuery.Create(Self);
  try
    with qry do begin
      Database := g_Database;
      Sql.Clear;
      Sql.Add('SELECT');
      Sql.Add('  FIELD1');
      Sql.Add(' ,FIELD2');
      Sql.Add(' ,FIELD3');
      Sql.Add('FROM');
      Sql.Add('  TABLE1');
      Sql.Add('WHERE SOME_FIELD = SOME_CONDITION');
      Open;
      //do something
      Close;
    end;  //with
  finally
    FreeAndNil(qry);
  end;  //try-finally
end;  //proc

Но мы обнаружили, что призыв к Sql.Add на самом деле очень дорого под ADO, потому что QueryChanged событие срабатывает каждый раз, когда вы меняете CommandText, Таким образом, замена вышеупомянутого была НАМНОГО быстрее:

procedure MyADOProc;
var
  qry: TADOQuery;
begin
  //fast(er) under ADO
  qry := TADOQuery.Create(Self);
  try
    with qry do begin
      Connection := g_Connection;
      Sql.Text := ' SELECT ';
        + '   FIELD1 '
        + '  ,FIELD2 '
        + '  ,FIELD3 '
        + ' FROM '
        + '  TABLE1 '
        + ' WHERE SOME_FIELD = SOME_CONDITION ';
      Open;
      //do something
      Close;
    end;  //with
  finally
    FreeAndNil(qry);
  end;  //try-finally
end;  //proc

А еще лучше, вы можете скопировать TADOQuery из ADODB.pas, переименуйте его под новым именем и вытащите QueryChanged Событие, которое, насколько я могу судить, не приносит ничего полезного. Затем используйте новую модифицированную версию TADOQuery вместо собственной.

type
  TADOQueryTurbo = class(TCustomADODataSet)
  private
    //
  protected
    procedure QueryChanged(Sender: TObject);
  public
    FSQL: TWideStrings;
    FRowsAffected: Integer;
    function GetSQL: TWideStrings;
    procedure SetSQL(const Value: TWideStrings);
    procedure Open;
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    function ExecSQL: Integer; {for TQuery compatibility}
    property RowsAffected: Integer read FRowsAffected;
  published
    property CommandTimeout;
    property DataSource;
    property EnableBCD;
    property ParamCheck;
    property Parameters;
    property Prepared;
    property SQL: TWideStrings read FSQL write SetSQL;
  end;
////////////////////////////////////////////////////////
////////////////////////////////////////////////////////
////////////////////////////////////////////////////////
constructor TADOQueryTurbo.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FSQL := TWideStringList.Create;
  TWideStringList(FSQL).OnChange := QueryChanged;
  Command.CommandText := 'SQL'; { Do not localize }
end;

destructor TADOQueryTurbo.Destroy;
begin
  inherited;
 inherited Destroy;
  FreeAndNil(FSQL);
end;

function TADOQueryTurbo.ExecSQL: Integer;
begin
  CommandText := FSQL.Text;
  inherited;
end;

function TADOQueryTurbo.GetSQL: TWideStrings;
begin
  Result := FSQL;
end;

procedure TADOQueryTurbo.Open;
begin
  CommandText := FSQL.Text;
  inherited Open;
end;

procedure TADOQueryTurbo.QueryChanged(Sender: TObject);
begin
// if not (csLoading in ComponentState) then
//    Close;
// CommandText := FSQL.Text;
end;

procedure TADOQueryTurbo.SetSQL(const Value: TWideStrings);
begin
  FSQL.Assign(Value);
  CommandText := FSQL.Text;
end;

3 ответа

Решение

Я не знаю о Delphi 2007, но я сделал то же самое с Delphi 7 и Oracle 8.

Вот что я сделал:

  • Установите TAdoDataSet.CursorLocation в соответствии с запросом:
    • clUseClient, если запрос выбирает записи для графического интерфейса пользователя, и запрос относительно "прост" - нет группировки или суммы
    • clUseServer, если запрос имеет некоторую агрегацию (сумма, группировка, подсчет)
  • Установите TAdoDataSet.CursorType согласно запросу:
    • ctForwardOnly для отчетов, где вам не нужно прокручивать набор данных - работает только с clUseServer
    • ctStatic для GUI. Это единственный режим, который работает с clUseClient
  • Установите TAdoDataSet.LockType в соответствии с запросом:
    • ltReadOnly для каждого набора данных, который не используется для редактирования (сетки, отчеты)
    • ltOptimistic, когда записи публикуются в базе данных сразу после изменения (например, пользователь редактирует данные в форме)
    • ltBatchOptimistic при изменении большого количества записей. Это для ситуаций, когда вы выбираете количество записей, затем выполняете их обработку, а затем отправляете обновления в базу данных в пакетном режиме. Это работает лучше всего в сочетании с clUseClient и ctStatic.
  • По моему опыту, поставщик Microsoft OLEDB для Oracle работал лучше, чем поставщик Oracle OleDb. Вы должны проверить это.
    Изменить: Проверьте комментарий Фабрицио о возможных проблемах с каплями.
  • Замените TAdoQUery на TAdoDataSet. TAdoQuery был создан для преобразования приложений из BDE в ADO, но рекомендацией Borland/Codegear было использование TAdoDataSet
  • Еще раз проверьте строку подключения Oracle, чтобы убедиться, что у вас нет задержки в сети. Как долго длится подключение к Oracle? Как долго длится TnsPing?

Я обнаружил проблемы с производительностью ADOExpress несколько лет назад:

Примечание: до того, как ADO стал стандартной частью Delphi, Borland продавал его как дополнение ADOExpress. Это были просто обертки вокруг COM-объектов Microsoft ActiveX Data Objects (ADO).

я проверил три сценария

  • использование ADO напрямую (то есть напрямую COM-объекты Microsoft)
  • использование ADOExpress (обертки объектов Borland вокруг ADO)
  • указав .DisableControls на TADOQuery перед звонком Open

Я узнал

  • использование Query.DisableControls сделать каждый звонок .Next В 50 раз быстрее
  • использование Query.Recordset.Fields.Items['columnName'].Value скорее, чем Query.FieldByName('columnName') сделать поиск значений в 2,7 раза быстрее
  • с помощью TADODataSet (стихи TADOQuery) не имеет значения

                                    Loop Results        Get Values 
    ADOExpress:                         28.0s              46.6s 
    ADOExpress w/DisableControls:        0.5s              17.0s 
    ADO (direct use of interfaces):      0.2s               4.7s 
    

Примечание. Эти значения предназначены для зацикливания 20 881 строки и поиска значений в 21 столбце.

Базовый плохой код:

var
   qry: TADOQuery;
begin
   qry := TADOQuery.Create(nil);
   try
      qry.SQL.Add(CommandText);
      qry.Open;
      while not qry.EOF do
      begin
         ...
         qry.Next;
      end;

Используйте DisableControls, чтобы сделать цикл на 5000% быстрее:

var
   qry: TADOQuery;
begin
   qry := TADOQuery.Create(nil);
   try 
      qry.DisableControls;
      qry.SQL.Add(CommandText);
      qry.Open;
      while not qry.EOF do
      begin
         ...
         qry.Next;
      end;

Используйте коллекцию Fields для ускорения поиска значений на 270%:

var
   qry: TADOQuery;
begin
   qry := TADOQuery.Create(nil);
   try 
      qry.DisableControls;
      qry.SQL.Add(CommandText);
      qry.Open;
      while not qry.EOF do
      begin
         value1 := VarAsString(qry.Recordset.Fields['FieldOne'].Value);
         value2 := VarAsInt(qry.Recordset.Fields['FieldTwo'].Value);
         value3 := VarAsInt64(qry.Recordset.Fields['FieldTwo'].Value);
         value4 := VarAsFloat(qry.Recordset.Fields['FieldThree'].Value);
         value5 := VarAsWideString(qry.Recordset.Fields['FieldFour'].Value);
         ...
         value56 := VarAsMoney(qry.Recordset.Fields['FieldFive'].Value);
         qry.Next;
      end;

Поскольку это достаточно распространенная проблема, мы создали вспомогательный метод для ее решения:

class function TADOHelper.Execute(const Connection: TADOConnection; 
       const CommandText: WideString): TADOQuery;
var
   rs: _Recordset;
   query: TADOQuery;
   nRecords: OleVariant;
begin
   Query := TADOQuery.Create(nil);
   Query.DisableControls; //speeds up Query.Next by a magnitude
   Query.Connection := Connection;
   Query.SQL.Text := CommandText;
   try
      Query.Open();
   except
      on E:Exception do
      begin
         Query.Free;
         raise;
      end;
   end;
   Result := Query;
end;

Для лучшей производительности, взгляните на наш открытый доступ к Oracle с открытым исходным кодом.

Если вы обрабатываете много TQuery, без использования компонентов DB, ​​у нас есть выделенный псевдокласс для использования прямого соединения OCI, как таковой:

 Q := TQuery.Create(aSQLDBConnection);
 try
   Q.SQL.Clear; // optional
   Q.SQL.Add('select * from DOMAIN.TABLE');
   Q.SQL.Add('  WHERE ID_DETAIL=:detail;');
   Q.ParamByName('DETAIL').AsString := '123420020100000430015';
   Q.Open;
   Q.First;    // optional
   while not Q.Eof do begin
     assert(Q.FieldByName('id_detail').AsString='123420020100000430015');
     Q.Next;
   end;
   Q.Close;    // optional
 finally
   Q.Free;
 end;

И я добавил некоторый уникальный доступ через вариант с поздним связыванием для написания прямого кода как такового:

procedure Test(Props: TOleDBConnectionProperties; const aName: RawUTF8);
var I: ISQLDBRows;
    Customer: Variant;
begin
  I := Props.Execute('select * from Domain.Customers where Name=?',[aName],@Customer);
  while I.Step do
    writeln(Customer.Name,' ',Customer.FirstName,' ',Customer.Address);
end;

var Props: TOleDBConnectionProperties;
begin
  Props := TSQLDBOracleConnectionProperties.Create(
    'TnsName','UserName','Password',CODEPAGE_US);
  try
    Test(Props,'Smith');
  finally
    Props.Free;
  end;
end;

Обратите внимание, что все поставщики OleDB содержат ошибки для обработки больших двоичных объектов: версия Microsoft просто не обрабатывает их, а версия Oracle будет случайным образом возвращать ноль для 1/4 строк...

В реальной базе данных я обнаружил, что наши прямые классы OCI в 2-5 раз быстрее, чем поставщик OleDB, без необходимости устанавливать этот поставщик. Вы даже можете использовать Oracle Instant Client, предоставляемый Oracle, который позволяет запускать ваши приложения без установки стандартного (огромного) клиента Oracle или наличия ORACLE_HOME. Просто доставьте DLL-файлы в тот же каталог, что и ваше приложение, и все заработает.

Другие вопросы по тегам