Performance Benchmarking of
PostgreSQL 17.5-1
Using a Java Object Model

Testing a Hierarchical Object Model with M:N Relationships,
Multiple Indexing Strategies, and Varying Database Sizes

This performance test evaluates PostgreSQL version 17.5-1 as the storage platform for the latest version of the RootDB object database, tested on May 20, 2025. The test procedures are identical to those previously conducted with earlier versions of PostgreSQL and MariaDB, as documented in link. However, note that the test computer has been upgraded: system memory has been increased from 16 GB to 32 GB, and the SATA-based SSD has been replaced with a significantly faster M.2 NVMe drive. The current performance of this upgraded storage device is demonstrated in the disk test results below.

Samsung_990_PRO_1_TB_ M2_NVMe_SSD_Benchmark_20250427

Performance evaluations are repeated here due to the hardware upgrades described above, improvements in RootDB’s internal algorithms, and the availability of a new version of the PostgreSQL database product. Developments in both RootDB and PostgreSQL may introduce changes that significantly affect performance. In particular, enhancements in indexing mechanisms, concurrency control, and transaction processing in PostgreSQL can have a substantial impact on RootDB performance.

The object model and the characteristics of the created test databases, including various indexing strategies and sizes, are described in the following section.

Object Model and Test Data for Performance Tests

The object model consists of 12 Java classes N1-N4, M1-M4 and S1-S4.

Object Model

Blue arrow relationships between classes are M:N (many-to-many). All M:N relationships have 1-3 child objects at random.

Black arrow relationships between classes are M:1 (many-to-one).

Twelve object databases were created to evaluate how performance is affected by the number of objects and the extent of indexing coverage.

The tests are categorized into three groups based on database size, with object counts of 12 x 100K, 12 x 1M, and 12 x 2M (where K=Kilo, M=Million), as shown in the table below.

Each category applies four different levels of indexing: no indexing (none), indexing only the value field in each class (value fields), indexing only the relationships of each class (all relations), and the broadest scope, which includes indexing all relationships in addition to the value field (value fields, all relations).

RootDB Object Databases on PostgreSQL
Database Name Number of Objects Indexes Creation Time (s), CT Objects per Second Database Size (GB)
       
ex100Knx 12 x 100K none 66 18187 1,47
ex100Kvx 12 x 100K value fields 70 17130
ex100Krx 12 x 100K all relations 206,42 5813
ex100Kvrx 12 x 100K value fields, all relations 206,11 5822 1,52
       
ex1Mnx 12 x 1M none 599 20044 14,84
ex1Mvx 12 x 1M value fields 696 17246
ex1Mrx 12 x 1M all relations 2066 5808
ex1Mvrx 12 x 1M value fields, all relations 2011 5966 15,13
       
ex2Mnx 12 x 2M none 1287 18643 29,54
ex2Mvx 12 x 2M value fields 1315 18256
ex2Mrx 12 x 2M all relations 4035 5948
ex2Mvrx 12 x 2M value fields, all relations 3979 6031 30,15

Processor = AMD Ryzen 7 3700X, Memory 32GB, Disk Samsung SSD 990 PRO M.2 NVMe 1TB

Creating object databases without indexing (none) or with only single-field indexing (value fields) on value fields is highly efficient, achieving write rates between 17,130 and 20,044 objects per second, independent of database size. Sustaining a throughput of approximately twenty thousand objects per second is an excellent result, particularly given that write performance remains consistent as the database grows.

Indexing all 14 relationships (with or without value fields) significantly reduces write performance, limiting throughput to 5,808–6,031 objects per second. Despite this reduction, the performance remains highly efficient, given that indexing is inherently a resource-intensive operation.

The excerpt below from Task Manager shows system behavior during concurrent writing by 20 threads, using the most extensive indexing configuration (value fields, all relations). During this operation, the CPU is fully utilized at 100%, while the SSD load remains at only about 4%. This suggests that the write performance is currently limited by CPU processing rather than disk throughput. Consequently, using a more powerful processor could potentially result in significantly higher object write speeds, as only a small fraction of the available disk capacity is being utilized.

