In terms of performance, relational databases such as PostgreSQL [18] and MySQL [17] are generally easy to scale vertically. Switching to a beefier CPU and/or adding more memory to your database server is more or less a standard operating procedure for increasing the queries per second (QPS) or transactions per second (TPS) that the DB can handle. On the other hand, scaling relational databases horizontally is much harder and typically depends on the type of workload you have.
For write-heavy workloads, we usually resort to techniques such as data sharding. Data sharding allows us to split (partition) the contents of one or more tables into multiple database nodes. This partitioning is achieved by means of a per-row shard key, which dictates which node is responsible for storing each row of the table. One caveat of this approach is that it introduces additional complexity at query time. While writes are quite efficient, reads are not trivial as the database might need to query each individual node and then aggregate the results together in order to answer even a simple query such as SELECT COUNT(*) FROM X.
On the other hand, if our workloads are read-heavy, horizontal scaling is usually achieved by spinning up read-replicas, which mirror updates to one or more primary nodes. Writes are always routed to the primary nodes while reads are handled by the read-replicas (ideally) or even by the primaries if the read-replicas cannot be reached.
While relational databases are a great fit for transactional workloads and complex queries, they are not the best tool for querying hierarchical data with arbitrary nesting or for modeling graph-like structures. Moreover, as the volume of stored data exceeds a particular threshold, queries take increasingly longer to run. Eventually, a point is reached where reporting queries that used to execute in real-time can only be processed as offline batch jobs. As a result, companies with high-volume data processing needs have been gradually shifting their focus toward NoSQL databases.