/*
* pg_dbms_stats.c
*
- * Copyright (c) 2009-2014, NIPPON TELEGRAPH AND TELEPHONE CORPORATION
+ * Copyright (c) 2009-2018, NIPPON TELEGRAPH AND TELEPHONE CORPORATION
* Portions Copyright (c) 1996-2013, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*/
#include "postgres.h"
+#include "access/sysattr.h"
#include "access/transam.h"
+#include "access/relation.h"
+#include "catalog/pg_index.h"
#include "catalog/pg_statistic.h"
#include "catalog/pg_type.h"
#include "catalog/namespace.h"
#include "catalog/pg_authid.h"
#include "commands/trigger.h"
+#include "common/hashfn.h"
#include "executor/spi.h"
#include "funcapi.h"
#include "optimizer/plancat.h"
+#include "optimizer/planner.h"
+#include "parser/parse_oper.h"
+#include "parser/parsetree.h"
#include "storage/bufmgr.h"
+#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/elog.h"
+#include "utils/fmgroids.h"
#include "utils/guc.h"
#include "utils/inval.h"
#include "utils/lsyscache.h"
#include "utils/selfuncs.h"
#include "utils/syscache.h"
#include "miscadmin.h"
-#if PG_VERSION_NUM >= 90200
#include "utils/rel.h"
-#endif
-#if PG_VERSION_NUM >= 90300
#include "access/htup_details.h"
#include "utils/catcache.h"
-#endif
+#include <math.h>
#include "pg_dbms_stats.h"
#define MAX_REL_CACHE 50 /* expected max # of rel stats entries */
+/*
+ * acl_ok of the returning VariableStatData must be set if set_acl_okk is
+ * true. The code is compiled only if the compile target PG version is match
+ * the above conditions. Conversely, acl_ok is added to the end of
+ * VariableStatData so we can safely omit setting it when the PG version
+ * pg_dbms_stats is loaded onto is out of the conditions.
+ */
+static bool set_acl_ok = false;
+
+#define get_attrs(pgatt_attrs) (&(pgatt_attrs))
+
/* Relation statistics cache entry */
typedef struct StatsRelationEntry
{
Oid relid; /* hash key must be at the head */
-
bool valid; /* T if the entry has valid stats */
-
+ bool invalidated; /* T if this relation has been
+ * invalidated */
BlockNumber relpages; /* # of pages as of last ANALYZE */
double reltuples; /* # of tuples as of last ANALYZE */
BlockNumber relallvisible; /* # of all-visible pages as of last
* ANALYZE */
BlockNumber curpages; /* # of pages as of lock/restore */
-
List *col_stats; /* list of StatsColumnEntry, each element
of which is pg_statistic record of this
relation. */
get_attavgwidth_hook_type prev_get_attavgwidth = NULL;
get_relation_stats_hook_type prev_get_relation_stats = NULL;
get_index_stats_hook_type prev_get_index_stats = NULL;
+planner_hook_type prev_planner_hook = NULL;
/* namings */
#define NSPNAME "dbms_stats"
/* rows_query(oid) RETURNS int4, float4, int4 */
static const char *rows_query =
- "SELECT relpages, reltuples, curpages"
-#if PG_VERSION_NUM >= 90200
- ", relallvisible"
-#endif
+ "SELECT relpages, reltuples, curpages, relallvisible"
" FROM " NSPNAME "." RELSTAT_TBLNAME
" WHERE relid = $1";
static SPIPlanPtr rows_plan = NULL;
/*
* The relation_stats_effective statistic cache is stored in hash table.
+ * rel_invalidated is set true if the hash has invalidated entries.
*/
static HTAB *rel_stats;
+static bool rel_invalidated = false;
/*
* The owner of pg_dbms_stats statistic tables.
PG_FUNCTION_INFO_V1(dbms_stats_invalidate_column_cache);
PG_FUNCTION_INFO_V1(dbms_stats_is_system_schema);
PG_FUNCTION_INFO_V1(dbms_stats_is_system_catalog);
+PG_FUNCTION_INFO_V1(dbms_stats_anyary_anyary);
+PG_FUNCTION_INFO_V1(dbms_stats_type_is_analyzable);
+PG_FUNCTION_INFO_V1(dbms_stats_anyarray_basetype);
extern Datum dbms_stats_merge(PG_FUNCTION_ARGS);
extern Datum dbms_stats_invalidate_relation_cache(PG_FUNCTION_ARGS);
extern Datum dbms_stats_invalidate_column_cache(PG_FUNCTION_ARGS);
extern Datum dbms_stats_is_system_schema(PG_FUNCTION_ARGS);
extern Datum dbms_stats_is_system_catalog(PG_FUNCTION_ARGS);
+extern Datum dbms_stats_anyary_anyary(PG_FUNCTION_ARGS);
+extern Datum dbms_stats_type_is_analyzable(PG_FUNCTION_ARGS);
+extern Datum dbms_stats_anyarray_basetype(PG_FUNCTION_ARGS);
static HeapTuple dbms_stats_merge_internal(HeapTuple lhs, HeapTuple rhs,
TupleDesc tupledesc);
void _PG_init(void);
void _PG_fini(void);
+/* hook functions */
static void dbms_stats_get_relation_info(PlannerInfo *root, Oid relid,
bool inhparent, RelOptInfo *rel);
static int32 dbms_stats_get_attavgwidth(Oid relid, AttrNumber attnum);
AttrNumber attnum, VariableStatData *vardata);
static bool dbms_stats_get_index_stats(PlannerInfo *root, Oid indexOid,
AttrNumber indexattnum, VariableStatData *vardata);
+static PlannedStmt *dbms_stats_planner(Query *parse, const char *query_string,
+ int cursorOptions,
+ ParamListInfo boundParams);
+/* internal functions */
static void get_merged_relation_stats(Oid relid, BlockNumber *pages,
double *tuples, double *allvisfrac, bool estimate);
static int32 get_merged_avgwidth(Oid relid, AttrNumber attnum);
HeapTuple tuple);
static bool execute_plan(SPIPlanPtr *plan, const char *query, Oid relid,
const AttrNumber *attnum, bool inh);
-static void StatsCacheRelCallback(Datum arg, Oid relid);
+static void statscache_rel_callback(Datum arg, Oid relid);
+static void cleanup_invalidated_cache(void);
static void init_rel_stats(void);
static void init_rel_stats_entry(StatsRelationEntry *entry, Oid relid);
+
/* copied from PG core source tree */
static void dbms_stats_estimate_rel_size(Relation rel, int32 *attr_widths,
BlockNumber *pages, double *tuples, double *allvisfrac,
extern void test_pg_dbms_stats(int *passed, int *total);
#endif
-/* SPI_keepplan() is since 9.2 */
-#if PG_VERSION_NUM < 90200
-#define SPI_keepplan(pplan) {\
-SPIPlanPtr tp = *plan;\
- *plan = SPI_saveplan(tp);\
- SPI_freeplan(tp);\
-}
-#endif
-
/*
* Module load callback
*/
}
#endif
+ {
+ /*
+ * Check the PG version this module loaded onto. This aid is required
+ * for binary backward compatibility within a major PG version.
+ */
+ int major_version = PG_VERSION_NUM / 100;
+ int minor_version = PG_VERSION_NUM % 100;
+
+ if (major_version >= 1000 ||
+ (major_version == 906 && minor_version >= 3) ||
+ (major_version == 905 && minor_version >= 7) ||
+ (major_version == 904 && minor_version >= 12) ||
+ (major_version == 903 && minor_version >= 17) ||
+ (major_version == 902 && minor_version >= 21))
+ set_acl_ok = true;
+ }
+
/* Define custom GUC variables. */
DefineCustomBoolVariable("pg_dbms_stats.use_locked_stats",
"Enable user defined statistics.",
get_relation_stats_hook = dbms_stats_get_relation_stats;
prev_get_index_stats = get_index_stats_hook;
get_index_stats_hook = dbms_stats_get_index_stats;
+ prev_planner_hook = planner_hook;
+ planner_hook = dbms_stats_planner;
/* Initialize hash table for statistics caching. */
init_rel_stats();
/* Also set up a callback for relcache SI invalidations */
- CacheRegisterRelcacheCallback(StatsCacheRelCallback, (Datum) 0);
+ CacheRegisterRelcacheCallback(statscache_rel_callback, (Datum) 0);
}
/*
get_attavgwidth_hook = prev_get_attavgwidth;
get_relation_stats_hook = prev_get_relation_stats;
get_index_stats_hook = prev_get_index_stats;
+ planner_hook = prev_planner_hook;
/* A function to unregister callback for relcache is NOT provided. */
}
/*
+ * Function to convert from any array from dbms_stats.anyarray.
+ */
+Datum
+dbms_stats_anyary_anyary(PG_FUNCTION_ARGS)
+{
+ ArrayType *arr = PG_GETARG_ARRAYTYPE_P(0);
+ if (ARR_NDIM(arr) != 1)
+ elog(ERROR, "array must be one-dimentional.");
+
+ PG_RETURN_ARRAYTYPE_P(arr);
+}
+
+/*
+ * Function to check if the type can have statistics.
+ */
+Datum
+dbms_stats_type_is_analyzable(PG_FUNCTION_ARGS)
+{
+ Oid typid = PG_GETARG_OID(0);
+ Oid eqopr;
+
+ if (!OidIsValid(typid))
+ PG_RETURN_BOOL(false);
+
+ get_sort_group_operators(typid, false, false, false,
+ NULL, &eqopr, NULL,
+ NULL);
+ PG_RETURN_BOOL(OidIsValid(eqopr));
+}
+
+/*
+ * Function to get base type of the value of the type dbms_stats.anyarray.
+ */
+Datum
+dbms_stats_anyarray_basetype(PG_FUNCTION_ARGS)
+{
+ ArrayType *arr = PG_GETARG_ARRAYTYPE_P(0);
+ Oid elemtype = arr->elemtype;
+ HeapTuple tp;
+ Form_pg_type typtup;
+ Name result;
+
+ if (!OidIsValid(elemtype))
+ elog(ERROR, "invalid base type oid: %u", elemtype);
+
+ tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(elemtype));
+ if (!HeapTupleIsValid(tp)) /* I trust you. */
+ elog(ERROR, "invalid base type oid: %u", elemtype);
+
+ typtup = (Form_pg_type) GETSTRUCT(tp);
+ result = (Name) palloc0(NAMEDATALEN);
+ StrNCpy(NameStr(*result), NameStr(typtup->typname), NAMEDATALEN);
+
+ ReleaseSysCache(tp);
+ PG_RETURN_NAME(result);
+}
+
+/*
* Find and store the owner of the dummy statistics table.
*
* We will access statistics tables using this owner
(errmsg("pg_dbms_stats: bad statistics"),
errdetail("column \"%s\" should not be null",
get_attname(StatisticRelationId,
- tupdesc->attrs[i]->attnum))));
+ get_attrs(tupdesc->attrs[i])->attnum,
+ true))));
return NULL; /* should not be null */
}
}
(errmsg("pg_dbms_stats: bad statistics"),
errdetail("column \"%s\" should not be null",
get_attname(StatisticRelationId,
- tupdesc->attrs[i]->attnum))));
+ get_attrs(tupdesc->attrs[i])->attnum,
+ true))));
return NULL; /* should not be null */
}
}
values[Anum_pg_statistic_stavalues1 + i - 1]);
if (arr == NULL || arr->elemtype != atttype)
{
- const char *attname = get_attname(relid, attnum);
+ const char *attname = get_attname(relid, attnum, true);
/*
* relid and attnum must be valid here because valid atttype
/*
* invalidate prepared statements and force re-planning with pg_dbms_stats.
*/
- rel = try_relation_open(relid, NoLock);
+ rel = try_relation_open(relid, AccessShareLock);
if (rel != NULL)
{
if (sta_col &&
rel->rd_rel->relkind == RELKIND_INDEX &&
(rel->rd_indextuple == NULL ||
- heap_attisnull(rel->rd_indextuple, Anum_pg_index_indexprs)))
+ heap_attisnull(rel->rd_indextuple, Anum_pg_index_indexprs, NULL)))
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is an index except an index expression",
CacheInvalidateRelcacheByRelid(rel->rd_index->indrelid);
CacheInvalidateRelcache(rel);
- relation_close(rel, NoLock);
+ relation_close(rel, AccessShareLock);
}
}
return false;
/* no such relation */
- rel = try_relation_open(relid, NoLock);
+ rel = try_relation_open(relid, AccessShareLock);
if (rel == NULL)
return false;
/* check by namespace name. */
schema_name = get_namespace_name(rel->rd_rel->relnamespace);
result = dbms_stats_is_system_schema_internal(schema_name);
- relation_close(rel, NoLock);
+ relation_close(rel, AccessShareLock);
return result;
}
* calculation.
*/
if (!inhparent)
- {
-#if PG_VERSION_NUM >= 90200
get_merged_relation_stats(relid, &rel->pages, &rel->tuples,
&rel->allvisfrac, true);
-#else
- get_merged_relation_stats(relid, &rel->pages, &rel->tuples,
- &allvisfrac, true);
-#endif
- }
else
return;
tuple = get_merged_column_stats(rte->relid, attnum, rte->inh);
vardata->statsTuple = tuple;
- if (tuple != NULL)
+ if (HeapTupleIsValid(tuple))
{
vardata->freefunc = FreeHeapTuple;
+
+ /*
+ * set acl_ok if required. See the definition of set_acl_ok for
+ * details.
+ */
+ if (set_acl_ok)
+ {
+ vardata->acl_ok =
+ (pg_class_aclcheck(rte->relid, GetUserId(),
+ ACL_SELECT) == ACLCHECK_OK) ||
+ (pg_attribute_aclcheck(rte->relid, attnum, GetUserId(),
+ ACL_SELECT) == ACLCHECK_OK);
+ }
+
return true;
}
}
AttrNumber indexattnum,
VariableStatData *vardata)
{
- if (pg_dbms_stats_use_locked_stats)
+ HeapTuple tuple;
+
+ if (!pg_dbms_stats_use_locked_stats)
+ goto next_plugin;
+
+ tuple = get_merged_column_stats(indexOid, indexattnum, false);
+ vardata->statsTuple = tuple;
+ if (tuple == NULL)
+ goto next_plugin;
+
+ vardata->freefunc = FreeHeapTuple;
+
+ /*
+ * set acl_ok if required. See the definition of set_acl_ok for details.
+ */
+ if (set_acl_ok)
{
- HeapTuple tuple;
+ /*
+ * XXX: we had to scan the whole the rel array since we got
+ * only the oid of the index.
+ */
+ int i;
- tuple = get_merged_column_stats(indexOid, indexattnum, false);
- vardata->statsTuple = tuple;
- if (tuple != NULL)
+ /* don't stop by this misassumption */
+ if (root->simple_rel_array == NULL)
{
- vardata->freefunc = FreeHeapTuple;
- return true;
+ elog(WARNING, "pg_dbms_stats internal error. relation has not been set up. index %d ignored", indexOid);
+ goto next_plugin;
+ }
+
+ /*
+ * scan over simple_rel_array_size to find the owner relation of the
+ * index with the oid
+ */
+ for (i = 1 ; i < root->simple_rel_array_size ; i++)
+ {
+ ListCell *lc;
+ RelOptInfo *rel = root->simple_rel_array[i];
+
+ /* there may be empty slots corresponding to non-baserel RTEs */
+ if (rel == NULL)
+ continue;
+
+ foreach (lc, rel->indexlist)
+ {
+ IndexOptInfo *index = (IndexOptInfo *) lfirst(lc);
+ RangeTblEntry *rte;
+
+ if (index->indexoid != indexOid)
+ continue;
+
+ /* This relation is the owner of the given index, go ahead */
+ rte = planner_rt_fetch(index->rel->relid, root);
+
+ /* don't stop by this error */
+ if (rte->rtekind != RTE_RELATION)
+ {
+ elog(WARNING, "pg_dbms_stats internal error. index %d is owned by a non-relation", indexOid);
+ goto next_plugin;
+ }
+
+ vardata->acl_ok =
+ (pg_class_aclcheck(rte->relid, GetUserId(),
+ ACL_SELECT) == ACLCHECK_OK);
+ break;
+ }
}
}
+ return true;
+
+next_plugin:
if (prev_get_index_stats)
return prev_get_index_stats(root, indexOid, indexattnum, vardata);
else
return false;
}
+
+/*
+ * dbms_stats_planner
+ * Hook function for planner_hook which cleans up invalidated statistics.
+ */
+static PlannedStmt *
+dbms_stats_planner(Query *parse, const char *query_string,
+ int cursorOptions, ParamListInfo boundParams)
+{
+ PlannedStmt *ret;
+
+ cleanup_invalidated_cache();
+
+ if (prev_planner_hook)
+ ret = (*prev_planner_hook) (parse, query_string,
+ cursorOptions, boundParams);
+ else
+ ret = standard_planner(parse, query_string,
+ cursorOptions, boundParams);
+
+ cleanup_invalidated_cache();
+
+ return ret;
+}
+
+
/*
* Extract binary value from given column.
*/
val = get_binary_datum(3, &isnull);
entry->curpages = isnull ? InvalidBlockNumber :
(BlockNumber) DatumGetInt32(val);
-#if PG_VERSION_NUM >= 90200
val = get_binary_datum(4, &isnull);
entry->relallvisible = isnull ? form->relallvisible :
(BlockNumber) DatumGetInt32(val);
-#endif
ReleaseSysCache(tuple);
}
rel = relation_open(relid, NoLock);
rel->rd_rel->relpages = entry->relpages;
rel->rd_rel->reltuples = entry->reltuples;
-#if PG_VERSION_NUM >= 90200
rel->rd_rel->relallvisible = entry->relallvisible;
-#endif
dbms_stats_estimate_rel_size(rel, NULL, pages, tuples, allvisfrac,
entry->curpages);
relation_close(rel, NoLock);
StatsRelationEntry *entry;
bool found;
- Assert(tuple == NULL || !heap_attisnull(tuple, 1));
+ Assert(tuple == NULL || !heap_attisnull(tuple, 1, NULL));
entry = hash_search(rel_stats, &relid, HASH_ENTER, &found);
if (!found)
Oid argtypes[3] = { OIDOID, INT2OID, BOOLOID };
int nargs;
Datum values[3];
- bool nulls[3] = { false, false, false };
Oid save_userid;
int save_sec_context;
values[1] = Int16GetDatum(attnum ? *attnum : 0);
values[2] = BoolGetDatum(inh);
- ret = SPI_execute_plan(*plan, values, nulls, true, 1);
+ ret = SPI_execute_plan(*plan, values, NULL, true, 1);
}
PG_CATCH();
{
}
/*
- * StatsCacheRelCallback
+ * statscache_rel_callback
* Relcache inval callback function
*
- * Invalidate cached statistic info of the given relid, or all cached statistic
- * info if relid == InvalidOid. We don't complain even when we don't have such
- * statistics.
+ * Invalidates cached statistics of the given relid, or all cached statistics
+ * if relid == InvalidOid. The statsTuple in the hash entries are directly
+ * passed to planner so we cannot remove them until planner ends. Just mark
+ * here then cleanup after planner finishes work.
+ */
+static void
+statscache_rel_callback(Datum arg, Oid relid)
+{
+ StatsRelationEntry *entry;
+
+ if (relid != InvalidOid)
+ {
+ bool found;
+
+ /*
+ * invalidate the entry for the specfied relation. Don't mind if found.
+ */
+ entry = hash_search(rel_stats, &relid, HASH_FIND, &found);
+ if (found)
+ {
+ entry->invalidated = true;
+ rel_invalidated = true;
+ }
+ }
+ else
+ {
+ /* invalidate all the entries of the hash */
+ HASH_SEQ_STATUS status;
+
+ hash_seq_init(&status, rel_stats);
+ while ((entry = hash_seq_search(&status)) != NULL)
+ {
+ entry->invalidated = true;
+ rel_invalidated = true;
+ }
+ }
+}
+
+/*
+ * cleanup_invalidated_cache()
+ * Cleanup invalidated stats cache
*
- * Note: arg is not used.
+ * removes invalidated cache entries.
*/
static void
-StatsCacheRelCallback(Datum arg, Oid relid)
+cleanup_invalidated_cache(void)
{
HASH_SEQ_STATUS status;
StatsRelationEntry *entry;
+ /* Return immediately if nothing to do */
+ if (!rel_invalidated)
+ return;
+
+ /*
+ * Reset rel_invalidated first so that we don't lose invalidations that
+ * happens during this round of cleanup.
+ */
+ rel_invalidated = false;
+
hash_seq_init(&status, rel_stats);
while ((entry = hash_seq_search(&status)) != NULL)
{
- if (relid == InvalidOid || relid == entry->relid)
- {
- ListCell *lc;
+ ListCell *lc;
- /* Mark the relation entry as INVALID */
- entry->valid = false;
+ if (!entry->invalidated)
+ continue;
- /* Discard every column statistics */
- foreach (lc, entry->col_stats)
- {
- StatsColumnEntry *ent = (StatsColumnEntry*) lfirst(lc);
+ /* Discard every column statistics */
+ foreach (lc, entry->col_stats)
+ {
+ StatsColumnEntry *ent = (StatsColumnEntry*) lfirst(lc);
- if (!ent->negative)
- pfree(ent->tuple);
- pfree(ent);
- }
- list_free(entry->col_stats);
- entry->col_stats = NIL;
+ if (!ent->negative)
+ pfree(ent->tuple);
+ pfree(ent);
}
- }
+ list_free(entry->col_stats);
- /* We always check throughout the list, so hash_seq_term is not necessary */
+ /* Finally remove the hash entry. */
+ hash_search(rel_stats, &entry->relid, HASH_REMOVE, NULL);
+ }
}
/*
{
entry->relid = relid;
entry->valid = false;
+ entry->invalidated = false;
entry->relpages = InvalidBlockNumber;
entry->reltuples = 0.0;
entry->relallvisible = InvalidBlockNumber;
* the attribute widths for estimation purposes.
*
* Note: This function is copied from plancat.c in core source tree of version
- * 9.2, and customized for pg_dbms_stats. Changes from orignal one are:
+ * 9.2, and customized for pg_dbms_stats. Changes from original one are:
* - rename by prefixing dbms_stats_
* - add 3 parameters (relpages, reltuples, curpage) to pass dummy curpage
* values.
* - Get current # of pages only when supplied curpages is InvalidBlockNumber
- * - get franction of all-visible-pages
+ * - get fraction of all-visible-pages
*/
static void
dbms_stats_estimate_rel_size(Relation rel, int32 *attr_widths,
{
case RELKIND_RELATION:
case RELKIND_INDEX:
-#if PG_VERSION_NUM >= 90300
case RELKIND_MATVIEW:
-#endif
case RELKIND_TOASTVALUE:
/* it has storage, ok to call the smgr */
if (curpages == InvalidBlockNumber)
/* coerce values in pg_class to more desirable types */
relpages = (BlockNumber) rel->rd_rel->relpages;
reltuples = (double) rel->rd_rel->reltuples;
-#if PG_VERSION_NUM >= 90200
relallvisible = (BlockNumber) rel->rd_rel->relallvisible;
-#else
- relallvisible = 0;
-#endif
+
/*
* If it's an index, discount the metapage while estimating the
* number of tuples. This is a kluge because it assumes more than
for (i = 1; i <= RelationGetNumberOfAttributes(rel); i++)
{
- Form_pg_attribute att = rel->rd_att->attrs[i - 1];
+ Form_pg_attribute att = get_attrs(rel->rd_att->attrs[i - 1]);
int32 item_width;
if (att->attisdropped)