taskManagerWvrx2M.jpg

PostgreSQL is well known for its robust indexing capabilities, and RootDB tests have confirmed its advantage over other open-source databases, demonstrating write performance that is 5 to 10 times more efficient in identical indexing scenarios.

Another particularly positive observation is that increasing the database size does not degrade object write performance. Write speed remains consistently stable, regardless of both database size and the extent of indexing.

The size of PostgreSQL databases appears to be largely unaffected by whether indexing is used. For example, the largest database without indexing occupies 29.54 GB, while the fully indexed version is only slightly larger at 30.15 GB. This suggests that PostgreSQL allocates a substantial amount of storage per record by default, and that indexing contributes relatively little to the overall space requirement.

The results above clearly show that PostgreSQL delivers superior performance when indexing is not used, and even when indexing is applied, its write speed of approximately 6,000 objects per second places it in a class of its own among open source database products. Although PostgreSQL may consume a relatively large amount of disk space, write performance is typically far more critical than storage efficiency in most database applications. Furthermore, indexing is essential for improving query execution speed, and its effectiveness is significantly more important than the additional disk space it requires.

RQL Queries with Variations in Indexing and Object Database Sizes

12 Object Databases Tested on PostgreSQL

The search conditions used for performance testing of the object databases described above are shown in the image below.

In all tests, the same search conditions are used, which searches M1 class objects from the database. The red X marks indicate the classes N3, M4, and S4, where search conditions are applied to the value fields within the data structure of class M1. The paths from the root class M1 to the search condition classes are shown with red dashed lines.

The found M1 objects are returned as complete data structures, including all objects that are reachable from the M1 root class. This of course includes classes N3, M4, and S4, which have the value search conditions, but also objects from classes M2, M3, and N4, which are reachable from class M1. All reachable classes from class M1 are marked with blue X marks.

Object Model

Blue arrow relationships between classes are M:N (many-to-many). All M:N relationships have 1-3 child objects at random.

Black arrow relationships between classes are M:1 (many-to-one).

The search conditons for the three value fields in classes N3 (value=976), S4 (value=50) and M4 (value=770) are represented as a RQL query expression below.

RQL query:

(m2.n3.value = 976, m2.m3.s4.value = 50, m2.n3.m4.value = 770)                        

The Java source code that executes the RQL query expression for the class M1 in the object database is shown below.

//Variable rdb is the opened RootDB object database.
//RootStore object is created for the M1 class.
RootStore r2 = rdb.getRootStore("fi.rootrql.ex100Knx.M1");
Query query =  Query.createQuery()
        		.addQ1Query(r2,
                    "(m2.n3.value = 976," +
                    " m2.m3.s4.value = 50," +
                    " m2.n3.m4.value = 770"
                    + ")");
List<M1> list = query.queryQ1Objects();
rdb.close();

An example of query results is shown below, containing the complete data structures of the retrieved four M1 objects. Note that, for example, the expanded [0]=M2 object has a list of three M3 objects in its field m3 and only the expanded object [2]=M3 has the searched value 50 in its s4=S4 object. Thus, the lists may contain objects that do not match the search criteria, but are included in the results because the returned M1 objects contain their complete data structures.

Query Result Example: The four found M1 objects are returned in a list, with the first M1 object expanded to display the value fields where the search conditions were applied.

ex100KnxQuery100Kresult1

The twelve object databases with variations in indexing and object counts were all tested using the same query described above. The results categorized by database size and indexing strategy are presented in the table below.

Cold Cache Test Scenario

Many database systems can accelerate subsequent searches if the same, or even partially overlapping, data has already been retrieved during a previous query. Therefore, the results shown below represent a cold start scenario, where the database service has just been restarted and no prior data is cached. In other words, from a database performance standpoint, the search results are expected to reflect the longest possible execution times.

RootDB Object Databases on PostgreSQL, Cold Cache Execution Times / Seconds  
Database Name Number of Objects Indexes Number of Found Objects Total Create Objects Search
             
ex100Knx 12 x 100K none 4 9,362 0,031 9,331
ex100Kvx 12 x 100K value fields 4 4,107 0,033 4,074
ex100Krx 12 x 100K all relations 4 2,500 0,021 2,479
ex100Kvrx 12 x 100K value fields, all relations 4 0,719 0,032 0,686
           
ex1Mnx 12 x 1M none 40 67,230 0,170 67,060
ex1Mvx 12 x 1M value fields 40 22,125 0,212 21,912
ex1Mrx 12 x 1M all relations 40 38,583 0,178 38,405
ex1Mvrx 12 x 1M value fields, all relations 40 5,421 0,205 5,215
           
ex2Mnx 12 x 2M none 80 139,454 0,333 139,121
ex2Mvx 12 x 2M value fields 80 42,472 0,371 42,101
ex2Mrx 12 x 2M all relations 80 75,216 0,334 74,882
ex2Mvrx 12 x 2M value fields, all relations 80 10,050 0,398 9,652

Processor = AMD Ryzen 7 3700X, Memory 32GB, Disk Samsung SSD 990 PRO M.2 NVMe 1TB

The number of M1 objects found is 4 in the smallest databases with 12 x 100K objects. The number of objects found increases with the size of the database, so that a database with 12 x 2M objects has 80 matches for the query conditions.

The focus of the results is of course on execution times and their changes depending on the size of the database and the indexing used. Three execution times are shown, which are the search time of the query (Search), the creation time of found M1 objects (Create Objects), and the total time (Total) summing up these two times. The results show that the performance can be evaluated solely on the basis of the search time of the query (Search), since the object creation time (Create Objects) per result object seems to be approximately the same in all cases.

No Indexes (none)

The query execution times (Search) are 9.331, 67.060, and 139.121 seconds for databases containing 12 × 100K, 12 × 1M, and 12 × 2M objects respectively (where K = thousand, M = million). Although these durations are long for interactive applications, they are good, possibly even very good, considering that no indexing was used. In scenarios where the primary objective is to maximize data insertion efficiency and minimize overhead, leaving indexes out is not only acceptable but can also be advantageous.

A notable strength of RootDB is its ability to maintain linear query execution time growth relative to database size, even without indexing and even when traversing complex many-to-many relationships. This linear scaling behavior ensures that query performance remains predictable and reliable, even with very large databases. Although search times can become lengthy, there is no risk of query execution freezing or crashing.

Naturally, the complexity of the object model and the number of relationships have a significant impact on execution time. However, even with highly complex structures, RootDB is capable of successfully executing queries, regardless of whether indexing is used.

Indexes of Value Fields (value fields)

An effective way to improve database query performance is to apply indexing to all value fields in the object model. However, indexing every value field is not always feasible, as it can significantly slow down data insertion and maintenance due to the overhead of managing multiple indexes and increased memory consumption. Fortunately, it is often possible to identify the most frequently queried fields during the database design phase and limit indexing to those fields only.

The query execution times (Search) are 4.074, 21.912, and 42.101 seconds for databases containing 12 × 100K, 12 × 1M, and 12 × 2M objects respectively (where K = thousand, M = million). Indexing only the value fields is sufficient to substantially improve query performance, reducing execution times to approximately one third of those observed in the unindexed case. When this improvement is combined with the excellent write performance of approximately 18,000 objects per second, which remains consistent regardless of database size, the overall result can be considered very strong.

Indexes of All Relations (all relations)

With smaller numbers of objects, indexing all relationships improves query performance more effectively than indexing only value fields. However, as the number of objects increases, the situation reverses, since value-based searches must rely on sequential scanning when value fields are not indexed. The query execution times (Search) are 2.479, 38.405, and 74.882 seconds for databases containing 12 × 100K, 12 × 1M, and 12 × 2M objects respectively (where K = thousand, M = million). Once again, query execution time increases approximately linearly with the growth of the database size.

Write performance remains consistent at approximately 6,000 objects per second, even when value fields are included in the indexing. Therefore, with larger numbers of objects, it appears beneficial to index both value fields and relationships to achieve balanced performance for both reading and writing.

Indexes of Value Fields and All Relations (value fields, all relations)

The best overall database search performance is naturally achieved when both value fields and all relationships are indexed. The query execution times (Search) are 0.686, 5.215, and 9.652 seconds for databases containing 12 × 100K, 12 × 1M, and 12 × 2M objects respectively (where K = thousand, M = million). As with all other query configurations, execution time increases approximately linearly with the growth of the database size.

When evaluating the quality of these query results, execution times should be considered in relation to the size of the database involved. A query time of 9.652 seconds may seem long for an interactive application, but when scaled to a database containing 24 million objects with multiple many to many relationships, it can be considered acceptable or even very good. At the other end, a query time well under one second with 1.2 million objects is more than sufficient for interactive use.

It is also worth emphasizing that the query results consist of complete objects of class M1. Each retrieved M1 object includes its entire data structure, containing all objects that are reachable from the M1 root. This level of completeness is a native feature of object databases and is difficult to replicate using traditional relational databases and the SQL language, where achieving similar results typically requires complex and costly programming effort.

Warm Cache Test Scenario

When a query is executed for the first time, the database engine must:

On subsequent executions, if the same data pages are still in the buffer pool (i.e., cached in memory), the system can:

The situation described above, where the same database search is executed again immediately, represents a warm cache test scenario. In such cases, it is very likely that execution times will improve significantly compared to the initial run, because data and index pages remain in memory. The table below presents the results of the previous cold cache tests when the same queries are re-executed, and therefore reflect execution times in a warm cache situation. Distinguishing between cold and warm cache performance is essential in benchmarking, as it reveals how well the database behaves both in first-time access scenarios and in repeated or steady-state workloads.

RootDB Object Databases on PostgreSQL, Warm Cache Execution Times / Seconds  
Database Name Number of Objects Indexes Number of Found Objects Total Create Objects Search
             
ex100Knx 12 x 100K none 4 6,174 0,032 6,141
ex100Kvx 12 x 100K value fields 4 4,244 0,035 4,209
ex100Krx 12 x 100K all relations 4 1,143 0,0130 1,130
ex100Kvrx 12 x 100K value fields, all relations 4 0,310 0,030 0,280
           
ex1Mnx 12 x 1M none 40 30,980 0,163 30,817
ex1Mvx 12 x 1M value fields 40 21,078 0,171 20,907
ex1Mrx 12 x 1M all relations 40 10,289 0,144 10,145
ex1Mvrx 12 x 1M value fields, all relations 40 2,621 0,174 2,447
           
ex2Mnx 12 x 2M none 80 60,876 0,311 60,567
ex2Mvx 12 x 2M value fields 80 40,955 0,323 40,632
ex2Mrx 12 x 2M all relations 80 20,237 0,293 19,944
ex2Mvrx 12 x 2M value fields, all relations 80 4,757 0,318 4,439

Processor = AMD Ryzen 7 3700X, Memory 32GB, Disk Samsung SSD 990 PRO M.2 NVMe 1TB

When comparing warm and cold cache results, execution times are significantly improved for all database queries except those using the value-only indexing strategy (value fields). This is because value indexing helps locate root objects quickly but provides no support for resolving the full object structure, which apparently remains the dominant cost in query execution and is not included in the cache by the first run. Below, the effects of warm cache conditions on query execution times are briefly reviewed for each of the four indexing strategies.

No Indexes (none)

The query execution times (Search) are 65.81%, 45.95%, and 43.54% of the corresponding cold cache times for databases containing 12 × 100K, 12 × 1M, and 12 × 2M objects respectively (where K = thousand, M = million). As the database size increases, the relative benefit of warm cache also increases. However, the improvement from 45.95% to 43.54% is only marginal, suggesting that 43.54% may be close to the maximum achievable warm cache performance in this configuration.

