The Postgres database management system was originally designed as object-relational. We remind why PostgreSQL is an object-relational rather than just relational database management system, and briefly discuss what kind of object-oriented features it offers. We demonstrate how these features can be used to manage hierarchies, and how applications can benefit using them.
The discussion is illustrated with examples of SQL code based on the postgres_air
database [6, https://github.com/hettie-d/postgres_air].
There are many ways to implement a hierarchy in a relational database. Techniques with names like adjacency lists and nested sets, not to mention some other RDBMS have built methods of managing hierarchies. Some of these techniques will work in PostgreSQL as well, but in this article, we will be looking at a method that is native to PostgreSQL that may not be obvious initially but is very powerful once you understand how it works.
Editor note: I wrote a book named: “Practical Graph Structures in SQL Server and Azure SQL”, and this method of managing hierarchies was not something I had seen previously! I had not seen similar in “Joe Celko’s Trees and Hierarchies in SQL for Smarties” either.
Introduction
The relational model came out from research labs into practice in the simplified form of SQL databases which gradually gain popularity and became dominating database model.
However, application developers figured out very soon that a narrow interpretation of relational model in the form of SQL is not perfect for applications other than financial transaction processing.
This happened during the period when programming languages were becoming object-oriented. Dozens of object-oriented models were proposed and some of them implemented as programming languages. Naturally, the idea to make databases object-oriented was in the air and several prototype systems were developed. This trend was summarized in [1].
The concepts of object-oriented databases were later formalized in the object-oriented database standard (ODMG) [3]. Despite aggressive expectations, object-oriented databases did not gain significant popularity, although they are suitable for some classes of applications.
At the same time other researchers were exploring more conservative approaches that could combine the relational model with object-oriented features. Their position is presented in [2]. The original Postgres system was created as a prototype demonstrating this approach. Currently all major SQL DBMSs follow this approach offering several features that can be viewed as object-oriented.
One of the most limiting simplifications of early versions of SQL (in comparison with relational model) is the restriction to fixed set of basic data types, namely, numeric types, character strings, dates and time). In contrast, the relational model requires that attribute values must belong to a domain. The elements of a domain are considered scalar (that is, their structure, if any, is not visible to the model) and an equality predicate must be defined on any domain. In plain words the latter means that it is possible to check if any two elements of the same domain are equal or are not equal. Nothing else is required, but any additional functions, operators and predicates are allowed, although the relational model itself does not provide anything for definition of such functions.
Note that such functions (or operators) are necessary to perform the operation. Relational domains are like abstract data types.
For example, consider domains of weights and lengths. Ideally, + is needed to sum weights with weights and lengths with lengths but not weights with lengths. However, both domains are represented with the same numeric type in SQL.
So, we can exploit the full power of relational or SQL query languages on top of domains that contain arbitrary complex values if we do not dive into the structure of these values explicitly. That is, we need functions to access any structural parts or other values that can be derived from the domain elements.
A composite data type is like a structure type known in many programming languages. It consists of a fixed number of named attributes. A row of an SQL table can be viewed as a composite type although early SQL does not define composite types explicitly and does not allow composite types as values of attributes.
Yet another way to go beyond the scalar values of relational domains is to use collections. A collection consist of several values of the same type, rather than just a single value. The ODMB standard specifies the following kinds of collections:
- A set consists of items that must be different. Sets do not provide any specific ordering of their elements.
- A bag is like set but duplicate values are allowed as elements of a bag.
- A list is an ordered set. An access to the list values is provided via operations like first, last, next, pervious.
- An array is like array in a programming language: the elements of an array are accessed using integer indexes.
- A dictionary is a set of key-value pairs.
The elements of a collection usually belong to a composite type.
To make objects from the structured data structures described above we need means for definition of object methods or functions. All DBMS supporting object features provide for user-defined functions of several different kinds. However, we do not discuss any kind of functions in this article.
Hierarchies
There are several ways to define what is a hierarchy. For the purposes of this article, we define a hierarchy as an instance of a composite type that contains collections among its attributes. The type of the collection elements can also contain collections.
Hierarchies are essential for almost any kind of business, as illustrated by the following examples:
- A receipt from a grocery store contains a list of purchased items.
- An organizational chart is a hierarchy (In its simplest form representing that each person has one direct report manager.)
- A book contains multiple chapters.
- An airline booking contains lists of passengers and flights.
- A flight contains list of boarding passes of passengers boarded on the airplane.
Note that last two examples contain same data organized into different hierarchies.
The importance of hierarchies for programming is obvious and was recognized long time ago. The list of data models in any database textbook starts from hierarchical data model. One of the first widely used systems (IBM IMS) provides hierarchical views for applications accessing the database. This system is usually called hierarchical although the internal structures used in this system are much more complex than just hierarchies and can be characterized as a kind of network data model.
Hierarchies were theoretically studied under the name of non-first normal form relations (NFNF or NF2). More detailed discussion of NFNF can be found in the article [5].
Modern data formats JSON and XML provide notation for representation of hierarchical data structures.
Hierarchies are so important and popular for the following reasons:
- Hierarchies are the simplest data structure that is more complex than row with fixed list of components.
- Hierarchies are flexible as collections may contain different number of elements in different instances of a hierarchy.
- Hierarchies have equivalent totally ordered representations.
- Hierarchies are suitable for transfer of meaningful data units over a network.
However, storing hierarchies in a database may result in several disadvantages.
If the relationship between root of a hierarchy and elements of a collection is not one-to-many, then redundancy is unavoidable. For example, if the booking hierarchy contains information on flights, then the information about each flight is stored as many times are there are bookings on this flight. Similarly, if the flight hierarchy contains collection of bookings, then each booking is stored as many times as are in this booking.
Note that this kind of redundancy was avoidable in the IBM IMS system because hierarchies returned to the application might be constructed dynamically using pointers interconnecting elements of different hierarchies.
Our take-out form the above is that hierarchies are desirable for data exchange over network but are not the best as the database storage structure.
Object-Oriented Features of PostgreSQL
Although the PostgreSQL database management system does not provide objects explicitly, it offers several features typically found in object-oriented models. We limit ourselves to discussion of data structures only in this article, although the most powerful features are related to user-defined functions of various kinds Note that the core of object features of PostgreSQL are inherited from the original Postgres prototype which was designed when neither object database models nor object models in general weren’t mature yet.
We mentioned above the importance of domains in the relational theory. Domains can be defined in PostgreSQL, but they are not as powerful as relational domains. A domain is defined as a sub-type of a basic type satisfying additional constraints. For example, we can define weight and height as positive float numbers:
1 2 |
psql> create domain weight_kg as float check (value >0); psql> create domain height_cm as float check (value >0); |
The system will ensure that the constraints are satisfied:
1 |
psql> select (-10.5)::weight_kg; |
This would cause the following error:
ERROR: value for domain weight_kg violates check constraint "weight_kg_check"
However, any operation defined on the basic type can also be applied to the values in the domain. So, a sum of weight and height will be still a valid expression.
1 |
psql> select 70::weight_kg + 180::height_cm as meaningless; |
Which would return the aptly named:
1 2 3 4 5 |
meaningless ------------ 250 (1 row) |
The PostgreSQL syntax for composite type definition is very similar to the syntax for table definition (CREATE TABLE) and actually any table also defines a composite type.
We’ll use the following composite type to show how to build collections in the PostgreSQL system. We define a composite type for dictionary entries:
1 2 3 |
psql> create type dict_entry as ( dict_key text, dict_value text); |
Values of a composite type are constructed using row( )
pseudo-function. Explicit cast to a composite type is usually needed to create a value of such type:
1 |
psql> select row('a-key', 'a-value')::dict_entry; |
The output:
1 2 3 4 5 |
row ----------------- (a-key,a-value) (1 row) |
To be able to build hierarchies we need collections as types of composite type attributes. The only kind of collections allowed as attribute type in PG is an array.
Let’s define a table containing a dictionary in every row:
1 2 3 |
psql> create table nested_dictionaries ( dict_name text, dict dict_entry[]); |
This table contains a collection as a column, so, it is not in the first normal form.
We use array constructor to create values of arrays in this section. The following SQL statement populates the table defined above:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
psql> insert into nested_dictionaries values ('first', array[ row('ke1','v11')::dict_entry, row('ke2','va12')::dict_entry] ), ('second', array[ row('ke1','v21')::dict_entry, row('ke2','va22')::dict_entry, row('ke3','va23')::dict_entry] ), ('third', NULL); |
What about other kinds of collections? They cannot be stored as column values, but arrays can be converted into sets or bags and can then be used in queries as table expressions. The set-valued pseudo-function unnest
extracts each element of an array argument as a separate row:
1 2 3 |
psql> select dict_name, d.* from nested_dictionaries nd, lateral unnest(nd.dict) d; |
The output:
1 2 3 4 5 6 7 8 9 |
dict_name | dict_key | dict_value -----------+----------+------------ first | ke1 | v11 first | ke2 | va12 second | ke1 | v21 second | ke2 | va22 second | ke3 | va23 (5 rows) |
This SQL statement converts nested table into first normal form. Of course, we can extract collection from a single row of the table using conditions in where
clause. The output is either set or bag depending on the existence of duplicates.
To store a set or bag as a column value, we have to convert it into an array. This can be done using array_agg
aggregate. Examples can be found in the nest section.
Building and Processing Hierarchies
In this section we discuss how to build hierarchies from relations in PostgreSQL and how to use them.
We already defined simple nested table above. Our more complex example is a travel itinerary: an airfare booking that contains several passengers and several flights. A complete itinerary extracted from the postgres_air
database requires data from 6 tables. In this article we build a simplified hierarchy that contains only two levels.
We need a composite type to represent a passenger in our hierarchy. PostgreSQL allows to use tables as type definitions, so we could just use the passenger
table. However, this table contains columns that are not needed in the hierarchy: the booking_id
column is redundant because the collection of passenger’s data will be embedded into itinerary. So, we define a type to represent a passenger in the itinerary:
1 2 3 4 5 |
psql> create type it_passenger as ( passenger_no int, first_name text, last_name text ); |
The composite type it_flight
combines data from two tables (booking_leg
and flight
):
1 2 3 4 5 6 7 |
psql> create type it_flight as ( leg_num int, flight_no text, scheduled_departure timestamptz, scheduled_arrival timestamptz, departure_airport char(3), arrival_airport char(3)); |
We are now ready to extract itineraries from the relations stored in the postgres_air
database. To do this, we create a view, each row of which contains a complete itinerary hierarchy for a booking. So, this view returns a nested (NFNF) relation.
PostgreSQL allows us to use relations (tables, views etc.) as composite type definitions. The query in the view definition uses sub-queries with array_agg
function to build nested collections (passengers and flights):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
psql> create or replace view itinerary as select b.booking_ref, b.email, (select array_agg( row( p.passenger_no, p.first_name, p.last_name)::it_passenger) from postgres_air.passenger p where p.booking_id = b.booking_id ) as passengers, (select array_agg( row( bl.leg_num, f.flight_no, f.scheduled_departure, f.scheduled_arrival, f.departure_airport, f.arrival_airport )::it_flight) from postgres_air.booking_leg bl join postgres_air.flight f on f.flight_id=bl.flight_id where bl.booking_id = b.booking_id) as flights from postgres_air.booking b; |
The output produces from this view for a booking looks like follows:
1 2 |
psql> select * from itinerary where booking_ref = 'FJ4B12'; |
The output looks like this:
1 2 3 4 5 |
booking_ref | email | passengers | flights -------------+--------------------------+---------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------- FJ4B12 | HARPER814581@magic.email | {"(,EVAN,JONES)","(,JAMESON,HUGH)","(,YARITZA,HARPER)"} | {"(1,30,\"2024-06-01 06:45:00+03\",\"2024-06-01 16:50:00+03\",ADD,CAN)","(2,31,\"2024-06-08 13:05:00+03\",\"2024-06-08 23:10:00+03\",CAN,ADD)"} (1 row) |
So, we can obtain complete itineraries from this view and pass then to an application in response to a single SQL query.
Why the number of queries is so important?
Nearly all applications wait for response from the database server for each query they. The time needed for processing of a query touching small number of rows is negligible in comparison with time needed for network round trip. To extract data contained in the hierarchy above, a typical object-relational mapper (ORM) would execute 11 queries, making access to the database really slow.
However, the application developers will not be happy with such representation of the output. To make hierarchies acceptable for application developers we have to convert it into another format.
The PostgreSQL system provides several features for processing hierarchical data represented in JSON and XML. Our choice is JSON for this article.
To show how JSON hierarchy can be converted into PostgreSQL relations we need some JSON data. To obtain such data we store output of the itinerary view converted to JSON in a table. Of course, such table is not needed if the generated JSON is just passed to an application. Our table have only one column and will contain one row:
1 2 |
psql> create table itinerary_json( booking_hierarchy json); |
The following SQL statement inserts itineraries for two bookings converted into a JSON array. So, the JSON value can contain itineraries for multiple bookings, and we can pass several itineraries to an application in one SQL query. This can be done with a SELECT
statement, but we need data to demonstrate conversion in the opposite direction (from JSON to relational tables).So, we store the output into a table and use RETURNING
clause to show how this output looks like.
To improve readability, we use jsonb_pretty
function. Note also that the view is used both as source relation and composite type definition: )
1 2 3 4 5 6 7 8 |
psql> insert into itinerary_json select to_json( array_agg( row(booking_ref, email, passengers, flights)::itinerary)) from itinerary where booking_ref in ('JK4B12', 'UTYJ12') returning jsonb_pretty(booking_hierarchy::jsonb); |
Which outputs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
jsonb_pretty -------------------------------------------------------------------- [ { "email": "GILMORE476574@magic.email", "flights": [ { "leg_num": 1, "flight_no": "110", "arrival_airport": "JFK", "departure_airport": "AMS", "scheduled_arrival": "2024-06-01T17:55:00+03:00", "scheduled_departure": "2024-06-01T10:25:00+03:00" }, { "leg_num": 2, "flight_no": "111", "arrival_airport": "AMS", "departure_airport": "JFK", "scheduled_arrival": "2024-06-09T11:55:00+03:00", "scheduled_departure": "2024-06-09T04:25:00+03:00" } ], "passengers": [ { "last_name": "WATERMAN", "first_name": "ADELYNN", "passenger_no": null }, { "last_name": "BLOOM", "first_name": "CAMILA", "passenger_no": null }, { "last_name": "ALLEN", "first_name": "GABRIELA", "passenger_no": null } ], "booking_ref": "JK4B12" }, { "email": "SMITH71225@magic.email", "flights": [ { "leg_num": 1, "flight_no": "483", "arrival_airport": "AUS", "departure_airport": "LAX", "scheduled_arrival": "2024-05-30T23:20:00+03:00", "scheduled_departure": "2024-05-30T20:35:00+03:00" }, { "leg_num": 2, "flight_no": "482", "arrival_airport": "LAX", "departure_airport": "AUS", "scheduled_arrival": "2024-06-03T20:45:00+03:00", "scheduled_departure": "2024-06-03T18:00:00+03:00" } ], "passengers": [ { "last_name": "PATTON", "first_name": "OLIVIA", "passenger_no": null }, { "last_name": "SMITH", "first_name": "NOAH", "passenger_no": null }, { "last_name": "SMITH", "first_name": "DAVID", "passenger_no": null } ], "booking_ref": "UTYJ12" } ] (1 row) |
The JSON output above is like the output that can be obtained from a document store.
If an application sends data in JSON format, a conversion into relational data is needed for handling of these data at the database side.
PostgreSQL provides several functions for processing SON. We mention here only those that can produce values of composite types (rather than scalar values).
The most powerful tool for conversion of JSON data into relations provided in PostgrQL is the JSON_TABLE
function. This function can many levels of a hierarchy in a single invocation. However, this function is not available in versions prior to 17, and the specification of conversion is somewhat complex.
An alternative is the json_potulate_recordset
function that converts one JSON object into values of a composite type at one level of hierarchy. This function converts values of JSON keys into values of composite type attributes with the same name. Missing values are replaced with defaults, extra JSON keys are ignored.
We need one more type to convert JSON data into relations:
1 2 3 4 5 |
psql> create type itinerary_top as ( booking_ref text, email text, passengers json, flights json); |
Our first SQL statement converts the hierarchy into two rows containing values of top level attributes, but nested collections are still in the form of JSON arrays:
1 2 3 4 5 |
psql> select booking_ref, email, passengers, flights from json_populate_recordset( NULL::itinerary_top, (select booking_hierarchy from itinerary_json)) top; |
Which outputs:
1 2 3 4 5 6 |
booking_ref | email | passengers | flights -------------+---------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- JK4B12 | GILMORE476574@magic.email | [{"passenger_no":null,"first_name":"ADELYNN","last_name":"WATERMAN"},{"passenger_no":null,"first_name":"CAMILA","last_name":"BLOOM"},{"passenger_no":null,"first_name":"GABRIELA","last_name":"ALLEN"}] | [{"leg_num":1,"flight_no":"110","scheduled_departure":"2024-06-01T10:25:00+03:00","scheduled_arrival":"2024-06-01T17:55:00+03:00","departure_airport":"AMS","arrival_airport":"JFK"},{"leg_num":2,"flight_no":"111","scheduled_departure":"2024-06-09T04:25:00+03:00","scheduled_arrival":"2024-06-09T11:55:00+03:00","departure_airport":"JFK","arrival_airport":"AMS"}] UTYJ12 | SMITH71225@magic.email | [{"passenger_no":null,"first_name":"OLIVIA","last_name":"PATTON"},{"passenger_no":null,"first_name":"NOAH","last_name":"SMITH"},{"passenger_no":null,"first_name":"DAVID","last_name":"SMITH"}] | [{"leg_num":1,"flight_no":"483","scheduled_departure":"2024-05-30T20:35:00+03:00","scheduled_arrival":"2024-05-30T23:20:00+03:00","departure_airport":"LAX","arrival_airport":"AUS"},{"leg_num":2,"flight_no":"482","scheduled_departure":"2024-06-03T18:00:00+03:00","scheduled_arrival":"2024-06-03T20:45:00+03:00","departure_airport":"AUS","arrival_airport":"LAX"}] (2 rows) |
We can use subqueries to convert JSON arrays into PostgreSQL arrays. The output of this query is exactly same as the output from the itinerary view, so we do not include it in the article.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
select booking_ref, email, (select array_agg( row( p.passenger_no, p.first_name, p.last_name)::it_passenger) from json_populate_recordset( NULL::it_passenger, top.passengers) p ) passengers, (select array_agg( row( f.leg_num, f.flight_no, f.scheduled_departure, f.scheduled_arrival, f.departure_airport, f.arrival_airport )::it_flight) from json_populate_recordset( NULL::it_flight, top.flights) f ) as flights from json_populate_recordset( NULL::itinerary_top, (select booking_hierarchy from itinerary_json)) top; |
Alternatively, we can use LATERAL JOIN
to combine each row of nested collections with the top-level row. This will produce a cartesian product of nested collection, which in general does not make much sense. However, the product of passengers and flights produces data needed for boarding passes: a boarding pass is issued for every passenger for all flights in a booking.
The SQL statement is here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
psql> select booking_ref, email, p.*,f.* from json_populate_recordset( NULL::itinerary_top, (select booking_hierarchy from itinerary_json)) top, lateral (select * from json_populate_recordset( NULL::it_passenger, top.passengers) ) p, lateral (select * from json_populate_recordset( NULL::it_flight, top.flights)) f ; |
The output to this statement is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
booking_ref | email | passenger_no | first_name | last_name | leg_num | flight_no | scheduled_departure | scheduled_arrival | departure_airport | arrival_airport -------------+---------------------------+--------------+------------+-----------+---------+-----------+------------------------+------------------------+-------------------+----------------- JK4B12 | GILMORE476574@magic.email | (null) | ADELYNN | WATERMAN | 1 | 110 | 2024-06-01 10:25:00+03 | 2024-06-01 17:55:00+03 | AMS | JFK JK4B12 | GILMORE476574@magic.email | (null) | ADELYNN | WATERMAN | 2 | 111 | 2024-06-09 04:25:00+03 | 2024-06-09 11:55:00+03 | JFK | AMS JK4B12 | GILMORE476574@magic.email | (null) | CAMILA | BLOOM | 1 | 110 | 2024-06-01 10:25:00+03 | 2024-06-01 17:55:00+03 | AMS | JFK JK4B12 | GILMORE476574@magic.email | (null) | CAMILA | BLOOM | 2 | 111 | 2024-06-09 04:25:00+03 | 2024-06-09 11:55:00+03 | JFK | AMS JK4B12 | GILMORE476574@magic.email | (null) | GABRIELA | ALLEN | 1 | 110 | 2024-06-01 10:25:00+03 | 2024-06-01 17:55:00+03 | AMS | JFK JK4B12 | GILMORE476574@magic.email | (null) | GABRIELA | ALLEN | 2 | 111 | 2024-06-09 04:25:00+03 | 2024-06-09 11:55:00+03 | JFK | AMS UTYJ12 | SMITH71225@magic.email | (null) | OLIVIA | PATTON | 1 | 483 | 2024-05-30 20:35:00+03 | 2024-05-30 23:20:00+03 | LAX | AUS UTYJ12 | SMITH71225@magic.email | (null) | OLIVIA | PATTON | 2 | 482 | 2024-06-03 18:00:00+03 | 2024-06-03 20:45:00+03 | AUS | LAX UTYJ12 | SMITH71225@magic.email | (null) | NOAH | SMITH | 1 | 483 | 2024-05-30 20:35:00+03 | 2024-05-30 23:20:00+03 | LAX | AUS UTYJ12 | SMITH71225@magic.email | (null) | NOAH | SMITH | 2 | 482 | 2024-06-03 18:00:00+03 | 2024-06-03 20:45:00+03 | AUS | LAX UTYJ12 | SMITH71225@magic.email | (null) | DAVID | SMITH | 1 | 483 | 2024-05-30 20:35:00+03 | 2024-05-30 23:20:00+03 | LAX | AUS UTYJ12 | SMITH71225@magic.email | (null) | DAVID | SMITH | 2 | 482 | 2024-06-03 18:00:00+03 | 2024-06-03 20:45:00+03 | AUS | LAX (12 rows) |
The relational data converted from JSON should be, most likely, serve as an input for INSERT
, UPDATE
, or DELETE
statements. Our itinerary hierarchies cannot be inserted into Postgres_air
database because we tried to make this hierarchy as simple as possible and did not include some of necessary columns into itinerary hierarchy.
More complex examples of hierarchies and data modification based on hierarchies can be found in the NORM
framework [7]. This framework also facilitates specification of search criteria and database modification based on JSON input.
Let’s look at the big picture again. Hierarchies are desirable as units of data exchange between applications and the database. Hierarchies are also supported with standardized communication formats (JSON and XML). Why not to store hierarchies in a database without any transformation, as document stores do?
There are several reasons not to do that:
- The database size. Our itinerary includes only part of data related to a booking. The complete hierarchy containing all booking data requires 6.8 Gb, while the relational tables from which this hierarchy was built occupy 4.6 Gb. Another hierarchy needed for an application can be constructed from the same tables but will occupy another 7.6 Gb.
- Redundancy. Our itinerary contains flight information in every booking on a flight. So, the flight information is stored in dozens copies at least.
- Performance: Indexing of hierarchies is more complex than indexing of relational tables.
- Complexity: More complex operations (such as joins and aggregations) on hierarchies are not more efficient that on relational tables.
The items above suggest that storing of hierarchies in a database almost never makes sense. Generation of hierarchies on demand and conversion them to relational table is a better option from both logical and performance perspectives.
Conclusion
Hierarchies are perfect for data exchange between applications and the database but are not suitable as an internal storage format. The PostgreSQL system provides everything needed for processing hierarchies.
A reasonable approach is to use hierarchies to transfer data between an application and a database but convert data to/from relational representation on demand.
This article shows how to convert data between tables and JSON format in PostgreSQL.
References
- Malcolm Atkinson, David DeWitt, David Maier, François Bancilhon, Klaus Dittrich, and Stanley Zdonik: The Object-Oriented Database System Manifesto. Proceedings of the First International Conference on Deductive and Object–Oriented Databases (DOOD89) Kyoto Research Park, Kyoto, Japan, 4–6 December 1989, Pages 223-240, 1990.
- Michael Stonebraker, Lawrence A. Rowe, Bruce G. Lindsay, Jim Gray, Michael J. Carey, Michael L. Brodie, Philip A. Bernstein, and David Beech. 1990. Third-generation database system manifesto. SIGMOD Rec. 19, 3 (Sep. 1990), 31–44. https://doi.org/10.1145/101077.390001
- The Object Data Management Standard: ODMG 3.0 R. G.G. Cattell, Douglas K. Barry, Mark Berler, Jeff Eastman, David Jordan, Craig Russell, Olaf Schadow, Torsten Stanienda, and Fernando Velez (EDS.). Morgan Kaufmann, 2000, ISBN 1-55860-647-5
- PostgreSQL Documentation. https://www.postgresql.org/docs/
- Joe Celko: First Normal Form Gets No Respect https://www.red-gate.com/simple-talk/databases/theory-and-design/first-normal-form-gets-no-respect/
- Postgres_air: A training database. https://github.com/hettie-d/postgres_air
- NORM Framework. https://github.com/hettie-d/NORM
Load comments