Library / SDK
cybertec-postgresql/pg_squeeze avatar
cybertec-postgresql/pg_squeeze

pg_squeeze: reclaiming bloat in PostgreSQL without locking the table

A PostgreSQL extension for automatic bloat cleanup

702 stars40 forksCNOASSERTION

At a glance

What is it?
pg_squeeze is a PostgreSQL extension that removes unused space from a table and can reorder tuples by a chosen index while regular reads and writes continue. Unlike pg_repack it is entirely server side, uses logical decoding instead of triggers, and runs as a background worker on a schedule you define.
Who is it for?
Adopt pg_squeeze if you run PostgreSQL with tables that accumulate bloat and you want vacuum-like cleanup that also reorders tuples by an index, because it does that work server side with logical decoding and no exclusive lock. Do not adopt it if you cannot add a replication slot and set wal_level, since the extension needs logical decoding to capture concurrent changes, and do not expect it to be a drop-in for VACUUM on small tables.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 31 days ago.
What is it written in?
Mainly C, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the extension actually does to a table

The README describes the extension in one sentence: it removes unused space from a table and optionally sorts tuples according to a particular index, as if the CLUSTER command was executed concurrently with regular reads and writes. That second half is what separates it from a plain VACUUM. VACUUM reclaims dead space into the free space map so that the space can be reused, but it leaves the physical order of the tuples alone, so a table that was updated in a random order stays random on disk, and index scans keep jumping around pages. pg_squeeze can move the tuples into index order as it reclaims the space, which improves locality for the index you pick. The stated aim is to replace pg_repack, and the comparison is explicit: while providing very similar functionality, pg_squeeze takes a different approach, implementing the functionality purely on the server side and using recent improvements of the PostgreSQL server.

Logical decoding instead of triggers

The second difference is the mechanism for capturing changes that happen while the work is in progress. The README says that, besides the use of background workers, an important difference is that pg_squeeze uses logical decoding instead of triggers to capture concurrent changes. That matters because a trigger based approach has to fire on every write to every watched row and pay the cost inside the writer's transaction, while logical decoding reads the change stream that the server is already producing for replication. The consequence for you as an operator is concrete: the extension needs a replication slot, so max_replication_slots has to be raised, and on servers below version 19 the wal_level setting must be changed to logical. Those are cluster-wide settings, they require a restart, and they change the operational profile of the instance, which is why pg_squeeze is a decision about the database rather than a drop-in function.

Building and installing the extension

The build is the standard PostgreSQL extension build. The README says to install PostgreSQL before proceeding and to make sure the pg_config binary is available, which is typically included in the -dev and -devel packages.

bash
git clone https://github.com/cybertec-postgresql/pg_squeeze.git
cd pg_squeeze
make
make install

The Makefile confirms the shape of the module. PG_CONFIG defaults to pg_config, MODULE_big is pg_squeeze, and the objects are pg_squeeze.o, concurrent.o, worker.o and pgstatapprox.o, which matches the source files in the repository root. The extension is named pg_squeeze, the data files are the SQL scripts from version 1.2 up to 1.9, the documentation is pg_squeeze.md, and the regression test suite is named squeeze and lives under test/ with expectations in expected/.

The three postgresql.conf settings and the extension command

After installing the binaries, three settings in postgresql.conf have to be adjusted, and each comment in the README tells you whether to add to an existing value or replace it.

yaml
max_replication_slots = 1 # ... or add 1 to the current value.
shared_preload_libraries = 'pg_squeeze' # ... or add the library to the existing ones.
output_plugin_libraries = 'pg_squeeze' # ... or add the library to the existing ones.

On a server whose version is lower than 19, one more setting is required.

yaml
wal_level = logical

Then you restart the cluster and create the extension.

sql
CREATE EXTENSION pg_squeeze;

The upgrade note deserves attention because it is a trap. The README says that when upgrading a database cluster with pg_squeeze installed, whether using pg_dumpall and restore or pg_upgrade, the new cluster must have pg_squeeze in shared_preload_libraries before you upgrade, otherwise the upgrade will fail.

Registering a table and giving it a schedule

Registration happens in the squeeze.tables table, and the simplest form inserts the schema, the table name and a schedule.

sql
INSERT INTO squeeze.tables (tabschema, tabname, schedule)
VALUES ('public', 'foo', ('{30}', '{22}', NULL, NULL, '{3, 5}'));

Once a table is registered, the README says its statistics are checked periodically, and whenever it meets the criteria to be squeezed, a task is added to a queue, and the tasks are processed sequentially in the order they were created. The schedule column resembles a crontab entry and is a composite type of minutes, hours, days_of_month, months and days_of_week. Minutes are 0 to 59 and hours 0 to 23, while days_of_month is 1 to 31, months 1 to 12 and days_of_week 0 to 7 where both 0 and 7 stand for Sunday. A NULL in the minutes, hours or months position means any value for that field, and for days_of_month and days_of_week at least one has to match or both have to be NULL. The example above therefore means the table is checked every Wednesday and Friday at 22:30.

The knobs that decide when a table is worth squeezing