As is typical for RootDB, the execution time increases linearly with the size of the database. This is clearly demonstrated by the ratio of the query times for the 12 × 1M and 12 × 2M databases, which is almost exactly two to one, matching the relative sizes of the databases.

Indexes of Value Fields (value fields)

The warm and cold cache execution times are nearly identical, indicating that value indexing offers no real performance benefit when the same data is queried again immediately. This is somewhat unexpected, given that the no-indexing strategy shows a clear improvement under warm cache conditions. Since both approaches perform the same relationship resolution without the aid of relationship indexing, it raises the question of why value indexing fails to make more effective use of cached data.

Although both the no-indexing (none) and value-only (value fields) indexing strategies ultimately resolve the same object data through relationship traversal, only the no-indexing case shows a significant benefit from warm cache. This is somewhat unexpected, as the same records are accessed to construct the full object structures in both cases. It is not entirely clear why value-only indexing does not result in similar caching behavior, but it may relate to differences in how PostgreSQL internally handles memory and cache management based on query structure or access patterns. This discrepancy highlights a need for further investigation to fully understand the underlying cause.

Indexes of All Relations (all relations)

The query execution times (Search) are 45.58%, 26.42%, and 26.63% of the corresponding cold cache times for databases containing 12 × 100K, 12 × 1M, and 12 × 2M objects respectively (where K = thousand, M = million). As the database size increases, the relative benefit of warm cache initially grows. However, the improvement appears to reach its maximum at the 12 × 1M size, since there is no further reduction in execution time when moving from 12 × 1M to 12 × 2M.

Indexes of Value Fields and All Relations (value fields, all relations)

The query execution times (Search) are 40.82%, 46.92%, and 45.99% of the corresponding cold cache times for databases containing 12 × 100K, 12 × 1M, and 12 × 2M objects respectively (where K = thousand, M = million). As the database size increases, the relative benefit of warm cache appears to decrease. However, this decline seems to level off at the 12 × 1M size, as there is no further change in performance when moving from 12 × 1M to 12 × 2M.

The observed leveling off in warm cache benefit at the 12 × 1M database size may be explained by the increasing size of the combined index structures for both value fields and relationships. While smaller databases likely allow all index pages to remain in memory, larger databases may exceed available cache capacity, requiring parts of the indexes to be reloaded from disk during query execution. As a result, the relative advantage of warm cache diminishes. This behavior suggests that beyond a certain point, the efficiency of caching is limited not by the presence of indexes, but by their ability to remain resident in memory.

RootDB on PostgreSQL: Test Summary

PostgreSQL has emerged as the most effective database platform for RootDB among all systems tested to date. It is capable of writing nearly 20,000 objects per second when no indexing is used or when only value fields are indexed, which is an outstanding result. Even when indexing is extended to cover all relationships in the object model, write performance remains strong, reaching approximately 6,000 objects per second.

When assessing PostgreSQL query performance, execution times should be evaluated relative to the size and complexity of the database. For example, a query time of 9.652 seconds may appear long in isolation, but for a database containing 24 million objects with multiple many to many relationships, it is a reasonable or even impressive result. At the smaller end, query times well under one second for 1.2 million objects are more than adequate for interactive applications.

It is also important to emphasize that the query results return complete objects according to the object model. Each root object includes its full data structure, containing all objects that are reachable from it. This level of completeness is a built-in feature of object databases and is difficult to replicate with traditional relational databases and SQL, where such results typically require complex and labor intensive programming.

Overall, PostgreSQL has proven to be a remarkably efficient and reliable platform for RootDB object databases. Its write performance stands out clearly among the systems tested, remaining stable regardless of database size or growth. Furthermore, PostgreSQL handles complex object models, including those with many to many relationships, with high efficiency even when indexing is applied. All of this is complemented by PostgreSQL's consistently predictable query performance, which is effectively utilized by RootDB's query execution algorithms. As a result, query times increase approximately linearly with database size, which reflects one of the key goals in the development of RootDB.

Links to other pages on this site.


Page content © 2025 

company name

Contact us:

mail address