SQL, Scaling, and What's Unique About Postgres

This blog post simplifies certain concepts to be brief. If you're looking for examples on how to override the PostgreSQL planner and executor, please check out pg_shard.c here.

By now, you've likely heard the line "SQL doesn't scale." If you take away all the marketing, what remains of this line is "SQL is hard to scale." At a high level, three reasons make scaling out SQL hard:

  1. Scaling is always hard. Scaling data, compared to scaling computations, is even harder.
  2. SQL means different things to different people. Depending on the context, it could mean transactional workloads, short reads/writes, real-time analytics, data warehousing, or triggers. It's hard to (know how to) scale out all these different use cases.
  3. SQL doesn't have the notion of "distribution" built into the language. This can be added into the database, but it isn't there in the language.

This blog post focuses on the third challenge. To make things concrete, let's take a look at how you insert entries using an example key-value store and through SQL.

-- Redis commands to hash-set properties for user billgates
HSET billgates race sayan
HSET billgates power 9001

-- Example SQL query to insert values for user billgates
INSERT INTO user (player_race, player_power, user_name)
  VALUES ('sayan', 9000 + 1, 'billgates');

In the first example, Redis simply hashes the key "billgates", finds the related shard, and routes the query to that shard. For the second example, the database first parses a plain text SQL query. The database then checks column values and their data types against the table schema, and also applies optimizations, such as constant folding. Only after these steps are performed, can the query routing logic kick in, and determine that "billgates" makes up the distribution key.

In other words, the routing logic in SQL needs to be coupled with the rest of the database. And this is for a simple INSERT. If you've ever looked into SELECT queries, you can imagine things getting quite complicated. Think of filter clauses, functions, operators, aggregates, sub-selects, window functions, and others.

All this functionality means even tighter integration with the database. The component that parallelizes SELECT queries now needs to tell apart a filter clause from an aggregate function. Contrast this with Map/Reduce, where the developer needs to represent their computations within a specific parallel programming paradigm.

In summary, when the user types up a SQL query, there's a fair bit of machinery associated with just understanding what that plain text query means. Now, if you are looking to scale out this SQL query, your "query distribution" logic needs to work together with the part that does the understanding.

This used to happen in one of four ways:

  1. Application level sharding: Push the distribution logic all the way up. The application developer then owns the complexity of partitioning and failure handling.
  2. Build a distributed database from scratch: One vendor owns the entire stack, and can couple their components at their own leisure. Building a SQL database takes a long time, and creating a community around it takes even longer.
  3. Use middleware for an open source database: Delegate the routing logic to an external process. The developer then needs to understand how to make their application interact with this external component, and how that component behaves during node failures.
  4. Fork an open source database: And make it distributed. The forked database supports the original's features as of the time of the fork, and diverges from it over time. It in essence becomes a separate project.

This last item on forking applies a lot to PostgreSQL. In fact, PostgreSQL has probably been forked more than any other database.

I don't know if it was one fork too many for the PostgreSQL community, an enthusiasm to involve more developers, or the modularly designed codebase. Either way, for the last few releases, PostgreSQL has been providing official APIs to extend SQL. In particular, the Postgres manual describes how to add new data types, operators, aggregates, background processes, foreign data wrappers, specialized indexes, and more.

If you like Postgres and want to extend it in a particular way, you can read through the manual, and implement your idea according to the API contract. You can then dynamically load in your extension (.so), and start using your extended database.

What makes things even more interesting are the lesser known, but more powerful, PostgreSQL hook APIs. If you're looking to scale out a SQL database, you need more than new functionality. As a database programmer, you need to be able to intercept an INSERT statement after it gets parsed and semantically checked, but before it gets planned.

Fortunately, you can change any database submodule's behavior in Postgres by intercepting the right hook.

For example, pg_shard transforms an incoming SQL query into one or more sub-queries, and therefore needs to run different planning logic than Postgres. No problem. Let's intercept the planner logic.

This way, we get to change Postgres' behavior for distributed tables. Also, we get to benefit from core functionality that Postgres has to offer. For example, we need to perform partition pruning to determine the shards that are relevant to an INSERT or SELECT query. That's easy, PostgreSQL has a constraint exclusion library for partition pruning. We'll just use that to prune away shards instead.

/*
 * _PG_init is called when the module is loaded. In this function we save the
 * previous planner, executor, and utility hooks. We then install our hooks to
 * intercept operations for distributed tables.
 */
void
_PG_init(void)
{
  PreviousPlannerHook = planner_hook;
  planner_hook = PgShardPlanner;
  ...
}

/*
 * PgShardPlanner implements custom planner logic to plan queries involving
 * distributed tables.
 */
static PlannedStmt *
PgShardPlanner(Query *query, int cursorOptions, ParamListInfo boundParams)
{
  ...
}

Of course, revamping the query planner or executor is trickier than adding a new index type. The API contracts aren't cleanly documented and require a fair amount of code reading. This was one of the biggest challenges for us; and we're hoping that cstore_fdw or pg_shard could serve as good examples for these APIs.

In summary, PostgreSQL's extensible architecture puts it in a unique place for scaling out SQL and also for adapting to evolving hardware trends. It could just be that the monolithic SQL database is dying. If so, long live Postgres!

pg_shard: Shard and scale out PostgreSQL

Today we’re excited to announce pg_shard, a transparent sharding extension for PostgreSQL. Without requiring any changes to your application code, pg_shard enables your tables and queries to be distributed across any number of PostgreSQL servers.

CitusDB customers have been clamoring for this functionality for some time. Previously, CitusDB only allowed batch loads: adding new data meant creating more shards using CitusDB’s special \stage command.

We agreed it would be awesome to “just INSERT” data into a cluster, but felt the underlying problem was bigger than Citus: given the fantastic extension capabilities of PostgreSQL, where was the easy-to-use open source sharding extension? So we decided to write one.

In particular, we made sure the extension plays nicely with existing PostgreSQL installations: it preserves the full PostgreSQL feature set for local tables while adding the ability to shard and replicate tables designated as distributed. SELECT, INSERT, UPDATE, and DELETE commands to such tables are then seamlessly spread across a set of PostgreSQL servers. To ensure pg_shard is fully useable as a stand-alone extension, we even implemented cross-shard SELECT functionality.

When used with CitusDB, pg_shard defers to CitusDB’s superior distributed planner during SELECT queries, but takes over during modification commands to give our users the real-time INSERTs they’d been craving.

To get the extension, head over to pg_shard’s page on GitHub.

Got questions?

If you have questions about pg_shard, please contact us using the pg_shard-users Google Group.

If you discover an issue when using pg_shard, please submit it to pg_shard’s issue tracker on GitHub.

Page 1 of 10

About

CitusDB is a scalable analytics database that's built on top of PostgreSQL.

In this blog, we share our ideas and experiences on databases and distributed systems.