PG Phriday: Mining for Metadata
Every good database engine has a system catalog that describes the myriad of structures that model and preserve our data. Of course this is expected, as it would be somewhat silly for a database system not to use tables to represent its internal mechanisms. But that doesn’t mean they have to be humanly readable, or even make sense without a series of views or esoteric functions to decipher them. The information_schema standard serves a necessary role in that regard, and the Postgres implementation is extremely comprehensive. Yet the regular Postgres catalog is also fairly usable, so let’s explore and see what we find.
At first glance, there is a veritable smorgasbord of catalog tables and views to choose from. So, where to start? Let’s look at something simple and see what tables are available:
\x
CREATE TABLE picard (
id SERIAL PRIMARY KEY,
full_name TEXT NOT NULL,
description TEXT
);
INSERT INTO picard (full_name, description)
SELECT a.id::TEXT, 'There are ' || a.id || ' lights!'
FROM generate_series(1, 1000000) a(id);
ANALYZE picard;
SELECT *
FROM pg_tables
WHERE schemaname NOT IN ('information_schema', 'pg_catalog');
-[ RECORD 1 ]--------
schemaname | public
tablename | picard
tableowner | sthomas
tablespace |
hasindexes | t
hasrules | f
hastriggers | f
This is all pretty basic info, right? Note that we had to remove tables in the information_schema
and pg_catalog schemas
or we’d have to wade through over 50 other records. Just more proof that Postgres uses its own catalogs to… track its catalogs. What may not be so obvious is that pg_tables
is actually a view.
A DBA might find this obvious, given the concept of normalization. For the uninitiated however, database object names might be referred to multiple times through several catalog entries. To avoid using the same text value in multiple places and risking mismatches or modifying the name everywhere should it change, Postgres uses surrogate keys. Again, this approach is extremely common, but makes views a necessity to decode those proxy values.
Let’s use pg_views
and examine the definition of pg_tables
as well as itself:
SELECT definition
FROM pg_views
WHERE viewname = 'pg_tables';
definition
----------------------------------------------------------------
SELECT n.nspname AS schemaname,
c.relname AS tablename,
pg_get_userbyid(c.relowner) AS tableowner,
t.spcname AS tablespace,
c.relhasindex AS hasindexes,
c.relhasrules AS hasrules,
c.relhastriggers AS hastriggers
FROM ((pg_class c
LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace)))
LEFT JOIN pg_tablespace t ON ((t.oid = c.reltablespace)))
WHERE (c.relkind = 'r'::"char");
SELECT definition
FROM pg_views
WHERE viewname = 'pg_views';
definition
--------------------------------------------------------------
SELECT n.nspname AS schemaname,
c.relname AS viewname,
pg_get_userbyid(c.relowner) AS viewowner,
pg_get_viewdef(c.oid) AS definition
FROM (pg_class c
LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace)))
WHERE (c.relkind = 'v'::"char");
What do we have, here? It appears as if both pg_tables
and pg_views
are derived from information in pg_class
. As it turns out, pg_class
is probably the most important catalog table in the system. It’s essentially a reference for nearly every database object available. Since it’s in the middle of a vast constellation of other references, practically every column in the table is a key to another low-level catalog.
Why are views a relkind
of ‘v’, while tables are designated ‘r’? Well in Postgres, that ‘r’ stands for ‘relation’, as tables describe relationships across data. Probably. Yet the documentation for pg_class
describes all contents as relations, not just tables. Further, the column names in pg_class
don’t reflect a consistent association to the table name. Let’s look at pg_namespace
, which decodes schema (namespace) names, to see why that’s relevant:
\d pg_namespace
Column | Type | Modifiers
----------+-----------+-----------
nspname | name | not null
nspowner | oid | not null
nspacl | aclitem[] |
Indexes:
"pg_namespace_nspname_index" UNIQUE, btree (nspname)
"pg_namespace_oid_index" UNIQUE, btree (oid)
Both pg_tables
and pg_views
utilize this table to decode pg_class.relnamespace
into a text value. Note the ’nsp’ prefix for all of the column names? That naming scheme is fairly consistent across most of the other catalog tables and views. The pg_proc
table that tracks functions (procedures) prefixes everything with ‘pro’, while pg_index
uses ‘ind’. From that perspective, pg_class
is a rather notable exception.
Though a bit perplexing and lost to the annals of history, one benefit of this abnormality is that it makes pg_class
easy to remember. It’s a short name, and since it’s used practically everywhere in the other catalog views, it’s almost synonymous with the catalog itself. Regardless of why, pg_class
is of the first places to look for nearly anything not covered by a ’nicer’ view. A fun example of this is exploiting the relpages
and reltuples
columns:
SELECT relname AS tablename, relpages*8 AS kb_used,
reltuples AS approx_rows
FROM pg_class
WHERE relname = 'picard';
tablename | kb_used | approx_rows
-----------+---------+-------------
picard | 66592 | 1e+06
These two are generally used by the query planner in various calculations to estimate query cost. For our purposes, they provide an excellent summary of the table’s overall impact on the database. We can use sorts to find the table with the most or least rows, the biggest table on disk, combine the two to seek out tables with abnormally large rows, and so on.
The relpages
column refers to the amount of database pages the table consumes. In Postgres, this defaults to 8KB, so in multiplying by 8, we can see how much physical space the table uses. In the vast majority of cases, this is more than sufficient and a quick shortcut when doing ad-hoc analysis of tables, but there are cases when an installation is compiled with a different default page size. So to be pedantic, we should repeat the same query using a couple provided decoding functions:
\x
SELECT relname AS tablename,
pg_relation_size(oid) AS bytes_used,
pg_total_relation_size(oid) AS total_bytes,
pg_size_pretty(pg_relation_size(oid)) AS pretty_size,
pg_size_pretty(pg_total_relation_size(oid)) AS pretty_total,
reltuples AS approx_rows
FROM pg_class
WHERE relname = 'picard';
-[ RECORD 1 ]+---------
tablename | picard
bytes_used | 68190208
total_bytes | 90726400
pretty_size | 65 MB
pretty_total | 87 MB
approx_rows | 1e+06
To provide a lowest common denominator, the pg_relation_size
function always returns the size of the object in bytes. The pg_size_pretty
function just makes that value human readable by transforming it into the number of megabytes, gigabytes, or so on. The reason we used both in the query is to demonstrate the functionality along with the fact we don’t want to use human readable output in calculations.
But what about pg_total_relation_size
? This function is a neat trick that combines the table size along with any related content, like indexes, TOAST data, and so on. Normally the only way to get that data would be to join pg_class
with itself multiple times with reltoastrelid
or pg_index
. That query is pretty unwieldy, so a function call is much easier to use. If we had omitted or changed our WHERE
clause, we could have divined size and row counts for whole swaths of tables in the database.
And what about pg_index
? Its job is to act as a swing table between a table and its indexes as described by pg_class
, as well as describe which columns are in each index, and other identifying information. But this particular catalog table is somewhat opaque, using multiple vector columns and trees to describe objects which can contain multiple other objects. It’s usually much easier to use pg_indexes
instead, which converts all of that into something human readable. For example:
\x
SELECT *
FROM pg_indexes
WHERE tablename = 'picard';
-[ RECORD 1 ]----------------------------------------------------------
schemaname | public
tablename | picard
indexname | picard_pkey
tablespace |
indexdef | CREATE UNIQUE INDEX picard_pkey ON picard USING btree (id)
There isn’t very much information here, actually. But we do get to use the table name to obtain all indexes without any joins, and we can also see the index definition itself. For more complicated dives, like finding all tables that use a particular column in their indexes, especially if we also want to include composite indexes, we’d have to grit our teeth and use pg_index
instead. Though even then, it’s not so bad:
SELECT c.relname AS tablename, i.relname AS indexname
FROM pg_index x
JOIN pg_class c ON (c.oid = x.indrelid)
JOIN pg_class i ON (i.oid = x.indexrelid)
JOIN pg_attribute a ON (
a.attrelid = x.indrelid AND a.attnum = ANY (x.indkey)
)
WHERE a.attname = 'id';
tablename | indexname
-----------+-------------
picard | picard_pkey
While this is hardly a novice-friendly example of multiple catalog tables, it does demonstrate that inquisitive data mining is possible. There’s a surprising amount of wealth stored in Postgres metadata if you’re brave enough to dig for it. Some masochistic DBAs have even constructed unholy amalgams of several catalog tables to derive potential index candidates, optimal column re-ordering for existing indexes, or even which indexes to remove entirely.
How is that even possible? Well, beyond the basic architecture metadata, Postgres collects statistics it stores in yet more tables and views. We’ll leave using these as an exercise for a future article, because they deserve to be explored in depth. Until then, explore the catalog and see what’s buried in its depths; you might be surprised what you find.