diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c index e890770..5e3c06f 100644 --- a/contrib/file_fdw/file_fdw.c +++ b/contrib/file_fdw/file_fdw.c @@ -20,6 +20,7 @@ #include "commands/copy.h" #include "commands/defrem.h" #include "commands/explain.h" +#include "commands/vacuum.h" #include "foreign/fdwapi.h" #include "foreign/foreign.h" #include "miscadmin.h" @@ -123,6 +124,9 @@ static void fileBeginForeignScan(ForeignScanState *node, int eflags); static TupleTableSlot *fileIterateForeignScan(ForeignScanState *node); static void fileReScanForeignScan(ForeignScanState *node); static void fileEndForeignScan(ForeignScanState *node); +static void fileAnalyzeForeignTable(Relation onerel, + VacuumStmt *vacstmt, + int elevel); /* * Helper functions @@ -136,6 +140,9 @@ static void estimate_size(PlannerInfo *root, RelOptInfo *baserel, static void estimate_costs(PlannerInfo *root, RelOptInfo *baserel, FileFdwPlanState *fdw_private, Cost *startup_cost, Cost *total_cost); +static int acquire_sample_rows(Relation onerel, HeapTuple *rows, int targrows, + double *totalrows, double *totaldeadrows, + BlockNumber *totalpages, int elevel); /* @@ -155,6 +162,7 @@ file_fdw_handler(PG_FUNCTION_ARGS) fdwroutine->IterateForeignScan = fileIterateForeignScan; fdwroutine->ReScanForeignScan = fileReScanForeignScan; fdwroutine->EndForeignScan = fileEndForeignScan; + fdwroutine->AnalyzeForeignTable = fileAnalyzeForeignTable; PG_RETURN_POINTER(fdwroutine); } @@ -645,6 +653,18 @@ fileReScanForeignScan(ForeignScanState *node) } /* + * fileAnalyzeForeignTable + * Analyze foreign table + */ +static void +fileAnalyzeForeignTable(Relation onerel, VacuumStmt *vacstmt, int elevel) +{ + do_analyze_rel(onerel, vacstmt, + elevel, false, + acquire_sample_rows); +} + +/* * Estimate size of a foreign table. * * The main result is returned in baserel->rows. We also set @@ -657,7 +677,6 @@ estimate_size(PlannerInfo *root, RelOptInfo *baserel, { struct stat stat_buf; BlockNumber pages; - int tuple_width; double ntuples; double nrows; @@ -677,16 +696,31 @@ estimate_size(PlannerInfo *root, RelOptInfo *baserel, fdw_private->pages = pages; - /* - * Estimate the number of tuples in the file. We back into this estimate - * using the planner's idea of the relation width; which is bogus if not - * all columns are being read, not to mention that the text representation - * of a row probably isn't the same size as its internal representation. - * FIXME later. - */ - tuple_width = MAXALIGN(baserel->width) + MAXALIGN(sizeof(HeapTupleHeaderData)); + if (baserel->pages > 0) + { + double density; - ntuples = clamp_row_est((double) stat_buf.st_size / (double) tuple_width); + density = baserel->tuples / (double) baserel->pages; + + ntuples = clamp_row_est(density * (double) pages); + } + else + { + int tuple_width; + + /* + * Estimate the number of tuples in the file. We back into this + * estimate using the planner's idea of the relation width; which is + * bogus if not all columns are being read, not to mention that the text + * representation of a row probably isn't the same size as its internal + * representation. FIXME later. + */ + tuple_width = MAXALIGN(baserel->width) + + MAXALIGN(sizeof(HeapTupleHeaderData)); + + ntuples = clamp_row_est((double) stat_buf.st_size / + (double) tuple_width); + } fdw_private->ntuples = ntuples; @@ -736,3 +770,156 @@ estimate_costs(PlannerInfo *root, RelOptInfo *baserel, run_cost += cpu_per_tuple * ntuples; *total_cost = *startup_cost + run_cost; } + +/* + * acquire_sample_rows -- acquire a random sample of rows from the table + * + * Selected rows are returned in the caller-allocated array rows[], which + * must have at least targrows entries. + * The actual number of rows selected is returned as the function result. + * We also count the total number of rows in the file, and return it into + * *totalrows. Note that *totaldeadrows is always set to 0. + * + * Note that the returned list of rows is not always in order by physical + * position in the file. Therefore, correlation estimates derived later + * may be meaningless, but it's OK because we don't use the estimates for + * now. + */ +static int +acquire_sample_rows(Relation onerel, HeapTuple *rows, int targrows, + double *totalrows, double *totaldeadrows, + BlockNumber *totalpages, int elevel) +{ + int numrows = 0; + double rowstoskip = -1; /* -1 means not set yet */ + double rstate; + HeapTuple tuple; + TupleDesc tupDesc; + Datum *values; + bool *nulls; + bool found; + char *filename; + struct stat stat_buf; + List *options; + CopyState cstate; + ErrorContextCallback errcontext; + + Assert(onerel); + Assert(targrows > 0); + + tupDesc = RelationGetDescr(onerel); + values = (Datum *) palloc(tupDesc->natts * sizeof(Datum)); + nulls = (bool *) palloc(tupDesc->natts * sizeof(bool)); + + /* Fetch options of foreign table */ + fileGetOptions(RelationGetRelid(onerel), &filename, &options); + + /* + * Get size of the file. + */ + if (stat(filename, &stat_buf) < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not stat file \"%s\": %m", + filename))); + + /* + * Convert size to pages for use in I/O cost estimate. + */ + *totalpages = (stat_buf.st_size + (BLCKSZ - 1)) / BLCKSZ; + if (*totalpages < 1) + *totalpages = 1; + + /* + * Create CopyState from FDW options. We always acquire all columns, so + * as to match the expected ScanTupleSlot signature. + */ + cstate = BeginCopyFrom(onerel, filename, NIL, options); + + /* Prepare for sampling rows */ + rstate = init_selection_state(targrows); + + /* Set up callback to identify error line number. */ + errcontext.callback = CopyFromErrorCallback; + errcontext.arg = (void *) cstate; + errcontext.previous = error_context_stack; + error_context_stack = &errcontext; + + *totalrows = 0; + for (;;) + { + /* + * Check for user-requested abort. + */ + CHECK_FOR_INTERRUPTS(); + + found = NextCopyFrom(cstate, NULL, values, nulls, NULL); + if (!found) + break; + + tuple = heap_form_tuple(tupDesc, values, nulls); + + /* + * The first targrows sample rows are simply copied into the reservoir. + * Then we start replacing tuples in the sample until we reach the end + * of the relation. This algorithm is from Jeff Vitter's paper (see + * full citation below). It works by repeatedly computing the number of + * tuples to skip before selecting a tuple, which replaces a randomly + * chosen element of the reservoir (current set of tuples). + * At all times the reservoir is a true random sample of the tuples + * we've passed over so far, so when we fall off the end of the relation + * we're done. + */ + if (numrows < targrows) + rows[numrows++] = heap_copytuple(tuple); + else + { + /* + * t in Vitter's paper is the number of records already + * processed. If we need to compute a new S value, we + * must use the not-yet-incremented value of samplerows as + * t. + */ + if (rowstoskip < 0) + rowstoskip = get_next_S(*totalrows, targrows, &rstate); + + if (rowstoskip <= 0) + { + /* + * Found a suitable tuple, so save it, replacing one + * old tuple at random + */ + int k = (int) (targrows * random_fract()); + + Assert(k >= 0 && k < targrows); + heap_freetuple(rows[k]); + rows[k] = heap_copytuple(tuple); + } + + rowstoskip -= 1; + } + + *totalrows += 1; + heap_freetuple(tuple); + } + *totaldeadrows = 0; + + /* Remove error callback. */ + error_context_stack = errcontext.previous; + + EndCopyFrom(cstate); + + pfree(values); + pfree(nulls); + + /* + * Emit some interesting relation info + */ + ereport(elevel, + (errmsg("\"%s\": scanned, " + "%d rows in sample, %d total rows", + RelationGetRelationName(onerel), + numrows, (int) *totalrows))); + + return numrows; +} diff --git a/contrib/file_fdw/input/file_fdw.source b/contrib/file_fdw/input/file_fdw.source index 8e3d553..21b6fb4 100644 --- a/contrib/file_fdw/input/file_fdw.source +++ b/contrib/file_fdw/input/file_fdw.source @@ -111,6 +111,11 @@ EXECUTE st(100); EXECUTE st(100); DEALLOCATE st; +-- statistics collection tests +ANALYZE agg_csv; +SELECT relpages, reltuples FROM pg_class WHERE relname = 'agg_csv'; +SELECT * FROM pg_stats WHERE tablename = 'agg_csv'; + -- tableoid SELECT tableoid::regclass, b FROM agg_csv; diff --git a/contrib/file_fdw/output/file_fdw.source b/contrib/file_fdw/output/file_fdw.source index 84f0750..c2daafe 100644 --- a/contrib/file_fdw/output/file_fdw.source +++ b/contrib/file_fdw/output/file_fdw.source @@ -174,6 +174,21 @@ EXECUTE st(100); (1 row) DEALLOCATE st; +-- statistics collection tests +ANALYZE agg_csv; +SELECT relpages, reltuples FROM pg_class WHERE relname = 'agg_csv'; + relpages | reltuples +----------+----------- + 1 | 3 +(1 row) + +SELECT * FROM pg_stats WHERE tablename = 'agg_csv'; + schemaname | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram +------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+-------------------------+-------------+-------------------+------------------------+---------------------- + public | agg_csv | a | f | 0 | 2 | -1 | | | {0,42,100} | -0.5 | | | + public | agg_csv | b | f | 0 | 4 | -1 | | | {0.09561,99.097,324.78} | 0.5 | | | +(2 rows) + -- tableoid SELECT tableoid::regclass, b FROM agg_csv; tableoid | b diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml index f7bf3d8..0a70b47 100644 --- a/doc/src/sgml/fdwhandler.sgml +++ b/doc/src/sgml/fdwhandler.sgml @@ -299,6 +299,30 @@ EndForeignScan (ForeignScanState *node); +void +AnalyzeForeignTable (Relation onerel, + VacuumStmt *vacstmt, + int elevel); + + + Collect statistics on a foreign table and store the results in the + pg_class and pg_statistics system catalogs. + This is optional, and if implemented, called when ANALYZE + command is run. + The statistics are used by the query planner in order to make good + choices of query plans. + The function can be implemented by writing a sampling function that + acquires a random sample of rows from an external data source and + then by calling do_analyze_rel, where you should pass + the sampling function as an argument. + Or the function can be directly implemented to get the statistics + from an external data source, transform it if neccesary, and store + it in the pg_class and pg_statistics system catalogs. + The function must be set to NULL if it isn't implemented. + + + + ForeignDataWrapper * GetForeignDataWrapper(Oid fdwid); diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml index 93c3ff5..54d0838 100644 --- a/doc/src/sgml/maintenance.sgml +++ b/doc/src/sgml/maintenance.sgml @@ -284,6 +284,10 @@ ANALYZE strictly as a function of the number of rows inserted or updated; it has no knowledge of whether that will lead to meaningful statistical changes. + Note that the autovacuum daemon does not issue ANALYZE + commands on foreign tables. It is recommended to run manually-managed + ANALYZE commands as needed, which typically are executed + according to a schedule by cron or Task Scheduler scripts. diff --git a/doc/src/sgml/ref/alter_foreign_table.sgml b/doc/src/sgml/ref/alter_foreign_table.sgml index c4cdaa8..3819007 100644 --- a/doc/src/sgml/ref/alter_foreign_table.sgml +++ b/doc/src/sgml/ref/alter_foreign_table.sgml @@ -36,6 +36,9 @@ ALTER FOREIGN TABLE [ IF EXISTS ] namecolumn [ RESTRICT | CASCADE ] ALTER [ COLUMN ] column [ SET DATA ] TYPE type ALTER [ COLUMN ] column { SET | DROP } NOT NULL + ALTER [ COLUMN ] column SET STATISTICS integer + ALTER [ COLUMN ] column SET ( attribute_option = value [, ... ] ) + ALTER [ COLUMN ] column RESET ( attribute_option [, ... ] ) ALTER [ COLUMN ] column OPTIONS ( [ ADD | SET | DROP ] option ['value'] [, ... ]) OWNER TO new_owner OPTIONS ( [ ADD | SET | DROP ] option ['value'] [, ... ]) @@ -104,6 +107,45 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name + SET STATISTICS + + + This form sets the per-column statistics-gathering target for + subsequent operations. + The target can be set in the range 0 to 10000; alternatively, + set it to -1 to revert to using the system default statistics + target (). + + + + + + SET ( attribute_option = value [, ... ] ) + RESET ( attribute_option [, ... ] ) + + + This form sets or resets a per-attribute option. Currently, the only + defined per-attribute option is n_distinct, which overrides + the number-of-distinct-values estimates made by subsequent + operations. When set to a positive value, + ANALYZE will assume that the column contains exactly the + specified number of distinct nonnull values. When set to a negative + value, which must be greater than or equal to -1, ANALYZE + will assume that the number of distinct nonnull values in the column is + linear in the size of the foreign table; the exact count is to be computed + by multiplying the estimated foreign table size by the absolute value of + the given number. For example, a value of -1 implies that all values in + the column are distinct, while a value of -0.5 implies that each value + appears twice on the average. This can be useful when the size of the + foreign table changes over time, since the multiplication by the number of + rows in the foreign table is not performed until query planning time. + Specify a value of 0 to revert to estimating the number of distinct values + normally. + + + + + OWNER diff --git a/doc/src/sgml/ref/analyze.sgml b/doc/src/sgml/ref/analyze.sgml index 8c9057b..524a1c9 100644 --- a/doc/src/sgml/ref/analyze.sgml +++ b/doc/src/sgml/ref/analyze.sgml @@ -39,9 +39,11 @@ ANALYZE [ VERBOSE ] [ table [ ( With no parameter, ANALYZE examines every table in the - current database. With a parameter, ANALYZE examines - only that table. It is further possible to give a list of column names, - in which case only the statistics for those columns are collected. + current database except for foreign tables. With a parameter, + ANALYZE examines only that table. For a foreign table, it is + necessary to specify the name of that table. It is further possible to + give a list of column names, in which case only the statistics for those + columns are collected. @@ -63,7 +65,8 @@ ANALYZE [ VERBOSE ] [ table [ ( The name (possibly schema-qualified) of a specific table to - analyze. Defaults to all tables in the current database. + analyze. Defaults to all tables in the current database except + for foreign tables. @@ -137,7 +140,9 @@ ANALYZE [ VERBOSE ] [ table [ ( ANALYZE is run. To avoid this, raise the amount of statistics collected by - ANALYZE, as described below. + ANALYZE, as described below. Note that the time + needed to analyze on foreign tables depends on the implementation of + the foreign data wrapper via which such tables are attached. diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c index 9cd6e67..442ee2f 100644 --- a/src/backend/commands/analyze.c +++ b/src/backend/commands/analyze.c @@ -23,6 +23,7 @@ #include "access/xact.h" #include "catalog/index.h" #include "catalog/indexing.h" +#include "catalog/pg_class.h" #include "catalog/pg_collation.h" #include "catalog/pg_inherits_fn.h" #include "catalog/pg_namespace.h" @@ -30,6 +31,8 @@ #include "commands/tablecmds.h" #include "commands/vacuum.h" #include "executor/executor.h" +#include "foreign/foreign.h" +#include "foreign/fdwapi.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_oper.h" @@ -78,14 +81,11 @@ typedef struct AnlIndexData int default_statistics_target = 100; /* A few variables that don't seem worth passing around as parameters */ -static int elevel = -1; - static MemoryContext anl_context = NULL; static BufferAccessStrategy vac_strategy; -static void do_analyze_rel(Relation onerel, VacuumStmt *vacstmt, bool inh); static void BlockSampler_Init(BlockSampler bs, BlockNumber nblocks, int samplesize); static bool BlockSampler_HasMore(BlockSampler bs); @@ -96,15 +96,15 @@ static void compute_index_stats(Relation onerel, double totalrows, MemoryContext col_context); static VacAttrStats *examine_attribute(Relation onerel, int attnum, Node *index_expr); -static int acquire_sample_rows(Relation onerel, HeapTuple *rows, - int targrows, double *totalrows, double *totaldeadrows); -static double random_fract(void); -static double init_selection_state(int n); -static double get_next_S(double t, int n, double *stateptr); -static int compare_rows(const void *a, const void *b); +static int acquire_sample_rows(Relation onerel, + HeapTuple *rows, int targrows, + double *totalrows, double *totaldeadrows, + BlockNumber *totalpages, int elevel); static int acquire_inherited_sample_rows(Relation onerel, HeapTuple *rows, int targrows, - double *totalrows, double *totaldeadrows); + double *totalrows, double *totaldeadrows, + BlockNumber *totalpages, int elevel); +static int compare_rows(const void *a, const void *b); static void update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats); static Datum std_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull); @@ -117,7 +117,9 @@ static Datum ind_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull); void analyze_rel(Oid relid, VacuumStmt *vacstmt, BufferAccessStrategy bstrategy) { + int elevel; Relation onerel; + FdwRoutine *fdwroutine; /* Set up static variables */ if (vacstmt->options & VACOPT_VERBOSE) @@ -182,10 +184,12 @@ analyze_rel(Oid relid, VacuumStmt *vacstmt, BufferAccessStrategy bstrategy) } /* - * Check that it's a plain table; we used to do this in get_rel_oids() but - * seems safer to check after we've locked the relation. + * Check that it's a plain table or foreign table; we used to do this + * in get_rel_oids() but seems safer to check after we've locked the + * relation. */ - if (onerel->rd_rel->relkind != RELKIND_RELATION) + if (onerel->rd_rel->relkind != RELKIND_RELATION && + onerel->rd_rel->relkind != RELKIND_FOREIGN_TABLE) { /* No need for a WARNING if we already complained during VACUUM */ if (!(vacstmt->options & VACOPT_VACUUM)) @@ -209,7 +213,9 @@ analyze_rel(Oid relid, VacuumStmt *vacstmt, BufferAccessStrategy bstrategy) } /* - * We can ANALYZE any table except pg_statistic. See update_attstats + * We can ANALYZE any table except pg_statistic. See update_attstats. + * A foreign table can be ANALYZEed if the AnalyzeForeignTable callback + * routine is provided. */ if (RelationGetRelid(onerel) == StatisticRelationId) { @@ -217,6 +223,20 @@ analyze_rel(Oid relid, VacuumStmt *vacstmt, BufferAccessStrategy bstrategy) return; } + if (onerel->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + { + fdwroutine = GetFdwRoutineByRelId(RelationGetRelid(onerel)); + + if (fdwroutine->AnalyzeForeignTable == NULL) + { + ereport(WARNING, + (errmsg("skipping \"%s\" --- underlying foreign-data wrapper cannot analyze it", + RelationGetRelationName(onerel)))); + relation_close(onerel, ShareUpdateExclusiveLock); + return; + } + } + /* * OK, let's do it. First let other backends know I'm in ANALYZE. */ @@ -227,13 +247,32 @@ analyze_rel(Oid relid, VacuumStmt *vacstmt, BufferAccessStrategy bstrategy) /* * Do the normal non-recursive ANALYZE. */ - do_analyze_rel(onerel, vacstmt, false); + ereport(elevel, + (errmsg("analyzing \"%s.%s\"", + get_namespace_name(RelationGetNamespace(onerel)), + RelationGetRelationName(onerel)))); + + if (onerel->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + fdwroutine->AnalyzeForeignTable(onerel, vacstmt, elevel); + else + do_analyze_rel(onerel, vacstmt, elevel, false, acquire_sample_rows); /* * If there are child tables, do recursive ANALYZE. */ if (onerel->rd_rel->relhassubclass) - do_analyze_rel(onerel, vacstmt, true); + { + Assert(onerel->rd_rel->relkind != RELKIND_FOREIGN_TABLE); + + ereport(elevel, + (errmsg("analyzing \"%s.%s\" inheritance tree", + get_namespace_name(RelationGetNamespace(onerel)), + RelationGetRelationName(onerel)))); + + do_analyze_rel(onerel, vacstmt, elevel, true, + acquire_inherited_sample_rows); + } + /* * Close source relation now, but keep lock so that no one deletes it @@ -255,8 +294,9 @@ analyze_rel(Oid relid, VacuumStmt *vacstmt, BufferAccessStrategy bstrategy) /* * do_analyze_rel() -- analyze one relation, recursively or not */ -static void -do_analyze_rel(Relation onerel, VacuumStmt *vacstmt, bool inh) +void +do_analyze_rel(Relation onerel, VacuumStmt *vacstmt, int elevel, + bool inh, AcquireSampleRowFunc acquirefunc) { int attr_cnt, tcnt, @@ -271,6 +311,7 @@ do_analyze_rel(Relation onerel, VacuumStmt *vacstmt, bool inh) numrows; double totalrows, totaldeadrows; + BlockNumber totalpages; HeapTuple *rows; PGRUsage ru0; TimestampTz starttime = 0; @@ -279,17 +320,6 @@ do_analyze_rel(Relation onerel, VacuumStmt *vacstmt, bool inh) int save_sec_context; int save_nestlevel; - if (inh) - ereport(elevel, - (errmsg("analyzing \"%s.%s\" inheritance tree", - get_namespace_name(RelationGetNamespace(onerel)), - RelationGetRelationName(onerel)))); - else - ereport(elevel, - (errmsg("analyzing \"%s.%s\"", - get_namespace_name(RelationGetNamespace(onerel)), - RelationGetRelationName(onerel)))); - /* * Set up a working context so that we can easily free whatever junk gets * created. @@ -447,11 +477,13 @@ do_analyze_rel(Relation onerel, VacuumStmt *vacstmt, bool inh) */ rows = (HeapTuple *) palloc(targrows * sizeof(HeapTuple)); if (inh) - numrows = acquire_inherited_sample_rows(onerel, rows, targrows, - &totalrows, &totaldeadrows); + numrows = acquirefunc(onerel, rows, targrows, + &totalrows, &totaldeadrows, + NULL, elevel); else - numrows = acquire_sample_rows(onerel, rows, targrows, - &totalrows, &totaldeadrows); + numrows = acquirefunc(onerel, rows, targrows, + &totalrows, &totaldeadrows, + &totalpages, elevel); /* * Compute the statistics. Temporary results during the calculations for @@ -532,7 +564,7 @@ do_analyze_rel(Relation onerel, VacuumStmt *vacstmt, bool inh) */ if (!inh) vac_update_relstats(onerel, - RelationGetNumberOfBlocks(onerel), + totalpages, totalrows, visibilitymap_count(onerel), hasindex, @@ -1015,7 +1047,8 @@ BlockSampler_Next(BlockSampler bs) */ static int acquire_sample_rows(Relation onerel, HeapTuple *rows, int targrows, - double *totalrows, double *totaldeadrows) + double *totalrows, double *totaldeadrows, + BlockNumber *totalpages, int elevel) { int numrows = 0; /* # rows now in reservoir */ double samplerows = 0; /* total # rows collected */ @@ -1030,6 +1063,8 @@ acquire_sample_rows(Relation onerel, HeapTuple *rows, int targrows, Assert(targrows > 0); totalblocks = RelationGetNumberOfBlocks(onerel); + if (totalpages) + *totalpages = totalblocks; /* Need a cutoff xmin for HeapTupleSatisfiesVacuum */ OldestXmin = GetOldestXmin(onerel->rd_rel->relisshared, true); @@ -1252,7 +1287,7 @@ acquire_sample_rows(Relation onerel, HeapTuple *rows, int targrows, } /* Select a random value R uniformly distributed in (0 - 1) */ -static double +double random_fract(void) { return ((double) random() + 1) / ((double) MAX_RANDOM_VALUE + 2); @@ -1272,14 +1307,14 @@ random_fract(void) * determines the number of records to skip before the next record is * processed. */ -static double +double init_selection_state(int n) { /* Initial value of W (for use when Algorithm Z is first applied) */ return exp(-log(random_fract()) / n); } -static double +double get_next_S(double t, int n, double *stateptr) { double S; @@ -1395,7 +1430,8 @@ compare_rows(const void *a, const void *b) */ static int acquire_inherited_sample_rows(Relation onerel, HeapTuple *rows, int targrows, - double *totalrows, double *totaldeadrows) + double *totalrows, double *totaldeadrows, + BlockNumber *totalpages, int elevel) { List *tableOIDs; Relation *rels; @@ -1458,6 +1494,8 @@ acquire_inherited_sample_rows(Relation onerel, HeapTuple *rows, int targrows, totalblocks += relblocks[nrels]; nrels++; } + if (totalpages) + *totalpages = totalblocks; /* * Now sample rows from each relation, proportionally to its fraction of @@ -1491,7 +1529,9 @@ acquire_inherited_sample_rows(Relation onerel, HeapTuple *rows, int targrows, rows + numrows, childtargrows, &trows, - &tdrows); + &tdrows, + NULL, + elevel); /* We may need to convert from child's rowtype to parent's */ if (childrows > 0 && diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 9853686..3031496 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -320,6 +320,8 @@ static void ATPrepSetStatistics(Relation rel, const char *colName, Node *newValue, LOCKMODE lockmode); static void ATExecSetStatistics(Relation rel, const char *colName, Node *newValue, LOCKMODE lockmode); +static void ATPrepSetOptions(Relation rel, const char *colName, + Node *options, LOCKMODE lockmode); static void ATExecSetOptions(Relation rel, const char *colName, Node *options, bool isReset, LOCKMODE lockmode); static void ATExecSetStorage(Relation rel, const char *colName, @@ -3021,7 +3023,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, break; case AT_SetOptions: /* ALTER COLUMN SET ( options ) */ case AT_ResetOptions: /* ALTER COLUMN RESET ( options ) */ - ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX); + ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX | ATT_FOREIGN_TABLE); + ATPrepSetOptions(rel, cmd->name, cmd->def, lockmode); /* This command never recurses */ pass = AT_PASS_MISC; break; @@ -4999,10 +5002,11 @@ ATPrepSetStatistics(Relation rel, const char *colName, Node *newValue, LOCKMODE * allowSystemTableMods to be turned on. */ if (rel->rd_rel->relkind != RELKIND_RELATION && - rel->rd_rel->relkind != RELKIND_INDEX) + rel->rd_rel->relkind != RELKIND_INDEX && + rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is not a table or index", + errmsg("\"%s\" is not a table, index or foreign table", RelationGetRelationName(rel)))); /* Permissions checks */ @@ -5071,6 +5075,26 @@ ATExecSetStatistics(Relation rel, const char *colName, Node *newValue, LOCKMODE } static void +ATPrepSetOptions(Relation rel, const char *colName, Node *options, + LOCKMODE lockmode) +{ + if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + { + ListCell *cell; + + foreach(cell, (List *) options) + { + DefElem *def = (DefElem *) lfirst(cell); + + if (pg_strcasecmp(def->defname, "n_distinct_inherited") == 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot set \"n_distinct_inherited\" for foreign tables"))); + } + } +} + +static void ATExecSetOptions(Relation rel, const char *colName, Node *options, bool isReset, LOCKMODE lockmode) { diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index dc2248b..b3d2078 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -1104,7 +1104,7 @@ describeOneTableDetails(const char *schemaname, bool printTableInitialized = false; int i; char *view_def = NULL; - char *headers[6]; + char *headers[7]; char **seq_values = NULL; char **modifiers = NULL; char **ptr; @@ -1395,7 +1395,7 @@ describeOneTableDetails(const char *schemaname, if (verbose) { headers[cols++] = gettext_noop("Storage"); - if (tableinfo.relkind == 'r') + if (tableinfo.relkind == 'r' || tableinfo.relkind == 'f') headers[cols++] = gettext_noop("Stats target"); /* Column comments, if the relkind supports this feature. */ if (tableinfo.relkind == 'r' || tableinfo.relkind == 'v' || @@ -1498,7 +1498,7 @@ describeOneTableDetails(const char *schemaname, false, false); /* Statistics target, if the relkind supports this feature */ - if (tableinfo.relkind == 'r') + if (tableinfo.relkind == 'r' || tableinfo.relkind == 'f') { printTableAddCell(&cont, PQgetvalue(res, i, firstvcol + 1), false, false); diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index 975d655..d113adf 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -409,6 +409,21 @@ static const SchemaQuery Query_for_list_of_tsvf = { NULL }; +static const SchemaQuery Query_for_list_of_tf = { + /* catname */ + "pg_catalog.pg_class c", + /* selcondition */ + "c.relkind IN ('r', 'f')", + /* viscondition */ + "pg_catalog.pg_table_is_visible(c.oid)", + /* namespace */ + "c.relnamespace", + /* result */ + "pg_catalog.quote_ident(c.relname)", + /* qualresult */ + NULL +}; + static const SchemaQuery Query_for_list_of_views = { /* catname */ "pg_catalog.pg_class c", @@ -2833,7 +2848,7 @@ psql_completion(char *text, int start, int end) /* ANALYZE */ /* If the previous word is ANALYZE, produce list of tables */ else if (pg_strcasecmp(prev_wd, "ANALYZE") == 0) - COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL); + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tf, NULL); /* WHERE */ /* Simple case of the word before the where being the table name */ diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h index 3deee66..f6a3c59 100644 --- a/src/include/commands/vacuum.h +++ b/src/include/commands/vacuum.h @@ -58,6 +58,14 @@ */ typedef struct VacAttrStats *VacAttrStatsP; +typedef int (*AcquireSampleRowFunc) (Relation onerel, + HeapTuple *rows, + int targrows, + double *totalrows, + double *totaldeadrows, + BlockNumber *totalpages, + int elevel); + typedef Datum (*AnalyzeAttrFetchFunc) (VacAttrStatsP stats, int rownum, bool *isNull); @@ -139,6 +147,14 @@ extern int vacuum_freeze_table_age; /* in commands/vacuum.c */ +extern void analyze_rel(Oid relid, VacuumStmt *vacstmt, + BufferAccessStrategy bstrategy); +extern void do_analyze_rel(Relation onerel, VacuumStmt *vacstmt, int elevel, + bool inh, AcquireSampleRowFunc acquirefunc); +extern double random_fract(void); +extern double init_selection_state(int n); +extern double get_next_S(double t, int n, double *stateptr); + extern void vacuum(VacuumStmt *vacstmt, Oid relid, bool do_toast, BufferAccessStrategy bstrategy, bool for_wraparound, bool isTopLevel); extern void vac_open_indexes(Relation relation, LOCKMODE lockmode, diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h index 854f177..fbb5b0b 100644 --- a/src/include/foreign/fdwapi.h +++ b/src/include/foreign/fdwapi.h @@ -12,8 +12,10 @@ #ifndef FDWAPI_H #define FDWAPI_H +#include "foreign/foreign.h" #include "nodes/execnodes.h" #include "nodes/relation.h" +#include "utils/rel.h" /* To avoid including explain.h here, reference ExplainState thus: */ struct ExplainState; @@ -50,6 +52,9 @@ typedef void (*ReScanForeignScan_function) (ForeignScanState *node); typedef void (*EndForeignScan_function) (ForeignScanState *node); +typedef void (*AnalyzeForeignTable_function) (Relation relation, + VacuumStmt *vacstmt, + int elevel); /* * FdwRoutine is the struct returned by a foreign-data wrapper's handler @@ -64,6 +69,11 @@ typedef struct FdwRoutine { NodeTag type; + /* + * These handlers are required to execute simple scans on a foreign + * table. If any of them was set to NULL, scans on a foreign table + * managed by FDW would fail. + */ GetForeignRelSize_function GetForeignRelSize; GetForeignPaths_function GetForeignPaths; GetForeignPlan_function GetForeignPlan; @@ -72,6 +82,12 @@ typedef struct FdwRoutine IterateForeignScan_function IterateForeignScan; ReScanForeignScan_function ReScanForeignScan; EndForeignScan_function EndForeignScan; + + /* + * Handlers below are optional. You can set any of them to NULL to + * tell PostgreSQL that FDW doesn't have the capability. + */ + AnalyzeForeignTable_function AnalyzeForeignTable; } FdwRoutine; diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out index ba86883..f1379a6 100644 --- a/src/test/regress/expected/foreign_data.out +++ b/src/test/regress/expected/foreign_data.out @@ -679,12 +679,12 @@ CREATE FOREIGN TABLE ft1 ( COMMENT ON FOREIGN TABLE ft1 IS 'ft1'; COMMENT ON COLUMN ft1.c1 IS 'ft1.c1'; \d+ ft1 - Foreign table "public.ft1" - Column | Type | Modifiers | FDW Options | Storage | Description ---------+---------+-----------+--------------------------------+----------+------------- - c1 | integer | not null | ("param 1" 'val1') | plain | ft1.c1 - c2 | text | | (param2 'val2', param3 'val3') | extended | - c3 | date | | | plain | + Foreign table "public.ft1" + Column | Type | Modifiers | FDW Options | Storage | Stats target | Description +--------+---------+-----------+--------------------------------+----------+--------------+------------- + c1 | integer | not null | ("param 1" 'val1') | plain | | ft1.c1 + c2 | text | | (param2 'val2', param3 'val3') | extended | | + c3 | date | | | plain | | Server: s0 FDW Options: (delimiter ',', quote '"', "be quoted" 'value') Has OIDs: no @@ -730,19 +730,24 @@ ERROR: cannot alter system column "xmin" ALTER FOREIGN TABLE ft1 ALTER COLUMN c7 OPTIONS (ADD p1 'v1', ADD p2 'v2'), ALTER COLUMN c8 OPTIONS (ADD p1 'v1', ADD p2 'v2'); ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 OPTIONS (SET p2 'V2', DROP p1); +ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 SET STATISTICS 10000; +ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 SET (n_distinct = 100); +ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 SET (n_distinct_inherited = 100); -- ERROR +ERROR: cannot set "n_distinct_inherited" for foreign tables +ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET STATISTICS -1; \d+ ft1 - Foreign table "public.ft1" - Column | Type | Modifiers | FDW Options | Storage | Description ---------+---------+-----------+--------------------------------+----------+------------- - c1 | integer | not null | ("param 1" 'val1') | plain | - c2 | text | | (param2 'val2', param3 'val3') | extended | - c3 | date | | | plain | - c4 | integer | | | plain | - c6 | integer | not null | | plain | - c7 | integer | | (p1 'v1', p2 'v2') | plain | - c8 | text | | (p2 'V2') | extended | - c9 | integer | | | plain | - c10 | integer | | (p1 'v1') | plain | + Foreign table "public.ft1" + Column | Type | Modifiers | FDW Options | Storage | Stats target | Description +--------+---------+-----------+--------------------------------+----------+--------------+------------- + c1 | integer | not null | ("param 1" 'val1') | plain | 10000 | + c2 | text | | (param2 'val2', param3 'val3') | extended | | + c3 | date | | | plain | | + c4 | integer | | | plain | | + c6 | integer | not null | | plain | | + c7 | integer | | (p1 'v1', p2 'v2') | plain | | + c8 | text | | (p2 'V2') | extended | | + c9 | integer | | | plain | | + c10 | integer | | (p1 'v1') | plain | | Server: s0 FDW Options: (delimiter ',', quote '"', "be quoted" 'value') Has OIDs: no diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql index 0c95672..03b5680 100644 --- a/src/test/regress/sql/foreign_data.sql +++ b/src/test/regress/sql/foreign_data.sql @@ -307,6 +307,10 @@ ALTER FOREIGN TABLE ft1 ALTER COLUMN xmin OPTIONS (ADD p1 'v1'); -- ERROR ALTER FOREIGN TABLE ft1 ALTER COLUMN c7 OPTIONS (ADD p1 'v1', ADD p2 'v2'), ALTER COLUMN c8 OPTIONS (ADD p1 'v1', ADD p2 'v2'); ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 OPTIONS (SET p2 'V2', DROP p1); +ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 SET STATISTICS 10000; +ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 SET (n_distinct = 100); +ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 SET (n_distinct_inherited = 100); -- ERROR +ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET STATISTICS -1; \d+ ft1 -- can't change the column type if it's used elsewhere CREATE TABLE use_ft1_column_type (x ft1);