Skip to content

Commit a63224b

Browse files
committed
Ensure we allocate NAMEDATALEN bytes for names in Index Only Scans
As an optimization, we store "name" columns as cstrings in btree indexes. Here we modify it so that Index Only Scans convert these cstrings back to names with NAMEDATALEN bytes rather than storing the cstring in the tuple slot, as was happening previously. Bug: #17855 Reported-by: Alexander Lakhin Reviewed-by: Alexander Lakhin, Tom Lane Discussion: https://postgr.es/m/[email protected] Backpatch-through: 12, all supported versions
1 parent 7562a9b commit a63224b

File tree

5 files changed

+141
-9
lines changed

5 files changed

+141
-9
lines changed

src/backend/executor/nodeIndexonlyscan.c

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,20 @@
3535
#include "access/tableam.h"
3636
#include "access/tupdesc.h"
3737
#include "access/visibilitymap.h"
38+
#include "catalog/pg_type.h"
3839
#include "executor/executor.h"
3940
#include "executor/nodeIndexonlyscan.h"
4041
#include "executor/nodeIndexscan.h"
4142
#include "miscadmin.h"
4243
#include "storage/bufmgr.h"
4344
#include "storage/predicate.h"
45+
#include "utils/builtins.h"
4446
#include "utils/rel.h"
4547

4648

4749
static TupleTableSlot *IndexOnlyNext(IndexOnlyScanState *node);
48-
static void StoreIndexTuple(TupleTableSlot *slot, IndexTuple itup,
49-
TupleDesc itupdesc);
50+
static void StoreIndexTuple(IndexOnlyScanState *node, TupleTableSlot *slot,
51+
IndexTuple itup, TupleDesc itupdesc);
5052

5153

5254
/* ----------------------------------------------------------------
@@ -205,7 +207,7 @@ IndexOnlyNext(IndexOnlyScanState *node)
205207
ExecForceStoreHeapTuple(scandesc->xs_hitup, slot, false);
206208
}
207209
else if (scandesc->xs_itup)
208-
StoreIndexTuple(slot, scandesc->xs_itup, scandesc->xs_itupdesc);
210+
StoreIndexTuple(node, slot, scandesc->xs_itup, scandesc->xs_itupdesc);
209211
else
210212
elog(ERROR, "no data returned for index-only scan");
211213

@@ -263,7 +265,8 @@ IndexOnlyNext(IndexOnlyScanState *node)
263265
* right now we don't need it elsewhere.
264266
*/
265267
static void
266-
StoreIndexTuple(TupleTableSlot *slot, IndexTuple itup, TupleDesc itupdesc)
268+
StoreIndexTuple(IndexOnlyScanState *node, TupleTableSlot *slot,
269+
IndexTuple itup, TupleDesc itupdesc)
267270
{
268271
/*
269272
* Note: we must use the tupdesc supplied by the AM in index_deform_tuple,
@@ -276,6 +279,37 @@ StoreIndexTuple(TupleTableSlot *slot, IndexTuple itup, TupleDesc itupdesc)
276279

277280
ExecClearTuple(slot);
278281
index_deform_tuple(itup, itupdesc, slot->tts_values, slot->tts_isnull);
282+
283+
/*
284+
* Copy all name columns stored as cstrings back into a NAMEDATALEN byte
285+
* sized allocation. We mark this branch as unlikely as generally "name"
286+
* is used only for the system catalogs and this would have to be a user
287+
* query running on those or some other user table with an index on a name
288+
* column.
289+
*/
290+
if (unlikely(node->ioss_NameCStringAttNums != NULL))
291+
{
292+
int attcount = node->ioss_NameCStringCount;
293+
294+
for (int idx = 0; idx < attcount; idx++)
295+
{
296+
int attnum = node->ioss_NameCStringAttNums[idx];
297+
Name name;
298+
299+
/* skip null Datums */
300+
if (slot->tts_isnull[attnum])
301+
continue;
302+
303+
/* allocate the NAMEDATALEN and copy the datum into that memory */
304+
name = (Name) MemoryContextAlloc(node->ss.ps.ps_ExprContext->ecxt_per_tuple_memory,
305+
NAMEDATALEN);
306+
307+
/* use namestrcpy to zero-pad all trailing bytes */
308+
namestrcpy(name, DatumGetCString(slot->tts_values[attnum]));
309+
slot->tts_values[attnum] = NameGetDatum(name);
310+
}
311+
}
312+
279313
ExecStoreVirtualTuple(slot);
280314
}
281315

@@ -473,8 +507,11 @@ ExecInitIndexOnlyScan(IndexOnlyScan *node, EState *estate, int eflags)
473507
{
474508
IndexOnlyScanState *indexstate;
475509
Relation currentRelation;
510+
Relation indexRelation;
476511
LOCKMODE lockmode;
477512
TupleDesc tupDesc;
513+
int indnkeyatts;
514+
int namecount;
478515

479516
/*
480517
* create state structure
@@ -547,7 +584,8 @@ ExecInitIndexOnlyScan(IndexOnlyScan *node, EState *estate, int eflags)
547584

548585
/* Open the index relation. */
549586
lockmode = exec_rt_fetch(node->scan.scanrelid, estate)->rellockmode;
550-
indexstate->ioss_RelationDesc = index_open(node->indexid, lockmode);
587+
indexRelation = index_open(node->indexid, lockmode);
588+
indexstate->ioss_RelationDesc = indexRelation;
551589

552590
/*
553591
* Initialize index-specific scan state
@@ -560,7 +598,7 @@ ExecInitIndexOnlyScan(IndexOnlyScan *node, EState *estate, int eflags)
560598
* build the index scan keys from the index qualification
561599
*/
562600
ExecIndexBuildScanKeys((PlanState *) indexstate,
563-
indexstate->ioss_RelationDesc,
601+
indexRelation,
564602
node->indexqual,
565603
false,
566604
&indexstate->ioss_ScanKeys,
@@ -574,7 +612,7 @@ ExecInitIndexOnlyScan(IndexOnlyScan *node, EState *estate, int eflags)
574612
* any ORDER BY exprs have to be turned into scankeys in the same way
575613
*/
576614
ExecIndexBuildScanKeys((PlanState *) indexstate,
577-
indexstate->ioss_RelationDesc,
615+
indexRelation,
578616
node->indexorderby,
579617
true,
580618
&indexstate->ioss_OrderByKeys,
@@ -603,6 +641,49 @@ ExecInitIndexOnlyScan(IndexOnlyScan *node, EState *estate, int eflags)
603641
indexstate->ioss_RuntimeContext = NULL;
604642
}
605643

644+
indexstate->ioss_NameCStringAttNums = NULL;
645+
indnkeyatts = indexRelation->rd_index->indnkeyatts;
646+
namecount = 0;
647+
648+
/*
649+
* The "name" type for btree uses text_ops which results in storing
650+
* cstrings in the indexed keys rather than names. Here we detect that in
651+
* a generic way in case other index AMs want to do the same optimization.
652+
* Check for opclasses with an opcintype of NAMEOID and an index tuple
653+
* descriptor with CSTRINGOID. If any of these are found, create an array
654+
* marking the index attribute number of each of them. StoreIndexTuple()
655+
* handles copying the name Datums into a NAMEDATALEN-byte allocation.
656+
*/
657+
658+
/* First, count the number of such index keys */
659+
for (int attnum = 0; attnum < indnkeyatts; attnum++)
660+
{
661+
if (indexRelation->rd_att->attrs[attnum].atttypid == CSTRINGOID &&
662+
indexRelation->rd_opcintype[attnum] == NAMEOID)
663+
namecount++;
664+
}
665+
666+
if (namecount > 0)
667+
{
668+
int idx = 0;
669+
670+
/*
671+
* Now create an array to mark the attribute numbers of the keys that
672+
* need to be converted from cstring to name.
673+
*/
674+
indexstate->ioss_NameCStringAttNums = (AttrNumber *)
675+
palloc(sizeof(AttrNumber) * namecount);
676+
677+
for (int attnum = 0; attnum < indnkeyatts; attnum++)
678+
{
679+
if (indexRelation->rd_att->attrs[attnum].atttypid == CSTRINGOID &&
680+
indexRelation->rd_opcintype[attnum] == NAMEOID)
681+
indexstate->ioss_NameCStringAttNums[idx++] = (AttrNumber) attnum;
682+
}
683+
}
684+
685+
indexstate->ioss_NameCStringCount = namecount;
686+
606687
/*
607688
* all done.
608689
*/

src/include/catalog/pg_opclass.dat

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,11 @@
9191
# Here's an ugly little hack to save space in the system catalog indexes.
9292
# btree doesn't ordinarily allow a storage type different from input type;
9393
# but cstring and name are the same thing except for trailing padding,
94-
# and we can safely omit that within an index entry. So we declare the
95-
# btree opclass for name as using cstring storage type.
94+
# so we choose to omit that within an index entry. Here we declare the
95+
# btree opclass for name as using cstring storage type. This does require
96+
# that we pad the cstring out with the full NAMEDATALEN bytes when performing
97+
# index-only scans. See corresponding hacks in ExecInitIndexOnlyScan() and
98+
# StoreIndexTuple().
9699
{ opcmethod => 'btree', opcname => 'name_ops', opcfamily => 'btree/text_ops',
97100
opcintype => 'name', opckeytype => 'cstring' },
98101

src/include/nodes/execnodes.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1690,6 +1690,8 @@ typedef struct IndexScanState
16901690
* TableSlot slot for holding tuples fetched from the table
16911691
* VMBuffer buffer in use for visibility map testing, if any
16921692
* PscanLen size of parallel index-only scan descriptor
1693+
* NameCStringAttNums attnums of name typed columns to pad to NAMEDATALEN
1694+
* NameCStringCount number of elements in the NameCStringAttNums array
16931695
* ----------------
16941696
*/
16951697
typedef struct IndexOnlyScanState
@@ -1709,6 +1711,8 @@ typedef struct IndexOnlyScanState
17091711
TupleTableSlot *ioss_TableSlot;
17101712
Buffer ioss_VMBuffer;
17111713
Size ioss_PscanLen;
1714+
AttrNumber *ioss_NameCStringAttNums;
1715+
int ioss_NameCStringCount;
17121716
} IndexOnlyScanState;
17131717

17141718
/* ----------------

src/test/regress/expected/index_including.out

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,3 +398,28 @@ Indexes:
398398
"tbl_c1_c2_c3_c4_key" UNIQUE CONSTRAINT, btree (c1, c2) INCLUDE (c3, c4)
399399

400400
DROP TABLE tbl;
401+
/*
402+
* 10. Test coverage for names stored as cstrings in indexes
403+
*/
404+
CREATE TABLE nametbl (c1 int, c2 name, c3 float);
405+
CREATE INDEX nametbl_c1_c2_idx ON nametbl (c2, c1) INCLUDE (c3);
406+
INSERT INTO nametbl VALUES(1, 'two', 3.0);
407+
VACUUM nametbl;
408+
SET enable_seqscan = 0;
409+
-- Ensure we get an index only scan plan
410+
EXPLAIN (COSTS OFF) SELECT c2, c1, c3 FROM nametbl WHERE c2 = 'two' AND c1 = 1;
411+
QUERY PLAN
412+
----------------------------------------------------
413+
Index Only Scan using nametbl_c1_c2_idx on nametbl
414+
Index Cond: ((c2 = 'two'::name) AND (c1 = 1))
415+
(2 rows)
416+
417+
-- Validate the results look sane
418+
SELECT c2, c1, c3 FROM nametbl WHERE c2 = 'two' AND c1 = 1;
419+
c2 | c1 | c3
420+
-----+----+----
421+
two | 1 | 3
422+
(1 row)
423+
424+
RESET enable_seqscan;
425+
DROP TABLE nametbl;

src/test/regress/sql/index_including.sql

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,22 @@ ALTER TABLE tbl ALTER c1 TYPE bigint;
217217
ALTER TABLE tbl ALTER c3 TYPE bigint;
218218
\d tbl
219219
DROP TABLE tbl;
220+
221+
/*
222+
* 10. Test coverage for names stored as cstrings in indexes
223+
*/
224+
CREATE TABLE nametbl (c1 int, c2 name, c3 float);
225+
CREATE INDEX nametbl_c1_c2_idx ON nametbl (c2, c1) INCLUDE (c3);
226+
INSERT INTO nametbl VALUES(1, 'two', 3.0);
227+
VACUUM nametbl;
228+
SET enable_seqscan = 0;
229+
230+
-- Ensure we get an index only scan plan
231+
EXPLAIN (COSTS OFF) SELECT c2, c1, c3 FROM nametbl WHERE c2 = 'two' AND c1 = 1;
232+
233+
-- Validate the results look sane
234+
SELECT c2, c1, c3 FROM nametbl WHERE c2 = 'two' AND c1 = 1;
235+
236+
RESET enable_seqscan;
237+
238+
DROP TABLE nametbl;

0 commit comments

Comments
 (0)