PostgreSQL Interview Questions and Answers Part 10 - Partitioning, VACUUM and maintenance

PostgreSQL Interview Q&A · Part 10 of 10

In this post we are going to cover partitioning, VACUUM and maintenance in PostgreSQL: dead tuples, bloat, autovacuum, partition pruning, retention, transaction ID wraparound and disk usage. These are 7 real PostgreSQL interview questions, asked the way they come up on the job.

Postgres needs housekeeping, because of the way MVCC works: old row versions stay in the table until something removes them. Most production incidents with Postgres, a table ten times bigger than its data, a disk that fills up, a database that refuses writes, come from this one topic. Every size and count in this post was measured, not estimated.

Every query below is complete, so you can copy it, run it on the sample data and get exactly the output shown under it.

VIDEO COMING SOON

Part 10 of the Real Time PostgreSQL Interview Questions and Answers series on TechBrothersIT.

Let's create some sample data

This post uses a tickets table with 100,000 rows, where autovacuum is switched off so we can run every maintenance step by hand, and a measurements table that is partitioned by month. The pgstattuple extension gives exact counts of live and dead rows. The script below creates everything this post needs. It was written for and run on PostgreSQL 16.

Sample data - SQL script (PostgreSQL)
CREATE EXTENSION IF NOT EXISTS pgstattuple;      -- exact live and dead row counts

-- 100,000 support tickets. Autovacuum is switched off for this table,
-- so that we can see every step by hand.
CREATE TABLE tickets WITH (autovacuum_enabled = false) AS
SELECT g AS ticket_id, 'open' AS status, repeat('x', 50) AS notes
FROM generate_series(1, 100000) AS g;
ALTER TABLE tickets ADD PRIMARY KEY (ticket_id);
ANALYZE tickets;

-- sensor readings, partitioned by month
CREATE TABLE measurements (
  id      BIGINT,
  logdate DATE NOT NULL,
  device  INT,
  reading NUMERIC(6,2)
) PARTITION BY RANGE (logdate);
CREATE TABLE measurements_2026_04 PARTITION OF measurements FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
CREATE TABLE measurements_2026_05 PARTITION OF measurements FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
CREATE TABLE measurements_2026_06 PARTITION OF measurements FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
INSERT INTO measurements
SELECT g, DATE '2026-04-01' + (g % 91), g % 50, (g % 400) / 10.0
FROM generate_series(1, 91000) AS g;
ANALYZE measurements;

Question 1: A nightly job updates the status of every row in a table. The row count never changes, but the table file doubled in size. Where did the space go?

Say this first

Into dead tuples. Because of MVCC, an UPDATE writes a new version of the row and leaves the old one in place, in case another transaction still needs it. Updating every row means two copies of every row until VACUUM cleans up.

Picture the table file as pages full of rows. 100,000 live rows fill the file. Now update every row. Postgres cannot overwrite them, so it adds 100,000 new versions at the end of the file. The old versions are now dead. Nobody will ever read them again, but they still occupy half of the file.

before
100,000 live rows  ·  9 MB
after UPDATE
100,000 dead rows  ·  9 MB
100,000 new versions  ·  9 MB
Same number of rows, twice the file. The dead half is waiting for VACUUM.

Let's measure it. Before the job: 100,000 live rows, zero dead rows, and the table is about 9 megabytes.

SQL - stat before
SELECT tuple_count      AS live_rows,
       dead_tuple_count AS dead_rows,
       pg_size_pretty(table_len)  AS table_size,
       pg_size_pretty(free_space) AS free_space
FROM pgstattuple('tickets');
Output (PostgreSQL 16)1 row
live_rowsdead_rowstable_sizefree_space
10000009216 kB81 kB

Now the nightly job: one UPDATE that touches every row. And measure again. Still 100,000 live rows, but also 100,000 dead rows, and the table is 18 megabytes.

SQL - update all
UPDATE tickets SET status = 'closed';
SQL - stat after update
SELECT tuple_count      AS live_rows,
       dead_tuple_count AS dead_rows,
       pg_size_pretty(table_len)  AS table_size,
       pg_size_pretty(free_space) AS free_space
FROM pgstattuple('tickets');
Output (PostgreSQL 16)1 row
live_rowsdead_rowstable_sizefree_space
10000010000018 MB153 kB

A DELETE behaves the same way. It only marks rows as dead, so the file does not shrink. This is normal Postgres behaviour, and cleaning it up is the job of VACUUM. That is the next question.

Interview tip

DELETE also leaves dead tuples. The file does not shrink by itself.

Question 2: After that job, you run VACUUM, but the table is still 18 megabytes on disk. A colleague suggests VACUUM FULL. What is the difference between VACUUM, VACUUM FULL and ANALYZE, and which do you run in production?

Say this first

VACUUM removes dead tuples and marks their space as reusable inside the file, without blocking reads or writes. VACUUM FULL rewrites the whole table into a new, smaller file, but holds an exclusive lock the entire time. ANALYZE only refreshes planner statistics.

Three commands, three jobs. Plain VACUUM cleans out the dead rows. The file keeps its size, but the freed space is used by the next inserts and updates. The table stays online. VACUUM FULL copies the live rows into a brand new file and deletes the old one. The disk space goes back to the operating system, but nobody can read or write the table while it runs. And ANALYZE does not clean anything. It samples the table and updates the statistics for the planner.

VACUUMVACUUM FULLANALYZE
Dead tuplesremovedremovednot touched
File size on disksame, space reusableshrinkssame
Lockreads and writes continueACCESS EXCLUSIVE: table offlinereads and writes continue
Extra disk needednoa full copy of the tableno
Use itroutinely (autovacuum)rarely, in a maintenance windowafter big data changes

Run a plain VACUUM on our table. The dead rows are gone: zero. The table is still 18 megabytes, but look at the free space: 9 megabytes inside the file, ready to be reused.

SQL - vacuum plain
VACUUM tickets;
SQL - stat after vacuum
SELECT tuple_count      AS live_rows,
       dead_tuple_count AS dead_rows,
       pg_size_pretty(table_len)  AS table_size,
       pg_size_pretty(free_space) AS free_space
FROM pgstattuple('tickets');
Output (PostgreSQL 16)1 row
live_rowsdead_rowstable_sizefree_space
100000018 MB9133 kB

Now VACUUM FULL. The table is back to 9 megabytes, and the free space is almost nothing. The file was rewritten.

SQL - vacuum full
VACUUM FULL tickets;
SQL - stat after full
SELECT tuple_count      AS live_rows,
       dead_tuple_count AS dead_rows,
       pg_size_pretty(table_len)  AS table_size,
       pg_size_pretty(free_space) AS free_space
FROM pgstattuple('tickets');
Output (PostgreSQL 16)1 row
live_rowsdead_rowstable_sizefree_space
10000009096 kB81 kB

In production, plain VACUUM, through autovacuum, is the daily tool. A table that is the same size every day does not need shrinking. If you really must give space back on a live system, use an online tool like pg_repack instead of VACUUM FULL.

Interview tip

Shrink a table online: pg_repack. VACUUM FULL = downtime for that table.

Question 3: Autovacuum is on, but one busy table keeps bloating. You even run VACUUM by hand, and the dead rows are still there. What do you check?

Say this first

Two things. First the trigger: by default autovacuum waits until 20 percent of the table is dead, which is far too late for a huge table, so lower it per table. Second, who holds an old snapshot: VACUUM cannot remove a row version that a long-running transaction might still see.

When does autovacuum start on a table? When the dead rows pass a threshold: 50 rows plus 20 percent of the table. For a table with 100 million rows, that means 20 million dead rows before anything happens. So for big, hot tables, set a much smaller scale factor on that table only. And the second cause: a transaction that has been open for hours. Every row version newer than its snapshot must be kept, so VACUUM runs, but removes nothing.

vacuum when   dead rows > 50 + 0.20 × rows in the table
100,000 rows
after 20,050 dead rows
100,000,000 rows
after 20,000,050 dead rows
Fix 1: per-table settingsautovacuum_vacuum_scale_factor = 0.01
Fix 2: end long transactionsidle in transaction, old replication slots

First the settings. Turn autovacuum back on for this table, with a scale factor of 1 percent and a threshold of 1,000 rows. The catalog shows the options stored on the table.

SQL - table settings
ALTER TABLE tickets SET (
  autovacuum_enabled = true,
  autovacuum_vacuum_scale_factor = 0.01,
  autovacuum_vacuum_threshold = 1000
);
SQL - show settings
SELECT unnest(reloptions) AS table_setting
FROM pg_class
WHERE relname = 'tickets';
Output (PostgreSQL 16)3 rows
table_setting
autovacuum_enabled=true
autovacuum_vacuum_scale_factor=0.01
autovacuum_vacuum_threshold=1000

Now the blocker. Session B opens a Repeatable Read transaction and reads the table. Its snapshot is now frozen. Session A updates 20,000 tickets and runs VACUUM. Look: 20,000 dead rows are still there. VACUUM ran, but it was not allowed to remove them.

SQL - session B old snapshot (session B)
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT COUNT(*) FROM tickets;
SQL - session A
UPDATE tickets SET status = 'reopened'
WHERE ticket_id <= 20000;
SQL - stat blocked
SELECT tuple_count      AS live_rows,
       dead_tuple_count AS dead_rows,
       pg_size_pretty(table_len)  AS table_size,
       pg_size_pretty(free_space) AS free_space
FROM pgstattuple('tickets');
Output (PostgreSQL 16)1 row
live_rowsdead_rowstable_sizefree_space
1000002000011 MB93 kB

Session B commits, we vacuum again, and now the dead rows are gone.

SQL - stat unblocked
SELECT tuple_count      AS live_rows,
       dead_tuple_count AS dead_rows,
       pg_size_pretty(table_len)  AS table_size,
       pg_size_pretty(free_space) AS free_space
FROM pgstattuple('tickets');
Output (PostgreSQL 16)1 row
live_rowsdead_rowstable_sizefree_space
100000011 MB1889 kB

So when a table bloats, look for the oldest transaction in pg_stat_activity, for sessions that are idle in transaction, for forgotten replication slots, and for prepared transactions. Any one of them can hold cleanup back for the whole database.

Question 4: A measurements table has billions of rows, and almost every query asks for one month. How does partitioning help, and how do you prove that Postgres reads only the partitions it needs?

Say this first

Partition the table by range on the date. Each month becomes its own smaller table. When the query filters on the partition key, the planner prunes the partitions that cannot match, and EXPLAIN shows only the ones that are scanned.

One logical table, three physical ones. Rows are routed to a partition by their date. A query for June carries a filter on the partition key. The planner compares it with the bounds of each partition, and throws April and May out of the plan before the query even starts. Only June is read.

SELECT ... FROM measurements WHERE logdate >= '2026-06-01' AND logdate < '2026-07-01'
measurements   PARTITION BY RANGE (logdate)
measurements_2026_04Apr 1 → May 1 · 30,000 rows
measurements_2026_05May 1 → Jun 1 · 31,000 rows
measurements_2026_06Jun 1 → Jul 1 · 30,000 rows
prunedprunedscanned

Here is how the rows are spread. The hidden column tableoid tells us which partition each row lives in: about 30,000 rows per month.

SQL - rows per partition
SELECT tableoid::regclass AS partition,
       COUNT(*) AS rows
FROM measurements
GROUP BY 1
ORDER BY 1;
Output (PostgreSQL 16)3 rows
partitionrows
measurements_2026_0430000
measurements_2026_0531000
measurements_2026_0630000

Now the June query. The plan has exactly one scan, on the June partition. April and May do not appear at all.

SQL - plan pruned
EXPLAIN (COSTS OFF)
SELECT COUNT(*) FROM measurements
WHERE logdate >= DATE '2026-06-01'
  AND logdate <  DATE '2026-07-01';
Query plan (PostgreSQL 16)
Aggregate
  ->  Seq Scan on measurements_2026_06 measurements
        Filter: ((logdate >= '2026-06-01'::date) AND (logdate < '2026-07-01'::date))

But filter on the device, which is not the partition key, and Postgres has to scan all three partitions. Partitioning only helps queries that filter on the key.

SQL - plan not pruned
EXPLAIN (COSTS OFF)
SELECT COUNT(*) FROM measurements
WHERE device = 7;
Query plan (PostgreSQL 16)
Aggregate
  ->  Append
        ->  Seq Scan on measurements_2026_04 measurements_1
              Filter: (device = 7)
        ->  Seq Scan on measurements_2026_05 measurements_2
              Filter: (device = 7)
        ->  Seq Scan on measurements_2026_06 measurements_3
              Filter: (device = 7)

So choose the partition key from your most common WHERE clause, and from how you expire data. And do not over-partition. Thousands of tiny partitions slow down planning. A partition should hold millions of rows, not thousands.

Question 5: You partitioned the table by month. Now three things surprise the team: the primary key on ID is rejected, an insert for next month fails, and deleting last year's data still takes hours. What is going on?

Say this first

A unique or primary key on a partitioned table must include the partition key. A row with no matching partition is rejected, unless a DEFAULT partition exists. And old data should be removed by detaching and dropping the partition, which is instant, not by DELETE.

