德尔福中的 BDE 与 ADO

发布于 2024-07-09 15:27:19 字数 4278 浏览 9 评论 0原文

请注意下面的编辑,以获取更多信息和可能的解决方案

我们最近修改了一个大型 Delphi 应用程序,以使用 ADO 连接和查询,而不是 BDE 连接和查询。 自从那次改变之后,性能变得很糟糕。

我已经分析了该应用程序,瓶颈似乎出现在对 TADOQuery.Open 的实际调用处。 换句话说,从代码的角度来看,除了重组应用程序以实际减少数据库的使用之外,我无能为力来改进这一点。

有人对如何提高 ADO 连接的 Delphi 应用程序的性能有建议吗? 我已经尝试了此处给出的建议,几乎没有任何影响。

为了了解性能差异,我对相同的大型操作进行了基准测试:

  • 在 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=*******';

回答 zendar 提出的问题:

我在 Windows Vista 和 XP 上使用 Delphi 2007。

后端是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

但是我们发现在ADO下调用Sql.Add实际上是非常昂贵的,因为每次更改 CommandText 时都会触发 QueryChanged 事件。 因此,用此替换上面的内容要快得多:

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

更好的是,您可以从 ADODB.pas 中复制 TADOQuery,将其重命名为新名称,然后删除 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;

Please note the Edit below for a lot more information, and a possible solution

We recently modified a large Delphi application to use ADO connections and queries instead of BDE connections and queries. Since that change, performance has become terrible.

I've profiled the application and the bottleneck seems to be at the actual call to TADOQuery.Open. In other words, there isn't much I can do from a code standpoint to improve this, other than restructuring the application to actually use the database less.

Does anyone have suggestions about how to improve the performance of an ADO-connected Delphi application? I've tried both of the suggestions given here, with virtually no impact.

To give an idea of the performance difference, I benchmarked the same large operation:

  • Under BDE: 11 seconds

  • Under ADO: 73 seconds

  • Under ADO after the changes referenced by that article: 72 seconds

We are using an Oracle back-end in a client-server environment. Local machines each maintain a separate connection to the database.

For the record, the connection string looks like this:

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=*******';

To answer the questions posed by zendar:

I'm using Delphi 2007 on Windows Vista and XP.

The back end is an Oracle 10g database.

As indicated by the connection string, we are using the OraOLEDB driver.

The MDAC version on my benchmark machine is 6.0.

Edit:

Under the BDE, we had a lot of code that looked like this:

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

But we found that the call to Sql.Add is actually very expensive under ADO, because the QueryChanged event is fired every time you change the CommandText. So replacing the above with this was MUCH faster:

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

Better yet, you can copy TADOQuery out of ADODB.pas, rename it under a new name, and rip out the QueryChanged event, which as far as I can tell, is not doing anything useful at all. Then use your new, modified version of TADOQuery, instead of the native one.

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;

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

他夏了夏天 2024-07-16 15:27:19

我不知道 Delphi 2007,但我对 Delphi 7 和 Oracle 8 做了同样的事情。

以下是我所做的事情:

  • 根据查询设置 TAdoDataSet.CursorLocation
    • clUseClient 如果查询获取 GUI 的记录并且查询相对“简单” - 没有分组或求和
    • clUseServer如果查询具有某种聚合(求和、分组、计数)
  • 根据查询设置TAdoDataSet.CursorType
    • ctForwardOnly 适用于不需要向后滚动数据集的报告 - 仅适用于clUseServer
    • ctStatic 用于 GUI。 这是唯一适用于 clUseClient
    • 的模式

  • 根据查询设置TAdoDataSet.LockType
    • ltReadOnly 对于不用于编辑的每个数据集(网格、报告)
    • 乐观当记录在更改后立即发布到数据库(例如用户编辑表单上的数据)
    • ltBatchOptimistic 当您更改大量记录时。 这适用于您获取大量记录,然后对它们进行一些处理,然后将更新批量发送到数据库的情况。 与 clUseClient 和 ctStatic 结合使用效果最佳。
  • 根据我的经验,Oracle 的 Microsoft OLEDB 提供程序比 Oracle OleDb 提供程序工作得更好。 你应该测试一下。
    编辑:检查 Fabricio 关于可能出现的斑点问题的评论。
  • TAdoQUery 替换为 TAdoDataSet。 TAdoQuery 是为了将应用程序从 BDE 转换为 ADO 而创建的,但 Borland/Codegear 建议使用 TAdoDataSet
  • 重新检查 Oracle 连接字符串,以确保没有网络延迟。 连接到 Oracle 需要多长时间? TnsPing 需要多长时间?

I don't know about Delphi 2007, but I did same thing with Delphi 7 and Oracle 8.

Here are things I did:

  • Set TAdoDataSet.CursorLocation according to query:
    • clUseClient if query fetches records for GUI and query is relatively "simple" - no grouping or sum
    • clUseServer if query have some sort of aggregation (sum, grouping, counting)
  • Set TAdoDataSet.CursorType according to query:
    • ctForwardOnly for reports where you don't need scroll back through dataset - works only with clUseServer
    • ctStatic for GUI. This is only mode that works with clUseClient
  • Set TAdoDataSet.LockType according to query:
    • ltReadOnly for every dataset that is not used for editing (grids, reports)
    • ltOptimistic when records are posted to database immediately after change (e.g. user editing data on form)
    • ltBatchOptimistic when you change large number of records. This is for situations where you fetch number of records, then do some processing on them and then send updates to database in batch. This works best combined with clUseClient and ctStatic.
  • In my experience, Microsoft OLEDB provider for Oracle worked better than Oracle OleDb provider. You should test that.
    Edit: Check Fabricio's comment about possible blob problems.
  • Replace TAdoQUery with TAdoDataSet. TAdoQuery was created for conversion of apps from BDE to ADO, but Borland/Codegear recomendation was to use TAdoDataSet
  • Recheck Oracle connection string to be sure that you do not have network latency. How long it lasts to connect to Oracle? How long is TnsPing?
痴情 2024-07-16 15:27:19

我几年前就发现了 ADOExpress 的性能问题:

注意:在 ADO 成为 Delphi 的标准部分之前,Borland 将其作为名为 ADOExpress 的插件进行销售。 它只是 Microsoft ActiveX 数据对象 (ADO) COM 对象的对象包装器。

我已经

  • 直接使用 ADO(即直接使用 Microsoft 的 COM 对象)
  • 使用 ADOExpress(Borland 围绕 ADO 的对象包装器)
  • 测试了三种场景,在调用 Open 之前在 TADOQuery 上指定 .DisableControls

我发现

  • 使用Query.DisableControls使每次调用.Next速度提高50倍,
  • 使用Query.Recordset.Fields.Items['columnName'].Value 而不是 Query.FieldByName('columnName') 使每个值查找速度提高 2.7 倍
  • 使用 TADODataSet(对比 TADOQuery) 没有区别

     循环结果获取值  
      ADOExpress:28.0秒46.6秒  
      带禁用控件的 ADOExpress:0.5s 17.0s  
      ADO(直接使用接口):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;

i found the performance problems with ADOExpress years ago:

Note: Before ADO became a standard part of Delphi, Borland was selling it as an addon called ADOExpress. It was simply object wrappers around Microsoft's ActiveX Data Objects (ADO) COM objects.

i had tested three scenarios

  • using ADO directly (i.e. Microsoft's COM objects directly)
  • using ADOExpress (Borland's object wrappers around ADO)
  • specifying .DisableControls on the TADOQuery before calling Open

i discovered

  • use Query.DisableControls to make each call .Next 50x faster
  • use Query.Recordset.Fields.Items['columnName'].Value rather than Query.FieldByName('columnName') to make each value lookup 2.7x faster
  • using TADODataSet (verses TADOQuery) makes no difference

                                    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 
    

Note: These values are for looping 20,881 rows, and looking up the values of 21 columns.

Baseline Bad Code:

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

Use DisableControls to make looping 5000% faster:

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;

Use Fields collection to make value lookups 270% faster:

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;

Since it is a common enough problem, we created a helper method to solve the issue:

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;
百变从容 2024-07-16 15:27:19

为了获得最佳性能,应该看看我们的 开源直接访问 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 提供程序在处理 BLOB 方面都存在错误:Microsoft 的版本只是不处理它们,而 Oracle 的版本将 为 1/4 行随机返回 null...

在真实数据库上,我发现我们的直接 OCI 类比 OleDB 提供程序快 2 到 5 倍,无需安装此提供程序。 您甚至可以使用 Oracle 提供的 Oracle Instant Client,它允许您无需安装标准(大型)Oracle 客户端或拥有 ORACLE_HOME 即可运行应用程序。 只需将 dll 文件放在与应用程序相同的目录中,它就可以工作。

For best performance, should get a look at our Open Source direct access to Oracle.

If you are processing a lot of TQuery, without using the DB components, we have a dedicated pseudo-class to use direct OCI connection, as such:

 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;

And I've added some unique access via a late-binding Variant, to write direct code as such:

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;

Note that all OleDB providers are buggy for handling BLOBs: Microsoft's version just do not handle them, and Oracle's version will randomly return null for 1/4 of rows...

On real database, I found out our direct OCI classes to be 2 to 5 times faster than the OleDB provider, without the need to install this provider. You can even use the Oracle Instant Client provided by Oracle which allows you to run your applications without installing the standard (huge) Oracle client or having an ORACLE_HOME. Just deliver the dll files in the same directory than your application, and it will work.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文