-
Notifications
You must be signed in to change notification settings - Fork 882
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimize planning times when hypertables have many chunks #502
Conversation
4c12952
to
931d7ca
Compare
This replaces #471. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quick review, will let other take deep dive
src/chunk.c
Outdated
chunk_scan_ctx_init(&ctx, hs, NULL); | ||
|
||
/* Abort the scan when the chunk is found */ | ||
ctx.early_abort = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment & code don't match, at least I don't think. It appears that you are not ending the scan when the chunk is found.
src/plan_expand_hypertable.c
Outdated
find_children_oids(HypertableRestrictInfo *hri, Hypertable *ht, LOCKMODE lockmode) | ||
{ | ||
/* | ||
* optimization: using the HRI only makes sense if we ar not using all the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: ar -> are
src/plan_expand_hypertable.c
Outdated
bool inhparent, | ||
RelOptInfo *rel) | ||
{ | ||
RangeTblEntry *rte = rt_fetch(rel->relid, root->parse->rtable); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this indentation seems wrong
Index rti = rel->relid; | ||
List *appinfos = NIL; | ||
HypertableRestrictInfo *hri; | ||
PlanRowMark *oldrc; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable (according to GCC on Ubuntu)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually nevermind, i see it getting used so I'm not sure why it complains.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess because it's getting used in Assert and nowhere else it gets stripped out in Release builds. May need to use it in a no-op to get rid of the warning.
Please fix this ASAP, because I have approx 50 chunks, and queries are very slow. What could be estimated fix date? |
Hi @goodkiller thanks for your interest in this PR. We're in the process of reviewing this new functionality and we aim to get it in for the next release. You mentioned ~50 chunks for your setup, which doesn't seem like a lot, actually. Wondering if you are experiencing some other issue? |
Hi @erimatnor |
931d7ca
to
2787ecd
Compare
@erimatnor @cevian Benchmark numbers look good to me. Big improvement for a dataset with 4000+ (600ms -> 36ms) chunks and a more modest improvement for one with only about 6 chunks (6.6ms -> 5.9-6ms). So even if it doesn't do a ton for the low end, it's not hurting performance and is a big boon for the many chunks case. Pending the fixes I suggested, it has my approval. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, I think the optimization is good. A bunch of nits and suggestions though.
src/plan_expand_hypertable.c
Outdated
*/ | ||
hri = hypertable_restrict_info_create(rel, ht); | ||
hypertable_restrict_info_add(hri, root, restrictinfo); | ||
inhOIDs = find_children_oids(hri, ht, lockmode); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
inhOIDs
-> inh_oids
src/plan_expand_hypertable.c
Outdated
|
||
foreach(l, inhOIDs) | ||
{ | ||
Oid childOID = lfirst_oid(l); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
childOID
-> child_oid
src/plan_expand_hypertable.c
Outdated
Oid childOID = lfirst_oid(l); | ||
Relation newrelation; | ||
RangeTblEntry *childrte; | ||
Index childRTindex; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
childRTIndex
-> child_rtindex
src/plan_expand_hypertable.c
Outdated
{ | ||
RangeTblEntry *rte = rt_fetch(rel->relid, root->parse->rtable); | ||
List *inhOIDs; | ||
Oid parentOID = relationObjectId; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parentOID
-> parent_oid
src/plan_expand_hypertable.c
Outdated
RelOptInfo *rel) | ||
{ | ||
RangeTblEntry *rte = rt_fetch(rel->relid, root->parse->rtable); | ||
List *inhOIDs; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
inhOIDs
-> inh_oids
src/plan_expand_hypertable.c
Outdated
lockmode)); | ||
return result; | ||
} | ||
else |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unnecessary else clause.
I'd do like suggested above.
src/hypertable_restrict_info.c
Outdated
{ | ||
DimensionRestrictInfo *dri = dimension_restrict_info_create(&ht->space->dimensions[i]); | ||
|
||
res->diminson_restriction[AttrNumberGetAttrOffset(ht->space->dimensions[i].column_attno)] = dri; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ohh, I see you are indexing by column_attno
instead of dimension ID, so the array can be sparse, hence pointer array. Is this ideal/necessary? Imagine a table with 100+ columns (which we've seen) where time is last. That would create a really sparse array.
Is it necessary to optimize getting the restriction from the array by attno? Without this, fetching would only be O(n)
with the number of dimensions, but could be optimized with hash table or tree if an issue (which it really wouldn't be unless really large number of dimensions)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this is the most efficient representation of this structure because it is most often accessed by attribute number. There is a max number of attributes in PostgreSQL (1500 or something) and each column only takes the size of a pointer so I don't believe the size here is really an issue. I'd rather make the access as efficient as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed the names to be more clear.
src/planner.c
Outdated
foreach(lc, query->rtable) | ||
{ | ||
RangeTblEntry *rte = lfirst(lc); | ||
Hypertable *ht = hypertable_cache_get_entry(hc, rte->relid); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this guaranteed to be non-NULL? Maybe add an Assert()
to make this clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No it can be NULL. plan_expand_hypertable_valid_hypertable
handles the NULL case.
src/planner.c
Outdated
@@ -323,18 +404,53 @@ timescaledb_set_rel_pathlist(PlannerInfo *root, | |||
cache_release(hcache); | |||
} | |||
|
|||
static void | |||
timescaledb_get_relation_info_hook(PlannerInfo *root, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the reasoning between expanding the append relation in this hook? Not saying it is wrong, but it seems non-obvious. At least there should be a comment explaining this, and what this hook function does in general (i.e., it expands the hypertable).
src/plan_expand_hypertable.h
Outdated
* | ||
* Slow planning time were previously seen because `expand_inherited_tables` expands all chunks of | ||
* a hypertable, without regard to constraints present in the query. Then, `get_relation_info` is | ||
* the called on all chunks before constraint exclusion. Getting the statistics an many chunks ends |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
then called...
717fa69
to
856b2e5
Compare
@RobAtticus @erimatnor Fixed all your comments (unless I replied directly to the msg) |
7f00c74
to
f0e23c2
Compare
src/chunk.c
Outdated
|
||
chunk_scan_ctx_foreach_chunk(ctx, chunk_is_complete, 1); | ||
|
||
return (ctx->data == NIL ? NULL : linitial(ctx->data)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make this function simply a wrapper around ...get_chunk_list
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
src/chunk.c
Outdated
} | ||
|
||
/* Get a list of chunks that each have N matching dimension constraints */ | ||
chunk_list = chunk_scan_ctx_get_chunk_list(&ctx); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't you just iterate the chunk scan context here with your own per-chunk handler instead of first creating a list? Seems you are adding new functionality when the equivalent functionality already exists, iterating information twice and doing unnecessary allocations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
return true; | ||
} | ||
else if (other->fd.range_start > coord && | ||
other->fd.range_start < to_cut->fd.range_end) | ||
{ | ||
/* Cut "after" the coordinate */ | ||
to_cut->fd.range_end = other->fd.range_start; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a new pgindent
thing or why this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes this seems a pgindent thing
} | ||
} | ||
|
||
bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
src/hypertable_restrict_info.c
Outdated
{ | ||
DimensionRestrictInfo *dri = dimension_restrict_info_create(&ht->space->dimensions[i]); | ||
|
||
res->dimension_restriction[AttrNumberGetAttrOffset(ht->space->dimensions[i].column_attno)] = dri; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still not sure about this sparse array. I think the most common case by far is 1 or 2 dimensions, so lookup by iterating the dimensions shouldn't be much worse than array indexing, at least not in any way that matters. I think it is a lot more common to have many columns, potentially partitioning on a high attribute number, than having lots of dimensions. If this proves a problem in the future, we can optimize with a hashtable or similar.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I agree a list would probably not be /bad/ I think the sparse array is more efficient because of O(1). Since we may have many clauses, I'm not sure why we wouldn't use this. The memory usage is limited as I mentioned before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's really only a benefit of O(1) lookups when you have big data sets and not with one or two elements, which is the common case here. I mean, honestly, most of the time your are creating a sparse array with one single element! (Or, am I missing something?). This seems like over-engineering of an otherwise very simple thing. I wouldn't push back if you had a strong argument here, like showing an important efficiency improvement (e.g., significantly faster planning times). But I think, when in doubt, we should go for simplicity and maintainability of the code with the option of optimizing in the future.
Since this seems like a "won't fix", I guess you strongly believe this is an important efficiency/speed optimization, to the extent that it is worth pushing it through. Thus I won't block the PR on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed - made it into a non-sparse array
src/hypertable_restrict_info.c
Outdated
dimension_restrict_info_closed_slices(DimensionRestrictInfoClosed *dri) | ||
{ | ||
if (dri->strategy == BTEqualStrategyNumber) | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unnecessary braces
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
src/plan_expand_hypertable.c
Outdated
|
||
/* Since baserestrictinfo is not yet set by the planner, we have to derive | ||
* it ourselves. It's safe for us to miss some restrict info clauses (this | ||
* will just results in more chunks being included) so this does not need |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
results -> result
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
src/plan_expand_hypertable.c
Outdated
List *result; | ||
|
||
/* | ||
* optimization: using the HRI only makes sense if we are not using all |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ambiguous comment: Is this optimization done now (doesn't look like it), or is it suggested?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
src/plan_expand_hypertable.c
Outdated
Oid parent_oid = relation_objectid; | ||
ListCell *l; | ||
Relation oldrelation = heap_open(parent_oid, NoLock); | ||
LOCKMODE lockmode = AccessShareLock; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this need to be a variable? I don't see it set anywhere else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
src/plan_expand_hypertable.c
Outdated
{ | ||
RangeTblEntry *rte = rt_fetch(rel->relid, root->parse->rtable); | ||
List *inh_oids; | ||
Oid parent_oid = relation_objectid; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this extra variable? Don't see it set anywhere. Is it a name clarity issue? Then why not just use the name for the function parameter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
f0e23c2
to
67b28dd
Compare
67b28dd
to
7c611d0
Compare
@erimatnor ready for another review |
Build is broken @cevian |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only a few remaining things.
src/hypertable_restrict_info.c
Outdated
{ | ||
DimensionRestrictInfo *dri = dimension_restrict_info_create(&ht->space->dimensions[i]); | ||
|
||
res->dimension_restriction[AttrNumberGetAttrOffset(ht->space->dimensions[i].column_attno)] = dri; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's really only a benefit of O(1) lookups when you have big data sets and not with one or two elements, which is the common case here. I mean, honestly, most of the time your are creating a sparse array with one single element! (Or, am I missing something?). This seems like over-engineering of an otherwise very simple thing. I wouldn't push back if you had a strong argument here, like showing an important efficiency improvement (e.g., significantly faster planning times). But I think, when in doubt, we should go for simplicity and maintainability of the code with the option of optimizing in the future.
Since this seems like a "won't fix", I guess you strongly believe this is an important efficiency/speed optimization, to the extent that it is worth pushing it through. Thus I won't block the PR on this.
src/plan_expand_hypertable.c
Outdated
Assert(rti != parse->resultRelation); | ||
oldrc = get_plan_rowmark(root->rowMarks, rti); | ||
if (oldrc && RowMarkRequiresRowShareLock(oldrc->markType)) | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would skip braces here. Also, non-conforming error message
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
src/chunk.c
Outdated
|
||
chunk_scan_ctx_destroy(&ctx); | ||
|
||
foreach(lc, oid_list) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not also do this work (locking) in append_chunk_oid
(which is what I meant in previous comment)? You are still iterating twice here and then I presume once more when creating the appendInfos. That's at least three iterations of the same data. Ideally, you'd do all work in one iteration. Any reason not to?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
6e67bd1
to
b014c87
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some nits.
src/chunk.c
Outdated
append_chunk_oid(ChunkScanCtx *scanctx, Chunk *chunk) | ||
{ | ||
if (chunk_is_complete(scanctx, chunk)) | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit of a style choice, and not a big issue for a small function, but I tend to favor early exits, in this case:
if (!chunk_is_complete(scanctx, chunk))
return false;
This makes code easier to read because you have less indentation and nesting and do not need go to the end of the function to know if the "negative" case means exit or executing some other code.
src/hypertable_restrict_info.c
Outdated
} | ||
|
||
static DimensionRestrictInfo * | ||
hypertable_restrict_info_get(HypertableRestrictInfo *hri, int attno) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the attno
parameter be of type AttrNumber
?
This planner optimization reduces planning times when a hypertable has many chunks. It does this by expanding hypertable chunks manually, eliding the `expand_inherited_tables` logic used by PG. Slow planning time were previously seen because `expand_inherited_tables` expands all chunks of a hypertable, without regard to constraints present in the query. Then, `get_relation_info` is the called on all chunks before constraint exclusion. Getting the statistics an many chunks ends up being expensive because RelationGetNumberOfBlocks has to open the file for each relation. This gets even worse under high concurrency. This logic solves this by expanding only the chunks needed to fulfil the query instead of all chunks. In effect, it moves chunk exclusion up in the planning process. But, we actually don't use constraint exclusion here, but rather a variant of range exclusion implemented by HypertableRestrictInfo.
We hit a bug in 9.6.5 fixed in 9.6.6 by commit 77cd0dc. Also changed extension is transitioning check to not palloc anything. This is more efficient and probably has slightly less side-effects on bugs like this.
b014c87
to
72fdda2
Compare
This planner optimization reduces planning times when a hypertable has many chunks.
It does this by expanding hypertable chunks manually, eliding the
expand_inherited_tables
logic used by PG.
Slow planning time were previously seen because
expand_inherited_tables
expands all chunks ofa hypertable, without regard to constraints present in the query. Then,
get_relation_info
isthe called on all chunks before constraint exclusion. Getting the statistics an many chunks ends
up being expensive because RelationGetNumberOfBlocks has to open the file for each relation.
This gets even worse under high concurrency.
This logic solves this by expanding only the chunks needed to fulfil the query instead of all chunks.
In effect, it moves chunk exclusion up in the planning process. But, we actually don't use constraint
exclusion here, but rather a variant of range exclusion implemented
by HypertableRestrictInfo.