What’s behind a simple SQL query?
By: Nikolay Dimolarov
Tags: Data Science, Postgres, Relational Databases, Data, Sql

What’s behind a simple SQL query?

A brief behind the scenes examination of what happens when an app/developer runs a SQL query to fetch data from a PostgresDB database. Nikolay Dimolarov Aug 6, 2020·7 min read

Where does meat come from? Photo by Vlad Chețan from Pexels

Just like most kids these days do not seem to know where their meat comes from (hint: not growing on trees but could be from a lab) or why the sky is blue (hint: blue light has shorter, smaller waves), an alarming amount of developers do not (really) know where their data comes from and how it is processed & stored. This article should be a starting point into an engineer’s journey in understanding how that works. That is a very good thing regardless of what framework and no-code platform marketers are trying to teach you. Especially if data really is the new oil (apparently it has been for some years now), it might pay off to see how databases work.

Disclaimer: this is not a SQL tutorial

Without further ado a simple SQL statement to fetch all users from a DB called users:

Disclaimer 2: the point is not to write fancy SQL statements but to showcase what is happening once you do

SELECT * FROM users;

So, how many of you actually know what is happening between pressing enter in your psql terminal and getting back that result?

Fundamentals

In order to understand this one has to start at the beginning — PostgresDB’s architecture.

At the end of the day it is a client/server application written in C++. A client is anyone who accesses the DB e.g. your psql terminal, your Lambda function, JDBC driver etc. and the server is the PGDB backend, which accepts your requests, stores the data etc.

Here is a basic architecture overview of PostgresDB that will be zoomed into throughout the article:

This is kind of oldschool but it still works :) Source: SlideShare

The Path of a Query

There is a pretty good overview of the “path of a query” in the depths of the official PostgresDB documentation. Here is what you should know:

1. Establish connection, transmit query & wait for results

PGDB can handle multiple connections concurrently (through the “postmaster”, see architecture) and for each connection it forks a new process (“postgres”, see architecture) to handle the requests (e.g. SQL statements) of that connection. In other words it is a simple “process per user” client/server model. The postmaster handles the initial connection and authentication and then hands that connection over to a new postgres process. Those processes communicate between each other with a shared memory and semaphores to ensure the overall data integrity even with concurrent connections (remember ACID?)

2. Parsing the query

This consists of 2 stages:

Parse: the PGDB parser utilises the UNIX tools bison and flex as parser and lexer in order to validate if an incoming query string (ASCII text) is a valid SQL query. This is done only with fixed rules about the syntax of SQL without any understanding of the underlying semantics of the query string. The output is a parse tree:

An example for a parse tree for a SELECT statement. Source: interdb.jp

Transform: uses the parser tree to construct a query tree, which contains the semantic interpretation of the query e.g. which tables, data types, operators and functions are referenced. The root of the query trees is the Query data structure defined here. The output:

Same example with the query tree as output. Source: interdb.jp

Mini deep-dive into the Query:

targetList: holds the output of the query and its datatype in this case the columns id and data, which are both integers

rtable: holds the reference to the table

jointree: holds the FROM and WHERE operators

sortClause: holds how data should be sorted

An important note straight from the official docs:

The reason for separating raw parsing from semantic analysis is that system catalog lookups can only be done within a transaction, and we do not wish to start a transaction immediately upon receiving a query string. The raw parsing stage is sufficient to identify the transaction control commands (BEGIN, ROLLBACK, etc), and these can then be correctly executed without any further analysis. Once we know that we are dealing with an actual query (such as SELECT or UPDATE), it is okay to start a transaction if we’re not already in one. Only then can the transformation process be invoked.

3. The Rewrite

PGDB’s rewrite system uses the query tree as input and performs transformations based on rules stored in its system catalogs that can be applied to the query tree. The output is again a query tree. A good example is the realization of views (virtual tables) where the rewrite system rewrites the user query to access the original tables in the view’s definition instead of the view’s alias.

A comprehensive example on how the rule system works for views can be found here.

4. The Query Plan

The planner/optimizer uses the rewritten query tree from the last step to create the optimal/cheapest (=fastest/most efficient) execution plan for the query — the query plan. The optimizer looks at all possible paths to execute the query. Unless the statement has a large number of joins exceeding the geqo_treshold where looking at all possibilities is not computationally feasible. In that case a Generic Query Optimizer is used instead.

Same example with a query plan/plan tree. Source: interdb.jp

The example in the diagram above shows a simple example where the executor has to do a sequential scan of the table tbl_a followed by a sort.

You can check your query plan by typing EXPLAIN in front of your query:

EXPLAIN SELECT (...)

5. The Executor

The official documentation explains it as clearly as one could:

The executor recursively steps through the plan tree and retrieves rows in the way represented by the plan. The executor makes use of the storage system while scanning relations, performs sorts and joins, evaluates qualifications and finally hands back the rows derived.

The executor is used to evaluate all 4 basic SQL query types SELECT, INSERT, DELETE and UPDATE. You can find more details on the executor’s steps for each query type here.

Bonus: GPU Acceleration

There are actually tools out there like PG Strom for GPU acceleration especially for the query optimizer and executor as shown in this diagram:

Again kind of old school but what’s not to like? :) Source: SlideShare

This is not something that was tested for the article but it might inspire you to check out what is out there as this seems to be a pretty cool technology.

Memory

OK. Now you know (1) what happens when you connect to a PGDB instance and (2) how SQL queries are parsed, optimized and executed. The only thing that is missing is how the data is stored to cover the (very) basics of how modern* databases work.

*You can take “modern” with a grain of salt given how old PostgresDB is but it is still very widely used and it is one of the main relational databases in use today.

There is a great explanation of this topic here, so below is just a brief summary.

(Very) High level overview of the memory architecture of PostgresDB. Source: severalnines.com

There are 2 types of memory:

1. Local memory

It is used by each postgres backend process. This memory is allocated by each process for query processing and consists of:

temp_buffers: storing temp tables by executor

work_mem: used by executor for joins etc.

maintenance_work_mem: maintenance ops like REINDEX

2. Shared memory

It is allocated when the PGDB server starts and is used by all backend processes to ensure data integrity (remember the beginning of the article?) This is also the memory that the backend process interfaces with — it does not normally interface directly with the persistent storage. This shared memory consists of:

Shared buffer pool: where pages within tables and indexes are loaded into

WAL buffer: Postgres has a write ahead log i.e. a transactional log making sure no data is lost due to server failure. WAL data is stored in the WAL buffer before being transferred to persistent storage.

Commit log: holds all transactional states as a part of the concurrency control mechanism

You can tune these values and how much memory is allocated to them to increase the performance of your database. Even more information on the utilities for each part of the memory can be found here.

Bringing it together/What about persistent storage?

What happens in the end? How is data being persisted? The executor can read/write from/to tables and indexes in the database through the buffer manager, which can access data from the persistent storage by adding it to the shared buffer pool.

Summary

By now you should have a basic understanding of the inner-workings of one of the most popular databases! At the end of the day you are just interfacing with a very smart C++ application — how cool is that? Hopefully this removes some of the stigma away from the “databases are complicated”. Maybe it will even inspire you to dig deeper and understand how you can optimize your applications by understanding better how your data is stored — by far and large one of the biggest bottlenecks in modern software engineering.

Sources: