Software and hardware configuration
The following tables try to sum up all available possibilities, and give some benchmark (average objects/second for writing or reading).
In these tables:
- 'SQLite3 (file full/off/exc)' indicates use of the internal SQLite3
engine, with or without Synchronous := smOff
and/or
DB.LockingMode := lmExclusive
;
- 'SQLite3 (mem)' stands for the internal SQLite3 engine running in
memory;
- 'SQLite3 (ext ...)' is about access to a SQLite3 engine as external
database , either as file or memory;
- 'TObjectList
' indicates a
TSQLRestServerStaticInMemory
instance, either static (with no SQL
support) or virtual (i.e. SQL featured via SQLite3 virtual table
mechanism) which may persist the data on disk as JSON or compressed
binary;
- 'NexusDB' is the free embedded edition, available from official site;
- 'Jet' stands for a MSAccess database engine, accessed via
OleDB.
- 'Oracle' shows the results of our direct OCI access layer
(SynDBOracle.pas
);
- 'Zeos *' indicates that the database was accessed directly via the ZDBC
layer;
- 'FireDAC *' stands for FireDAC library;
- 'UniDAC *' stands for UniDAC library;
- 'BDE *' when using a BDE connection;
- 'ODBC *' for a direct access to ODBC.
This list of database providers is to be extended in the future. Any feedback is welcome!
Numbers are expressed in rows/second (or objects/second). This benchmark was compiled with Delphi XE4, since newer compilers tends to give better results, mainly thanks to function in-lining (which was not existing e.g. in Delphi 6-7).
Note that these tests are not about the relative speed of each database engine, but reflect the current status of the integration of several DB libraries within the mORMot database access.
Benchmark was run on a Core i7 notebook, running Windows
7, with a standard SSD, including anti-virus and background
applications:
- Linked to a shared Oracle 11.2.0.1 database over 100 Mb
Ethernet;
- MS SQL Express 2008 R2 running locally in 64 bit mode;
- IBM DB2 Express-C edition 10.5 running locally in 64 bit mode;
- PostgreSQL 9.2.7 running locally in 64 bit mode;
- MySQL 5.6.16 running locally in 64 bit mode;
- Firebird embedded in revision 2.5.2;
- NexusDB 3.11 in Free Embedded Version.
So it was a development environment, very similar to low-cost production site, not dedicated to give best performance. During the process, CPU was noticeable used only for SQLite3 in-memory and TObjectList - most of the time, the bottleneck is not the CPU, but the storage or network. As a result, rates and timing may vary depending on network and server load, but you get results similar to what could be expected on customer side, with an average hardware configuration. When using high-head servers and storage, running on a tuned Linux configuration, you can expect even better numbers.
Tests were compiled with the Delphi XE4 32 bit mode target platform. Most of the tests do pass when compiled as a 64 bit executable, with the exception of some providers (like Jet), not available on this platform. Speed results are almost the same, only slightly slower; so we won't show them here.
You can compile the "15 - External DB performance
" supplied
sample code, and run the very same benchmark on your own configuration.
Feedback is welcome!
From our tests, the UniDAC version we were using had huge stability issues when used with DB2: the tests did not pass, and the DB2 server just hang processing the queries, whereas there was no problem with other libraries. It may have been fixed since, but you won't find any "UniDAC DB2" results in the benchmark below in the meanwhile.
Insertion speed
Here we insert 5,000 rows of data, with diverse scenarios:
- 'Direct' stands for a individual Client.Add()
insertion;
- 'Batch' mode will be described 28;
- 'Trans' indicates that all insertion is nested within a transaction - which
makes a great difference, e.g. with a SQlite3 database.
Direct | Batch | Trans | Batch Trans | |
SQLite3 (file full) | 462 | 356 | 95377 | 130086 |
SQLite3 (file off) | 844 | 821 | 100389 | 136675 |
SQLite3 (file off exc) | 28847 | 35316 | 102599 | 144258 |
SQLite3 (mem) | 89456 | 120513 | 104249 | 146933 |
TObjectList (static) | 314465 | 543892 | 326370 | 542652 |
TObjectList (virtual) | 325393 | 545672 | 298846 | 545018 |
SQLite3 (ext full) | 424 | 11297 | 102049 | 164636 |
SQLite3 (ext off) | 830 | 21406 | 109706 | 189250 |
SQLite3 (ext off exc) | 41589 | 180759 | 108481 | 192071 |
SQLite3 (ext mem) | 101440 | 234576 | 113530 | 190142 |
ODBC SQLite3 | 492 | 11746 | 35367 | 82425 |
ZEOS SQlite3 | 494 | 11851 | 56206 | 85705 |
FireDAC SQlite3 | 26369 | 50306 | 49755 | 155115 |
UniDAC SQlite3 | 477 | 8725 | 26552 | 38756 |
ODBC Firebird | 1495 | 18056 | 13485 | 17731 |
ZEOS Firebird | 9733 | 13429 | 26348 | 30616 |
FireDAC Firebird | 24233 | 52021 | 24791 | 52111 |
UniDAC Firebird | 5986 | 14809 | 6522 | 14948 |
Jet | 4235 | 4424 | 4954 | 5094 |
NexusDB | 5998 | 15494 | 7687 | 18619 |
Oracle | 226 | 56112 | 1133 | 52367 |
ODBC Oracle | 236 | 1664 | 1515 | 7709 |
FireDAC Oracle | 118 | 48575 | 1519 | 12566 |
UniDAC Oracle | 164 | 5701 | 1215 | 2884 |
BDE Oracle | 489 | 927 | 839 | 1022 |
MSSQL local | 5246 | 54360 | 12988 | 62453 |
ODBC MSSQL | 4911 | 18652 | 11541 | 20976 |
FireDAC MSSQL | 5016 | 7341 | 11686 | 51242 |
UniDAC MSSQL | 4392 | 29768 | 8649 | 33464 |
ODBC DB2 | 4792 | 48387 | 14085 | 70104 |
FireDAC DB2 | 4452 | 48635 | 11014 | 52781 |
ZEOS PostgreSQL | 4196 | 26663 | 9689 | 38735 |
ODBC PostgreSQL | 4068 | 19515 | 5130 | 27843 |
FireDAC PostgreSQL | 4181 | 37000 | 10111 | 36483 |
UniDAC PostgreSQL | 2705 | 18563 | 4442 | 22317 |
ODBC MySQL | 3160 | 38309 | 10856 | 47630 |
ZEOS MySQL | 3426 | 34037 | 12217 | 40186 |
FireDAC MySQL | 3078 | 43053 | 10955 | 45781 |
UniDAC MySQL | 3119 | 27772 | 11246 | 33288 |
Due to its ACID implementation, SQLite3 process on file waits for the hard-disk to have finished flushing its data, therefore it is the reason why it is slower than other engines at individual row insertion (less than 10 objects per second with a mechanical hardrive instead of a SDD) outside the scope of a transaction.
So if you want to reach the best writing performance in your application
with the default engine, you should better use transactions and regroup all
writing into services or a BATCH process. Another possibility could be to
execute DB.Synchronous := smOff
and/or DB.LockingMode :=
lmExclusive
at SQLite3 engine level before process: in case of
power loss at wrong time it may corrupt the database file, but it will increase
the rate by a factor of 50 (with hard drive), as stated
by the "off" and "off exc" rows of the table. Note that
by default, the FireDAC library set both options, so results above are
to be compared with "SQLite3 off exc" rows.
For both our direct Oracle access SynDBOracle.pas
unit
and FireDAC library , BATCH process benefits of the array binding
feature a lot (known as Array DML in FireDAC/AnyDAC).
For most engines, our ORM kernel is able to generate the appropriate SQL
statement for speeding up bulk insertion. For instance:
- SQlite3, MySQL, PostgreSQL, MSSQL 2008, DB2, MySQL or
NexusDB handle INSERT
statements with multiple
INSERT INTO .. VALUES (..),(..),(..)..
;
- Oracle handles INSERT INTO .. INTO .. SELECT 1 FROM
DUAL
(weird syntax, isn't it?);
- Firebird implements EXECUTE BLOCK
.
As a result, some engines show a nice speed boost when
BatchAdd()
is used. Even SQLite3 is faster when used as
external engine, in respect to direct execution! This feature is at ORM/SQL
level, so it benefits to any external database library. Of course, if a given
library has a better implementation pattern (e.g. our direct Oracle or
FireDAC with native array binding), it is used instead.
Reading speed
Now the same data is retrieved via the ORM layer:
- 'By one' states that one object is read per call (ORM generates a
SELECT * FROM table WHERE ID=?
for Client.Retrieve()
method);
- 'All *' is when all 5000 objects are read in a single call (i.e. running
SELECT * FROM table
from a FillPrepare()
method
call), either forced to use the virtual table layer, or with direct static
call.
Here are some reading speed values, in objects/second:
By one | All Virtual | All Direct | |
SQLite3 (file full) | 27284 | 558721 | 550842 |
SQLite3 (file off) | 26896 | 549450 | 526149 |
SQLite3 (file off exc) | 128077 | 557537 | 535905 |
SQLite3 (mem) | 127106 | 557537 | 563316 |
TObjectList (static) | 300012 | 912408 | 913742 |
TObjectList (virtual) | 303287 | 402706 | 866551 |
SQLite3 (ext full) | 135380 | 267436 | 553158 |
SQLite3 (ext off) | 133696 | 262977 | 543065 |
SQLite3 (ext off exc) | 134698 | 264186 | 558596 |
SQLite3 (ext mem) | 137487 | 259713 | 557475 |
ODBC SQLite3 | 19461 | 136600 | 201280 |
ZEOS SQlite3 | 33541 | 200835 | 306955 |
FireDAC SQlite3 | 7683 | 83532 | 112470 |
UniDAC SQlite3 | 2522 | 74030 | 96420 |
ODBC Firebird | 3446 | 69607 | 97585 |
ZEOS Firebird | 20296 | 91974 | 107229 |
FireDAC Firebird | 2376 | 46276 | 56269 |
UniDAC Firebird | 2189 | 66886 | 88102 |
Jet | 2640 | 166112 | 258277 |
NexusDB | 1413 | 120845 | 208246 |
Oracle | 1558 | 120977 | 159861 |
ODBC Oracle | 1620 | 43441 | 45764 |
FireDAC Oracle | 1231 | 42149 | 54795 |
UniDAC Oracle | 688 | 27083 | 30093 |
BDE Oracle | 860 | 3870 | 4036 |
MSSQL local | 10135 | 210837 | 437905 |
ODBC MSSQL | 12458 | 147544 | 256502 |
FireDAC MSSQL | 3776 | 72123 | 94091 |
UniDAC MSSQL | 2505 | 93231 | 135932 |
ODBC DB2 | 7649 | 84880 | 124486 |
FireDAC DB2 | 3155 | 71456 | 88264 |
ZEOS PostgreSQL | 8833 | 158760 | 223583 |
ODBC PostgreSQL | 10361 | 85680 | 120913 |
FireDAC PostgreSQL | 2261 | 58252 | 79002 |
UniDAC PostgreSQL | 864 | 86900 | 122856 |
ODBC MySQL | 10143 | 65538 | 82447 |
ZEOS MySQL | 2052 | 171803 | 245772 |
FireDAC MySQL | 3636 | 75081 | 105028 |
UniDAC MySQL | 4798 | 99940 | 146968 |
The SQLite3 layer gives amazing reading results, which makes it a
perfect fit for most typical ORM use. When running with DB.LockingMode :=
lmExclusive
defined (i.e. "off exc" rows), reading speed is very high,
and benefits from exclusive access to the database file. External database
access is only required when data is expected to be shared with other
processes.
In the above table, it appears that all libraries based on
DB.pas
are slower than the others for reading speed. In fact,
TDataSet
sounds to be a real bottleneck, due to its internal data
marshalling. Even FireDAC, which is known to be very optimized for
speed, is limited by the TDataSet
structure. Our direct classes,
or even ZEOS/ZDBC performs better, since they are able to output JSON content
with no additional marshalling.
For both writing and reading, TObjectList
/
TSQLRestServerStaticInMemory
engine gives impressive results, but
has the weakness of being in-memory, so it is not ACID by design, and the data
has to fit in memory. Note that indexes are available for IDs and stored
AS_UNIQUE
properties.
As a consequence, search of non-unique values may be slow: the engine has to
loop through all rows of data. But for unique values (defined as stored
AS_UNIQUE
), both insertion and search speed is awesome, due to its
optimized O(1) hash algorithm - see the following benchmark, especially the
"By name" row for "TObjectList" columns, which correspond to
a search of an unique RawUTF8
property value via this hashing
method.
SQLite3 (file full) | SQLite3 (file off) | SQLite3 (mem) | TObjectList (static) | TObjectList (virt.) | SQLite3 (ext file full) | SQLite3 (ext file off) | SQLite3 (ext mem) | Oracle | Jet | |
By one | 10461 | 10549 | 44737 | 103577 | 103553 | 43367 | 44099 | 45220 | 901 | 1074 |
By name | 9694 | 9651 | 32350 | 70534 | 60153 | 22785 | 22240 | 23055 | 889 | 1071 |
All Virt. | 167095 | 162956 | 168651 | 253292 | 118203 | 97083 | 90592 | 94688 | 56639 | 52764 |
All Direct | 167123 | 144250 | 168577 | 254284 | 256383 | 170794 | 165601 | 168856 | 88342 | 75999 |
Above table results were run on a Core 2 duo laptop, so numbers are lower than with the previous tables.
During the tests, internal caching was disabled, so you may expect speed enhancements for real applications, when data is more read than written: for instance, when an object is retrieved from the cache, you achieve more than 1,00,000 read requests per second, whatever database is used.
Analysis and use case proposal
When declared as virtual table (via a VirtualTableRegister
call), you have the full power of SQL (including JOINs) at hand, with
incredibly fast CRUD operations: 100,000 requests per second for objects read
and write, including serialization and Client-Server communication!
Some providers are first-class citizens to mORMot, like
SQLite3, Oracle, MS SQL, PostgreSQL, MySQL or IBM
DB2. You can connect to them without the bottleneck of the DB.pas
unit, nor any restriction of your Delphi license (a Starter edition is
enough).
First of all, SQLite3 is still to be considered, even for a production server. Thanks to mORMot's architecture and design, this "embedded" database could be used as main database engine for a client-server application with heavy concurrent access - if you have doubts about its scaling abilities, see this blog article. Here, "embedded" is not restricted to "mobile", but sounds like a self-contained, zero-configuration proven engine.
Most recognized closed source databases are available:
- Direct access to Oracle gives impressive results in BATCH mode (aka
array binding). It may be an obligation if your end-customer stores already its
data in such a server, for instance, and want to leverage the licensing cost of
its own IT solution. Oracle Express edition is free, but somewhat
heavy and limited in terms of data/hardware size (see its licensing
terms);
- MS SQL Server, directly accessed via OleDB (or
ODBC) gives pretty good timing. A MS SQL Server 2008 R2
Express instance is pretty well integrated with the Windows
environment, for a very affordable price (i.e. for free) - the LocalDB
(MSI installer) edition is enough to start with, but also with data/hardware
size limitation, just like Oracle Express;
- IBM DB2 is another good candidate, and the Express-C ("C"
standing for Community) offers a no-charge opportunity to run an industry
standard engine, with no restriction on the data size, and somewhat high
hardware limitations (16 GB of RAM and 2 CPU cores for the latest 10.5 release)
or enterprise-level features;
- NexusDB may be considered, if you have existing Delphi code and data
- but it is less known and recognized as the its commercial competitors.
Open Source databases are worth considering, especially in
conjunction with an Open Source framework like mORMot:
- MySQL is the well-known engine used by a lot of web sites, mainly
with LAMP (Linux MySQL Apache PHP) configurations. Windows is
not the best platform to run it, but it could be a fairly good candidate,
especially in its MariaDB fork, which sounds more attractive those
days than the official main version, owned by Oracle;
- PostgreSQL is an Enterprise class database, with amazing features
among its Open Source alternatives, and really competes with commercial
solutions. Even under Windows, we think it is easy to install and administrate,
and uses less resource than the other commercial engines.
- Firebird gave pretty consistent timing, when accessed via Zeos/ZDBC.
We show here the embedded version, but the server edition is worth considering,
even if it has a less
To access those databases, OleDB, ODBC or ZDBC providers may also be used,
with direct access. But mORMot is very open-minded: you can use any
DB.pas
provider, e.g. FireDAC, UniDAC,
DBExpress, NexusDB or even the BDE, but with the
additional layer introduced by using a TDataSet
instance, at
reading.
Therefore, the typical use may be the following (int. meaning "internal", ext. for "external", mem for "in-memory"):
Database | Use case |
int. SQLite3 file | Created by default. General safe data handling, with amazing speed in "off exc" mode |
int. SQLite3 mem | Created with :memory: file name.Fast data handling with no persistence (e.g. for testing or temporary storage) |
TObjectList static |
Created with StaticDataCreate .Best possible performance for small amount of data, without ACID nor SQL |
TObjectList virtual |
Created with VirtualTableRegister .Best possible performance for SQL over small amount of data (or even unlimited amount under Win64), if ACID is not required nor complex SQL |
ext. SQLite3 file | Created with VirtualTableExternalRegister External
back-end, e.g. for disk spanning |
ext. SQLite3 mem | Created with VirtualTableExternalRegister Fast external
back-end (e.g. for testing) |
ext. Oracle / MS SQL / DB2 / PostgreSQL / MySQL / Firebird | Created with VirtualTableExternalRegister Fast, secure
and industry standard back-ends; data can be shared outside
mORMot |
ext. NexusDB | Created with VirtualTableExternalRegister The free
embedded version let the whole engine be included within your executable, and
use any existing code, but SQlite3 sounds like a better option |
ext. Jet | Created with VirtualTableExternalRegister Could be used
as a data exchange format (e.g. with Office applications) |
ext. Zeos | Created with VirtualTableExternalRegister Allow access to
several external engines, with direct Zeos/ZDBC access which will by-pass the
DB.pas unit and its TDataSet bottleneck - and we will
also prefer an active Open Source project! |
ext. FireDAC/UniDAC | Created with VirtualTableExternalRegister Allow access to
several external engines, including the DB.pas unit and its
TDataSet bottleneck |
Whatever database back-end is used, don't forget that mORMot design
will allow you to switch from one library to another, just by changing a
TSQLDBConnectionProperties
class type. And note that you can
mix external engines, on purpose: you are not tied to one single
engine, but the database access can be tuned for each ORM table, according to
your project needs.
Feedback is welcome in our forum, as usual.