Commit fab25024 authored by Peter Geoghegan's avatar Peter Geoghegan

Consider secondary factors during nbtree splits.

Teach nbtree to give some consideration to how "distinguishing"
candidate leaf page split points are.  This should not noticeably affect
the balance of free space within each half of the split, while still
making suffix truncation truncate away significantly more attributes on
average.

The logic for choosing a leaf split point now uses a fallback mode in
the case where the page is full of duplicates and it isn't possible to
find even a minimally distinguishing split point.  When the page is full
of duplicates, the split should pack the left half very tightly, while
leaving the right half mostly empty.  Our assumption is that logical
duplicates will almost always be inserted in ascending heap TID order
with v4 indexes.  This strategy leaves most of the free space on the
half of the split that will likely be where future logical duplicates of
the same value need to be placed.

The number of cycles added is not very noticeable.  This is important
because deciding on a split point takes place while at least one
exclusive buffer lock is held.  We avoid using authoritative insertion
scankey comparisons to save cycles, unlike suffix truncation proper.  We
use a faster binary comparison instead.

Note that even pg_upgrade'd v3 indexes make use of these optimizations.
Benchmarking has shown that even v3 indexes benefit, despite the fact
that suffix truncation will only truncate non-key attributes in INCLUDE
indexes.  Grouping relatively similar tuples together is beneficial in
and of itself, since it reduces the number of leaf pages that must be
accessed by subsequent index scans.

