Skip to content

Commit f789b77

Browse files
committed
Fix index-only scan plans when not all index columns can be returned.
If an index has both returnable and non-returnable columns, and one of the non-returnable columns is an expression using a Var that is in a returnable column, then a query returning that expression could result in an index-only scan plan that attempts to read the non-returnable column, instead of recomputing the expression from the returnable column as intended. To fix, redefine the "indextlist" list of an IndexOnlyScan plan node as containing null Consts in place of any non-returnable columns. This solves the problem by preventing setrefs.c from falsely matching to such entries. The executor is happy since it only cares about the exposed types of the entries, and ruleutils.c doesn't care because a correct plan won't reference those entries. I considered some other ways to prevent setrefs.c from doing the wrong thing, but this way seems good since (a) it allows a very localized fix, (b) it makes the indextlist structure more compact in many cases, and (c) the indextlist is now a more faithful representation of what the index AM will actually produce, viz. nulls for any non-returnable columns. This is easier to hit since we introduced included columns, but it's possible to construct failing examples without that, as per the added regression test. Hence, back-patch to all supported branches. Per bug #17350 from Louis Jachiet. Discussion: https://postgr.es/m/17350-b5bdcf476e5badbb@postgresql.org
1 parent d536d03 commit f789b77

File tree

4 files changed

+92
-3
lines changed

4 files changed

+92
-3
lines changed

src/backend/optimizer/plan/createplan.c

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <math.h>
2121

2222
#include "access/sysattr.h"
23+
#include "catalog/pg_am.h"
2324
#include "catalog/pg_class.h"
2425
#include "foreign/fdwapi.h"
2526
#include "miscadmin.h"
@@ -181,6 +182,7 @@ static IndexOnlyScan *make_indexonlyscan(List *qptlist, List *qpqual,
181182
List *indexqual, List *indexorderby,
182183
List *indextlist,
183184
ScanDirection indexscandir);
185+
static List *make_indexonly_tlist(IndexOptInfo *indexinfo);
184186
static BitmapIndexScan *make_bitmap_indexscan(Index scanrelid, Oid indexid,
185187
List *indexqual,
186188
List *indexqualorig);
@@ -590,7 +592,7 @@ create_scan_plan(PlannerInfo *root, Path *best_path, int flags)
590592
if (best_path->pathtype == T_IndexOnlyScan)
591593
{
592594
/* For index-only scan, the preferred tlist is the index's */
593-
tlist = copyObject(((IndexPath *) best_path)->indexinfo->indextlist);
595+
tlist = copyObject(make_indexonly_tlist(((IndexPath *) best_path)->indexinfo));
594596

595597
/*
596598
* Transfer sortgroupref data to the replacement tlist, if
@@ -2909,7 +2911,7 @@ create_indexscan_plan(PlannerInfo *root,
29092911
indexoid,
29102912
fixed_indexquals,
29112913
fixed_indexorderbys,
2912-
best_path->indexinfo->indextlist,
2914+
make_indexonly_tlist(best_path->indexinfo),
29132915
best_path->indexscandir);
29142916
else
29152917
scan_plan = (Scan *) make_indexscan(tlist,
@@ -5232,6 +5234,53 @@ make_indexonlyscan(List *qptlist,
52325234
return node;
52335235
}
52345236

5237+
/*
5238+
* make_indexonly_tlist
5239+
*
5240+
* Construct the indextlist for an IndexOnlyScan plan node.
5241+
* We must replace any column that can't be returned by the index AM
5242+
* with a null Const of the appropriate datatype. This is necessary
5243+
* to prevent setrefs.c from trying to use the value of such a column,
5244+
* and anyway it makes the indextlist a better representative of what
5245+
* the indexscan will really return. (We do this here, not where the
5246+
* IndexOptInfo is originally constructed, because earlier planner
5247+
* steps need to know what is in such columns.)
5248+
*/
5249+
static List *
5250+
make_indexonly_tlist(IndexOptInfo *indexinfo)
5251+
{
5252+
List *result;
5253+
int i;
5254+
ListCell *lc;
5255+
5256+
/* We needn't work hard for the common case of btrees. */
5257+
if (indexinfo->relam == BTREE_AM_OID)
5258+
return indexinfo->indextlist;
5259+
5260+
result = NIL;
5261+
i = 0;
5262+
foreach(lc, indexinfo->indextlist)
5263+
{
5264+
TargetEntry *indextle = (TargetEntry *) lfirst(lc);
5265+
5266+
if (indexinfo->canreturn[i])
5267+
result = lappend(result, indextle);
5268+
else
5269+
{
5270+
TargetEntry *newtle = makeNode(TargetEntry);
5271+
Node *texpr = (Node *) indextle->expr;
5272+
5273+
memcpy(newtle, indextle, sizeof(TargetEntry));
5274+
newtle->expr = (Expr *) makeNullConst(exprType(texpr),
5275+
exprTypmod(texpr),
5276+
exprCollation(texpr));
5277+
result = lappend(result, newtle);
5278+
}
5279+
i++;
5280+
}
5281+
return result;
5282+
}
5283+
52355284
static BitmapIndexScan *
52365285
make_bitmap_indexscan(Index scanrelid,
52375286
Oid indexid,

src/include/nodes/plannodes.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,8 @@ typedef struct IndexScan
421421
* indextlist, which represents the contents of the index as a targetlist
422422
* with one TLE per index column. Vars appearing in this list reference
423423
* the base table, and this is the only field in the plan node that may
424-
* contain such Vars.
424+
* contain such Vars. Note however that index columns that the AM can't
425+
* reconstruct are replaced by null Consts in indextlist.
425426
* ----------------
426427
*/
427428
typedef struct IndexOnlyScan

src/test/regress/expected/gist.out

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,34 @@ and p <@ box(point(5,5), point(6, 6));
236236
(11 rows)
237237

238238
drop index gist_tbl_multi_index;
239+
-- Test that we don't try to return the value of a non-returnable
240+
-- column in an index-only scan. (This isn't GIST-specific, but
241+
-- it only applies to index AMs that can return some columns and not
242+
-- others, so GIST with appropriate opclasses is a convenient test case.)
243+
create index gist_tbl_multi_index on gist_tbl using gist (circle(p,1), p);
244+
explain (verbose, costs off)
245+
select circle(p,1) from gist_tbl
246+
where p <@ box(point(5, 5), point(5.3, 5.3));
247+
QUERY PLAN
248+
---------------------------------------------------------------
249+
Index Only Scan using gist_tbl_multi_index on public.gist_tbl
250+
Output: circle(p, '1'::double precision)
251+
Index Cond: (gist_tbl.p <@ '(5.3,5.3),(5,5)'::box)
252+
(3 rows)
253+
254+
select circle(p,1) from gist_tbl
255+
where p <@ box(point(5, 5), point(5.3, 5.3));
256+
circle
257+
-----------------
258+
<(5,5),1>
259+
<(5.05,5.05),1>
260+
<(5.1,5.1),1>
261+
<(5.15,5.15),1>
262+
<(5.2,5.2),1>
263+
<(5.25,5.25),1>
264+
<(5.3,5.3),1>
265+
(7 rows)
266+
239267
-- Clean up
240268
reset enable_seqscan;
241269
reset enable_bitmapscan;

src/test/regress/sql/gist.sql

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,17 @@ and p <@ box(point(5,5), point(6, 6));
126126

127127
drop index gist_tbl_multi_index;
128128

129+
-- Test that we don't try to return the value of a non-returnable
130+
-- column in an index-only scan. (This isn't GIST-specific, but
131+
-- it only applies to index AMs that can return some columns and not
132+
-- others, so GIST with appropriate opclasses is a convenient test case.)
133+
create index gist_tbl_multi_index on gist_tbl using gist (circle(p,1), p);
134+
explain (verbose, costs off)
135+
select circle(p,1) from gist_tbl
136+
where p <@ box(point(5, 5), point(5.3, 5.3));
137+
select circle(p,1) from gist_tbl
138+
where p <@ box(point(5, 5), point(5.3, 5.3));
139+
129140
-- Clean up
130141
reset enable_seqscan;
131142
reset enable_bitmapscan;

0 commit comments

Comments
 (0)