The metadata columns are where the operational judgement lives. free_space_extra is the minimum percentage of extra free space needed to trigger processing, and the adjective extra refers to the fact that free space derived from fillfactor is not a reason to squeeze the table. If fillfactor is 60, at least 40 percent of each page should stay free during normal operation, so a value of 30 means 70 percent free is required to make the table interesting, and the default is 50. min_size is the minimum disk space in megabytes the table must occupy to be eligible, defaulting to 8. vacuum_max_age is the maximum time since the last VACUUM for the free space map to be considered fresh, defaulting to 1 hour, because once that interval has elapsed the portion of dead tuples might be significant. max_retry is the maximum number of extra attempts if the first processing failed, typically because the table definition changed while the table was being squeezed, and the default is 0, meaning no retry.

Clustering, tablespaces and the ANALYZE step

Three more columns decide what happens after the space is reclaimed. clustering_index names an existing index of the processed table, and once processing finishes, the tuples of the table are physically sorted by the key of that index. rel_tablespace names an existing tablespace the table should be moved into, with NULL meaning the table stays where it is, and ind_tablespaces is a two dimensional array mapping index names to tablespace names, with unmapped indexes staying in the original tablespace. The README calls out the special case: if a tablespace is specified for the table but not for the indexes, the table moves and the indexes stay, so the table's tablespace is not the default for indexes as one might expect. skip_analyze controls whether processing is followed by an ANALYZE command, and the default is false, so ANALYZE is performed. The README also notes that squeeze.table is the only table a user should manipulate directly, with the rest driven by the background worker.

The limits, the licence and what it replaces

Several constraints are worth stating before you deploy this. The extension only works where logical decoding is available, so a managed PostgreSQL that does not expose wal_level or replication slots rules it out, and the identity index requirement rules out tables without a primary key unless you set the replica identity explicitly with ALTER TABLE... REPLICA IDENTITY USING INDEX. The alternative approach, and the one the README names, is pg_repack, which uses both server and client side code, while pg_squeeze implements the same functionality purely on the server side and runs concurrently with reads and writes, at the cost of requiring a replication slot and the extra work of a background worker. There is also the ordinary trade-off of any tool that rewrites a table: the work is real I/O on your instance, and the scheduling exists precisely so that it is spread out rather than run at a peak. On the paperwork side, the licence is listed as other, a custom licence GitHub cannot classify, so the LICENSE file in the repository is the thing to read. The project is not archived, the last push was on 2026-09-22, and the recent tags are REL1_9_4 from 2026-07-29, REL1_9_3 and REL1_9_2 both from 2026-06-19, so the 1.9 series is where the work is happening.

Editorial conclusion

Adopt pg_squeeze if you run PostgreSQL with tables that accumulate bloat and you want vacuum-like cleanup that also reorders tuples by an index, because it does that work server side with logical decoding and no exclusive lock. Do not adopt it if you cannot add a replication slot and set wal_level, since the extension needs logical decoding to capture concurrent changes, and do not expect it to be a drop-in for VACUUM on small tables. Before you enable it, register each table in squeeze.tables with a schedule, confirm every table has a primary key or an explicit replica identity, and read the upgrade note about shared_preload_libraries before running pg_upgrade.

Frequently asked questions

How does pg_squeeze differ from pg_repack?

The README says pg_squeeze aims to replace pg_repack and takes a different approach: it implements the functionality purely on the server side, which makes configuration and use simpler, and it uses recent improvements of the PostgreSQL server, including background workers and logical decoding instead of triggers to capture concurrent changes.

Which PostgreSQL settings does pg_squeeze need?

The README asks you to set max_replication_slots to at least 1, and to add pg_squeeze to shared_preload_libraries and to output_plugin_libraries. On a server version lower than 19 you also need wal_level set to logical, and then you restart the cluster and run CREATE EXTENSION pg_squeeze.

What does a table need before pg_squeeze can process it?

The README says the table must have an identity index, which is necessary to process changes other transactions might do while pg_squeeze is working. A primary key constraint provides it when the replica identity is DEFAULT or FULL, and otherwise you set it explicitly with the ALTER TABLE ... REPLICA IDENTITY USING INDEX command.

How does the squeeze.tables schedule work?

The schedule is a composite type resembling a crontab entry with minutes, hours, days_of_month, months and days_of_week. A NULL in minutes, hours or months means any value, and for days_of_month and days_of_week at least one must match or both must be NULL. The example in the README checks a table every Wednesday and Friday at 22:30.

What happens to a table's indexes and tablespaces during a squeeze?

You can set clustering_index so the tuples are physically sorted by that index when processing finishes, and rel_tablespace to move the table. The README notes the special case that if a tablespace is given for the table but not for the indexes, the table moves and the indexes stay, so the table's tablespace is not the default for indexes.

Official sources

  1. cybertec-postgresql/pg_squeeze on GitHub
  2. Issues
  3. README
  4. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/cybertec-postgresql-pg-squeeze.svg)](https://hysenlabs.com/projects/cybertec-postgresql-pg-squeeze)

Was this useful?