Skip to content

Add DeferredChunkScan custom scan - #10292

Open
svenklemm wants to merge 10 commits into
mainfrom
sven/hypertablescan
Open

Add DeferredChunkScan custom scan#10292
svenklemm wants to merge 10 commits into
mainfrom
sven/hypertablescan

Conversation

@svenklemm

@svenklemm svenklemm commented Jul 20, 2026

Copy link
Copy Markdown
Member

Add a new custom scan node for LIMIT queries that leaves the
hypertable unexpanded during initial planning and defers chunk
enumeration and scanning to execution time.
This gives huge speedup to LIMIT queries on hypertables with many chunks.

Warm planning time (ms), median of 15, ORDER BY ts LIMIT 1
┌─────────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬────────┐
│         config          │   1   │  10   │  50   │  100  │ 1,000 │ 5,000 │ 10,000 │
├─────────────────────────┼───────┼───────┼───────┼───────┼───────┼───────┼────────┤
│ uncompr, expanded       │ 0.010 │ 0.037 │ 0.173 │ 0.372 │  4.39 │  35.9 │   84.3 │
├─────────────────────────┼───────┼───────┼───────┼───────┼───────┼───────┼────────┤
│ uncompr, HypertableScan │ 0.003 │ 0.003 │ 0.003 │ 0.002 │ 0.002 │ 0.002 │  0.002 │
├─────────────────────────┼───────┼───────┼───────┼───────┼───────┼───────┼────────┤
│ compr, expanded         │ 0.019 │ 0.136 │ 0.636 │ 1.361 │  27.1 │ 169.9 │  363.1 │
├─────────────────────────┼───────┼───────┼───────┼───────┼───────┼───────┼────────┤
│ compr, HypertableScan   │ 0.003 │ 0.003 │ 0.003 │ 0.002 │ 0.002 │ 0.003 │  0.003 │
└─────────────────────────┴───────┴───────┴───────┴───────┴───────┴───────┴────────┘

Warm execution time (ms), median of 15, ORDER BY ts LIMIT 1

┌─────────────────────────┬───────┬───────┬───────┬───────┬────────┬───────┬────────┐
│         config          │   1   │  10   │  50   │  100  │ 1,000  │ 5,000 │ 10,000 │
├─────────────────────────┼───────┼───────┼───────┼───────┼────────┼───────┼────────┤
│ uncompr, expanded       │ 0.002 │ 0.005 │ 0.015 │ 0.093 │   0.88 │  9.97 │   23.6 │
├─────────────────────────┼───────┼───────┼───────┼───────┼────────┼───────┼────────┤
│ uncompr, HypertableScan │ 0.021 │ 0.022 │ 0.018 │ 0.017 │  0.018 │ 0.017 │  0.018 │
├─────────────────────────┼───────┼───────┼───────┼───────┼────────┼───────┼────────┤
│ compr, expanded         │ 0.004 │ 0.015 │ 0.143 │ 0.301 │   6.33 │  42.4 │   92.3 │
├─────────────────────────┼───────┼───────┼───────┼───────┼────────┼───────┼────────┤
│ compr, HypertableScan   │ 0.037 │ 0.037 │ 0.029 │ 0.029 │ 6.1e-2 │ 0.030 │  0.031 │
└─────────────────────────┴───────┴───────┴───────┴───────┴────────┴───────┴────────┘

@github-actions

Copy link
Copy Markdown

@Poroma-Banerjee, @natalya-aksman: please review this pull request.

Powered by pull-review

@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.87185% with 53 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...rc/nodes/deferred_chunk_scan/deferred_chunk_scan.c 88.29% 16 Missing and 34 partials ⚠️
src/planner/planner.c 57.14% 1 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

Comment on lines +552 to +556
/*
* Cap the batch at HS_FETCH_BATCH_SIZE: the per-chunk query already carries
* the LIMIT, and the fetch loop refills across batches, so a large pushed
* LIMIT must not turn into one oversized cur_tuples allocation.
*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment written by a chatbot?

* are no more chunks.
*/
static bool
open_next_chunk(HypertableScanState *state)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered SPI? Just curious what are pros/cons.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial version did use SPI but there where some problems with nested SPI and cursor mode. plpgsql uses spi internally leading to nesting.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, connect/finish must be strictly nested. Maybe you could run them inside "exec" of this node and the named cursor could persist across this, but I'm not sure how all of this works and whether it will be performant enough.

Comment thread tsl/src/nodes/hypertable_scan/hypertable_scan.c Outdated
Comment thread src/planner/planner.c Outdated
#include "ts_catalog/catalog.h"
#include "utils.h"

/* Rows fetched per PortalRunFetch when there is no pushed LIMIT to bound it. */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we generally want to run this node when there's no LIMIT? I'd expect it to do no better than a normal plan in this case.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have to visit all the chunks, planning the chunks upfront will come out on top, might still be beneficial if you favor fast first tuple. For initial version we want to require LIMIT.

List *querytree = pg_analyze_and_rewrite_fixedparams(raw, sql, NULL, 0, NULL);
Query *query = linitial_node(Query, querytree);
PlannedStmt *plan =
pg_plan_query(query, sql, CURSOR_OPT_FAST_PLAN | CURSOR_OPT_NO_SCROLL, NULL);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "fast plan" parameter means we only expect to return 10% of tuples from the cursor. It seems that in this case it's not true and we actually expect to return all of them? Given the LIMIT that is already in the per-chunk query.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to favor smaller startup cost

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have LIMIT in the query for that. This flag additionally reduces the LIMIT ten times. There'a a justification given for cursor, but why is this correct to use here?

}

/*
* In unordered mode we scan chunks in creation_time DESC order, with id DESC as a

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we order by creation time for unordered mode? If we don't care about order, it would be easier to return them in descending chunk id order and not have to handle any ties. If we do care about it, let's add some explanation.

MemoryContextAlloc(state->chunk_mcxt, sizeof(HeapTuple) * state->batch_size);
state->cur_nrows = 0;
state->cur_row = 0;
PortalRunFetch(state->cur_portal, FETCH_FORWARD, state->batch_size, state->dest);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this node we only need to return one tuple at a time, so there's no need to use tuple store to materialize intermediate results. Consider using PortalRunSelect returning one row at a time.

bool descending;
Oid sortop; /* sort operator, for EXPLAIN */
bool nulls_first; /* NULLS FIRST, for EXPLAIN */
int push_limit;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand the logic correctly, the LIMIT clause is fully handled by the above Limit node, so this node doesn't do any limit tracking itself, and just keeps returning at most LIMIT + OFFSET rows from each chunk in sequence? Would be good to explain this in the comments.

HS_PRIV_DESCENDING, /* 1 if the ordered sort is descending */
HS_PRIV_NULLS_FIRST, /* 1 if the ordered sort is NULLS FIRST */
HS_PRIV_COUNT
} HypertableScanPrivIndex;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PrivIndex reads like an index into custom_private itself, consider renaming.

@svenklemm svenklemm changed the title Add HypertableScan custom scan Add DeferredChunkScan custom scan Jul 22, 2026
@svenklemm svenklemm modified the milestones: v3.0.0, v2.30.0 Jul 23, 2026

@akuzm akuzm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left some comments here and we discussed this on a call, overall I think we have a good idea of what the initial implementation should look like.

@svenklemm
svenklemm force-pushed the sven/hypertablescan branch 3 times, most recently from 1b68765 to e49575b Compare August 1, 2026 21:20
Add a new custom scan node for LIMIT queries that leaves the
hypertable unexpanded during initial planning and defers chunk
enumeration and scanning to execution time.
This gives huge speedup to LIMIT queries on hypertables with many chunks.

Warm planning time (ms), median of 15, ORDER BY ts LIMIT 1

┌────────────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬────────┐
│          config            │   1   │  10   │  50   │  100  │ 1,000 │ 5,000 │ 10,000 │
├────────────────────────────┼───────┼───────┼───────┼───────┼───────┼───────┼────────┤
│ uncompr, expanded          │ 0.010 │ 0.037 │ 0.173 │ 0.372 │  4.39 │  35.9 │   84.3 │
├────────────────────────────┼───────┼───────┼───────┼───────┼───────┼───────┼────────┤
│ uncompr, DeferredChunkScan │ 0.003 │ 0.003 │ 0.003 │ 0.002 │ 0.002 │ 0.002 │  0.002 │
├────────────────────────────┼───────┼───────┼───────┼───────┼───────┼───────┼────────┤
│ compr, expanded            │ 0.019 │ 0.136 │ 0.636 │ 1.361 │  27.1 │ 169.9 │  363.1 │
├────────────────────────────┼───────┼───────┼───────┼───────┼───────┼───────┼────────┤
│ compr, DeferredChunkScan   │ 0.003 │ 0.003 │ 0.003 │ 0.002 │ 0.002 │ 0.003 │  0.003 │
└────────────────────────────┴───────┴───────┴───────┴───────┴───────┴───────┴────────┘

Warm execution time (ms), median of 15, ORDER BY ts LIMIT 1

┌────────────────────────────┬───────┬───────┬───────┬───────┬────────┬───────┬────────┐
│          config            │   1   │  10   │  50   │  100  │ 1,000  │ 5,000 │ 10,000 │
├────────────────────────────┼───────┼───────┼───────┼───────┼────────┼───────┼────────┤
│ uncompr, expanded          │ 0.002 │ 0.005 │ 0.015 │ 0.093 │   0.88 │  9.97 │   23.6 │
├────────────────────────────┼───────┼───────┼───────┼───────┼────────┼───────┼────────┤
│ uncompr, DeferredChunkScan │ 0.021 │ 0.022 │ 0.018 │ 0.017 │  0.018 │ 0.017 │  0.018 │
├────────────────────────────┼───────┼───────┼───────┼───────┼────────┼───────┼────────┤
│ compr, expanded            │ 0.004 │ 0.015 │ 0.143 │ 0.301 │   6.33 │  42.4 │   92.3 │
├────────────────────────────┼───────┼───────┼───────┼───────┼────────┼───────┼────────┤
│ compr, DeferredChunkScan   │ 0.037 │ 0.037 │ 0.029 │ 0.029 │ 6.1e-2 │ 0.030 │  0.031 │
└────────────────────────────┴───────┴───────┴───────┴───────┴────────┴───────┴────────┘
@svenklemm
svenklemm force-pushed the sven/hypertablescan branch from e49575b to 069a94d Compare August 2, 2026 09:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants