mirror of
https://github.com/timescale/timescaledb.git
synced 2025-05-16 10:33:27 +08:00
This patch changes INSERTs into compressed chunks to no longer be immediately compressed but stored in the uncompressed chunk instead and later merged with the compressed chunk by a separate job. This greatly simplifies the INSERT-codepath as we no longer have to rewrite the target of INSERTs and on-the-fly compress leading to a roughly 2x improvement on INSERT rate into compressed chunk. Additionally this improves TRIGGER-support for INSERTs into compressed chunks. This is a necessary refactoring to allow UPSERT/UPDATE/DELETE on compressed chunks in follow-patches.
1688 lines
49 KiB
C
1688 lines
49 KiB
C
/*
|
|
* This file and its contents are licensed under the Apache License 2.0.
|
|
* Please see the included NOTICE for copyright information and
|
|
* LICENSE-APACHE for a copy of the license.
|
|
*/
|
|
#include <postgres.h>
|
|
#include <access/tsmapi.h>
|
|
#include <access/xact.h>
|
|
#include <catalog/namespace.h>
|
|
#include <commands/extension.h>
|
|
#include <executor/nodeAgg.h>
|
|
#include <miscadmin.h>
|
|
#include <nodes/makefuncs.h>
|
|
#include <nodes/plannodes.h>
|
|
#include <optimizer/appendinfo.h>
|
|
#include <optimizer/clauses.h>
|
|
#include <optimizer/optimizer.h>
|
|
#include <optimizer/pathnode.h>
|
|
#include <optimizer/paths.h>
|
|
#include <optimizer/planner.h>
|
|
#include <optimizer/restrictinfo.h>
|
|
#include <optimizer/tlist.h>
|
|
#include <parser/parsetree.h>
|
|
#include <utils/elog.h>
|
|
#include <utils/fmgroids.h>
|
|
#include <utils/guc.h>
|
|
#include <utils/lsyscache.h>
|
|
#include <utils/memutils.h>
|
|
#include <utils/selfuncs.h>
|
|
#include <utils/timestamp.h>
|
|
|
|
#include "compat/compat-msvc-enter.h"
|
|
#include <catalog/pg_constraint.h>
|
|
#include <nodes/nodeFuncs.h>
|
|
#include <optimizer/cost.h>
|
|
#include <optimizer/plancat.h>
|
|
#include <parser/analyze.h>
|
|
#include <tcop/tcopprot.h>
|
|
#include "compat/compat-msvc-exit.h"
|
|
|
|
#include <math.h>
|
|
|
|
#include "annotations.h"
|
|
#include "chunk.h"
|
|
#include "cross_module_fn.h"
|
|
#include "debug_assert.h"
|
|
#include "dimension.h"
|
|
#include "dimension_slice.h"
|
|
#include "dimension_vector.h"
|
|
#include "extension.h"
|
|
#include "func_cache.h"
|
|
#include "guc.h"
|
|
#include "hypertable_cache.h"
|
|
#include "import/allpaths.h"
|
|
#include "license_guc.h"
|
|
#include "nodes/chunk_append/chunk_append.h"
|
|
#include "nodes/chunk_dispatch_plan.h"
|
|
#include "nodes/constraint_aware_append/constraint_aware_append.h"
|
|
#include "nodes/hypertable_modify.h"
|
|
#include "partitioning.h"
|
|
#include "planner/planner.h"
|
|
#include "utils.h"
|
|
|
|
#include "compat/compat.h"
|
|
#if PG13_GE
|
|
#include <common/hashfn.h>
|
|
#else
|
|
#include <utils/hashutils.h>
|
|
#endif
|
|
|
|
#ifdef USE_TELEMETRY
|
|
#include "telemetry/functions.h"
|
|
#endif
|
|
|
|
/* define parameters necessary to generate the baserel info hash table interface */
|
|
typedef struct BaserelInfoEntry
|
|
{
|
|
Oid reloid;
|
|
Hypertable *ht;
|
|
|
|
uint32 status; /* hash status */
|
|
} BaserelInfoEntry;
|
|
|
|
#define SH_PREFIX BaserelInfo
|
|
#define SH_ELEMENT_TYPE BaserelInfoEntry
|
|
#define SH_KEY_TYPE Oid
|
|
#define SH_KEY reloid
|
|
#define SH_EQUAL(tb, a, b) ((a) == (b))
|
|
#define SH_HASH_KEY(tb, key) murmurhash32(key)
|
|
#define SH_SCOPE static
|
|
#define SH_DECLARE
|
|
#define SH_DEFINE
|
|
|
|
// We don't need most of the generated functions and there is no way to not
|
|
// generate them.
|
|
#ifdef __GNUC__
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wunused-function"
|
|
#endif
|
|
|
|
// Generate the baserel info hash table functions.
|
|
#include "lib/simplehash.h"
|
|
#ifdef __GNUC__
|
|
|
|
#pragma GCC diagnostic pop
|
|
#endif
|
|
|
|
void _planner_init(void);
|
|
void _planner_fini(void);
|
|
|
|
static planner_hook_type prev_planner_hook;
|
|
static set_rel_pathlist_hook_type prev_set_rel_pathlist_hook;
|
|
static get_relation_info_hook_type prev_get_relation_info_hook;
|
|
static create_upper_paths_hook_type prev_create_upper_paths_hook;
|
|
static void cagg_reorder_groupby_clause(RangeTblEntry *subq_rte, Index rtno, List *outer_sortcl,
|
|
List *outer_tlist);
|
|
|
|
/*
|
|
* We mark range table entries (RTEs) in a query with TS_CTE_EXPAND if we'd like
|
|
* to control table expansion ourselves. We exploit the ctename for this purpose
|
|
* since it is not used for regular (base) relations.
|
|
*
|
|
* Note that we cannot use this mark as a general way to identify hypertable
|
|
* RTEs. Child RTEs, for instance, will inherit this value from the parent RTE
|
|
* during expansion. While we can prevent this happening in our custom table
|
|
* expansion, we also have to account for the case when our custom expansion
|
|
* is turned off with a GUC.
|
|
*/
|
|
static const char *TS_CTE_EXPAND = "ts_expand";
|
|
|
|
/*
|
|
* Controls which type of fetcher to use to fetch data from the data nodes.
|
|
* There is no place to store planner-global custom information (such as in
|
|
* PlannerInfo). Because of this, we have to use the global variable that is
|
|
* valid inside the scope of timescaledb_planner().
|
|
* Note that that function can be called recursively, e.g. when evaluating a
|
|
* SQL function at the planning time. We only have to determine the fetcher type
|
|
* in the outermost scope, so we distinguish it by that the fetcher type is set
|
|
* to the invalid value of 'auto'.
|
|
*/
|
|
DataFetcherType ts_data_node_fetcher_scan_type = AutoFetcherType;
|
|
|
|
/*
|
|
* A simplehash hash table that records the chunks and their corresponding
|
|
* hypertables, and also the plain baserels. We use it to tell whether a
|
|
* relation is a hypertable chunk, inside the classify_relation function.
|
|
* It is valid inside the scope of timescaledb_planner().
|
|
* That function can be called recursively, e.g. when we evaluate a SQL function,
|
|
* and this cache is initialized only at the top-level call.
|
|
*/
|
|
static struct BaserelInfo_hash *ts_baserel_info = NULL;
|
|
|
|
/*
|
|
* Add information about a chunk to the baserel info cache. Used to cache the
|
|
* chunk info at the plan time chunk exclusion.
|
|
*/
|
|
void
|
|
add_baserel_cache_entry_for_chunk(Oid chunk_reloid, Hypertable *hypertable)
|
|
{
|
|
Assert(hypertable != NULL);
|
|
Assert(ts_baserel_info != NULL);
|
|
|
|
bool found = false;
|
|
BaserelInfoEntry *entry = BaserelInfo_insert(ts_baserel_info, chunk_reloid, &found);
|
|
if (found)
|
|
{
|
|
/* Already cached. */
|
|
Assert(entry->ht != NULL);
|
|
return;
|
|
}
|
|
|
|
/* Fill the cache entry. */
|
|
entry->ht = hypertable;
|
|
}
|
|
|
|
static void
|
|
rte_mark_for_expansion(RangeTblEntry *rte)
|
|
{
|
|
Assert(rte->rtekind == RTE_RELATION);
|
|
Assert(rte->ctename == NULL);
|
|
rte->ctename = (char *) TS_CTE_EXPAND;
|
|
rte->inh = false;
|
|
}
|
|
|
|
bool
|
|
ts_rte_is_marked_for_expansion(const RangeTblEntry *rte)
|
|
{
|
|
if (NULL == rte->ctename)
|
|
return false;
|
|
|
|
if (rte->ctename == TS_CTE_EXPAND)
|
|
return true;
|
|
|
|
return strcmp(rte->ctename, TS_CTE_EXPAND) == 0;
|
|
}
|
|
|
|
/*
|
|
* Planner-global hypertable cache.
|
|
*
|
|
* Each invocation of the planner (and our hooks) should reference the same
|
|
* cache object. Since we warm the cache when pre-processing the query (prior to
|
|
* invoking the planner), we'd like to ensure that we use the same cache object
|
|
* throughout the planning of that query so that we can trust that the cache
|
|
* holds the objects it was warmed with. Since the planner can be invoked
|
|
* recursively, we also need to stack and pop cache objects.
|
|
*/
|
|
static List *planner_hcaches = NIL;
|
|
|
|
static Cache *
|
|
planner_hcache_push(void)
|
|
{
|
|
Cache *hcache = ts_hypertable_cache_pin();
|
|
|
|
planner_hcaches = lcons(hcache, planner_hcaches);
|
|
|
|
return hcache;
|
|
}
|
|
|
|
static void
|
|
planner_hcache_pop(bool release)
|
|
{
|
|
Cache *hcache;
|
|
|
|
Assert(list_length(planner_hcaches) > 0);
|
|
|
|
hcache = linitial(planner_hcaches);
|
|
|
|
planner_hcaches = list_delete_first(planner_hcaches);
|
|
|
|
if (release)
|
|
{
|
|
ts_cache_release(hcache);
|
|
/* If we pop a stack and discover a new hypertable cache, the basrel
|
|
* cache can contain invalid entries, so we reset it. */
|
|
if (planner_hcaches != NIL && hcache != linitial(planner_hcaches))
|
|
BaserelInfo_reset(ts_baserel_info);
|
|
}
|
|
}
|
|
|
|
static bool
|
|
planner_hcache_exists(void)
|
|
{
|
|
return planner_hcaches != NIL;
|
|
}
|
|
|
|
static Cache *
|
|
planner_hcache_get(void)
|
|
{
|
|
if (planner_hcaches == NIL)
|
|
return NULL;
|
|
|
|
return (Cache *) linitial(planner_hcaches);
|
|
}
|
|
|
|
/*
|
|
* Get the Hypertable corresponding to the given relid.
|
|
*
|
|
* This function gets a hypertable from a pre-warmed hypertable cache. If
|
|
* noresolve is specified (true), then it will do a cache-only lookup (i.e., it
|
|
* will not try to scan metadata for a new entry to put in the cache). This
|
|
* allows fast lookups during planning to also determine if something is _not_ a
|
|
* hypertable.
|
|
*/
|
|
Hypertable *
|
|
ts_planner_get_hypertable(const Oid relid, const unsigned int flags)
|
|
{
|
|
Cache *cache = planner_hcache_get();
|
|
|
|
if (NULL == cache)
|
|
return NULL;
|
|
|
|
return ts_hypertable_cache_get_entry(cache, relid, flags);
|
|
}
|
|
|
|
bool
|
|
ts_rte_is_hypertable(const RangeTblEntry *rte, bool *isdistributed)
|
|
{
|
|
Hypertable *ht = ts_planner_get_hypertable(rte->relid, CACHE_FLAG_CHECK);
|
|
|
|
if (isdistributed && ht != NULL)
|
|
*isdistributed = hypertable_is_distributed(ht);
|
|
|
|
return ht != NULL;
|
|
}
|
|
|
|
#define IS_UPDL_CMD(parse) \
|
|
((parse)->commandType == CMD_UPDATE || (parse)->commandType == CMD_DELETE)
|
|
|
|
typedef struct
|
|
{
|
|
Query *rootquery;
|
|
Query *current_query;
|
|
PlannerInfo *root;
|
|
/*
|
|
* The number of distributed hypertables in the query and its subqueries.
|
|
* Specifically, we count range table entries here, so using the same
|
|
* distributed table twice counts as two tables. No matter whether it's the
|
|
* same physical table or not, the range table entries can be scanned
|
|
* concurrently, and more than one of them being distributed means we have
|
|
* to use the cursor fetcher so that these scans can be interleaved.
|
|
*/
|
|
int num_distributed_tables;
|
|
} PreprocessQueryContext;
|
|
|
|
/*
|
|
* Preprocess the query tree, including, e.g., subqueries.
|
|
*
|
|
* Preprocessing includes:
|
|
*
|
|
* 1. Identifying all range table entries (RTEs) that reference
|
|
* hypertables. This will also warm the hypertable cache for faster lookup
|
|
* of both hypertables (cache hit) and non-hypertables (cache miss),
|
|
* without having to scan the metadata in either case.
|
|
*
|
|
* 2. Turning off inheritance for hypertable RTEs that we expand ourselves.
|
|
*
|
|
* 3. Reordering of GROUP BY clauses for continuous aggregates.
|
|
*
|
|
* 4. Constifying now() expressions for primary time dimension.
|
|
*/
|
|
static bool
|
|
preprocess_query(Node *node, PreprocessQueryContext *context)
|
|
{
|
|
if (node == NULL)
|
|
return false;
|
|
|
|
if (IsA(node, FromExpr) && ts_guc_enable_optimizations)
|
|
{
|
|
FromExpr *from = castNode(FromExpr, node);
|
|
if (from->quals)
|
|
{
|
|
if (ts_guc_enable_now_constify)
|
|
{
|
|
from->quals =
|
|
ts_constify_now(context->root, context->current_query->rtable, from->quals);
|
|
}
|
|
/*
|
|
* We only amend space constraints for UPDATE/DELETE and SELECT FOR UPDATE
|
|
* as for normal SELECT we use our own hypertable expansion which can handle
|
|
* constraints on hashed space dimensions without further help.
|
|
*/
|
|
if (context->current_query->commandType != CMD_SELECT ||
|
|
context->current_query->rowMarks != NIL)
|
|
{
|
|
from->quals = ts_add_space_constraints(context->root,
|
|
context->current_query->rtable,
|
|
from->quals);
|
|
}
|
|
}
|
|
}
|
|
|
|
else if (IsA(node, Query))
|
|
{
|
|
Query *query = castNode(Query, node);
|
|
Query *prev_query;
|
|
Cache *hcache = planner_hcache_get();
|
|
ListCell *lc;
|
|
Index rti = 1;
|
|
bool ret;
|
|
|
|
foreach (lc, query->rtable)
|
|
{
|
|
RangeTblEntry *rte = lfirst_node(RangeTblEntry, lc);
|
|
Hypertable *ht;
|
|
|
|
switch (rte->rtekind)
|
|
{
|
|
case RTE_SUBQUERY:
|
|
if (ts_guc_enable_optimizations && ts_guc_enable_cagg_reorder_groupby &&
|
|
query->commandType == CMD_SELECT)
|
|
{
|
|
/* applicable to selects on continuous aggregates */
|
|
List *outer_tlist = query->targetList;
|
|
List *outer_sortcl = query->sortClause;
|
|
cagg_reorder_groupby_clause(rte, rti, outer_sortcl, outer_tlist);
|
|
}
|
|
break;
|
|
case RTE_RELATION:
|
|
/* This lookup will warm the cache with all hypertables in the query */
|
|
ht = ts_hypertable_cache_get_entry(hcache, rte->relid, CACHE_FLAG_MISSING_OK);
|
|
|
|
if (ht)
|
|
{
|
|
/* Mark hypertable RTEs we'd like to expand ourselves */
|
|
if (ts_guc_enable_optimizations && ts_guc_enable_constraint_exclusion &&
|
|
!IS_UPDL_CMD(context->rootquery) && query->resultRelation == 0 &&
|
|
query->rowMarks == NIL && rte->inh)
|
|
rte_mark_for_expansion(rte);
|
|
|
|
if (TS_HYPERTABLE_HAS_COMPRESSION_TABLE(ht))
|
|
{
|
|
int compr_htid = ht->fd.compressed_hypertable_id;
|
|
|
|
/* Also warm the cache with the compressed
|
|
* companion hypertable */
|
|
ht = ts_hypertable_cache_get_entry_by_id(hcache, compr_htid);
|
|
Assert(ht != NULL);
|
|
}
|
|
|
|
if (hypertable_is_distributed(ht))
|
|
{
|
|
context->num_distributed_tables++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* To properly keep track of SELECT FROM ONLY <chunk> we
|
|
* have to mark the rte here because postgres will set
|
|
* rte->inh to false (when it detects the chunk has no
|
|
* children which is true for all our chunks) before it
|
|
* reaches set_rel_pathlist hook. But chunks from queries
|
|
* like SELECT .. FROM ONLY <chunk> has rte->inh set to
|
|
* false and other chunks have rte->inh set to true.
|
|
* We want to distinguish between the two cases here by
|
|
* marking the chunk when rte->inh is true.
|
|
*/
|
|
Chunk *chunk = ts_chunk_get_by_relid(rte->relid, false);
|
|
if (chunk && rte->inh)
|
|
rte_mark_for_expansion(rte);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
rti++;
|
|
}
|
|
prev_query = context->current_query;
|
|
context->current_query = query;
|
|
ret = query_tree_walker(query, preprocess_query, context, 0);
|
|
context->current_query = prev_query;
|
|
return ret;
|
|
}
|
|
|
|
return expression_tree_walker(node, preprocess_query, context);
|
|
}
|
|
|
|
static PlannedStmt *
|
|
#if PG13_GE
|
|
timescaledb_planner(Query *parse, const char *query_string, int cursor_opts,
|
|
ParamListInfo bound_params)
|
|
#else
|
|
timescaledb_planner(Query *parse, int cursor_opts, ParamListInfo bound_params)
|
|
#endif
|
|
{
|
|
PlannedStmt *stmt;
|
|
ListCell *lc;
|
|
/*
|
|
* Volatile is needed because these are the local variables that are
|
|
* modified between setjmp/longjmp calls.
|
|
*/
|
|
volatile bool reset_fetcher_type = false;
|
|
volatile bool reset_baserel_info = false;
|
|
|
|
/*
|
|
* If we are in an aborted transaction, reject all queries.
|
|
* While this state will not happen during normal operation it
|
|
* can happen when executing plpgsql procedures.
|
|
*/
|
|
if (IsAbortedTransactionBlockState())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_IN_FAILED_SQL_TRANSACTION),
|
|
errmsg("current transaction is aborted, "
|
|
"commands ignored until end of transaction block")));
|
|
|
|
planner_hcache_push();
|
|
if (ts_baserel_info == NULL)
|
|
{
|
|
/*
|
|
* The calls to timescaledb_planner can be recursive (e.g. when
|
|
* evaluating an immutable SQL function at planning time). We want to
|
|
* create and destroy the per-query baserel info table only at the
|
|
* top-level call, hence this flag.
|
|
*/
|
|
reset_baserel_info = true;
|
|
|
|
/*
|
|
* This is a per-query cache, so we create it in the current memory
|
|
* context for the top-level call of this function, which hopefully
|
|
* should exist for the duration of the query. Message or portal
|
|
* memory contexts could also be suitable, but they don't exist for
|
|
* SPI calls.
|
|
*/
|
|
ts_baserel_info = BaserelInfo_create(CurrentMemoryContext,
|
|
/* nelements = */ 1,
|
|
/* private_data = */ NULL);
|
|
}
|
|
|
|
PG_TRY();
|
|
{
|
|
PreprocessQueryContext context = { 0 };
|
|
PlannerGlobal glob = {
|
|
.boundParams = bound_params,
|
|
};
|
|
PlannerInfo root = {
|
|
.glob = &glob,
|
|
};
|
|
|
|
context.root = &root;
|
|
context.rootquery = parse;
|
|
context.current_query = parse;
|
|
|
|
if (ts_extension_is_loaded())
|
|
{
|
|
#ifdef USE_TELEMETRY
|
|
ts_telemetry_function_info_gather(parse);
|
|
#endif
|
|
/*
|
|
* Preprocess the hypertables in the query and warm up the caches.
|
|
*/
|
|
preprocess_query((Node *) parse, &context);
|
|
|
|
/*
|
|
* Determine which type of fetcher to use. If set by GUC, use what
|
|
* is set. If the GUC says 'auto', use the COPY fetcher if we
|
|
* have at most one distributed table in the query. This enables
|
|
* parallel plans on data nodes, which speeds up the query.
|
|
* We can't use parallel plans with the cursor fetcher, because the
|
|
* cursors don't support parallel execution. This is because a
|
|
* cursor can be suspended at any time, then some arbitrary user
|
|
* code can be executed, and then the cursor is resumed. The
|
|
* parallel infrastructure doesn't have enough reentrability to
|
|
* survive this.
|
|
* We have to use a cursor fetcher when we have multiple distributed
|
|
* tables, because we might first have to get some rows from one
|
|
* table and then from another, without running either of them to
|
|
* completion first. This happens e.g. when doing a join. If we had
|
|
* a connection per table, we could avoid this requirement.
|
|
*
|
|
* Note that this function can be called recursively, e.g. when
|
|
* trying to evaluate an SQL function at the planning stage. We must
|
|
* only set/reset the fetcher type at the topmost level, that's why
|
|
* we check it's not already set.
|
|
*/
|
|
if (ts_data_node_fetcher_scan_type == AutoFetcherType)
|
|
{
|
|
reset_fetcher_type = true;
|
|
|
|
if (context.num_distributed_tables >= 2)
|
|
{
|
|
if (ts_guc_remote_data_fetcher == CopyFetcherType)
|
|
{
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("COPY fetcher not supported"),
|
|
errhint("COPY fetching of data is not supported in "
|
|
"queries with multiple distributed hypertables."
|
|
" Use cursor fetcher instead.")));
|
|
}
|
|
ts_data_node_fetcher_scan_type = CursorFetcherType;
|
|
}
|
|
else
|
|
{
|
|
if (ts_guc_remote_data_fetcher == AutoFetcherType)
|
|
{
|
|
ts_data_node_fetcher_scan_type = CopyFetcherType;
|
|
}
|
|
else
|
|
{
|
|
ts_data_node_fetcher_scan_type = ts_guc_remote_data_fetcher;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (prev_planner_hook != NULL)
|
|
/* Call any earlier hooks */
|
|
#if PG13_GE
|
|
stmt = (prev_planner_hook) (parse, query_string, cursor_opts, bound_params);
|
|
#else
|
|
stmt = (prev_planner_hook) (parse, cursor_opts, bound_params);
|
|
#endif
|
|
else
|
|
/* Call the standard planner */
|
|
#if PG13_GE
|
|
stmt = standard_planner(parse, query_string, cursor_opts, bound_params);
|
|
#else
|
|
stmt = standard_planner(parse, cursor_opts, bound_params);
|
|
#endif
|
|
|
|
if (ts_extension_is_loaded())
|
|
{
|
|
/*
|
|
* Our top-level HypertableInsert plan node that wraps ModifyTable needs
|
|
* to have a final target list that is the same as the ModifyTable plan
|
|
* node, and we only have access to its final target list after
|
|
* set_plan_references() (setrefs.c) has run at the end of
|
|
* standard_planner. Therefore, we fixup the final target list for
|
|
* HypertableInsert here.
|
|
*/
|
|
ts_hypertable_modify_fixup_tlist(stmt->planTree);
|
|
|
|
foreach (lc, stmt->subplans)
|
|
{
|
|
Plan *subplan = (Plan *) lfirst(lc);
|
|
|
|
if (subplan)
|
|
ts_hypertable_modify_fixup_tlist(subplan);
|
|
}
|
|
}
|
|
|
|
if (reset_baserel_info)
|
|
{
|
|
Assert(ts_baserel_info != NULL);
|
|
BaserelInfo_destroy(ts_baserel_info);
|
|
ts_baserel_info = NULL;
|
|
}
|
|
|
|
if (reset_fetcher_type)
|
|
{
|
|
ts_data_node_fetcher_scan_type = AutoFetcherType;
|
|
}
|
|
}
|
|
PG_CATCH();
|
|
{
|
|
if (reset_baserel_info)
|
|
{
|
|
Assert(ts_baserel_info != NULL);
|
|
BaserelInfo_destroy(ts_baserel_info);
|
|
ts_baserel_info = NULL;
|
|
}
|
|
|
|
if (reset_fetcher_type)
|
|
{
|
|
ts_data_node_fetcher_scan_type = AutoFetcherType;
|
|
}
|
|
|
|
/* Pop the cache, but do not release since caches are auto-released on
|
|
* error */
|
|
planner_hcache_pop(false);
|
|
PG_RE_THROW();
|
|
}
|
|
PG_END_TRY();
|
|
|
|
planner_hcache_pop(true);
|
|
|
|
return stmt;
|
|
}
|
|
|
|
static RangeTblEntry *
|
|
get_parent_rte(const PlannerInfo *root, Index rti)
|
|
{
|
|
ListCell *lc;
|
|
|
|
/* Fast path when arrays are setup */
|
|
if (root->append_rel_array != NULL && root->append_rel_array[rti] != NULL)
|
|
{
|
|
AppendRelInfo *appinfo = root->append_rel_array[rti];
|
|
return planner_rt_fetch(appinfo->parent_relid, root);
|
|
}
|
|
|
|
foreach (lc, root->append_rel_list)
|
|
{
|
|
AppendRelInfo *appinfo = lfirst_node(AppendRelInfo, lc);
|
|
|
|
if (appinfo->child_relid == rti)
|
|
return planner_rt_fetch(appinfo->parent_relid, root);
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
* Fetch cached baserel entry. If it does not exists, create an entry for this
|
|
* relid.
|
|
* If this relid corresponds to a chunk, cache additional chunk
|
|
* related metadata: like chunk_status and pointer to hypertable entry.
|
|
* It is okay to cache a pointer to the hypertable, since this cache is
|
|
* confined to the lifetime of the query and not used across queries.
|
|
* If the parent reolid is known, the caller can specify it to avoid the costly
|
|
* lookup. Otherwise pass InvalidOid.
|
|
*/
|
|
static BaserelInfoEntry *
|
|
get_or_add_baserel_from_cache(Oid chunk_reloid, Oid parent_reloid)
|
|
{
|
|
Hypertable *ht = NULL;
|
|
/* First, check if this reloid is in cache. */
|
|
bool found = false;
|
|
BaserelInfoEntry *entry = BaserelInfo_insert(ts_baserel_info, chunk_reloid, &found);
|
|
if (found)
|
|
{
|
|
return entry;
|
|
}
|
|
|
|
if (OidIsValid(parent_reloid))
|
|
{
|
|
ht = ts_planner_get_hypertable(parent_reloid, CACHE_FLAG_CHECK);
|
|
|
|
#ifdef USE_ASSERT_CHECKING
|
|
/* Sanity check on the caller-specified hypertable reloid. */
|
|
int32 parent_hypertable_id = ts_chunk_get_hypertable_id_by_relid(chunk_reloid);
|
|
if (parent_hypertable_id != INVALID_HYPERTABLE_ID)
|
|
{
|
|
Assert(ts_hypertable_id_to_relid(parent_hypertable_id) == parent_reloid);
|
|
|
|
if (ht != NULL)
|
|
{
|
|
Assert(ht->fd.id == parent_hypertable_id);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
/* Hypertable reloid not specified by the caller, look it up by
|
|
* an expensive metadata scan.
|
|
*/
|
|
int32 hypertable_id = ts_chunk_get_hypertable_id_by_relid(chunk_reloid);
|
|
|
|
if (hypertable_id != INVALID_HYPERTABLE_ID)
|
|
{
|
|
/* Hypertable reloid not specified by the caller, look it up. */
|
|
parent_reloid = ts_hypertable_id_to_relid(hypertable_id);
|
|
Ensure(OidIsValid(parent_reloid),
|
|
"unable to get valid parent Oid for hypertable %d",
|
|
hypertable_id);
|
|
|
|
ht = ts_planner_get_hypertable(parent_reloid, CACHE_FLAG_NONE);
|
|
Assert(ht != NULL);
|
|
Assert(ht->fd.id == hypertable_id);
|
|
}
|
|
}
|
|
|
|
/* Cache the result. */
|
|
entry->ht = ht;
|
|
return entry;
|
|
}
|
|
|
|
/*
|
|
* Classify a planned relation.
|
|
*
|
|
* This makes use of cache warming that happened during Query preprocessing in
|
|
* the first planner hook.
|
|
*/
|
|
static TsRelType
|
|
classify_relation(const PlannerInfo *root, const RelOptInfo *rel, Hypertable **ht)
|
|
{
|
|
Assert(ht != NULL);
|
|
*ht = NULL;
|
|
|
|
if (rel->reloptkind != RELOPT_BASEREL && rel->reloptkind != RELOPT_OTHER_MEMBER_REL)
|
|
{
|
|
return TS_REL_OTHER;
|
|
}
|
|
|
|
RangeTblEntry *rte = planner_rt_fetch(rel->relid, root);
|
|
|
|
if (!OidIsValid(rte->relid))
|
|
{
|
|
return TS_REL_OTHER;
|
|
}
|
|
|
|
if (rel->reloptkind == RELOPT_BASEREL)
|
|
{
|
|
/*
|
|
* To correctly classify relations in subqueries we cannot call
|
|
* ts_planner_get_hypertable with CACHE_FLAG_CHECK which includes
|
|
* CACHE_FLAG_NOCREATE flag because the rel might not be in cache yet.
|
|
*/
|
|
*ht = ts_planner_get_hypertable(rte->relid, CACHE_FLAG_MISSING_OK);
|
|
|
|
if (*ht != NULL)
|
|
{
|
|
return TS_REL_HYPERTABLE;
|
|
}
|
|
|
|
/*
|
|
* This is either a chunk seen as a standalone table, or a non-chunk
|
|
* baserel. We need a costly chunk metadata scan to distinguish between
|
|
* them, so we cache the result of this lookup to avoid doing it
|
|
* repeatedly.
|
|
*/
|
|
BaserelInfoEntry *entry = get_or_add_baserel_from_cache(rte->relid, InvalidOid);
|
|
*ht = entry->ht;
|
|
return *ht ? TS_REL_CHUNK_STANDALONE : TS_REL_OTHER;
|
|
}
|
|
|
|
Assert(rel->reloptkind == RELOPT_OTHER_MEMBER_REL);
|
|
|
|
RangeTblEntry *parent_rte = get_parent_rte(root, rel->relid);
|
|
|
|
/*
|
|
* An entry of reloptkind RELOPT_OTHER_MEMBER_REL might still
|
|
* be a hypertable here if it was pulled up from a subquery
|
|
* as happens with UNION ALL for example. So we have to
|
|
* check for that to properly detect that pattern.
|
|
*/
|
|
if (parent_rte->rtekind == RTE_SUBQUERY)
|
|
{
|
|
*ht = ts_planner_get_hypertable(rte->relid,
|
|
rte->inh ? CACHE_FLAG_MISSING_OK : CACHE_FLAG_CHECK);
|
|
|
|
return *ht ? TS_REL_HYPERTABLE : TS_REL_OTHER;
|
|
}
|
|
|
|
if (parent_rte->relid == rte->relid)
|
|
{
|
|
/*
|
|
* A PostgreSQL table expansion peculiarity -- "self child", the root
|
|
* table that is expanded as a child of itself. This happens when our
|
|
* expansion code is turned off.
|
|
*/
|
|
*ht = ts_planner_get_hypertable(rte->relid, CACHE_FLAG_CHECK);
|
|
return *ht != NULL ? TS_REL_HYPERTABLE_CHILD : TS_REL_OTHER;
|
|
}
|
|
|
|
/*
|
|
* Either an other baserel or a chunk seen when expanding the hypertable.
|
|
* Use the baserel cache to determine what it is.
|
|
*/
|
|
BaserelInfoEntry *entry = get_or_add_baserel_from_cache(rte->relid, parent_rte->relid);
|
|
*ht = entry->ht;
|
|
return *ht ? TS_REL_CHUNK_CHILD : TS_REL_OTHER;
|
|
}
|
|
|
|
extern void ts_sort_transform_optimization(PlannerInfo *root, RelOptInfo *rel);
|
|
|
|
static inline bool
|
|
should_chunk_append(Hypertable *ht, PlannerInfo *root, RelOptInfo *rel, Path *path, bool ordered,
|
|
int order_attno)
|
|
{
|
|
if (
|
|
#if PG14_LT
|
|
root->parse->commandType != CMD_SELECT ||
|
|
#else
|
|
/*
|
|
* We only support chunk exclusion on UPDATE/DELETE when no JOIN is involved on PG14+.
|
|
*/
|
|
((root->parse->commandType == CMD_DELETE || root->parse->commandType == CMD_UPDATE) &&
|
|
bms_num_members(root->all_baserels) > 1) ||
|
|
#endif
|
|
!ts_guc_enable_chunk_append || hypertable_is_distributed(ht))
|
|
return false;
|
|
|
|
switch (nodeTag(path))
|
|
{
|
|
case T_AppendPath:
|
|
/*
|
|
* If there are clauses that have mutable functions, or clauses that reference
|
|
* Params this Path might benefit from startup or runtime exclusion
|
|
*/
|
|
{
|
|
AppendPath *append = castNode(AppendPath, path);
|
|
ListCell *lc;
|
|
|
|
/* Don't create ChunkAppend with no children */
|
|
if (list_length(append->subpaths) == 0)
|
|
return false;
|
|
|
|
foreach (lc, rel->baserestrictinfo)
|
|
{
|
|
RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc);
|
|
|
|
if (contain_mutable_functions((Node *) rinfo->clause) ||
|
|
ts_contain_param((Node *) rinfo->clause))
|
|
return true;
|
|
}
|
|
return false;
|
|
break;
|
|
}
|
|
case T_MergeAppendPath:
|
|
/*
|
|
* Can we do ordered append
|
|
*/
|
|
{
|
|
MergeAppendPath *merge = castNode(MergeAppendPath, path);
|
|
PathKey *pk;
|
|
ListCell *lc;
|
|
|
|
if (!ordered || path->pathkeys == NIL || list_length(merge->subpaths) == 0)
|
|
return false;
|
|
|
|
/*
|
|
* Check for partial compressed chunks.
|
|
*
|
|
* When partial compressed chunks are present we can not do 1-level
|
|
* ordered append. We instead need nested Appends to correctly preserve
|
|
* ordering. For now we skip ordered append optimization when we encounter
|
|
* partial chunks.
|
|
*/
|
|
foreach (lc, merge->subpaths)
|
|
{
|
|
Path *child = lfirst(lc);
|
|
RelOptInfo *chunk_rel = child->parent;
|
|
if (chunk_rel->fdw_private)
|
|
{
|
|
TimescaleDBPrivate *private = chunk_rel->fdw_private;
|
|
if (private->chunk && ts_chunk_is_partial(private->chunk))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/* cannot support ordered append with OSM chunks. OSM chunk
|
|
* ranges are not recorded with the catalog
|
|
*/
|
|
if (ht && ts_chunk_get_osm_chunk_id(ht->fd.id) != INVALID_CHUNK_ID)
|
|
return false;
|
|
pk = linitial_node(PathKey, path->pathkeys);
|
|
|
|
/*
|
|
* Check PathKey is compatible with Ordered Append ordering
|
|
* we created when expanding hypertable.
|
|
* Even though ordered is true on the RelOptInfo we have to
|
|
* double check that current Path fulfills requirements for
|
|
* Ordered Append transformation because the RelOptInfo may
|
|
* be used for multiple Pathes.
|
|
*/
|
|
Expr *em_expr = find_em_expr_for_rel(pk->pk_eclass, rel);
|
|
|
|
/*
|
|
* If this is a join the ordering information might not be
|
|
* for the current rel and have no EquivalenceMember.
|
|
*/
|
|
|
|
if (!em_expr)
|
|
return false;
|
|
|
|
if (IsA(em_expr, Var) && castNode(Var, em_expr)->varattno == order_attno)
|
|
return true;
|
|
else if (IsA(em_expr, FuncExpr) && list_length(path->pathkeys) == 1)
|
|
{
|
|
FuncExpr *func = castNode(FuncExpr, em_expr);
|
|
FuncInfo *info = ts_func_cache_get_bucketing_func(func->funcid);
|
|
Expr *transformed;
|
|
|
|
if (info != NULL)
|
|
{
|
|
transformed = info->sort_transform(func);
|
|
if (IsA(transformed, Var) &&
|
|
castNode(Var, transformed)->varattno == order_attno)
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
break;
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static inline bool
|
|
should_constraint_aware_append(PlannerInfo *root, Hypertable *ht, Path *path)
|
|
{
|
|
/* Constraint-aware append currently expects children that scans a real
|
|
* "relation" (e.g., not an "upper" relation). So, we do not run it on a
|
|
* distributed hypertable because the append children are typically
|
|
* per-server relations without a corresponding "real" table in the
|
|
* system. Further, per-server appends shouldn't need runtime pruning in any
|
|
* case. */
|
|
if (root->parse->commandType != CMD_SELECT || hypertable_is_distributed(ht))
|
|
return false;
|
|
|
|
return ts_constraint_aware_append_possible(path);
|
|
}
|
|
|
|
static bool
|
|
rte_should_expand(const RangeTblEntry *rte)
|
|
{
|
|
bool is_hypertable = ts_rte_is_hypertable(rte, NULL);
|
|
|
|
return is_hypertable && !rte->inh && ts_rte_is_marked_for_expansion(rte);
|
|
}
|
|
|
|
static void
|
|
reenable_inheritance(PlannerInfo *root, RelOptInfo *rel, Index rti, RangeTblEntry *rte)
|
|
{
|
|
bool set_pathlist_for_current_rel = false;
|
|
double total_pages;
|
|
bool reenabled_inheritance = false;
|
|
|
|
for (int i = 1; i < root->simple_rel_array_size; i++)
|
|
{
|
|
RangeTblEntry *in_rte = root->simple_rte_array[i];
|
|
|
|
if (rte_should_expand(in_rte))
|
|
{
|
|
RelOptInfo *in_rel = root->simple_rel_array[i];
|
|
Hypertable *ht = ts_planner_get_hypertable(in_rte->relid, CACHE_FLAG_NOCREATE);
|
|
|
|
Assert(ht != NULL && in_rel != NULL);
|
|
ts_plan_expand_hypertable_chunks(ht, root, in_rel);
|
|
|
|
in_rte->inh = true;
|
|
reenabled_inheritance = true;
|
|
/* Redo set_rel_consider_parallel, as results of the call may no longer be valid
|
|
* here (due to adding more tables to the set of tables under consideration here).
|
|
* This is especially true if dealing with foreign data wrappers. */
|
|
|
|
/*
|
|
* An entry of reloptkind RELOPT_OTHER_MEMBER_REL might still
|
|
* be a hypertable here if it was pulled up from a subquery
|
|
* as happens with UNION ALL for example.
|
|
*/
|
|
if (in_rel->reloptkind == RELOPT_BASEREL ||
|
|
in_rel->reloptkind == RELOPT_OTHER_MEMBER_REL)
|
|
{
|
|
Assert(in_rte->relkind == RELKIND_RELATION);
|
|
ts_set_rel_size(root, in_rel, i, in_rte);
|
|
}
|
|
|
|
/* if we're activating inheritance during a hypertable's pathlist
|
|
* creation then we're past the point at which postgres will add
|
|
* paths for the children, and we have to do it ourselves. We delay
|
|
* the actual setting of the pathlists until after this loop,
|
|
* because set_append_rel_pathlist will eventually call this hook again.
|
|
*/
|
|
if (in_rte == rte)
|
|
{
|
|
Assert(rti == (Index) i);
|
|
set_pathlist_for_current_rel = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!reenabled_inheritance)
|
|
return;
|
|
|
|
total_pages = 0;
|
|
for (int i = 1; i < root->simple_rel_array_size; i++)
|
|
{
|
|
RelOptInfo *brel = root->simple_rel_array[i];
|
|
|
|
if (brel == NULL)
|
|
continue;
|
|
|
|
Assert(brel->relid == (Index) i); /* sanity check on array */
|
|
|
|
if (IS_DUMMY_REL(brel))
|
|
continue;
|
|
|
|
if (IS_SIMPLE_REL(brel))
|
|
total_pages += (double) brel->pages;
|
|
}
|
|
root->total_table_pages = total_pages;
|
|
|
|
if (set_pathlist_for_current_rel)
|
|
{
|
|
bool do_distributed;
|
|
|
|
Hypertable *ht = ts_planner_get_hypertable(rte->relid, CACHE_FLAG_NOCREATE);
|
|
Assert(ht != NULL);
|
|
|
|
/* the hypertable will have been planned as if it was a regular table
|
|
* with no data. Since such a plan would be cheaper than any real plan,
|
|
* it would always be used, and we need to remove these plans before
|
|
* adding ours.
|
|
*
|
|
* Also, if it's a distributed hypertable and per data node queries are
|
|
* enabled then we will be throwing this below append path away. So only
|
|
* build it otherwise
|
|
*/
|
|
do_distributed = !IS_DUMMY_REL(rel) && hypertable_is_distributed(ht) &&
|
|
ts_guc_enable_per_data_node_queries;
|
|
|
|
rel->pathlist = NIL;
|
|
rel->partial_pathlist = NIL;
|
|
/* allow a session parameter to override the use of this datanode only path */
|
|
#ifdef TS_DEBUG
|
|
if (do_distributed)
|
|
{
|
|
const char *allow_dn_path =
|
|
GetConfigOption("timescaledb.debug_allow_datanode_only_path", true, false);
|
|
if (allow_dn_path && pg_strcasecmp(allow_dn_path, "on") != 0)
|
|
{
|
|
do_distributed = false;
|
|
elog(DEBUG2, "creating per chunk append paths");
|
|
}
|
|
else
|
|
elog(DEBUG2, "avoiding per chunk append paths");
|
|
}
|
|
#endif
|
|
|
|
if (!do_distributed)
|
|
ts_set_append_rel_pathlist(root, rel, rti, rte);
|
|
}
|
|
}
|
|
|
|
static void
|
|
apply_optimizations(PlannerInfo *root, TsRelType reltype, RelOptInfo *rel, RangeTblEntry *rte,
|
|
Hypertable *ht)
|
|
{
|
|
if (!ts_guc_enable_optimizations)
|
|
return;
|
|
|
|
switch (reltype)
|
|
{
|
|
case TS_REL_HYPERTABLE_CHILD:
|
|
/* empty table so nothing to optimize */
|
|
break;
|
|
case TS_REL_CHUNK_STANDALONE:
|
|
case TS_REL_CHUNK_CHILD:
|
|
ts_sort_transform_optimization(root, rel);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
/*
|
|
* Since the sort optimization adds new paths to the rel it has
|
|
* to happen before any optimizations that replace pathlist.
|
|
*/
|
|
if (ts_cm_functions->set_rel_pathlist_query != NULL)
|
|
ts_cm_functions->set_rel_pathlist_query(root, rel, rel->relid, rte, ht);
|
|
|
|
if (reltype == TS_REL_HYPERTABLE &&
|
|
#if PG14_GE
|
|
(root->parse->commandType == CMD_SELECT || root->parse->commandType == CMD_DELETE ||
|
|
root->parse->commandType == CMD_UPDATE)
|
|
#else
|
|
/*
|
|
* For PG < 14 commandType will be CMD_SELECT even when planning DELETE so we
|
|
* check resultRelation instead.
|
|
*/
|
|
root->parse->resultRelation == 0
|
|
#endif
|
|
)
|
|
{
|
|
TimescaleDBPrivate *private = ts_get_private_reloptinfo(rel);
|
|
bool ordered = private->appends_ordered;
|
|
int order_attno = private->order_attno;
|
|
List *nested_oids = private->nested_oids;
|
|
ListCell *lc;
|
|
|
|
Assert(ht != NULL);
|
|
|
|
foreach (lc, rel->pathlist)
|
|
{
|
|
Path **pathptr = (Path **) &lfirst(lc);
|
|
|
|
switch (nodeTag(*pathptr))
|
|
{
|
|
case T_AppendPath:
|
|
case T_MergeAppendPath:
|
|
if (should_chunk_append(ht, root, rel, *pathptr, ordered, order_attno))
|
|
*pathptr = ts_chunk_append_path_create(root,
|
|
rel,
|
|
ht,
|
|
*pathptr,
|
|
false,
|
|
ordered,
|
|
nested_oids);
|
|
else if (should_constraint_aware_append(root, ht, *pathptr))
|
|
*pathptr = ts_constraint_aware_append_path_create(root, *pathptr);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
foreach (lc, rel->partial_pathlist)
|
|
{
|
|
Path **pathptr = (Path **) &lfirst(lc);
|
|
|
|
switch (nodeTag(*pathptr))
|
|
{
|
|
case T_AppendPath:
|
|
case T_MergeAppendPath:
|
|
if (should_chunk_append(ht, root, rel, *pathptr, false, 0))
|
|
*pathptr =
|
|
ts_chunk_append_path_create(root, rel, ht, *pathptr, true, false, NIL);
|
|
else if (should_constraint_aware_append(root, ht, *pathptr))
|
|
*pathptr = ts_constraint_aware_append_path_create(root, *pathptr);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool
|
|
valid_hook_call(void)
|
|
{
|
|
return ts_extension_is_loaded() && planner_hcache_exists();
|
|
}
|
|
|
|
static bool
|
|
dml_involves_hypertable(PlannerInfo *root, Hypertable *ht, Index rti)
|
|
{
|
|
Index result_rti = root->parse->resultRelation;
|
|
RangeTblEntry *result_rte = planner_rt_fetch(result_rti, root);
|
|
|
|
return result_rti == rti || ht->main_table_relid == result_rte->relid;
|
|
}
|
|
|
|
static void
|
|
timescaledb_set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel, Index rti, RangeTblEntry *rte)
|
|
{
|
|
TsRelType reltype;
|
|
Hypertable *ht;
|
|
|
|
/* Quick exit if this is a relation we're not interested in */
|
|
if (!valid_hook_call() || !OidIsValid(rte->relid) || IS_DUMMY_REL(rel))
|
|
{
|
|
if (prev_set_rel_pathlist_hook != NULL)
|
|
(*prev_set_rel_pathlist_hook)(root, rel, rti, rte);
|
|
return;
|
|
}
|
|
|
|
reltype = classify_relation(root, rel, &ht);
|
|
|
|
/* Check for unexpanded hypertable */
|
|
if (!rte->inh && ts_rte_is_marked_for_expansion(rte))
|
|
reenable_inheritance(root, rel, rti, rte);
|
|
|
|
if (ts_guc_enable_optimizations)
|
|
ts_planner_constraint_cleanup(root, rel);
|
|
|
|
/* Call other extensions. Do it after table expansion. */
|
|
if (prev_set_rel_pathlist_hook != NULL)
|
|
(*prev_set_rel_pathlist_hook)(root, rel, rti, rte);
|
|
|
|
if (ts_cm_functions->set_rel_pathlist != NULL)
|
|
ts_cm_functions->set_rel_pathlist(root, rel, rti, rte);
|
|
|
|
switch (reltype)
|
|
{
|
|
case TS_REL_HYPERTABLE_CHILD:
|
|
if (ts_guc_enable_optimizations && IS_UPDL_CMD(root->parse))
|
|
ts_planner_constraint_cleanup(root, rel);
|
|
|
|
break;
|
|
case TS_REL_CHUNK_STANDALONE:
|
|
case TS_REL_CHUNK_CHILD:
|
|
/* Check for UPDATE/DELETE (DML) on compressed chunks */
|
|
if (IS_UPDL_CMD(root->parse) && dml_involves_hypertable(root, ht, rti))
|
|
{
|
|
if (ts_cm_functions->set_rel_pathlist_dml != NULL)
|
|
ts_cm_functions->set_rel_pathlist_dml(root, rel, rti, rte, ht);
|
|
break;
|
|
}
|
|
TS_FALLTHROUGH;
|
|
default:
|
|
apply_optimizations(root, reltype, rel, rte, ht);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* This hook is meant to editorialize about the information the planner gets
|
|
* about a relation. We use it to attach our own metadata to hypertable and
|
|
* chunk relations that we need during planning. We also expand hypertables
|
|
* here. */
|
|
static void
|
|
timescaledb_get_relation_info_hook(PlannerInfo *root, Oid relation_objectid, bool inhparent,
|
|
RelOptInfo *rel)
|
|
{
|
|
Hypertable *ht;
|
|
|
|
if (prev_get_relation_info_hook != NULL)
|
|
prev_get_relation_info_hook(root, relation_objectid, inhparent, rel);
|
|
|
|
if (!valid_hook_call())
|
|
return;
|
|
|
|
switch (classify_relation(root, rel, &ht))
|
|
{
|
|
case TS_REL_HYPERTABLE:
|
|
{
|
|
/* This only works for PG12 because for earlier versions the inheritance
|
|
* expansion happens too early during the planning phase
|
|
*/
|
|
RangeTblEntry *rte = planner_rt_fetch(rel->relid, root);
|
|
Query *query = root->parse;
|
|
/* Mark hypertable RTEs we'd like to expand ourselves.
|
|
* Hypertables inside inlineable functions don't get marked during the query
|
|
* preprocessing step. Therefore we do an extra try here. However, we need to
|
|
* be careful for UPDATE/DELETE as Postgres (in at least version 12) plans them
|
|
* in a complicated way (see planner.c:inheritance_planner). First, it runs the
|
|
* UPDATE/DELETE through the planner as a simulated SELECT. It uses the results
|
|
* of this fake planning to adapt its own UPDATE/DELETE plan. Then it's planned
|
|
* a second time as a real UPDATE/DELETE, but with requiredPerms set to 0, as it
|
|
* assumes permission checking has been done already during the first planner call.
|
|
* We don't want to touch the UPDATE/DELETEs, so we need to check all the regular
|
|
* conditions here that are checked during preprocess_query, as well as the
|
|
* condition that rte->requiredPerms is not requiring UPDATE/DELETE on this rel.
|
|
*/
|
|
if (ts_guc_enable_optimizations && ts_guc_enable_constraint_exclusion && inhparent &&
|
|
rte->ctename == NULL && !IS_UPDL_CMD(query) && query->resultRelation == 0 &&
|
|
query->rowMarks == NIL && (rte->requiredPerms & (ACL_UPDATE | ACL_DELETE)) == 0)
|
|
{
|
|
rte_mark_for_expansion(rte);
|
|
}
|
|
ts_create_private_reloptinfo(rel);
|
|
ts_plan_expand_timebucket_annotate(root, rel);
|
|
break;
|
|
}
|
|
case TS_REL_CHUNK_STANDALONE:
|
|
case TS_REL_CHUNK_CHILD:
|
|
{
|
|
ts_create_private_reloptinfo(rel);
|
|
|
|
if (ts_guc_enable_transparent_decompression && TS_HYPERTABLE_HAS_COMPRESSION_TABLE(ht))
|
|
{
|
|
RangeTblEntry *chunk_rte = planner_rt_fetch(rel->relid, root);
|
|
Chunk *chunk = ts_chunk_get_by_relid(chunk_rte->relid, true);
|
|
|
|
if (chunk->fd.compressed_chunk_id > 0)
|
|
{
|
|
Relation uncompressed_chunk = table_open(relation_objectid, NoLock);
|
|
|
|
ts_get_private_reloptinfo(rel)->compressed = true;
|
|
|
|
/* Planning indexes are expensive, and if this is a compressed chunk, we
|
|
* know we'll never need to use indexes on the uncompressed version, since
|
|
* all the data is in the compressed chunk anyway. Therefore, it is much
|
|
* faster if we simply trash the indexlist here and never plan any useless
|
|
* IndexPaths at all
|
|
*/
|
|
rel->indexlist = NIL;
|
|
|
|
/* Relation size estimates are messed up on compressed chunks due to there
|
|
* being no actual pages for the table in the storage manager.
|
|
*/
|
|
rel->pages = (BlockNumber) uncompressed_chunk->rd_rel->relpages;
|
|
rel->tuples = (double) uncompressed_chunk->rd_rel->reltuples;
|
|
if (rel->pages == 0)
|
|
rel->allvisfrac = 0.0;
|
|
else if (uncompressed_chunk->rd_rel->relallvisible >= (int32) rel->pages)
|
|
rel->allvisfrac = 1.0;
|
|
else
|
|
rel->allvisfrac =
|
|
(double) uncompressed_chunk->rd_rel->relallvisible / rel->pages;
|
|
|
|
table_close(uncompressed_chunk, NoLock);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case TS_REL_HYPERTABLE_CHILD:
|
|
/* When postgres expands an inheritance tree it also adds the
|
|
* parent hypertable as child relation. Since for a hypertable the
|
|
* parent will never have any data we can mark this relation as
|
|
* dummy relation so it gets ignored in later steps. This is only
|
|
* relevant for code paths that use the postgres inheritance code
|
|
* as we don't include the hypertable as child when expanding the
|
|
* hypertable ourself.
|
|
* We do exclude distributed hypertables for now to not alter
|
|
* the trigger behaviour on access nodes, which would otherwise
|
|
* no longer fire.
|
|
*/
|
|
if (IS_UPDL_CMD(root->parse) && !hypertable_is_distributed(ht))
|
|
mark_dummy_rel(rel);
|
|
break;
|
|
case TS_REL_OTHER:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static bool
|
|
join_involves_hypertable(const PlannerInfo *root, const RelOptInfo *rel)
|
|
{
|
|
int relid = -1;
|
|
|
|
while ((relid = bms_next_member(rel->relids, relid)) >= 0)
|
|
{
|
|
const RangeTblEntry *rte = planner_rt_fetch(relid, root);
|
|
|
|
if (rte != NULL)
|
|
/* This might give a false positive for chunks in case of PostgreSQL
|
|
* expansion since the ctename is copied from the parent hypertable
|
|
* to the chunk */
|
|
return ts_rte_is_marked_for_expansion(rte);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool
|
|
involves_hypertable(PlannerInfo *root, RelOptInfo *rel)
|
|
{
|
|
if (rel->reloptkind == RELOPT_JOINREL)
|
|
return join_involves_hypertable(root, rel);
|
|
|
|
Hypertable *ht;
|
|
return classify_relation(root, rel, &ht) == TS_REL_HYPERTABLE;
|
|
}
|
|
|
|
/*
|
|
* Replace INSERT (ModifyTablePath) paths on hypertables.
|
|
*
|
|
* From the ModifyTable description: "Each ModifyTable node contains
|
|
* a list of one or more subplans, much like an Append node. There
|
|
* is one subplan per result relation."
|
|
*
|
|
* The subplans produce the tuples for INSERT, while the result relation is the
|
|
* table we'd like to insert into.
|
|
*
|
|
* The way we redirect tuples to chunks is to insert an intermediate "chunk
|
|
* dispatch" plan node, between the ModifyTable and its subplan that produces
|
|
* the tuples. When the ModifyTable plan is executed, it tries to read a tuple
|
|
* from the intermediate chunk dispatch plan instead of the original
|
|
* subplan. The chunk plan reads the tuple from the original subplan, looks up
|
|
* the chunk, sets the executor's resultRelation to the chunk table and finally
|
|
* returns the tuple to the ModifyTable node.
|
|
*
|
|
* We also need to wrap the ModifyTable plan node with a HypertableInsert node
|
|
* to give the ChunkDispatchState node access to the ModifyTableState node in
|
|
* the execution phase.
|
|
*
|
|
* Conceptually, the plan modification looks like this:
|
|
*
|
|
* Original plan:
|
|
*
|
|
* ^
|
|
* |
|
|
* [ ModifyTable ] -> resultRelation
|
|
* ^
|
|
* | Tuple
|
|
* |
|
|
* [ subplan ]
|
|
*
|
|
*
|
|
* Modified plan:
|
|
*
|
|
* [ HypertableModify ]
|
|
* ^
|
|
* |
|
|
* [ ModifyTable ] -> resultRelation
|
|
* ^ ^
|
|
* | Tuple / <Set resultRelation to the matching chunk table>
|
|
* | /
|
|
* [ ChunkDispatch ]
|
|
* ^
|
|
* | Tuple
|
|
* |
|
|
* [ subplan ]
|
|
*
|
|
* For PG < 14, the modifytable plan is modified for INSERTs only.
|
|
* For PG14+, we modify the plan for DELETEs as well.
|
|
*
|
|
*/
|
|
static List *
|
|
replace_hypertable_modify_paths(PlannerInfo *root, List *pathlist, RelOptInfo *input_rel)
|
|
{
|
|
List *new_pathlist = NIL;
|
|
ListCell *lc;
|
|
|
|
foreach (lc, pathlist)
|
|
{
|
|
Path *path = lfirst(lc);
|
|
|
|
if (IsA(path, ModifyTablePath))
|
|
{
|
|
ModifyTablePath *mt = castNode(ModifyTablePath, path);
|
|
|
|
if (
|
|
#if PG14_GE
|
|
/* We only route UPDATE/DELETE through our CustomNode for PG 14+ because
|
|
* the codepath for earlier versions is different. */
|
|
mt->operation == CMD_UPDATE || mt->operation == CMD_DELETE ||
|
|
#endif
|
|
#if PG15_GE
|
|
mt->operation == CMD_MERGE ||
|
|
#endif
|
|
mt->operation == CMD_INSERT)
|
|
{
|
|
RangeTblEntry *rte = planner_rt_fetch(mt->nominalRelation, root);
|
|
Hypertable *ht = ts_planner_get_hypertable(rte->relid, CACHE_FLAG_CHECK);
|
|
|
|
#if PG15_GE
|
|
if (ht && mt->operation == CMD_MERGE)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("The MERGE command does not support hypertables in this "
|
|
"version"),
|
|
errhint("Check https://github.com/timescale/timescaledb/issues/4929 "
|
|
"for more information and current status")));
|
|
#endif
|
|
if (ht && (mt->operation == CMD_INSERT || !hypertable_is_distributed(ht)))
|
|
{
|
|
path = ts_hypertable_modify_path_create(root, mt, ht, input_rel);
|
|
}
|
|
}
|
|
}
|
|
|
|
new_pathlist = lappend(new_pathlist, path);
|
|
}
|
|
|
|
return new_pathlist;
|
|
}
|
|
|
|
static void
|
|
timescaledb_create_upper_paths_hook(PlannerInfo *root, UpperRelationKind stage,
|
|
RelOptInfo *input_rel, RelOptInfo *output_rel, void *extra)
|
|
{
|
|
Query *parse = root->parse;
|
|
bool partials_found = false;
|
|
TsRelType reltype = TS_REL_OTHER;
|
|
Hypertable *ht = NULL;
|
|
|
|
if (prev_create_upper_paths_hook != NULL)
|
|
prev_create_upper_paths_hook(root, stage, input_rel, output_rel, extra);
|
|
|
|
if (!ts_extension_is_loaded())
|
|
return;
|
|
|
|
if (input_rel != NULL)
|
|
reltype = classify_relation(root, input_rel, &ht);
|
|
|
|
if (ts_cm_functions->create_upper_paths_hook != NULL)
|
|
ts_cm_functions
|
|
->create_upper_paths_hook(root, stage, input_rel, output_rel, reltype, ht, extra);
|
|
|
|
if (output_rel != NULL)
|
|
{
|
|
/* Modify for INSERTs on a hypertable */
|
|
if (output_rel->pathlist != NIL)
|
|
output_rel->pathlist =
|
|
replace_hypertable_modify_paths(root, output_rel->pathlist, input_rel);
|
|
|
|
if (parse->hasAggs && stage == UPPERREL_GROUP_AGG)
|
|
{
|
|
/* Existing AggPaths are modified here.
|
|
* No new AggPaths should be added after this if there
|
|
* are partials. */
|
|
partials_found = ts_plan_process_partialize_agg(root, output_rel);
|
|
}
|
|
}
|
|
|
|
if (!ts_guc_enable_optimizations || input_rel == NULL || IS_DUMMY_REL(input_rel))
|
|
return;
|
|
|
|
if (!involves_hypertable(root, input_rel))
|
|
return;
|
|
|
|
if (stage == UPPERREL_GROUP_AGG && output_rel != NULL)
|
|
{
|
|
if (!partials_found)
|
|
ts_plan_add_hashagg(root, input_rel, output_rel);
|
|
|
|
if (parse->hasAggs)
|
|
ts_preprocess_first_last_aggregates(root, root->processed_tlist);
|
|
}
|
|
}
|
|
|
|
static bool
|
|
contain_param_exec_walker(Node *node, void *context)
|
|
{
|
|
if (node == NULL)
|
|
return false;
|
|
|
|
if (IsA(node, Param))
|
|
return true;
|
|
|
|
return expression_tree_walker(node, contain_param_exec_walker, context);
|
|
}
|
|
|
|
bool
|
|
ts_contain_param(Node *node)
|
|
{
|
|
return contain_param_exec_walker(node, NULL);
|
|
}
|
|
|
|
static List *
|
|
fill_missing_groupclause(List *new_groupclause, List *orig_groupclause)
|
|
{
|
|
if (new_groupclause != NIL)
|
|
{
|
|
ListCell *gl;
|
|
foreach (gl, orig_groupclause)
|
|
{
|
|
SortGroupClause *gc = lfirst_node(SortGroupClause, gl);
|
|
|
|
if (list_member_ptr(new_groupclause, gc))
|
|
continue; /* already in list */
|
|
new_groupclause = lappend(new_groupclause, gc);
|
|
}
|
|
}
|
|
return new_groupclause;
|
|
}
|
|
|
|
static bool
|
|
check_cagg_view_rte(RangeTblEntry *rte)
|
|
{
|
|
ContinuousAgg *cagg = NULL;
|
|
ListCell *rtlc;
|
|
bool found = false;
|
|
Query *viewq = rte->subquery;
|
|
Assert(rte->rtekind == RTE_SUBQUERY);
|
|
|
|
if (list_length(viewq->rtable) != 3) /* a view has 3 entries */
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/* should cache this information for cont. aggregates */
|
|
foreach (rtlc, viewq->rtable)
|
|
{
|
|
RangeTblEntry *rte = lfirst_node(RangeTblEntry, rtlc);
|
|
|
|
if (!OidIsValid(rte->relid))
|
|
break;
|
|
|
|
if ((cagg = ts_continuous_agg_find_by_relid(rte->relid)) != NULL)
|
|
found = true;
|
|
}
|
|
return found;
|
|
}
|
|
|
|
/* Note that it modifies the passed in Query
|
|
* select * from (select a, b, max(c), min(d) from ...
|
|
group by a, b)
|
|
order by b;
|
|
* is transformed as
|
|
* SELECT * from (select a, b, max(c), min(d) from ..
|
|
* group by B desc, A <------ note the change in order here
|
|
* )
|
|
* order by b desc;
|
|
* we transform only if order by is a subset of group-by
|
|
* transformation is applicable only to continuous aggregates
|
|
* Parameters:
|
|
* subq_rte - rte for subquery (inner query that will be modified)
|
|
* outer_sortcl -- outer query's sort clause
|
|
* outer_tlist - outer query's target list
|
|
*/
|
|
static void
|
|
cagg_reorder_groupby_clause(RangeTblEntry *subq_rte, Index rtno, List *outer_sortcl,
|
|
List *outer_tlist)
|
|
{
|
|
bool not_found = true;
|
|
Query *subq;
|
|
ListCell *lc;
|
|
Assert(subq_rte->rtekind == RTE_SUBQUERY);
|
|
subq = subq_rte->subquery;
|
|
if (outer_sortcl && subq->groupClause && subq->sortClause == NIL &&
|
|
check_cagg_view_rte(subq_rte))
|
|
{
|
|
List *new_groupclause = NIL;
|
|
/* we are going to modify this. so make a copy and use it
|
|
if we replace */
|
|
List *subq_groupclause_copy = copyObject(subq->groupClause);
|
|
foreach (lc, outer_sortcl)
|
|
{
|
|
SortGroupClause *outer_sc = (SortGroupClause *) lfirst(lc);
|
|
TargetEntry *outer_tle = get_sortgroupclause_tle(outer_sc, outer_tlist);
|
|
not_found = true;
|
|
if (IsA(outer_tle->expr, Var) && ((Index) ((Var *) outer_tle->expr)->varno == rtno))
|
|
{
|
|
int outer_attno = ((Var *) outer_tle->expr)->varattno;
|
|
TargetEntry *subq_tle = list_nth(subq->targetList, outer_attno - 1);
|
|
if (subq_tle->ressortgroupref > 0)
|
|
{
|
|
/* get group clause corresponding to this */
|
|
SortGroupClause *subq_gclause =
|
|
get_sortgroupref_clause(subq_tle->ressortgroupref, subq_groupclause_copy);
|
|
subq_gclause->sortop = outer_sc->sortop;
|
|
subq_gclause->nulls_first = outer_sc->nulls_first;
|
|
Assert(subq_gclause->eqop == outer_sc->eqop);
|
|
new_groupclause = lappend(new_groupclause, subq_gclause);
|
|
not_found = false;
|
|
}
|
|
}
|
|
if (not_found)
|
|
break;
|
|
}
|
|
/* all order by found in group by clause */
|
|
if (new_groupclause != NIL && not_found == false)
|
|
{
|
|
/* use new groupby clause for this subquery/view */
|
|
subq->groupClause = fill_missing_groupclause(new_groupclause, subq_groupclause_copy);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
_planner_init(void)
|
|
{
|
|
prev_planner_hook = planner_hook;
|
|
planner_hook = timescaledb_planner;
|
|
prev_set_rel_pathlist_hook = set_rel_pathlist_hook;
|
|
set_rel_pathlist_hook = timescaledb_set_rel_pathlist;
|
|
|
|
prev_get_relation_info_hook = get_relation_info_hook;
|
|
get_relation_info_hook = timescaledb_get_relation_info_hook;
|
|
|
|
prev_create_upper_paths_hook = create_upper_paths_hook;
|
|
create_upper_paths_hook = timescaledb_create_upper_paths_hook;
|
|
}
|
|
|
|
void
|
|
_planner_fini(void)
|
|
{
|
|
planner_hook = prev_planner_hook;
|
|
set_rel_pathlist_hook = prev_set_rel_pathlist_hook;
|
|
get_relation_info_hook = prev_get_relation_info_hook;
|
|
create_upper_paths_hook = prev_create_upper_paths_hook;
|
|
}
|