Skip to content
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

Implement aggregation and grouping pushdown #1

Merged
merged 17 commits into from
Dec 27, 2021

Conversation

gruuya
Copy link

@gruuya gruuya commented Dec 13, 2021

Multicorn support for Python FDW instances pushdown of an arbitrary combination of bare aggregations and/or groupings.

  • Accompanying PR demonstrating a particular implementation (in Elasticsearch) is Multicorn aggregation/grouping pushdown support postgres-elasticsearch-fdw#1.
  • For now it does not support pushdown of HAVING clauses or WHERE clauses in case of aggregations. This case results in full record fetch and then subsequent filtering/aggregation on the PG side.
  • Does not support pushdown of ORDER BY clauses, but in this case it does push down the aggregation, and performs only the ordering of returned aggregations on the PG side (so it's an improvement, albeit there's still some work to be done on doing sorting on the remote server).
  • Also not supported are aggregations with DISTINCT or COUNT(*) for the time being (defaults to full record fetch and subsequent processing on PG side).
  • Implementation was guided by postgres_fdw and other FDW implementations.

CU-1x57q56

The current implementation provides a mechanism for pushing down aggregation and/or grouping
queries into the foreign data source. The Python side of the implementation will now receive
two new kwargs, `aggs` and `group_clauses`, in which case it should return the corresponding
aggreagation result.

Still left to implement is consulting the Python side whether remote aggregation is possible
at all, and if so which agregation functions are valid.

Also missing are some more advanced aggregation cases (aggregating multiple functions, or
handling `HAVING` clause for example). This is to be implemented separately.
Add a method to FDW Python instance that provides info on whether the pushdown is supported at all,
and if so gives data for more granular decisions (for now only list of aggregation functions). Consult
this method in `multicornGetForeignUpperPaths`.
Currently the parsing is incomplete for simple WHERE clauses due to the lack of T_OpExpr and T_Const cases
in multicorn_foreign_expr_walker. Therefore, all WHERE clauses will be treated as local conditions, and not
pushed down.
For the first iteration disable pushdown of `COUNT(*)`, like for `DISTINCT` clauses.

These can be added later on, and tested on their eqivalents in ES, `doc_count` and `cardinality`.
Copy link

@mildbyte mildbyte left a comment

Choose a reason for hiding this comment

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

I took a first pass at this and left some comments; will try understanding it deeper in the morning. Pretty impressive!

Comment on lines 221 to 222
The FDW has to inspect every sort, and respond which one are handled.
The sorts are cumulatives.

Choose a reason for hiding this comment

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

Copypaste error here?

Copy link
Author

Choose a reason for hiding this comment

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

Yup, thanks.


Return:
None if pushdown not supported, otherwise a dictionary containing
more granular details for the planning phase, in the form:

Choose a reason for hiding this comment

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

Needs docs on the expected dict output

Copy link
Author

Choose a reason for hiding this comment

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

Adding docs for it in the next commit.

column to be used in the aggregation operation. Result should be
returned under the provided aggregation key.
group_clauses (list): A list of columns used in GROUP BY statements.
The result should be returned for each column name provided.

Choose a reason for hiding this comment

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

What does this mean -- does every row we return need to have an entry for everything in columns + aggs?

Copy link
Author

Choose a reason for hiding this comment

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

What I meant to say is that whenever there is a group_clauses kwarg, then for each column specified there the returned response should have a corresponding value for each row using that column name as the key.

I re-worded the docstring as above, hopefully this clarifies it.

src/python.c Outdated
p_object = PyMapping_GetItemString(p_upperrel_pushdown, "agg_functions");
if (p_object != NULL && p_object != Py_None)
{
state->agg_functions = PyMapping_Keys(p_object);

Choose a reason for hiding this comment

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

I don't think you ever DECREF state->agg_functions, so this will slowly leak. I'd extract the contents into a separate List here and get rid of the PyObject here so that you also don't have to mess with the Python API in foreign_expr_walker.

Copy link
Author

Choose a reason for hiding this comment

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

Good catch. I initially tried the route you mentioned but was stuck extracting Python Unicode objects into a PG List, so I went with this instead. Let me get back at this.

Copy link
Author

Choose a reason for hiding this comment

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

Ok, I've now added storing of supported agg functions to a List.

foreach(lc_groupc, state->group_clauses)
{
PyObject *column = PyUnicode_FromString(strVal(lfirst(lc_groupc)));
PyList_Append(group_clauses, column);

Choose a reason for hiding this comment

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

I think (but not entirely sure, since https://docs.python.org/3/c-api/list.html#c.PyList_Append doesn't mention it -- some evidence in https://stackoverflow.com/questions/3512414/does-this-pylist-appendlist-py-buildvalue-leak) that PyList_Append increments the refcounter, so you need to DECREF the column here.

Copy link
Author

Choose a reason for hiding this comment

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

Makes sense, added DECREF.

@@ -1453,6 +1560,391 @@ multicornIsForeignScanParallelSafe(PlannerInfo *root, RelOptInfo *rel,
}
#endif

/*

Choose a reason for hiding this comment

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

That's a lot of code! Can you mark the parts taken from other FDWs (here and in deparse) and parts that you added yourself so that I know where to concentrate the review? Currently it kind of makes sense to me but knowing where it came from would make it clearer.

Copy link
Author

Choose a reason for hiding this comment

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

Sure, I can do that. I can add some comments like // MY CODE START and // MY CODE END if that helps. Just keep in mind that the parts taken from other FDWs are also trimmed down, i.e. I've thrown away the irrelevant stuff so it's not 1-1.

Copy link
Author

Choose a reason for hiding this comment

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

Done - I've enclosed all deviations from common FDW code (as used in postgres_fdw and other implementations) with the above comments in multicorn.c and deparse.c (other files should be more easier to parse I think).

Again worth mentioning that common FDW code that I've "appropriated" was pruned.


initStringInfo(agg_key);
appendStringInfoString(agg_key, strVal(function));
appendStringInfoString(agg_key, ".");

Choose a reason for hiding this comment

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

Just to check my understanding, does the Python FDW get a dict of {"functionname.colname": {"function": "functionname", "column": "colname"}} and is then expected to return a surrogate functionname.colname column in its response? e.g. https://github.com/splitgraph/postgres-elasticsearch-fdw/pull/1/files#diff-45ed0634a3ed30705f0b30dce58a096decc81bdf04af2df3906bc56d692c3de4R88-R92

Copy link
Author

Choose a reason for hiding this comment

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

Yes, that is correct.

src/python.c Outdated
pushdown_upperrel = true;
}

Py_DECREF(p_upperrel_pushdown);

Choose a reason for hiding this comment

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

Should this DECREF be inside of the if (p_upperrel_pushdown != NULL && p_upperrel_pushdown != Py_None) like other decrefs?

Copy link
Author

Choose a reason for hiding this comment

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

We'd need to decrement a Py_None reference, which would leak if the Py_DECREF was in the if statement I believe.

That said, in the case of p_upperrel_pushdown being a null pointer it seems the proper approach is to use Py_XDECREF, like in the case of pythonDictToTuple function.

Also seems like I should do something similar for p_object inside the outer if statement.

Adding those changes now.

@gruuya gruuya merged commit 3391858 into master Dec 27, 2021
@gruuya gruuya deleted the implement-agg-grouping-pushdown-cu-1x57q56 branch December 27, 2021 15:29
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