It's hard trying to manage a postgresql database by yourself especially when you have little experience with databases. I started by writing simple queries which were required for analytics. But the systems were also facing other issues which pushed me to delve deeper into postgres. The majority of the issues were quite simple and were there due to the lack of a DBA/DBRE. During my journey with postgres, I came across Accidental DBA. This helped me quite a bit at that time but there were still a few things that I picked up over the years. I wanted to write this document to share what I have learned over the years and make it a little easier for another Accidental DBA to work with postgres. Since I have primarily worked with RDS Postgres (managed postgres service provided by AWS), there might be a few things related to postgres setup missing here.
To connect to postgres, you need psql or any other client which supports postgres. You can download psql (part of postgresql-client package) from here according to your operating system.
If you just want to play around with postgres, you can easily set it up on your local using docker. You can install docker by following it's official documentation here.
After setting up docker, run this command in your terminal to run a postgres container in the background.
docker run --name postgres-playground -d postgres
You can run this command connect to postgres once the container is ready.
docker exec -it postgres-playground psql -U postgres postgres
Stop and remove the container.
docker stop postgres-playground
docker rm postgres-playground
The above setup is only for playing with postgres and is not for production use.
PostgreSQL provides concurrent access to the database using MVCC. Without this, if someone writes a row to a database and someone else accesses the same row simultaneously, they would see missing or inconsistent data.. MVCC helps you provide Isolation which guarantees concurrent access to the data.
Let's assume that a bank has only two users having 50$ each, the total balance in the bank being 100$. Now, user A transfers 50$ to user B. At T1, the bank manager would see 100$ as bank balance. At T3, the output would still be 100$ in this case because postgres provides isolation and both the transactions have a different view of that table.
Every table in postgres has some additional system columns. ctid is one such column that stores the physical location of that row. You can use a query like this to get the ctid of a row.
SELECT ctid, * FROM <table name> LIMIT 10;
Deleting a row updates the system columns for that row so that it's not visible for future transactions. Updating a row creates a new copy of the row and updates the previous row so that it's not visible for future transactions.
Because of how MVCC is implemented, tuples that are updated & deleted in a table are not physically deleted from their table. This increases the size of tables if VACUUM
is not run frequently on that table. To handle this increasing storage, you can run VACUUM
manually or make sure autovacuum
is running. VACUUM
goes through each table and marks the older versions of tuples for deletion. VACUUM
doesn't free up disk space, but can be reused for future inserts on this table. To free up disk space and completely remove bloat from that table, you can run VACUUM FULL
but it takes an exclusive lock on the table.
It's not recommended to run
VACUUM FULL
on a production database.
- Create an empty table.
CREATE TABLE tbl (id bigserial primary key, col text);
- Insert two rows and check their physical location.
INSERT INTO tbl(col) VALUES ('a'), ('b');
SELECT ctid, * FROM tbl;
- Delete the row where col value is
a
and check their physical location.
DELETE FROM tbl WHERE col = 'a';
SELECT ctid, * FROM tbl;
- Update the row where col value is
b
and check their physical location.
UPDATE tbl SET col = 'c' WHERE col = 'b';
SELECT ctid, * FROM tbl;
You will notice that the physical location of that row has now changed.
- Run VACUUM FULL and check physical location of rows.
VACUUM FULL tbl;
SELECT ctid, * FROM tbl;
You will notice that the physical location has changed again after running VACUUM FULL
.
- MVCC Unmasked by Bruce Momjian - Slides | Video
- Postgres, MVCC, and you or, Why COUNT(*) is slow by David Wolever - Slides | Video
psql is the official CLI shipped with PostgreSQL. It's good to know how to move around a database, especially during an incident and psql is a perfect tool for that. Check this cheat sheet to get familiar with psql.
To fix the slow and poorly written queries to ensure optimal performance for your database, EXPLAIN
and EXPLAIN ANALYZE
are used to identify bottlenecks in query execution. Check this to get an idea of how queries get executed. After identifying the issue, you can create an index, rewrite the query, or add more resources depending on the use case.
You can set up one of these tools to get better visibility into your database:
You can also set up pgbadger, a tool that parses logs and generates a report on database usage and workload. You can use this to find out slow queries that need fixing or tune postgresql parameters for your workload. Since pgbgadger works on logs, you won't get a real-time view of your database instance.
There are a lot of metrics you might want to track, but these are one of the most important ones
- CPU
- Memory
- Connections
- IOPS
Performance Insights in AWS RDS
Performance Insights is a feature of RDS which shows you running queries in real-time.
Most of the time, the performance problems in a database are due to a missing index. There are different types of indexes available. And some might give good performance gains depending on the use case. By default, postgres creates a B-Tree index.
While creating an index, I try to follow these rules:
- Don't create an index if it's not going to be used. Unnecessary indexes will slow down your writes.
Multi-column indexes
: If your queries have multiple conditions, a multi-column index can help. The order of columns is matters here. Let's assume you have a table with the below structure with a B-Tree index on (col1, col2).
Table "public.tbl1"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+----------------------------------
id | integer | | not null | nextval('tbl1_id_seq'::regclass)
col1 | integer | | |
col2 | integer | | |
Indexes:
"tbl1_col1_col2_idx" btree (col1, col2)
If you make a query like:
SELECT * FROM tbl1 WHERE col1 = 10 AND col2 = 20
- Index scanSELECT * FROM tbl1 WHERE col1 = 10
- Index scanSELECT * FROM tbl1 WHERE col2 = 20
- Sequential scan. Depends on the data distribution and theWHERE
clause.- Partial indexes: If you know that a part of the
WHERE
clause would have a fixed condition. e.g.WHERE active = TRUE AND col1 = ?
whereactive = TRUE
is the only condition on theactive
column in your queries, you can create a partial index. Partial indexes are smaller in size and are more performant as well. - Indexes on expressions: You can create an index on an expression as well (e.g.
lower(textcol1)
). If queries on a table have some expressions, it's a good idea to create an index on that expression.
Run queries/connections_per_user.sql and check if max_running_time
is high for state
- active
and idle in transaction
. There is a problem if the number of connections or max_running_time
for queries in active
or idle in transaction
state is high. high
is subjective here and depends on the database size and type of workload. In my experience, number of active
connections should be less than number of database's cpu cores and max_running_time
should be less than 1 second.
- If time for
idle in transaction
queries is high, then- either your application is taking time to commit transactions because its under heavy load or is doing some time consuming task before committing. Or application is not handling transactions properly.
- some dev started a transaction and didn't commit it and left the connection open
- If time for
active
queries is high, then- Queries are in waiting state. Queries can be in waiting state for multiple reasons. In queries/active_running_queries.sql output, check
wait_event_type
andwait_event
to figure out why the query is in waiting state. Reference: https://www.postgresql.org/docs/current/monitoring-stats.html#WAIT-EVENT-TABLE (Its better to check documentaton for postgres version you are workng with). Most of the time, its because:- two or more connections are working (lock) on the same row.
- Someone executed some migrations and because of an
idle in transaction
oractive
query, this DDL query took a lock on that table preventing queries on table.
- Query is expensive and is taking time. In this case
wait_event_type
andwait_event
is null or its taking time to read data from disk (wait_event_type
isIO
). If the number of queries is also high, your database is probably under high CPU Utilisation. Fix for this is:- check if an index is missing. You can use
EXPLAIN
andEXPLAIN ANALYZE
(NEVER USEEXPLAIN ANALYZE
for a query which will modify data. This actually executes the query.) - providing more memory so that indexes can fit into memory or data can be cached into memory. For this, check disk reads. If its high, tune params (work_mem, shared_buffers, effective_cache_size) or increase memory for the instance.
- increase CPU because the number of queries executed per second is high.
- check if an index is missing. You can use
- Queries are in waiting state. Queries can be in waiting state for multiple reasons. In queries/active_running_queries.sql output, check
Having queries in idle in transaction
state can cause a lot of issues in the long run. Because a query is in idle in transaction
state and that connection holds a lock on a table, VACUUM
won't run on that table because of which the size of that table might keep on increasing.
-
Connection pooling - every postgresql connection is a forked process on postgres. Lots of connections & disconnections can result in increased CPU utilization of your database. Setting up a connection pooler can create a lot of impact in performance. Most web frameworks provides connection pooling out of the box, but you can also setup external connection poolers like pgbouncer or pgpool-II.
-
Indexes - one of most common problems which can result in low performance is missing indexes. Using pgbadger you can identify slow queries and identify tables which are missing indexes. Postgresql provides a variety of indexes (btree, brin, gin, etc.).
-
Parameter tuning - I have seen people vertically scale postgres without attempting to understand bottlenecks causing performance problems. It is really important to tune your parameters for your workloads to gain desired performance. You can generate a default configuration using PGTune. But don't forget to understand more about your workloads and tune accordingly.
-
Partitioning - Partitioning can help you achieve easier archiving and better performance of large tables. You can partition tables either using declarative partitioning or inheritance based partitioning. Simple log or event tables are generally good candidates for partitioning. For more details, check Postgresql documentation
Postgres offers a lot of data types. While designing schema for a table, its quite useful to know about them and where to use them. These are some of the questions I come across:
Its better to use int when you know that won't exceed the limit of int. Changing a column from int to bigint can result in a massive downtime because postgres will need to rewrite the entire column.
json is just like a text column with a json validation whereas in case of jsonb column, data is stored in a binary format. Because of which insert and update operations are a little slower for jsonb as compared to json. If data in the column is just going to be logs and are not going to be queried, its better to use a json column. For more details check this article.
char(n) is fixed length with blanks padded whereas varchar(n) is a variable length string with a limit. Text on other hand has no limits. Most of the time, its better to use text data type. If a check on length is required, you can add a constraint for that on a text column. For more information check postgres documentation here.
Always keep backups enabled and test them regularly. Gitlab faced an issue with backups in the past which resulted in a data loss. You can check their postmortem here.
Minor version upgrades just require a restart where as a major version upgrade will need to update data on disk which can take quite some time. Its recommended to test the upgrade and the application with the newer version before actually doing it in production.