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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@ true until the next version shipped.

### Added

- The planner's estimate of how many time buckets a `GROUP BY date_trunc(...)`
or `date_bin(...)` produces now reads a per-column extent
(`pgcolumnar.column_extent`) in one index lookup, instead of every row group's
zone map (#1304, for #1292). Planning cost no longer grows with the table.
- **Writes:** a write adds its own extent row when it commits.
- **Recomputed:** every operation that drops or rewrites row groups (`expire`,
`compact_rewrite`, `VACUUM`, `recluster`) recomputes the extent before
committing. So it is never narrower than the data, and wider only by rows
deleted but still stored.
- **Fallback:** where the zone maps cannot give a bound, the planner reads them
as before: a column added after older groups, or a type with no ordering.
- **Inspection:** `pgcolumnar.column_extent(rel, col)` shows the bound.
- **Tests:** `test/column_extent.sh` and an independent
`test_column_extent.py`.

- Approximate distinct counts and percentiles over columnar tables are tested
with the `hll` and `tdigest` extensions (#1296 phase 2), and a how-to section
states the tested versions: hll 2.19 on PostgreSQL 15 to 18 (it does not build
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ OBJS = \
src/columnar_tableam.o \
src/columnar_storage.o \
src/columnar_metadata.o \
src/columnar_extent.o \
src/columnar_write_state.o \
src/columnar_compression.o \
src/columnar_encoding.o \
Expand Down
31 changes: 31 additions & 0 deletions docs/sql-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,37 @@ which counts as appended. The record is internal storage metadata and `pg_dump`
does not carry it, so a restored table reports no sorted groups until you sort it
again.

### pgcolumnar.column_extent(rel regclass, col name)

Shows the bound the planner uses for a column's smallest and largest value, when
it estimates how many time buckets a `GROUP BY date_trunc(...)` or `date_bin(...)`
produces. Returns one row, or none:

| Column | Type | Meaning |
| --- | --- | --- |
| `complete` | bool | Always true when a row is returned. |
| `minimum` | text | The smallest value, printed by the column's type. NULL if the column holds no non-NULL value. |
| `maximum` | text | The largest value, printed the same way. |

The bound is kept in `pgcolumnar.column_extent`, and the planner reads it in one
index lookup however many row groups the table has. Writes widen it when they
commit. Every operation that drops or rewrites row groups recomputes it before
committing: `expire`, `compact_rewrite`, `VACUUM`, `recluster`. So it can be
wider than the data only by rows that are deleted but still stored.

No row means the planner reads every row group's zone map instead, which gives
the same answer at a cost that grows with the table. That happens for:

- a column added after some row groups were written;
- a type with no ordering;
- a table written by an earlier version, until its next recompute.

The values are column data, so the caller needs `SELECT` on the table.

```sql
SELECT minimum, maximum FROM pgcolumnar.column_extent('events', 'ts');
```

### pgcolumnar.maintenance_due(rel regclass, compact_due_fraction float8 DEFAULT 0.2, recluster_due_fraction float8 DEFAULT 0.05)

Reports whether an online maintenance verb is worth running on a table, from its
Expand Down
30 changes: 30 additions & 0 deletions pgcolumnar--1.0-alpha4--1.0-alpha5.sql
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,36 @@ CREATE TABLE pgcolumnar.retired_group (
CREATE UNIQUE INDEX retired_group_pkey
ON pgcolumnar.retired_group USING btree (storage_id, group_number);

/* ---------------------------------------------------------------------------
* pgcolumnar.column_extent (#1304 option 5)
*
* A bound on every value of a column, for the planner's bucket estimate: one
* index lookup instead of every row group's zone map. Rows are inserted and
* deleted, never updated: each committing writer adds one row (complete =
* false) for what it flushed, and a recompute over every visible zone map
* replaces the rows it saw with one complete row. The planner trusts the rows
* only when a complete one is visible; then their fold is a true bound.
* minimum and maximum are encoded as the zone maps encode them; NULL when the
* row saw no value. No GRANT: the values are column data, like zone_map's.
* ------------------------------------------------------------------------- */
CREATE TABLE pgcolumnar.column_extent (
storage_id bigint NOT NULL,
column_index smallint NOT NULL,
complete bool NOT NULL,
minimum bytea,
maximum bytea
);
CREATE INDEX column_extent_pkey
ON pgcolumnar.column_extent USING btree (storage_id, column_index);

CREATE FUNCTION pgcolumnar.column_extent(rel regclass, col name)
RETURNS TABLE (complete bool, minimum text, maximum text)
LANGUAGE C STRICT STABLE
AS 'MODULE_PATHNAME', 'pgcolumnar_column_extent';

COMMENT ON FUNCTION pgcolumnar.column_extent(regclass, name)
IS 'the bound on a column the planner reads from pgcolumnar.column_extent, printed by the column type; no row when it would read the zone maps instead (#1304); requires SELECT on the table';


/* ---------------------------------------------------------------------------
* Incremental rollups, phase 1: append-only (#1294).
Expand Down
30 changes: 30 additions & 0 deletions pgcolumnar--1.0-alpha5.sql
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,36 @@ CREATE TABLE pgcolumnar.retired_group (
CREATE UNIQUE INDEX retired_group_pkey
ON pgcolumnar.retired_group USING btree (storage_id, group_number);

/* ---------------------------------------------------------------------------
* pgcolumnar.column_extent (#1304 option 5)
*
* A bound on every value of a column, for the planner's bucket estimate: one
* index lookup instead of every row group's zone map. Rows are inserted and
* deleted, never updated: each committing writer adds one row (complete =
* false) for what it flushed, and a recompute over every visible zone map
* replaces the rows it saw with one complete row. The planner trusts the rows
* only when a complete one is visible; then their fold is a true bound.
* minimum and maximum are encoded as the zone maps encode them; NULL when the
* row saw no value. No GRANT: the values are column data, like zone_map's.
* ------------------------------------------------------------------------- */
CREATE TABLE pgcolumnar.column_extent (
storage_id bigint NOT NULL,
column_index smallint NOT NULL,
complete bool NOT NULL,
minimum bytea,
maximum bytea
);
CREATE INDEX column_extent_pkey
ON pgcolumnar.column_extent USING btree (storage_id, column_index);

CREATE FUNCTION pgcolumnar.column_extent(rel regclass, col name)
RETURNS TABLE (complete bool, minimum text, maximum text)
LANGUAGE C STRICT STABLE
AS 'MODULE_PATHNAME', 'pgcolumnar_column_extent';

COMMENT ON FUNCTION pgcolumnar.column_extent(regclass, name)
IS 'the bound on a column the planner reads from pgcolumnar.column_extent, printed by the column type; no row when it would read the zone maps instead (#1304); requires SELECT on the table';

/* ---------------------------------------------------------------------------
* Access method (spec 8.1)
* ------------------------------------------------------------------------- */
Expand Down
29 changes: 29 additions & 0 deletions src/columnar.h
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,34 @@ extern List *PgColumnarReadWholeChunkZoneMapsForColumn(uint64 storageId, int col
extern void PgColumnarDeleteMetadata(uint64 storageId);
extern void PgColumnarRecordRetention(uint64 storageId, uint64 groupNumber);

/* pgcolumnar.column_extent (#1304 option 5): columnar_metadata.c */
typedef struct NativeExtentRow
{
ItemPointerData tid;
bool complete;
bool hasValues; /* false: the row's writer saw only NULLs */
char *minimum;
int minimumLen;
char *maximum;
int maximumLen;
} NativeExtentRow;
extern bool PgColumnarExtentCatalogPresent(void);
extern void PgColumnarInsertExtentRow(uint64 storageId, int columnIndex, bool complete,
const char *minimum, int minimumLen,
const char *maximum, int maximumLen);
extern List *PgColumnarReadExtentRows(uint64 storageId, int columnIndex, Snapshot snapshot);
extern void PgColumnarDeleteExtentRow(ItemPointer tid);
extern Oid PgColumnarStorageRelationOid(uint64 storageId, Snapshot snapshot);

/* the extent's lifecycle: columnar_extent.c */
extern void PgColumnarExtentNote(Oid relid, uint64 storageId, int columnIndex,
const NativeZoneMapMetadata *z);
extern void PgColumnarExtentMarkRecompute(uint64 storageId);
extern void PgColumnarExtentPreCommit(void);
extern void PgColumnarExtentAtXactEnd(void);
extern bool PgColumnarExtentBound(Relation rel, int columnIndex, Snapshot snapshot,
Datum *minimum, Datum *maximum, bool *hasValues);

/* declared retention, read by pgcolumnar.expire (#403 item 5a) */
extern bool PgColumnarReadTtl(Oid relid, char **column, Interval **interval);

Expand Down Expand Up @@ -1007,6 +1035,7 @@ extern char *PgColumnarDecompressValueStream(const char *comp, uint32 compLen,
#define PGCOLUMNAR_LOCKCLASS_STORAGE_ROW 102
#define PGCOLUMNAR_LOCKCLASS_UNIQUE_KEY 103
#define PGCOLUMNAR_LOCKCLASS_ROW_IDENTITY 104
#define PGCOLUMNAR_LOCKCLASS_EXTENT 105

/* -------------------------------------------------------------------------
* concurrent unique-key insert serialization (pgcolumnar_unique.c, issue #5)
Expand Down
Loading
Loading