Three rules. Each partition has its own index, and Postgres cannot check uniqueness across partitions, so the key must contain the partition column. Every row needs a home. If no partition covers its date, the insert fails. Create partitions ahead of time. And retention is where partitioning shines. Dropping a partition removes a whole month as one file operation: no row-by-row delete, no dead tuples, no vacuum.

1. KeysPRIMARY KEY (id) ✗
PRIMARY KEY (id, logdate) ✓
2. Missing partitionJuly row, no July partition → error
create partitions ahead, or a DEFAULT
3. RetentionDELETE: hours, bloat, vacuum
DROP partition: instant

Rule one. A primary key on ID alone is rejected. Add the partition column, and it is accepted.

SQL - pk rejected
ALTER TABLE measurements
  ADD PRIMARY KEY (id);
Error (PostgreSQL 16)ERROR: unique constraint on partitioned table must include all partitioning columns DETAIL: PRIMARY KEY constraint on table "measurements" lacks column "logdate" which is part of the partition key.
SQL - pk accepted
ALTER TABLE measurements
  ADD PRIMARY KEY (id, logdate);

Result: ALTER TABLE

Rule two. Insert a reading for July. There is no July partition, so the row is refused.

SQL - no partition
INSERT INTO measurements
VALUES (999999, DATE '2026-07-15', 1, 20.5);
Error (PostgreSQL 16)ERROR: no partition of relation "measurements" found for row DETAIL: Partition key of the failing row contains (logdate) = (2026-07-15).

Rule three. Retention for April: detach the partition, then drop it. Two quick statements, and 30,000 rows are gone. Only May and June are left.

SQL - drop april
ALTER TABLE measurements
  DETACH PARTITION measurements_2026_04;
DROP TABLE measurements_2026_04;
SQL - partitions left
SELECT tableoid::regclass AS partition, COUNT(*) AS rows
FROM measurements GROUP BY 1 ORDER BY 1;
Output (PostgreSQL 16)2 rows
partitionrows
measurements_2026_0531000
measurements_2026_0630000

On a busy system, detach with the CONCURRENTLY option so queries are not blocked. And automate the creation of future partitions, with a scheduled job or the pg_partman extension, so rule two never wakes you up at midnight on the first of the month.

Interview tip

DETACH PARTITION ... CONCURRENTLY on busy tables. Automate new partitions (pg_partman).

Question 6: Monitoring raises an alert: transaction ID age is over one billion on the main database. The team has never heard of it. What is transaction ID wraparound, and why is it serious?

Say this first

Transaction IDs are 32-bit numbers, so about 4 billion, used in a circle. A row is visible only to transactions after the one that created it, so very old rows must be frozen by VACUUM before the counter comes around. If that never happens, Postgres stops accepting writes to protect the data.

Think of the transaction counter as a clock face. Every row remembers the transaction that created it. Rows about two billion transactions behind the current one would suddenly look like they are in the future, and vanish. To prevent that, VACUUM freezes old rows: it marks them as visible to everyone, forever. Autovacuum forces this when a table's age reaches 200 million. If freezing is blocked for too long, Postgres first warns, and near the limit it refuses new write transactions until you vacuum.

XID age
200 millionautovacuum forces a freeze
(autovacuum_freeze_max_age)
Getting close to 2 billionwarnings in the log:
vacuum this database
About 2 billionnew write transactions refused
until VACUUM completes
Freezing = VACUUM marks old rows as visible to all transactions, so their XID no longer matters.

Here is the number to monitor. The age of each database's frozen transaction ID. On this fresh server the ages are tiny. In production, alert long before one billion.

SQL - database age
SELECT datname,
       age(datfrozenxid) AS xid_age
FROM pg_database
WHERE datname = 'postgres' OR datistemplate
ORDER BY datname;
Output (PostgreSQL 16)3 rows
datnamexid_age
postgres1446
template01446
template11446

And per table, to find which table is holding the database back.

SQL - table age
SELECT c.relname,
       age(c.relfrozenxid) AS xid_age
FROM pg_class c
WHERE c.relkind = 'r'
  AND c.relnamespace = 'public'::regnamespace
ORDER BY xid_age DESC
LIMIT 5;
Output (PostgreSQL 16)3 rows
relnamexid_age
measurements_2026_0516
measurements_2026_0615
tickets4

The usual causes are the same blockers as before: a transaction open for days, an abandoned replication slot, a prepared transaction, or autovacuum switched off on a big table. Fix the blocker, then run VACUUM FREEZE on the oldest tables.

Interview tip

Alert on age(datfrozenxid). Fix the blocker, then VACUUM FREEZE the oldest tables.

Question 7: The disk on the database server is 85 percent full, and it grows every week. How do you find out which tables and indexes are using the space?

Say this first

Ask the catalog. pg_total_relation_size returns a table with its indexes and TOAST data, pg_relation_size the table alone, and pg_indexes_size its indexes. Sort by the total, and compare the table part with the index part.

One table on disk is really three things. The heap: the table's own rows. The indexes, which together can be bigger than the table. And TOAST, the side storage for large values like long text and JSON. The total relation size adds all three, and that is the number that matters for the disk.

Heapthe rows
pg_relation_size
+
Indexesall indexes of the table
pg_indexes_size
+
TOASTlarge values stored aside
pg_total_relation_size: everything the table costs on disk

Here is the query I keep at hand. For every table in the schema: the total, the table alone, and the indexes, largest first.

SQL - largest tables
SELECT c.relname AS table_name,
  pg_size_pretty(pg_total_relation_size(c.oid)) AS total,
  pg_size_pretty(pg_relation_size(c.oid))       AS table_only,
  pg_size_pretty(pg_indexes_size(c.oid))        AS indexes
FROM pg_class c
WHERE c.relkind = 'r'
  AND c.relnamespace = 'public'::regnamespace
ORDER BY pg_total_relation_size(c.oid) DESC
LIMIT 5;
Output (PostgreSQL 16)3 rows
table_nametotaltable_onlyindexes
tickets13 MB11 MB2648 kB
measurements_2026_052576 kB1584 kB968 kB
measurements_2026_062496 kB1536 kB936 kB

Read the result like this. A table whose indexes are larger than its data probably has unused or duplicate indexes. A table that is far bigger than its row count explains is bloated, so check its dead rows and its autovacuum settings. And a table that simply has too much history needs a retention plan, which usually means partitions you can drop.

Indexes bigger than the tablelook for unused or duplicate indexes
Size far above the row countbloat: check dead rows and autovacuum
Too much historyretention: partitions you can drop

Key points to remember

  • UPDATE and DELETE leave dead tuples. VACUUM makes that space reusable
  • VACUUM FULL shrinks the file but locks the table. Plain VACUUM does not
  • Tune autovacuum per table, and hunt long transactions that block cleanup
  • Partition by the column you filter and expire by, and drop old partitions
  • Monitor age(relfrozenxid) and the largest relations before they become incidents

The full SQL script

Here is everything in one script: the sample data and every query from this post, in order.

Show the full SQL script (sample data and every query)
Full script - copy and run top to bottom
-- ========================================================================
-- PostgreSQL Interview Q&A #10 - Partitioning, VACUUM and maintenance
-- TechBrothersIT - run on PostgreSQL 16
-- ========================================================================

-- Sample data
CREATE EXTENSION IF NOT EXISTS pgstattuple;      -- exact live and dead row counts

-- 100,000 support tickets. Autovacuum is switched off for this table,
-- so that we can see every step by hand.
CREATE TABLE tickets WITH (autovacuum_enabled = false) AS
SELECT g AS ticket_id, 'open' AS status, repeat('x', 50) AS notes
FROM generate_series(1, 100000) AS g;
ALTER TABLE tickets ADD PRIMARY KEY (ticket_id);
ANALYZE tickets;

-- sensor readings, partitioned by month
CREATE TABLE measurements (
  id      BIGINT,
  logdate DATE NOT NULL,
  device  INT,
  reading NUMERIC(6,2)
) PARTITION BY RANGE (logdate);
CREATE TABLE measurements_2026_04 PARTITION OF measurements FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
CREATE TABLE measurements_2026_05 PARTITION OF measurements FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
CREATE TABLE measurements_2026_06 PARTITION OF measurements FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
INSERT INTO measurements
SELECT g, DATE '2026-04-01' + (g % 91), g % 50, (g % 400) / 10.0
FROM generate_series(1, 91000) AS g;
ANALYZE measurements;

-- Step 1 (question 1): stat before
SELECT tuple_count      AS live_rows,
       dead_tuple_count AS dead_rows,
       pg_size_pretty(table_len)  AS table_size,
       pg_size_pretty(free_space) AS free_space
FROM pgstattuple('tickets');

-- Step 2 (question 1): update all
UPDATE tickets SET status = 'closed';

-- Step 3 (question 1): stat after update
SELECT tuple_count      AS live_rows,
       dead_tuple_count AS dead_rows,
       pg_size_pretty(table_len)  AS table_size,
       pg_size_pretty(free_space) AS free_space
FROM pgstattuple('tickets');

-- Step 4 (question 2): vacuum plain
VACUUM tickets;

-- Step 5 (question 2): stat after vacuum
SELECT tuple_count      AS live_rows,
       dead_tuple_count AS dead_rows,
       pg_size_pretty(table_len)  AS table_size,
       pg_size_pretty(free_space) AS free_space
FROM pgstattuple('tickets');

-- Step 6 (question 2): vacuum full
VACUUM FULL tickets;

-- Step 7 (question 2): stat after full
SELECT tuple_count      AS live_rows,
       dead_tuple_count AS dead_rows,
       pg_size_pretty(table_len)  AS table_size,
       pg_size_pretty(free_space) AS free_space
FROM pgstattuple('tickets');

-- Step 8 (question 3): table settings
ALTER TABLE tickets SET (
  autovacuum_enabled = true,
  autovacuum_vacuum_scale_factor = 0.01,
  autovacuum_vacuum_threshold = 1000
);

-- Step 9 (question 3): show settings
SELECT unnest(reloptions) AS table_setting
FROM pg_class
WHERE relname = 'tickets';

-- Step 10 (question 3): session B old snapshot  [run in session B]
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT COUNT(*) FROM tickets;

-- Step 11 (question 3): session A
UPDATE tickets SET status = 'reopened'
WHERE ticket_id <= 20000;

-- Step 12 (question 3): stat blocked
SELECT tuple_count      AS live_rows,
       dead_tuple_count AS dead_rows,
       pg_size_pretty(table_len)  AS table_size,
       pg_size_pretty(free_space) AS free_space
FROM pgstattuple('tickets');

-- Step 13 (question 3): stat unblocked
SELECT tuple_count      AS live_rows,
       dead_tuple_count AS dead_rows,
       pg_size_pretty(table_len)  AS table_size,
       pg_size_pretty(free_space) AS free_space
FROM pgstattuple('tickets');

-- Step 14 (question 4): rows per partition
SELECT tableoid::regclass AS partition,
       COUNT(*) AS rows
FROM measurements
GROUP BY 1
ORDER BY 1;

-- Step 15 (question 4): plan pruned
EXPLAIN (COSTS OFF)
SELECT COUNT(*) FROM measurements
WHERE logdate >= DATE '2026-06-01'
  AND logdate <  DATE '2026-07-01';

-- Step 16 (question 4): plan not pruned
EXPLAIN (COSTS OFF)
SELECT COUNT(*) FROM measurements
WHERE device = 7;

-- Step 17 (question 5): pk rejected
ALTER TABLE measurements
  ADD PRIMARY KEY (id);

-- Step 18 (question 5): pk accepted
ALTER TABLE measurements
  ADD PRIMARY KEY (id, logdate);

-- Step 19 (question 5): no partition
INSERT INTO measurements
VALUES (999999, DATE '2026-07-15', 1, 20.5);

-- Step 20 (question 5): drop april
ALTER TABLE measurements
  DETACH PARTITION measurements_2026_04;
DROP TABLE measurements_2026_04;

-- Step 21 (question 5): partitions left
SELECT tableoid::regclass AS partition, COUNT(*) AS rows
FROM measurements GROUP BY 1 ORDER BY 1;

-- Step 22 (question 6): database age
SELECT datname,
       age(datfrozenxid) AS xid_age
FROM pg_database
WHERE datname = 'postgres' OR datistemplate
ORDER BY datname;

-- Step 23 (question 6): table age
SELECT c.relname,
       age(c.relfrozenxid) AS xid_age
FROM pg_class c
WHERE c.relkind = 'r'
  AND c.relnamespace = 'public'::regnamespace
ORDER BY xid_age DESC
LIMIT 5;

-- Step 24 (question 7): largest tables
SELECT c.relname AS table_name,
  pg_size_pretty(pg_total_relation_size(c.oid)) AS total,
  pg_size_pretty(pg_relation_size(c.oid))       AS table_only,
  pg_size_pretty(pg_indexes_size(c.oid))        AS indexes
FROM pg_class c
WHERE c.relkind = 'r'
  AND c.relnamespace = 'public'::regnamespace
ORDER BY pg_total_relation_size(c.oid) DESC
LIMIT 5;

The sample data on this page is invented for teaching. Every output table and query plan is the real result of the query above it, run on PostgreSQL 16 over that sample data.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.