Skip to content

Commit 7cfc9bb

Browse files
akuzmtimescale-automation
authored andcommitted
Fix potential incorrect stddev(float8/4) in columnar pipeline (#10209)
Postgres uses the same float8_accum transition function for both avg() and stddev() for these types. TimescaleDB used to use a simplified implementation for avg(), because it doesn't require tracking sum of squares. However, Postgres uses a shared transition state for aggregates sharing a transition functions, as given by AggRef.aggtransno. This leads to potentially wrong result when both avg() and stddev() of same variable are computed in one query. Change the aggregation states in columnar aggregation to follow aggtransno. Remove the "float8 accum with no squares" implementation because it cannot be safely used in general case, and checking for special cases is complicated. (cherry picked from commit 58e9840)
1 parent 83391fe commit 7cfc9bb

12 files changed

Lines changed: 116 additions & 107 deletions

.unreleased/wrong-stddev

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixes: #10209 Potentially wrong result of stddev(float4/8) when used together with avg() in columnar query execution pipeline.

tsl/src/nodes/vector_agg/exec.c

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -711,55 +711,70 @@ vector_agg_begin(CustomScanState *node, EState *estate, int eflags)
711711
const int tlist_length = list_length(aggregated_tlist);
712712

713713
/*
714-
* First, count how many grouping columns and aggregate functions we have.
714+
* First, count how many grouping columns and aggregate transition states we have.
715715
*/
716-
int agg_functions_counter = 0;
716+
int max_aggtransno = -1;
717717
int grouping_column_counter = 0;
718718
for (int i = 0; i < tlist_length; i++)
719719
{
720720
TargetEntry *tlentry = list_nth_node(TargetEntry, aggregated_tlist, i);
721721
if (IsA(tlentry->expr, Aggref))
722722
{
723-
agg_functions_counter++;
723+
max_aggtransno = Max(max_aggtransno, castNode(Aggref, tlentry->expr)->aggtransno);
724724
}
725725
else
726726
{
727727
/* This is a grouping column. */
728728
grouping_column_counter++;
729729
}
730730
}
731-
Assert(agg_functions_counter + grouping_column_counter == tlist_length);
732731

733732
/*
734733
* Allocate the storage for definitions of aggregate function and grouping
735734
* columns.
736735
*/
737-
vector_agg_state->num_agg_defs = agg_functions_counter;
736+
vector_agg_state->num_agg_defs = max_aggtransno + 1;
738737
vector_agg_state->agg_defs =
739738
palloc0(sizeof(*vector_agg_state->agg_defs) * vector_agg_state->num_agg_defs);
740739

741740
vector_agg_state->num_grouping_columns = grouping_column_counter;
742741
vector_agg_state->grouping_columns = palloc0(sizeof(*vector_agg_state->grouping_columns) *
743742
vector_agg_state->num_grouping_columns);
744743

744+
/*
745+
* Some aggregates share transition state, but we shouldn't be able to get
746+
* more entries than the input targetlist.
747+
*/
748+
Assert(vector_agg_state->num_agg_defs + vector_agg_state->num_grouping_columns <= tlist_length);
749+
745750
/*
746751
* Loop through the aggregated targetlist again and fill the definitions.
747752
*/
748-
agg_functions_counter = 0;
749753
grouping_column_counter = 0;
750754
for (int i = 0; i < tlist_length; i++)
751755
{
752756
TargetEntry *tlentry = list_nth_node(TargetEntry, aggregated_tlist, i);
753757
if (IsA(tlentry->expr, Aggref))
754758
{
755759
/* This is an aggregate function. */
756-
VectorAggDef *def = &vector_agg_state->agg_defs[agg_functions_counter++];
757-
def->output_offset = i;
758-
759760
Aggref *aggref = castNode(Aggref, tlentry->expr);
760761

762+
VectorAggDef *def = &vector_agg_state->agg_defs[aggref->aggtransno];
763+
761764
VectorAggFunctions *func = get_vector_aggregate(aggref->aggfnoid, aggref->inputcollid);
762765
Assert(func != NULL);
766+
767+
if (def->func.agg_init != NULL)
768+
{
769+
/*
770+
* Already initialized for another aggregate sharing the same
771+
* transition state.
772+
*/
773+
Assert(def->func.agg_init == func->agg_init);
774+
Assert(def->func.agg_vector == func->agg_vector);
775+
continue;
776+
}
777+
763778
def->func = *func;
764779

765780
if (list_length(aggref->args) > 0)
@@ -1108,9 +1123,11 @@ vector_agg_exec(CustomScanState *node)
11081123
/*
11091124
* If we have more partial aggregation results, continue returning them.
11101125
*/
1126+
List *aggregated_tlist =
1127+
castNode(CustomScan, vector_agg_state->custom.ss.ps.plan)->custom_scan_tlist;
11111128
GroupingPolicy *grouping = vector_agg_state->grouping;
11121129
MemoryContext old_context = MemoryContextSwitchTo(econtext->ecxt_per_tuple_memory);
1113-
bool have_partial = grouping->gp_do_emit(grouping, aggregated_slot);
1130+
bool have_partial = grouping->gp_do_emit(grouping, aggregated_tlist, aggregated_slot);
11141131
MemoryContextSwitchTo(old_context);
11151132
if (have_partial)
11161133
{
@@ -1202,7 +1219,7 @@ vector_agg_exec(CustomScanState *node)
12021219
* If we have partial aggregation results, start returning them.
12031220
*/
12041221
old_context = MemoryContextSwitchTo(econtext->ecxt_per_tuple_memory);
1205-
have_partial = grouping->gp_do_emit(grouping, aggregated_slot);
1222+
have_partial = grouping->gp_do_emit(grouping, aggregated_tlist, aggregated_slot);
12061223
MemoryContextSwitchTo(old_context);
12071224
if (have_partial)
12081225
{

tsl/src/nodes/vector_agg/exec.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ typedef struct VectorAggDef
1818
{
1919
VectorAggFunctions func;
2020
Expr *argument;
21-
int output_offset;
2221
List *filter_clauses;
2322

2423
/*
@@ -42,6 +41,10 @@ typedef struct VectorAggState
4241
{
4342
CustomScanState custom;
4443

44+
/*
45+
* Postgres makes some Aggrefs share the transition state. This array tracks
46+
* the unique transition states indexed by Aggref.aggtransno.
47+
*/
4548
int num_agg_defs;
4649
VectorAggDef *agg_defs;
4750

tsl/src/nodes/vector_agg/function/float48_accum_single.c

Lines changed: 9 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66

77
/*
88
* Vectorized implementation of a Postgres float{4,8}_accum() transition
9-
* function for a single type. They use the same Youngs-Cramer state, but for
10-
* AVG we can skip calculating the Sxx variable.
9+
* function for a single type.
1110
*/
1211

1312
#ifdef GENERATE_DISPATCH_TABLE
@@ -17,20 +16,16 @@
1716
extern VectorAggFunctions FUNCTION_NAME(argdef);
1817

1918
/*
20-
* Helper macros to generate the cases for the given argument type. We support
21-
* different aggregate functions based on whether we calculate the Sxx variable.
19+
* Helper macros to generate the cases for the given argument type.
2220
*/
23-
#ifdef NEED_SXX
2421
#define ACCUM_CASE_HELPER(PG_TYPE) \
2522
case F_STDDEV_##PG_TYPE: \
2623
case F_STDDEV_SAMP_##PG_TYPE: \
2724
case F_STDDEV_POP_##PG_TYPE: \
2825
case F_VARIANCE_##PG_TYPE: \
2926
case F_VAR_SAMP_##PG_TYPE: \
30-
case F_VAR_POP_##PG_TYPE:
31-
#else
32-
#define ACCUM_CASE_HELPER(PG_TYPE) case F_AVG_##PG_TYPE:
33-
#endif
27+
case F_VAR_POP_##PG_TYPE: \
28+
case F_AVG_##PG_TYPE:
3429

3530
#define ACCUM_CASE(PG_TYPE) ACCUM_CASE_HELPER(PG_TYPE)
3631

@@ -49,9 +44,7 @@ typedef struct
4944
{
5045
double N;
5146
double Sx;
52-
#ifdef NEED_SXX
5347
double Sxx;
54-
#endif
5548
} FUNCTION_NAME(state);
5649

5750
static void
@@ -84,17 +77,7 @@ FUNCTION_NAME(emit)(void *agg_state, Datum *out_result, bool *out_isnull)
8477
*/
8578
((float8 *) ARR_DATA_PTR(result))[0] = state->N;
8679
((float8 *) ARR_DATA_PTR(result))[1] = state->Sx;
87-
((float8 *) ARR_DATA_PTR(result))[2] =
88-
/*
89-
* Sxx should be NaN if any of the inputs are infinite or NaN. This is
90-
* checked by float8_combine even if it's not used for the actual
91-
* calculations.
92-
*/
93-
0. * state->Sx
94-
#ifdef NEED_SXX
95-
+ state->Sxx
96-
#endif
97-
;
80+
((float8 *) ARR_DATA_PTR(result))[2] = state->Sxx;
9881

9982
*out_result = PointerGetDatum(result);
10083
*out_isnull = false;
@@ -104,12 +87,8 @@ FUNCTION_NAME(emit)(void *agg_state, Datum *out_result, bool *out_isnull)
10487
* Youngs-Cramer update for rows after the first.
10588
*/
10689
static pg_attribute_always_inline void
107-
FUNCTION_NAME(update)(const uint64 *filter, const CTYPE *values, int row, double *N, double *Sx
108-
#ifdef NEED_SXX
109-
,
110-
double *Sxx
111-
#endif
112-
)
90+
FUNCTION_NAME(update)(const uint64 *filter, const CTYPE *values, int row, double *N, double *Sx,
91+
double *Sxx)
11392
{
11493
const CTYPE newval = values[row];
11594
if (!arrow_row_is_valid(filter, row))
@@ -123,11 +102,9 @@ FUNCTION_NAME(update)(const uint64 *filter, const CTYPE *values, int row, double
123102
*/
124103
const double newN = *N + 1.0;
125104
const double newSx = *Sx + newval;
126-
#ifdef NEED_SXX
127105
Assert(*N > 0.0);
128106
const double tmp = newval * newN - newSx;
129107
*Sxx += tmp * tmp / (*N * newN);
130-
#endif
131108

132109
*N = newN;
133110
*Sx = newSx;
@@ -137,67 +114,43 @@ FUNCTION_NAME(update)(const uint64 *filter, const CTYPE *values, int row, double
137114
* Combine two Youngs-Cramer states following the float8_combine() function.
138115
*/
139116
static pg_attribute_always_inline void
140-
FUNCTION_NAME(combine)(double *inout_N, double *inout_Sx,
141-
#ifdef NEED_SXX
142-
double *inout_Sxx,
143-
#endif
144-
double N2, double Sx2
145-
#ifdef NEED_SXX
146-
,
147-
double Sxx2
148-
#endif
149-
)
117+
FUNCTION_NAME(combine)(double *inout_N, double *inout_Sx, double *inout_Sxx, double N2, double Sx2,
118+
double Sxx2)
150119
{
151120
const double N1 = *inout_N;
152121
const double Sx1 = *inout_Sx;
153-
#ifdef NEED_SXX
154122
const double Sxx1 = *inout_Sxx;
155-
#endif
156123

157124
if (unlikely(N1 == 0))
158125
{
159126
*inout_N = N2;
160127
*inout_Sx = Sx2;
161-
#ifdef NEED_SXX
162128
*inout_Sxx = Sxx2;
163-
#endif
164129
return;
165130
}
166131

167132
if (unlikely(N2 == 0))
168133
{
169134
*inout_N = N1;
170135
*inout_Sx = Sx1;
171-
#ifdef NEED_SXX
172136
*inout_Sxx = Sxx1;
173-
#endif
174137
return;
175138
}
176139

177140
const double combinedN = N1 + N2;
178141
const double combinedSx = Sx1 + Sx2;
179-
#ifdef NEED_SXX
180142
const double tmp = Sx1 / N1 - Sx2 / N2;
181143
const double combinedSxx = Sxx1 + Sxx2 + N1 * N2 * tmp * tmp / combinedN;
182-
#endif
183144

184145
*inout_N = combinedN;
185146
*inout_Sx = combinedSx;
186-
#ifdef NEED_SXX
187147
*inout_Sxx = combinedSxx;
188-
#endif
189148
}
190149

191-
#ifdef NEED_SXX
192150
#define UPDATE(filter, values, row, N, Sx, Sxx) \
193151
FUNCTION_NAME(update)(filter, values, row, N, Sx, Sxx)
194152
#define COMBINE(inout_N, inout_Sx, inout_Sxx, N2, Sx2, Sxx2) \
195153
FUNCTION_NAME(combine)(inout_N, inout_Sx, inout_Sxx, N2, Sx2, Sxx2)
196-
#else
197-
#define UPDATE(filter, values, row, N, Sx, Sxx) FUNCTION_NAME(update)(filter, values, row, N, Sx)
198-
#define COMBINE(inout_N, inout_Sx, inout_Sxx, N2, Sx2, Sxx2) \
199-
FUNCTION_NAME(combine)(inout_N, inout_Sx, N2, Sx2)
200-
#endif
201154

202155
static pg_attribute_always_inline void
203156
FUNCTION_NAME(vector_impl)(void *agg_state, size_t n, const CTYPE *values, const uint64 *filter,
@@ -214,13 +167,10 @@ FUNCTION_NAME(vector_impl)(void *agg_state, size_t n, const CTYPE *values, const
214167
*/
215168
double Narray[UNROLL_SIZE] = { 0 };
216169
double Sxarray[UNROLL_SIZE] = { 0 };
217-
#ifdef NEED_SXX
218170
double Sxxarray[UNROLL_SIZE] = { 0 };
219-
#endif
220171

221172
size_t row = 0;
222173

223-
#ifdef NEED_SXX
224174
/*
225175
* Initialize each state with the first matching row. We do this separately
226176
* to make the actual update function branchless, namely the computation of
@@ -251,7 +201,6 @@ FUNCTION_NAME(vector_impl)(void *agg_state, size_t n, const CTYPE *values, const
251201
{
252202
UPDATE(filter, values, row, &Narray[inner], &Sxarray[inner], &Sxxarray[inner]);
253203
}
254-
#endif
255204

256205
/*
257206
* Unrolled loop.
@@ -300,7 +249,6 @@ FUNCTION_NAME(one)(void *restrict agg_state, const CTYPE value)
300249
*/
301250
const double newN = state->N + 1.0;
302251
const double newSx = state->Sx + value;
303-
#ifdef NEED_SXX
304252
if (state->N > 0.0)
305253
{
306254
const double tmp = value * newN - newSx;
@@ -310,7 +258,6 @@ FUNCTION_NAME(one)(void *restrict agg_state, const CTYPE value)
310258
{
311259
state->Sxx = 0 * value;
312260
}
313-
#endif
314261

315262
state->N = newN;
316263
state->Sx = newSx;

tsl/src/nodes/vector_agg/function/float48_accum_templates.c

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,5 @@
2020
#include "template_helper.h"
2121
#include <compression/arrow_c_data_interface.h>
2222

23-
#ifndef GENERATE_DISPATCH_TABLE
24-
25-
#endif
26-
27-
/*
28-
* Templated parts for vectorized avg(float).
29-
*/
30-
#define AGG_NAME accum_no_squares
31-
#include "float48_accum_types.c"
32-
33-
/*
34-
* Templated parts for vectorized functions that use the Sxx state (stddev etc).
35-
*/
3623
#define AGG_NAME accum_with_squares
37-
#define NEED_SXX
3824
#include "float48_accum_types.c"

tsl/src/nodes/vector_agg/function/float48_accum_types.c

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66

77
/*
88
* Functions handled by *accum() aggregate functions states, implementation for
9-
* all types. They use the same Youngs-Cramer state, but for AVG we can skip
10-
* calculating the Sxx variable.
9+
* all types.
1110
*/
1211

1312
#define PG_TYPE FLOAT4
@@ -23,4 +22,3 @@
2322
#include "float48_accum_single.c"
2423

2524
#undef AGG_NAME
26-
#undef NEED_SXX

tsl/src/nodes/vector_agg/grouping_policy.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ typedef struct GroupingPolicy
4545
/*
4646
* Emit a partial aggregation result into the result slot.
4747
*/
48-
bool (*gp_do_emit)(GroupingPolicy *gp, TupleTableSlot *aggregated_slot);
48+
bool (*gp_do_emit)(GroupingPolicy *gp, List *aggregated_tlist, TupleTableSlot *aggregated_slot);
4949

5050
/*
5151
* Destroy the grouping policy.

0 commit comments

Comments
 (0)