Skip to content
Draft
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
9 changes: 9 additions & 0 deletions src/decorator/options/ColumnOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,13 @@ export interface ColumnOptions extends ColumnCommonOptions {
* SRID (Spatial Reference ID (EPSG code))
*/
srid?: number;

/**
* HEX: Per-column planner statistics target. Postgres only.
* Maps to `ALTER TABLE ... ALTER COLUMN ... SET STATISTICS N`. Controls how many
* rows ANALYZE samples for this column's histogram (cluster default is typically 100).
* Use a higher value (e.g. 1000) on hot predicate columns where the cluster default
* undercounts `n_distinct` and causes plan instability.
*/
statisticsTarget?: number;
}
26 changes: 25 additions & 1 deletion src/driver/postgres/PostgresQueryRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,13 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
});
}

// SET STATISTICS can't ride inside CREATE TABLE; emit as follow-up ALTERs.
table.columns
.filter(column => column.statisticsTarget != null)
.forEach(column => {
upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${column.name}" SET STATISTICS ${column.statisticsTarget}`));
});

await this.executeQueries(upQueries, downQueries);
}

Expand Down Expand Up @@ -630,6 +637,10 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
downQueries.push(new Query(`COMMENT ON COLUMN ${this.escapePath(table)}."${column.name}" IS ${this.escapeComment(column.comment)}`));
}

if (column.statisticsTarget != null) {
upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${column.name}" SET STATISTICS ${column.statisticsTarget}`));
}

await this.executeQueries(upQueries, downQueries);

clonedTable.addColumn(column);
Expand Down Expand Up @@ -853,6 +864,14 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
downQueries.push(new Query(`COMMENT ON COLUMN ${this.escapePath(table)}."${newColumn.name}" IS ${this.escapeComment(oldColumn.comment)}`));
}

if (oldColumn.statisticsTarget !== newColumn.statisticsTarget) {
// pg attstattarget: -1 = cluster default. map null/undefined to -1 so clearing an override emits the right reset.
const upTarget = newColumn.statisticsTarget == null ? -1 : newColumn.statisticsTarget;
const downTarget = oldColumn.statisticsTarget == null ? -1 : oldColumn.statisticsTarget;
upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${newColumn.name}" SET STATISTICS ${upTarget}`));
downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${oldColumn.name}" SET STATISTICS ${downTarget}`));
}

if (newColumn.isPrimary !== oldColumn.isPrimary) {
const primaryColumns = clonedTable.primaryColumns;

Expand Down Expand Up @@ -1471,7 +1490,8 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
SELECT columns.*,
pg_catalog.col_description(('"' || table_catalog || '"."' || table_schema || '"."' || table_name || '"')::regclass::oid, ordinal_position) AS description,
('"' || "udt_schema" || '"."' || "udt_name" || '"')::"regtype" AS "regtype",
pg_catalog.format_type("col_attr"."atttypid", "col_attr"."atttypmod") AS "format_type"
pg_catalog.format_type("col_attr"."atttypid", "col_attr"."atttypmod") AS "format_type",
"col_attr"."attstattarget" AS "statistics_target"
FROM "information_schema"."columns"
LEFT JOIN "pg_catalog"."pg_attribute" AS "col_attr"
ON "col_attr"."attname" = "columns"."column_name"
Expand Down Expand Up @@ -1696,6 +1716,10 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
tableColumn.charset = dbColumn["character_set_name"];
if (dbColumn["collation_name"])
tableColumn.collation = dbColumn["collation_name"];
// pg attstattarget: -1 = cluster default. only carry an explicit override into the model so the diff doesn't fire on every column.
if (dbColumn["statistics_target"] != null && Number(dbColumn["statistics_target"]) >= 0) {
tableColumn.statisticsTarget = Number(dbColumn["statistics_target"]);
}
return tableColumn;
}));

Expand Down
8 changes: 8 additions & 0 deletions src/metadata/ColumnMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,12 @@ export class ColumnMetadata {
*/
srid?: number;

/**
* HEX: Per-column planner statistics target. Postgres only.
* Maps to `ALTER TABLE ... ALTER COLUMN ... SET STATISTICS N`.
*/
statisticsTarget?: number;

// ---------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------
Expand Down Expand Up @@ -406,6 +412,8 @@ export class ColumnMetadata {
this.spatialFeatureType = options.args.options.spatialFeatureType;
if (options.args.options.srid !== undefined)
this.srid = options.args.options.srid;
if (options.args.options.statisticsTarget !== undefined)
this.statisticsTarget = options.args.options.statisticsTarget;
if (this.isTreeLevel)
this.type = options.connection.driver.mappedDataTypes.treeLevel;
if (this.isCreateDate) {
Expand Down
8 changes: 8 additions & 0 deletions src/schema-builder/options/TableColumnOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,12 @@ export interface TableColumnOptions {
* SRID (Spatial Reference ID (EPSG code))
*/
srid?: number;

/**
* HEX: Per-column planner statistics target. Postgres only.
* Maps to `ALTER TABLE ... ALTER COLUMN ... SET STATISTICS N`, which controls how many
* rows ANALYZE samples for this column's histogram (cluster default is typically 100).
* Use -1 to revert to the cluster default. Stored in `pg_attribute.attstattarget`.
*/
statisticsTarget?: number | null;
}
11 changes: 10 additions & 1 deletion src/schema-builder/table/TableColumn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ export class TableColumn {
*/
srid?: number;

/**
* HEX: Per-column planner statistics target. Postgres only.
* Maps to `ALTER TABLE ... ALTER COLUMN ... SET STATISTICS N`. Stored in
* `pg_attribute.attstattarget`. -1 means cluster default (the postgres convention).
*/
statisticsTarget?: number | null;

// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -171,6 +178,7 @@ export class TableColumn {
this.generatedType = options.generatedType;
this.spatialFeatureType = options.spatialFeatureType;
this.srid = options.srid;
this.statisticsTarget = options.statisticsTarget;
}
}

Expand Down Expand Up @@ -207,7 +215,8 @@ export class TableColumn {
isArray: this.isArray,
comment: this.comment,
spatialFeatureType: this.spatialFeatureType,
srid: this.srid
srid: this.srid,
statisticsTarget: this.statisticsTarget
});
}

Expand Down
3 changes: 2 additions & 1 deletion src/schema-builder/util/TableUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export class TableUtils {
enum: columnMetadata.enum ? columnMetadata.enum.map(val => val + "") : columnMetadata.enum,
enumName: columnMetadata.enumName,
spatialFeatureType: columnMetadata.spatialFeatureType,
srid: columnMetadata.srid
srid: columnMetadata.srid,
statisticsTarget: columnMetadata.statisticsTarget
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {expect} from "chai";
import "reflect-metadata";
import {Connection} from "../../../../src";
import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils";
import {Test} from "./entity/Test";

describe("columns > statistics target", () => {

let connections: Connection[];
before(async () => connections = await createTestingConnections({
entities: [Test],
// postgres-only feature; other drivers ignore the option.
enabledDrivers: ["postgres"]
}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));

it("should round-trip statistics target through createTable and loadTables", () => Promise.all(connections.map(async connection => {
const table = (await connection.createQueryRunner().getTable("test"))!;

expect(table.findColumnByName("a")!.statisticsTarget).to.be.equal(undefined);
expect(table.findColumnByName("b")!.statisticsTarget).to.be.equal(1000);
expect(table.findColumnByName("c")!.statisticsTarget).to.be.equal(5000);
})));

it("should emit ALTER ... SET STATISTICS when target changes on changeColumn", () => Promise.all(connections.map(async connection => {
const queryRunner = connection.createQueryRunner();
const table = (await queryRunner.getTable("test"))!;
const oldColumn = table.findColumnByName("b")!;
const newColumn = oldColumn.clone();
newColumn.statisticsTarget = 2500;

queryRunner.enableSqlMemory();
await queryRunner.changeColumn(table, oldColumn, newColumn);
const memory = queryRunner.getMemorySql();
queryRunner.disableSqlMemory();

const ups = memory.upQueries.map(q => q.query);
const downs = memory.downQueries.map(q => q.query);

expect(ups.some(sql => /ALTER TABLE .*"test".* ALTER COLUMN "b" SET STATISTICS 2500/.test(sql))).to.be.true;
expect(downs.some(sql => /ALTER TABLE .*"test".* ALTER COLUMN "b" SET STATISTICS 1000/.test(sql))).to.be.true;

await queryRunner.release();
})));

it("should emit SET STATISTICS -1 when clearing the target", () => Promise.all(connections.map(async connection => {
const queryRunner = connection.createQueryRunner();
const table = (await queryRunner.getTable("test"))!;
const oldColumn = table.findColumnByName("c")!;
const newColumn = oldColumn.clone();
newColumn.statisticsTarget = undefined;

queryRunner.enableSqlMemory();
await queryRunner.changeColumn(table, oldColumn, newColumn);
const memory = queryRunner.getMemorySql();
queryRunner.disableSqlMemory();

const ups = memory.upQueries.map(q => q.query);
const downs = memory.downQueries.map(q => q.query);

expect(ups.some(sql => /ALTER TABLE .*"test".* ALTER COLUMN "c" SET STATISTICS -1/.test(sql))).to.be.true;
expect(downs.some(sql => /ALTER TABLE .*"test".* ALTER COLUMN "c" SET STATISTICS 5000/.test(sql))).to.be.true;

await queryRunner.release();
})));

it("should emit SET STATISTICS on addColumn when the new column has a target", () => Promise.all(connections.map(async connection => {
const queryRunner = connection.createQueryRunner();
const table = (await queryRunner.getTable("test"))!;
const newColumn = table.findColumnByName("b")!.clone();
newColumn.name = "d";
newColumn.statisticsTarget = 750;

queryRunner.enableSqlMemory();
await queryRunner.addColumn(table, newColumn);
const memory = queryRunner.getMemorySql();
queryRunner.disableSqlMemory();

const ups = memory.upQueries.map(q => q.query);

expect(ups.some(sql => /ALTER TABLE .*"test".* ADD "d" /.test(sql))).to.be.true;
expect(ups.some(sql => /ALTER TABLE .*"test".* ALTER COLUMN "d" SET STATISTICS 750/.test(sql))).to.be.true;

await queryRunner.release();
})));

});
22 changes: 22 additions & 0 deletions test/functional/columns/statistics-target/entity/Test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {Entity} from "../../../../../src/decorator/entity/Entity";
import {Column} from "../../../../../src/decorator/columns/Column";
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";

@Entity()
export class Test {

@PrimaryGeneratedColumn()
id: number;

// Default cluster target (no override).
@Column()
a: string;

// Explicit override.
@Column({ statisticsTarget: 1000 })
b: string;

// Larger override for a hot predicate column.
@Column({ statisticsTarget: 5000 })
c: string;
}
Loading