Author: Peter Geoghegan
Reviewed-By: Heikki Linnakangas
Discussion: https://postgr.es/m/CAH2-WzmmoLNQOj9mAD78iQHfWLJDszHEDrAzGTUMG3mVh5xWPw@mail.gmail.com
parent dd299df8
...@@ -13,6 +13,6 @@ top_builddir = ../../../.. ...@@ -13,6 +13,6 @@ top_builddir = ../../../..
include $(top_builddir)/src/Makefile.global include $(top_builddir)/src/Makefile.global
OBJS = nbtcompare.o nbtinsert.o nbtpage.o nbtree.o nbtsearch.o \ OBJS = nbtcompare.o nbtinsert.o nbtpage.o nbtree.o nbtsearch.o \
nbtutils.o nbtsort.o nbtvalidate.o nbtxlog.o nbtsplitloc.o nbtutils.o nbtsort.o nbtvalidate.o nbtxlog.o
include $(top_srcdir)/src/backend/common.mk include $(top_srcdir)/src/backend/common.mk
...@@ -143,9 +143,9 @@ Lehman and Yao assume fixed-size keys, but we must deal with ...@@ -143,9 +143,9 @@ Lehman and Yao assume fixed-size keys, but we must deal with
variable-size keys. Therefore there is not a fixed maximum number of variable-size keys. Therefore there is not a fixed maximum number of
keys per page; we just stuff in as many as will fit. When we split a keys per page; we just stuff in as many as will fit. When we split a
page, we try to equalize the number of bytes, not items, assigned to page, we try to equalize the number of bytes, not items, assigned to
each of the resulting pages. Note we must include the incoming item in pages (though suffix truncation is also considered). Note we must include
this calculation, otherwise it is possible to find that the incoming the incoming item in this calculation, otherwise it is possible to find
item doesn't fit on the split page where it needs to go! that the incoming item doesn't fit on the split page where it needs to go!
The Deletion Algorithm The Deletion Algorithm
---------------------- ----------------------
...@@ -649,6 +649,47 @@ variable-length types, such as text. An opclass support function could ...@@ -649,6 +649,47 @@ variable-length types, such as text. An opclass support function could
manufacture the shortest possible key value that still correctly separates manufacture the shortest possible key value that still correctly separates
each half of a leaf page split. each half of a leaf page split.
There is sophisticated criteria for choosing a leaf page split point. The
general idea is to make suffix truncation effective without unduly
influencing the balance of space for each half of the page split. The
choice of leaf split point can be thought of as a choice among points
*between* items on the page to be split, at least if you pretend that the
incoming tuple was placed on the page already (you have to pretend because
there won't actually be enough space for it on the page). Choosing the
split point between two index tuples where the first non-equal attribute
appears as early as possible results in truncating away as many suffix
attributes as possible. Evenly balancing space among each half of the
split is usually the first concern, but even small adjustments in the
precise split point can allow truncation to be far more effective.
Suffix truncation is primarily valuable because it makes pivot tuples
smaller, which delays splits of internal pages, but that isn't the only
reason why it's effective. Even truncation that doesn't make pivot tuples
smaller due to alignment still prevents pivot tuples from being more
restrictive than truly necessary in how they describe which values belong
on which pages.
While it's not possible to correctly perform suffix truncation during
internal page splits, it's still useful to be discriminating when splitting
an internal page. The split point that implies a downlink be inserted in
the parent that's the smallest one available within an acceptable range of
the fillfactor-wise optimal split point is chosen. This idea also comes
from the Prefix B-Tree paper. This process has much in common with what
happens at the leaf level to make suffix truncation effective. The overall
effect is that suffix truncation tends to produce smaller, more
discriminating pivot tuples, especially early in the lifetime of the index,
while biasing internal page splits makes the earlier, smaller pivot tuples
end up in the root page, delaying root page splits.
Logical duplicates are given special consideration. The logic for
selecting a split point goes to great lengths to avoid having duplicates
span more than one page, and almost always manages to pick a split point
between two user-key-distinct tuples, accepting a completely lopsided split
if it must. When a page that's already full of duplicates must be split,
the fallback strategy assumes that duplicates are mostly inserted in
ascending heap TID order. The page is split in a way that leaves the left
half of the page mostly full, and the right half of the page mostly empty.
Notes About Data Representation Notes About Data Representation
------------------------------- -------------------------------
......
This diff is collapsed.
This diff is collapsed.
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
#include "access/relscan.h" #include "access/relscan.h"
#include "miscadmin.h" #include "miscadmin.h"
#include "utils/array.h" #include "utils/array.h"
#include "utils/datum.h"
#include "utils/lsyscache.h" #include "utils/lsyscache.h"
#include "utils/memutils.h" #include "utils/memutils.h"
#include "utils/rel.h" #include "utils/rel.h"
...@@ -2295,6 +2296,60 @@ _bt_keep_natts(Relation rel, IndexTuple lastleft, IndexTuple firstright, ...@@ -2295,6 +2296,60 @@ _bt_keep_natts(Relation rel, IndexTuple lastleft, IndexTuple firstright,
return keepnatts; return keepnatts;
} }
/*
* _bt_keep_natts_fast - fast bitwise variant of _bt_keep_natts.
*
* This is exported so that a candidate split point can have its effect on
* suffix truncation inexpensively evaluated ahead of time when finding a
* split location. A naive bitwise approach to datum comparisons is used to
* save cycles.
*
* The approach taken here usually provides the same answer as _bt_keep_natts
* will (for the same pair of tuples from a heapkeyspace index), since the
* majority of btree opclasses can never indicate that two datums are equal
* unless they're bitwise equal (once detoasted). Similarly, result may
* differ from the _bt_keep_natts result when either tuple has TOASTed datums,
* though this is barely possible in practice.
*
* These issues must be acceptable to callers, typically because they're only
* concerned about making suffix truncation as effective as possible without
* leaving excessive amounts of free space on either side of page split.
* Callers can rely on the fact that attributes considered equal here are
* definitely also equal according to _bt_keep_natts.
*/
int
_bt_keep_natts_fast(Relation rel, IndexTuple lastleft, IndexTuple firstright)
{
TupleDesc itupdesc = RelationGetDescr(rel);
int keysz = IndexRelationGetNumberOfKeyAttributes(rel);
int keepnatts;
keepnatts = 1;
for (int attnum = 1; attnum <= keysz; attnum++)
{
Datum datum1,
datum2;
bool isNull1,
isNull2;
Form_pg_attribute att;
datum1 = index_getattr(lastleft, attnum, itupdesc, &isNull1);
datum2 = index_getattr(firstright, attnum, itupdesc, &isNull2);
att = TupleDescAttr(itupdesc, attnum - 1);
if (isNull1 != isNull2)
break;
if (!isNull1 &&
!datumIsEqual(datum1, datum2, att->attbyval, att->attlen))
break;
keepnatts++;
}
return keepnatts;
}
/* /*
* _bt_check_natts() -- Verify tuple has expected number of attributes. * _bt_check_natts() -- Verify tuple has expected number of attributes.
* *
......
...@@ -160,11 +160,15 @@ typedef struct BTMetaPageData ...@@ -160,11 +160,15 @@ typedef struct BTMetaPageData
* For pages above the leaf level, we use a fixed 70% fillfactor. * For pages above the leaf level, we use a fixed 70% fillfactor.
* The fillfactor is applied during index build and when splitting * The fillfactor is applied during index build and when splitting
* a rightmost page; when splitting non-rightmost pages we try to * a rightmost page; when splitting non-rightmost pages we try to
* divide the data equally. * divide the data equally. When splitting a page that's entirely
* filled with a single value (duplicates), the effective leaf-page
* fillfactor is 96%, regardless of whether the page is a rightmost
* page.
*/ */
#define BTREE_MIN_FILLFACTOR 10 #define BTREE_MIN_FILLFACTOR 10
#define BTREE_DEFAULT_FILLFACTOR 90 #define BTREE_DEFAULT_FILLFACTOR 90
#define BTREE_NONLEAF_FILLFACTOR 70 #define BTREE_NONLEAF_FILLFACTOR 70
#define BTREE_SINGLEVAL_FILLFACTOR 96
/* /*
* In general, the btree code tries to localize its knowledge about * In general, the btree code tries to localize its knowledge about
...@@ -711,6 +715,13 @@ extern bool _bt_doinsert(Relation rel, IndexTuple itup, ...@@ -711,6 +715,13 @@ extern bool _bt_doinsert(Relation rel, IndexTuple itup,
extern Buffer _bt_getstackbuf(Relation rel, BTStack stack); extern Buffer _bt_getstackbuf(Relation rel, BTStack stack);
extern void _bt_finish_split(Relation rel, Buffer bbuf, BTStack stack); extern void _bt_finish_split(Relation rel, Buffer bbuf, BTStack stack);
/*
* prototypes for functions in nbtsplitloc.c
*/
extern OffsetNumber _bt_findsplitloc(Relation rel, Page page,
OffsetNumber newitemoff, Size newitemsz, IndexTuple newitem,
bool *newitemonleft);
/* /*
* prototypes for functions in nbtpage.c * prototypes for functions in nbtpage.c
*/ */
...@@ -777,6 +788,8 @@ extern bool btproperty(Oid index_oid, int attno, ...@@ -777,6 +788,8 @@ extern bool btproperty(Oid index_oid, int attno,
bool *res, bool *isnull); bool *res, bool *isnull);
extern IndexTuple _bt_truncate(Relation rel, IndexTuple lastleft, extern IndexTuple _bt_truncate(Relation rel, IndexTuple lastleft,
IndexTuple firstright, BTScanInsert itup_key); IndexTuple firstright, BTScanInsert itup_key);
extern int _bt_keep_natts_fast(Relation rel, IndexTuple lastleft,
IndexTuple firstright);
extern bool _bt_check_natts(Relation rel, bool heapkeyspace, Page page, extern bool _bt_check_natts(Relation rel, bool heapkeyspace, Page page,
OffsetNumber offnum); OffsetNumber offnum);
extern void _bt_check_third_page(Relation rel, Relation heap, extern void _bt_check_third_page(Relation rel, Relation heap,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment