Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 1 addition & 49 deletions drivers/pg/optimize.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,60 +10,12 @@ import (
"github.com/jackc/pgx/v5/pgconn"
)

// deadTupleThreshold is the minimum fraction of dead tuples a partitioned
// parent must accumulate across its partitions before OptimizeStorage will
// vacuum it.
const deadTupleThreshold = 0.1

// Sum n_dead_tup and n_live_tup across every leaf partition of the parent;
const optimizeStorageStatsQuery = `
SELECT
COALESCE(SUM(stat.n_dead_tup), 0),
COALESCE(SUM(stat.n_live_tup), 0)
FROM pg_partition_tree($1::regclass) tree
LEFT JOIN pg_stat_user_tables stat ON stat.relid = tree.relid
WHERE tree.isleaf
`

type optimizeStorageConn interface {
Exec(ctx context.Context, sql string, arguments ...any) (pgconn.CommandTag, error)
QueryRow(ctx context.Context, sql string, arguments ...any) pgx.Row
}

func optimizeStorage(ctx context.Context, conn optimizeStorageConn) error {
var targets []string
for _, table := range []string{"node", "edge"} {
var dead, live int64
if err := conn.QueryRow(ctx, optimizeStorageStatsQuery, table).Scan(&dead, &live); err != nil {
return fmt.Errorf("query dead tuple stats for %s: %w", table, err)
}

total := dead + live
var deadTupleRatio float64
if total > 0 {
deadTupleRatio = float64(dead) / float64(total)
}

slog.InfoContext(ctx, "Queried PostgreSQL table storage statistics",
slog.String("table", table),
slog.Int64("dead_tuples", dead),
slog.Int64("live_tuples", live),
slog.Int64("total_tuples", total),
slog.Float64("dead_tuple_ratio", deadTupleRatio),
slog.Float64("dead_tuple_threshold", deadTupleThreshold),
)

if total == 0 {
continue
}
if deadTupleRatio >= deadTupleThreshold {
targets = append(targets, table)
}
}

if len(targets) == 0 {
return nil
}
targets := []string{"node", "edge"}

// Targeting the partitioned parents cascades to every partition.
stmt := "VACUUM (ANALYZE) " + strings.Join(targets, ", ")
Expand Down
61 changes: 1 addition & 60 deletions drivers/pg/optimize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package pg

import (
"context"
"errors"
"testing"

"github.com/jackc/pgx/v5"
Expand All @@ -11,67 +10,15 @@ import (
)

func TestOptimizeStorage(t *testing.T) {
t.Run("skips vacuum when dead tuple ratios are below threshold", func(t *testing.T) {
t.Run("always vacuums node and edge", func(t *testing.T) {
ctx := context.Background()
conn := newOptimizeStorageMockConn(t)

expectOptimizeStorageStats(conn, "node", 9, 91)
expectOptimizeStorageStats(conn, "edge", 0, 0)

require.NoError(t, optimizeStorage(ctx, conn))
require.NoError(t, conn.ExpectationsWereMet())
})

t.Run("vacuums node only", func(t *testing.T) {
ctx := context.Background()
conn := newOptimizeStorageMockConn(t)

expectOptimizeStorageStats(conn, "node", 10, 90)
expectOptimizeStorageStats(conn, "edge", 9, 91)
expectOptimizeStorageVacuum(conn, "VACUUM (ANALYZE) node")

require.NoError(t, optimizeStorage(ctx, conn))
require.NoError(t, conn.ExpectationsWereMet())
})

t.Run("vacuums edge only", func(t *testing.T) {
ctx := context.Background()
conn := newOptimizeStorageMockConn(t)

expectOptimizeStorageStats(conn, "node", 9, 91)
expectOptimizeStorageStats(conn, "edge", 10, 90)
expectOptimizeStorageVacuum(conn, "VACUUM (ANALYZE) edge")

require.NoError(t, optimizeStorage(ctx, conn))
require.NoError(t, conn.ExpectationsWereMet())
})

t.Run("vacuums node and edge", func(t *testing.T) {
ctx := context.Background()
conn := newOptimizeStorageMockConn(t)

expectOptimizeStorageStats(conn, "node", 10, 90)
expectOptimizeStorageStats(conn, "edge", 10, 90)
expectOptimizeStorageVacuum(conn, "VACUUM (ANALYZE) node, edge")

require.NoError(t, optimizeStorage(ctx, conn))
require.NoError(t, conn.ExpectationsWereMet())
})

t.Run("returns query error", func(t *testing.T) {
ctx := context.Background()
conn := newOptimizeStorageMockConn(t)
expectedErr := errors.New("stats unavailable")

conn.ExpectQuery(optimizeStorageStatsQuery).
WithArgs("node").
WillReturnError(expectedErr)

err := optimizeStorage(ctx, conn)
require.ErrorIs(t, err, expectedErr)
require.ErrorContains(t, err, "query dead tuple stats for node")
require.NoError(t, conn.ExpectationsWereMet())
})
}

func newOptimizeStorageMockConn(t *testing.T) pgxmock.PgxConnIface {
Expand All @@ -83,12 +30,6 @@ func newOptimizeStorageMockConn(t *testing.T) pgxmock.PgxConnIface {
return conn
}

func expectOptimizeStorageStats(conn pgxmock.PgxConnIface, table string, dead, live int64) {
conn.ExpectQuery(optimizeStorageStatsQuery).
WithArgs(table).
WillReturnRows(pgxmock.NewRows([]string{"dead", "live"}).AddRow(dead, live))
}

func expectOptimizeStorageVacuum(conn pgxmock.PgxConnIface, stmt string) {
conn.ExpectExec(stmt).
WithArgs(pgx.QueryExecModeSimpleProtocol).
Expand Down
Loading