You probably know that a fresh initialized PostgreSQL cluster comes with three databases by default: template0, template1, and postgres. If you want to know why they are there, and what their purpose is, check this post. In this post we’ll look at something you may never have noticed: Why is the postgres database, even after a fresh initdb, slightly larger than template1 and template0?
Let’s start from scratch and initialize a new cluster:
postgres@debian10pg:/home/postgres/ [pg14] initdb -D /var/tmp/dummy
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.
The database cluster will be initialized with locale "en_US.UTF-8".
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".
Data page checksums are disabled.
creating directory /var/tmp/dummy ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting default time zone ... Europe/Zurich
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok
initdb: warning: enabling "trust" authentication for local connections
You can change this by editing pg_hba.conf or using the option -A, or
--auth-local and --auth-host, the next time you run initdb.
Success. You can now start the database server using:
pg_ctl -D /var/tmp/dummy -l logfile start
Once we start start that up and ask for the size of the databases, you’ll notice the difference:
postgres@debian10pg:/home/postgres/ [pg14] export PGPORT=8888
postgres@debian10pg:/home/postgres/ [pg14] pg_ctl -D /var/tmp/dummy start
waiting for server to start....2021-05-20 11:04:49.527 CEST [2708] LOG: starting PostgreSQL 14devel on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
2021-05-20 11:04:49.528 CEST [2708] LOG: listening on IPv6 address "::1", port 8888
2021-05-20 11:04:49.528 CEST [2708] LOG: listening on IPv4 address "127.0.0.1", port 8888
2021-05-20 11:04:49.541 CEST [2708] LOG: listening on Unix socket "/tmp/.s.PGSQL.8888"
2021-05-20 11:04:49.557 CEST [2709] LOG: database system was shut down at 2021-05-20 11:03:23 CEST
2021-05-20 11:04:49.566 CEST [2708] LOG: database system is ready to accept connections
done
server started
postgres@debian10pg:/home/postgres/ [pg14] psql -c "l+"
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges | Size | Tablespace | Description
-----------+----------+----------+-------------+-------------+-----------------------+---------+------------+--------------------------------------------
postgres | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | | 8265 kB | pg_default | default administrative connection database
template0 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/postgres +| 8113 kB | pg_default | unmodifiable empty database
| | | | | postgres=CTc/postgres | | |
template1 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/postgres +| 8113 kB | pg_default | default template for new databases
| | | | | postgres=CTc/postgres | | |
(3 rows)
The results may differ in your environment, depending on what version of PostgreSQL you are using. This is 14devel, but it doesn’t really matter: You will see that the “postgres” database is a bit larger than the other two. But how can this be? The first database which is created by initdb, is template1. The other two are just copies of it. But then they should all have the same size, shouldn’t they?
To understand what’s going on here, we first need to know how the database size is calculated, when you use the “l+” shortcut. This is easy to check:
postgres=# set ECHO_HIDDEN on
postgres=# l+
********* QUERY **********
SELECT d.datname as "Name",
pg_catalog.pg_get_userbyid(d.datdba) as "Owner",
pg_catalog.pg_encoding_to_char(d.encoding) as "Encoding",
d.datcollate as "Collate",
d.datctype as "Ctype",
pg_catalog.array_to_string(d.datacl, E'n') AS "Access privileges",
CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')
THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))
ELSE 'No Access'
END as "Size",
t.spcname as "Tablespace",
pg_catalog.shobj_description(d.oid, 'pg_database') as "Description"
FROM pg_catalog.pg_database d
JOIN pg_catalog.pg_tablespace t on d.dattablespace = t.oid
ORDER BY 1;
**************************
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges | Size | Tablespace | Description
-----------+----------+----------+-------------+-------------+-----------------------+---------+------------+--------------------------------------------
postgres | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | | 8265 kB | pg_default | default administrative connection database
template0 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/postgres +| 8113 kB | pg_default | unmodifiable empty database
| | | | | postgres=CTc/postgres | | |
template1 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/postgres +| 8113 kB | pg_default | default template for new databases
| | | | | postgres=CTc/postgres | | |
(3 rows)
The statement that is used in the background uses the pg_database_size administrative function to calculate the size for each database. But what is pg_database_size actually doing? Time to check the source. pg_database_size is defined in “src/backend/utils/adt/dbsize.c”:
Datum
pg_database_size_oid(PG_FUNCTION_ARGS)
{
Oid dbOid = PG_GETARG_OID(0);
int64 size;
size = calculate_database_size(dbOid);
if (size == 0)
PG_RETURN_NULL();
PG_RETURN_INT64(size);
}
This in fact calls “calculate_database_size”, which is defined in the same file. If you check this one you’ll see this:
...
/* Shared storage in pg_global is not counted */
/* Include pg_default storage */
snprintf(pathname, sizeof(pathname), "base/%u", dbOid);
totalsize = db_dir_size(pathname);
/* Scan the non-default tablespaces */
snprintf(dirpath, MAXPGPATH, "pg_tblspc");
dirdesc = AllocateDir(dirpath);
...
All it does is to calculate the size of the directory in db_dir_size (and any tablespaces if there are any):
/* Return physical size of directory contents, or 0 if dir doesn't exist */
static int64
db_dir_size(const char *path)
...
while ((direntry = ReadDir(dirdesc, path)) != NULL)
{
It is looping over all the files and then sums up the size of the files it finds there. Nothing is excluded as you can easily verify:
postgres@debian10pg:/home/postgres/ [pg14] cd /var/tmp/dummy/base/
postgres@debian10pg:/var/tmp/dummy/base/ [pg14] oid2name
All databases:
Oid Database Name Tablespace
----------------------------------
13974 postgres pg_default
13973 template0 pg_default
1 template1 pg_default
postgres@debian10pg:/var/tmp/dummy/base/ [pg14] cd 13974
postgres@debian10pg:/var/tmp/dummy/base/13974/ [pg14] dd if=/dev/zero of=dummy bs=1M count=100
100+0 records in
100+0 records out
104857600 bytes (105 MB, 100 MiB) copied, 0.0924525 s, 1.1 GB/s
postgres@debian10pg:/var/tmp/dummy/base/13974/ [pg14] psql -c "l+"
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges | Size | Tablespace | Description
-----------+----------+----------+-------------+-------------+-----------------------+---------+------------+--------------------------------------------
postgres | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | | 108 MB | pg_default | default administrative connection database
template0 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/postgres +| 8113 kB | pg_default | unmodifiable empty database
| | | | | postgres=CTc/postgres | | |
template1 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/postgres +| 8113 kB | pg_default | default template for new databases
| | | | | postgres=CTc/postgres | | |
(3 rows)
postgres@debian10pg:/var/tmp/dummy/base/13974/ [pg14] rm dummy
All I did here, is to create a file a bit larger than 100MB in the directory of the “postgres” database, and asked for the database size once more. As you can see above, the size of this file is counted.
What information can we get out of this? The difference between the “postgres” and the “template0” and “template1” databases must come from the files on disk. Let’s look at the directories:
postgres@debian10pg:/home/postgres/ [pg14] du -s /var/tmp/dummy/base/13974/ 8280 /var/tmp/dummy/base/13974/ ## postgres postgres@debian10pg:/home/postgres/ [pg14] du -s /var/tmp/dummy/base/1/ 8124 /var/tmp/dummy/base/1/ ## template1
Same picture here: “postgres” is larger than “template1” but what is the difference. Actually the difference can easily be spotted:
postgres@debian10pg:/home/postgres/ [pg14] ls -ltr /var/tmp/dummy/base/13974/ | tail -3 # postgres -rw------- 1 postgres postgres 8192 May 20 11:03 1247_vm -rw------- 1 postgres postgres 114688 May 20 11:03 1247 -rw------- 1 postgres postgres 156486 May 20 11:06 pg_internal.init postgres@debian10pg:/home/postgres/ [pg14] ls -ltr /var/tmp/dummy/base/1/ | tail -3 # template1 -rw------- 1 postgres postgres 24576 May 20 11:03 13792_fsm -rw------- 1 postgres postgres 65536 May 20 11:03 13792 -rw------- 1 postgres postgres 106496 May 20 11:03 1259
To verify that this file really is the reason: Stop the instance, delete the file and compare again:
postgres@debian10pg:/home/postgres/ [pg14] pg_ctl -D /var/tmp/dummy/ stop waiting for server to shut down.... done server stopped postgres@debian10pg:/home/postgres/ [pg14] rm /var/tmp/dummy/base/13974/pg_internal.init postgres@debian10pg:/home/postgres/ [pg14] du -s /var/tmp/dummy/base/13974/ 8124 /var/tmp/dummy/base/13974/ postgres@debian10pg:/home/postgres/ [pg14] du -s /var/tmp/dummy/base/1/ 8124 /var/tmp/dummy/base/1/
Exactly the same now. Once you start the instance again, the file will be re-created:
postgres@debian10pg:/home/postgres/ [pg14] pg_ctl -D /var/tmp/dummy/ start
postgres@debian10pg:/home/postgres/ [pg14] ls -ltr /var/tmp/dummy/base/13974/ | tail -3
-rw------- 1 postgres postgres 770048 May 20 11:03 1255
-rw------- 1 postgres postgres 8192 May 20 11:03 1247_vm
-rw------- 1 postgres postgres 114688 May 20 11:03 1247
postgres@debian10pg:/home/postgres/ [pg14] psql -c "l+"
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges | Size | Tablespace | Description
-----------+----------+----------+-------------+-------------+-----------------------+---------+------------+--------------------------------------------
postgres | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | | 8265 kB | pg_default | default administrative connection database
template0 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/postgres +| 8113 kB | pg_default | unmodifiable empty database
| | | | | postgres=CTc/postgres | | |
template1 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/postgres +| 8113 kB | pg_default | default template for new databases
| | | | | postgres=CTc/postgres | | |
(3 rows)
postgres@debian10pg:/home/postgres/ [pg14] ls -ltr /var/tmp/dummy/base/13974/ | tail -3
-rw------- 1 postgres postgres 8192 May 20 11:03 1247_vm
-rw------- 1 postgres postgres 114688 May 20 11:03 1247
-rw------- 1 postgres postgres 156486 May 20 11:45 pg_internal.init
Last question for today: What is that file for? The answer is, once more, in the source code (src/include/utils/relcache.h):
/* * Name of relcache init file(s), used to speed up backend startup */ #define RELCACHE_INIT_FILENAME "pg_internal.init"