Data definition

The ORM record used for this test is defined as such:

type
  TSQLRecordSample = class(TSQLRecord)
  private
    fFirstName: RawUTF8;
    fLastName: RawUTF8;
    fAmount: currency;
    fBirthDate: TDateTime;
    fLastChange: TModTime;
    fCreatedAt: TCreateTime;
  published
    property FirstName: RawUTF8 index 40 read fFirstName write fFirstName;
    property LastName: RawUTF8 index 40 read fLastName write fLastName;
    property Amount: currency read fAmount write fAmount;
    property BirthDate: TDateTime read fBirthDate write fBirthDate;
    property LastChange: TModTime read fLastChange;
    property CreatedAt: TCreateTime read fCreatedAt write fCreatedAt;
  end;

So this is something you may have to use in your application, not a blank table with one row.
Note that we have specified index 40 for FirstName / LastName properties, to ensure that it would be sized to 40 widechars (without this index attribute, it would have created a NCLOB, which is much slower than a NVARCHAR2).

Insertion speed

The process consisted in inserting 5000 rows, with or without a BATCH mode, nested with or without a transaction.

The benchmark code is similar to this:

    Props := PropsClass.Create(Server,aDatabaseName,aUserID,aPassWord);
    try
      VirtualTableExternalRegister(Model,TSQLRecordSample,Props,'SampleRecord');
      Client := TSQLRestClientDB.Create(Model,nil,':memory:',TSQLRestServerDB,false,'');
      try
        Client.Server.CreateMissingTables;
        if aUseTransactions then
          Client.TransactionBegin(TSQLRecordSample);
        if not aUseBatch then
          SetLength(Res,Stat.fNumberOfElements);
        Value.BirthDate := EncodeDate(1900,1,1);
        for i := 1 to Stat.fNumberOfElements do begin
          Value.Amount := i*0.01;
          Value.LastName := Int32ToUtf8(i);
          Value.FirstName := 'Nam'#$c3#$a9' '+Value.LastName; // 'Namé' in UTF-8
          if aUseBatch then begin
            if Client.BatchCount=0 then
              Client.BatchStart(TSQLRecordSample);
            Client.BatchAdd(Value,true);
          end else
            Res[i-1] := Client.Add(Value,true);
          Value.BirthDate := Value.BirthDate+1;
        end;
        if aUseBatch then
          Client.BatchSend(Res);
        if aUseTransactions then
          Client.Commit;

For SQlite3 without transactions, it inserted only 30 rows instead of 5000, since SQLite3 is by design very slow outside a transaction, due to its ACID behavior which expects the data to be flushed to the hard-drive after every COMMIT, so after every INSERT if no transaction is defined.

Similarly, the Jet engine was tested only on 500 rows, since it was identified to be slow (no OleDB BULK mode is handled by now in SynOleDB).

Now, some data.

Benchmark was run on a good old Core 2 Duo workstation (no SSD), with anti-virus and background applications, over a 100 Mb corporate network, linked to a shared Oracle 11g database.
So it was a development environment, very similar to production site, not dedicated to give best performance. As a results, rates and timing may vary depending on network and server load, but you get results similar to what could be expected on customer side.

  Oracle     SQLite3     SQLite3 (memory)   Jet
Direct 476 9 26155 720
Batch 46373 10 31746 780
Trans 713 13295 32259 920
Batch Trans  53478 14436 40143 962

For Oracle database, insertion was speed up from 476 to 53478 rows per second!
More than a 100 x speed factor, with no code change on the client side - good plain BatchAdd() method used, as stated above.

You can note that Jet is slow, when compared to SQlite3 or Oracle, when it deals with insertion. And it consumes much more memory and disk space than SQLite3.
It is also not surprising that SQlite3 in-memory tables - identified as "SQLite3 (memory)" above - are pretty fast, but that due to its asynchronous remote work, Oracle is even faster than this in-memory database! Does make sense for the amount of money to be paid for its license. :)

Read speed

The code for reading is similar to the following (we did not include the internal assertions to enhance readability in this blog article):

          // one by one retrieve values from server
          for i := 0 to Stat.fNumberOfElements-1 do
            VClient.Retrieve(Res[i],Value);
          // retrieve all rows using SQLite3 virtual module (via ORDER clause)
          Value.FillPrepare(Client,'order by RowId');
          while Value.FillOne do begin
          end;
          // retrieve all rows by-passing SQLite3 virtual module (SELECT * from DB)
          Value.FillPrepare(Client);
          while Value.FillOne do begin
          end;

Data is therefore retrieved:

  • One row per one row (using a Retrieve method over an ID);
  • All rows at once (using a FillPrepare method), through the virtual table engine: the 'order by RowID' clause ensures it will pass via the SQLite3 virtual table module;
  • All rows at once (also via FillPrepare), directly accessing to the SynDB classes: in this release, we have added a direct request detection for a SELECT * statement: it is up to 30-50% faster (see "All Virtual" / "All Direct" values below), for instance when you call TSQLRecord.FillPrepare(aClient) with no where clause, to retrieve all rows at once.

Here are some speed results, run on the same hardware, over data just inserted:

  Oracle    SQLite3    SQLite3 (memory)   Jet
By one 794 44337 44408 1078
All Virtual   51406 90955 91177 51466
All Direct 79363 166950 166995 108908

If you want to use a map/reduce algorithm in your application, in addition to ORM data access, all those enhancements may speed up a lot your process. Reading and writing huge amount of data has never been so fast and easy: you may even be tempted to replace stored-procedure process by high-level code implemented in your Domain service. N-tier separation would benefit from it.

Note that all those tests were performed locally and in-process, via a TSQLRestClientDB instance. For both insertion and reading, a Client-Server architecture (e.g. using HTTP/1.1 for mORMot clients) will give even better results for BATCH and retrieve all modes.
Remember also that those values refer to virtual-table external databases: for classic TSQLRecord instance without a previous call to VirtualTableExternalRegister (i.e. internal SQlite3 tables), it will be even faster.

Sounds like we have gone far away into optimizations for external tables, here.
IMHO it would be pretty hard to find a faster ORM around.
And if you compare with classic TQuery process, you could be amazed by the speed achieved for free with mORMot, even with a Delphi Starter edition (the framework does not need a Professional nor Architect version)!

Feedback is welcome on our forum.
And since you have the benchmark program at hand, so you can publish and share your own data and graphs if you like to.