1 /*-------------------------------------------------------------------------
2 *
3 * ginvacuum.c
4 * delete & vacuum routines for the postgres GIN
5 *
6 *
7 * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
8 * Portions Copyright (c) 1994, Regents of the University of California
9 *
10 * IDENTIFICATION
11 * src/backend/access/gin/ginvacuum.c
12 *-------------------------------------------------------------------------
13 */
14
15 #include "postgres.h"
16
17 #include "access/gin_private.h"
18 #include "access/ginxlog.h"
19 #include "access/xloginsert.h"
20 #include "commands/vacuum.h"
21 #include "miscadmin.h"
22 #include "postmaster/autovacuum.h"
23 #include "storage/indexfsm.h"
24 #include "storage/lmgr.h"
25 #include "storage/predicate.h"
26 #include "utils/memutils.h"
27
28 struct GinVacuumState
29 {
30 Relation index;
31 IndexBulkDeleteResult *result;
32 IndexBulkDeleteCallback callback;
33 void *callback_state;
34 GinState ginstate;
35 BufferAccessStrategy strategy;
36 MemoryContext tmpCxt;
37 };
38
39 /*
40 * Vacuums an uncompressed posting list. The size of the must can be specified
41 * in number of items (nitems).
42 *
43 * If none of the items need to be removed, returns NULL. Otherwise returns
44 * a new palloc'd array with the remaining items. The number of remaining
45 * items is returned in *nremaining.
46 */
47 ItemPointer
ginVacuumItemPointers(GinVacuumState * gvs,ItemPointerData * items,int nitem,int * nremaining)48 ginVacuumItemPointers(GinVacuumState *gvs, ItemPointerData *items,
49 int nitem, int *nremaining)
50 {
51 int i,
52 remaining = 0;
53 ItemPointer tmpitems = NULL;
54
55 /*
56 * Iterate over TIDs array
57 */
58 for (i = 0; i < nitem; i++)
59 {
60 if (gvs->callback(items + i, gvs->callback_state))
61 {
62 gvs->result->tuples_removed += 1;
63 if (!tmpitems)
64 {
65 /*
66 * First TID to be deleted: allocate memory to hold the
67 * remaining items.
68 */
69 tmpitems = palloc(sizeof(ItemPointerData) * nitem);
70 memcpy(tmpitems, items, sizeof(ItemPointerData) * i);
71 }
72 }
73 else
74 {
75 gvs->result->num_index_tuples += 1;
76 if (tmpitems)
77 tmpitems[remaining] = items[i];
78 remaining++;
79 }
80 }
81
82 *nremaining = remaining;
83 return tmpitems;
84 }
85
86 /*
87 * Create a WAL record for vacuuming entry tree leaf page.
88 */
89 static void
xlogVacuumPage(Relation index,Buffer buffer)90 xlogVacuumPage(Relation index, Buffer buffer)
91 {
92 Page page = BufferGetPage(buffer);
93 XLogRecPtr recptr;
94
95 /* This is only used for entry tree leaf pages. */
96 Assert(!GinPageIsData(page));
97 Assert(GinPageIsLeaf(page));
98
99 if (!RelationNeedsWAL(index))
100 return;
101
102 /*
103 * Always create a full image, we don't track the changes on the page at
104 * any more fine-grained level. This could obviously be improved...
105 */
106 XLogBeginInsert();
107 XLogRegisterBuffer(0, buffer, REGBUF_FORCE_IMAGE | REGBUF_STANDARD);
108
109 recptr = XLogInsert(RM_GIN_ID, XLOG_GIN_VACUUM_PAGE);
110 PageSetLSN(page, recptr);
111 }
112
113
114 typedef struct DataPageDeleteStack
115 {
116 struct DataPageDeleteStack *child;
117 struct DataPageDeleteStack *parent;
118
119 BlockNumber blkno; /* current block number */
120 Buffer leftBuffer; /* pinned and locked rightest non-deleted page
121 * on left */
122 bool isRoot;
123 } DataPageDeleteStack;
124
125
126 /*
127 * Delete a posting tree page.
128 */
129 static void
ginDeletePage(GinVacuumState * gvs,BlockNumber deleteBlkno,BlockNumber leftBlkno,BlockNumber parentBlkno,OffsetNumber myoff,bool isParentRoot)130 ginDeletePage(GinVacuumState *gvs, BlockNumber deleteBlkno, BlockNumber leftBlkno,
131 BlockNumber parentBlkno, OffsetNumber myoff, bool isParentRoot)
132 {
133 Buffer dBuffer;
134 Buffer lBuffer;
135 Buffer pBuffer;
136 Page page,
137 parentPage;
138 BlockNumber rightlink;
139
140 /*
141 * This function MUST be called only if someone of parent pages hold
142 * exclusive cleanup lock. This guarantees that no insertions currently
143 * happen in this subtree. Caller also acquires Exclusive locks on
144 * deletable, parent and left pages.
145 */
146 lBuffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, leftBlkno,
147 RBM_NORMAL, gvs->strategy);
148 dBuffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, deleteBlkno,
149 RBM_NORMAL, gvs->strategy);
150 pBuffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, parentBlkno,
151 RBM_NORMAL, gvs->strategy);
152
153 page = BufferGetPage(dBuffer);
154 rightlink = GinPageGetOpaque(page)->rightlink;
155
156 /*
157 * Any insert which would have gone on the leaf block will now go to its
158 * right sibling.
159 */
160 PredicateLockPageCombine(gvs->index, deleteBlkno, rightlink);
161
162 START_CRIT_SECTION();
163
164 /* Unlink the page by changing left sibling's rightlink */
165 page = BufferGetPage(lBuffer);
166 GinPageGetOpaque(page)->rightlink = rightlink;
167
168 /* Delete downlink from parent */
169 parentPage = BufferGetPage(pBuffer);
170 #ifdef USE_ASSERT_CHECKING
171 do
172 {
173 PostingItem *tod = GinDataPageGetPostingItem(parentPage, myoff);
174
175 Assert(PostingItemGetBlockNumber(tod) == deleteBlkno);
176 } while (0);
177 #endif
178 GinPageDeletePostingItem(parentPage, myoff);
179
180 page = BufferGetPage(dBuffer);
181
182 /*
183 * we shouldn't change rightlink field to save workability of running
184 * search scan
185 */
186
187 /*
188 * Mark page as deleted, and remember last xid which could know its
189 * address.
190 */
191 GinPageSetDeleted(page);
192 GinPageSetDeleteXid(page, ReadNewTransactionId());
193
194 MarkBufferDirty(pBuffer);
195 MarkBufferDirty(lBuffer);
196 MarkBufferDirty(dBuffer);
197
198 if (RelationNeedsWAL(gvs->index))
199 {
200 XLogRecPtr recptr;
201 ginxlogDeletePage data;
202
203 /*
204 * We can't pass REGBUF_STANDARD for the deleted page, because we
205 * didn't set pd_lower on pre-9.4 versions. The page might've been
206 * binary-upgraded from an older version, and hence not have pd_lower
207 * set correctly. Ditto for the left page, but removing the item from
208 * the parent updated its pd_lower, so we know that's OK at this
209 * point.
210 */
211 XLogBeginInsert();
212 XLogRegisterBuffer(0, dBuffer, 0);
213 XLogRegisterBuffer(1, pBuffer, REGBUF_STANDARD);
214 XLogRegisterBuffer(2, lBuffer, 0);
215
216 data.parentOffset = myoff;
217 data.rightLink = GinPageGetOpaque(page)->rightlink;
218 data.deleteXid = GinPageGetDeleteXid(page);
219
220 XLogRegisterData((char *) &data, sizeof(ginxlogDeletePage));
221
222 recptr = XLogInsert(RM_GIN_ID, XLOG_GIN_DELETE_PAGE);
223 PageSetLSN(page, recptr);
224 PageSetLSN(parentPage, recptr);
225 PageSetLSN(BufferGetPage(lBuffer), recptr);
226 }
227
228 ReleaseBuffer(pBuffer);
229 ReleaseBuffer(lBuffer);
230 ReleaseBuffer(dBuffer);
231
232 END_CRIT_SECTION();
233
234 gvs->result->pages_deleted++;
235 }
236
237
238 /*
239 * Scans posting tree and deletes empty pages. Caller must lock root page for
240 * cleanup. During scan path from root to current page is kept exclusively
241 * locked. Also keep left page exclusively locked, because ginDeletePage()
242 * needs it. If we try to relock left page later, it could deadlock with
243 * ginStepRight().
244 */
245 static bool
ginScanToDelete(GinVacuumState * gvs,BlockNumber blkno,bool isRoot,DataPageDeleteStack * parent,OffsetNumber myoff)246 ginScanToDelete(GinVacuumState *gvs, BlockNumber blkno, bool isRoot,
247 DataPageDeleteStack *parent, OffsetNumber myoff)
248 {
249 DataPageDeleteStack *me;
250 Buffer buffer;
251 Page page;
252 bool meDelete = false;
253 bool isempty;
254
255 if (isRoot)
256 {
257 me = parent;
258 }
259 else
260 {
261 if (!parent->child)
262 {
263 me = (DataPageDeleteStack *) palloc0(sizeof(DataPageDeleteStack));
264 me->parent = parent;
265 parent->child = me;
266 me->leftBuffer = InvalidBuffer;
267 }
268 else
269 me = parent->child;
270 }
271
272 buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno,
273 RBM_NORMAL, gvs->strategy);
274
275 if (!isRoot)
276 LockBuffer(buffer, GIN_EXCLUSIVE);
277
278 page = BufferGetPage(buffer);
279
280 Assert(GinPageIsData(page));
281
282 if (!GinPageIsLeaf(page))
283 {
284 OffsetNumber i;
285
286 me->blkno = blkno;
287 for (i = FirstOffsetNumber; i <= GinPageGetOpaque(page)->maxoff; i++)
288 {
289 PostingItem *pitem = GinDataPageGetPostingItem(page, i);
290
291 if (ginScanToDelete(gvs, PostingItemGetBlockNumber(pitem), false, me, i))
292 i--;
293 }
294
295 if (GinPageRightMost(page) && BufferIsValid(me->child->leftBuffer))
296 {
297 UnlockReleaseBuffer(me->child->leftBuffer);
298 me->child->leftBuffer = InvalidBuffer;
299 }
300 }
301
302 if (GinPageIsLeaf(page))
303 isempty = GinDataLeafPageIsEmpty(page);
304 else
305 isempty = GinPageGetOpaque(page)->maxoff < FirstOffsetNumber;
306
307 if (isempty)
308 {
309 /* we never delete the left- or rightmost branch */
310 if (BufferIsValid(me->leftBuffer) && !GinPageRightMost(page))
311 {
312 Assert(!isRoot);
313 ginDeletePage(gvs, blkno, BufferGetBlockNumber(me->leftBuffer),
314 me->parent->blkno, myoff, me->parent->isRoot);
315 meDelete = true;
316 }
317 }
318
319 if (!meDelete)
320 {
321 if (BufferIsValid(me->leftBuffer))
322 UnlockReleaseBuffer(me->leftBuffer);
323 me->leftBuffer = buffer;
324 }
325 else
326 {
327 if (!isRoot)
328 LockBuffer(buffer, GIN_UNLOCK);
329
330 ReleaseBuffer(buffer);
331 }
332
333 if (isRoot)
334 ReleaseBuffer(buffer);
335
336 return meDelete;
337 }
338
339
340 /*
341 * Scan through posting tree leafs, delete empty tuples. Returns true if there
342 * is at least one empty page.
343 */
344 static bool
ginVacuumPostingTreeLeaves(GinVacuumState * gvs,BlockNumber blkno)345 ginVacuumPostingTreeLeaves(GinVacuumState *gvs, BlockNumber blkno)
346 {
347 Buffer buffer;
348 Page page;
349 bool hasVoidPage = false;
350 MemoryContext oldCxt;
351
352 /* Find leftmost leaf page of posting tree and lock it in exclusive mode */
353 while (true)
354 {
355 PostingItem *pitem;
356
357 buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno,
358 RBM_NORMAL, gvs->strategy);
359 LockBuffer(buffer, GIN_SHARE);
360 page = BufferGetPage(buffer);
361
362 Assert(GinPageIsData(page));
363
364 if (GinPageIsLeaf(page))
365 {
366 LockBuffer(buffer, GIN_UNLOCK);
367 LockBuffer(buffer, GIN_EXCLUSIVE);
368 break;
369 }
370
371 Assert(PageGetMaxOffsetNumber(page) >= FirstOffsetNumber);
372
373 pitem = GinDataPageGetPostingItem(page, FirstOffsetNumber);
374 blkno = PostingItemGetBlockNumber(pitem);
375 Assert(blkno != InvalidBlockNumber);
376
377 UnlockReleaseBuffer(buffer);
378 }
379
380 /* Iterate all posting tree leaves using rightlinks and vacuum them */
381 while (true)
382 {
383 oldCxt = MemoryContextSwitchTo(gvs->tmpCxt);
384 ginVacuumPostingTreeLeaf(gvs->index, buffer, gvs);
385 MemoryContextSwitchTo(oldCxt);
386 MemoryContextReset(gvs->tmpCxt);
387
388 if (GinDataLeafPageIsEmpty(page))
389 hasVoidPage = true;
390
391 blkno = GinPageGetOpaque(page)->rightlink;
392
393 UnlockReleaseBuffer(buffer);
394
395 if (blkno == InvalidBlockNumber)
396 break;
397
398 buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno,
399 RBM_NORMAL, gvs->strategy);
400 LockBuffer(buffer, GIN_EXCLUSIVE);
401 page = BufferGetPage(buffer);
402 }
403
404 return hasVoidPage;
405 }
406
407 static void
ginVacuumPostingTree(GinVacuumState * gvs,BlockNumber rootBlkno)408 ginVacuumPostingTree(GinVacuumState *gvs, BlockNumber rootBlkno)
409 {
410 if (ginVacuumPostingTreeLeaves(gvs, rootBlkno))
411 {
412 /*
413 * There is at least one empty page. So we have to rescan the tree
414 * deleting empty pages.
415 */
416 Buffer buffer;
417 DataPageDeleteStack root,
418 *ptr,
419 *tmp;
420
421 buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, rootBlkno,
422 RBM_NORMAL, gvs->strategy);
423
424 /*
425 * Lock posting tree root for cleanup to ensure there are no
426 * concurrent inserts.
427 */
428 LockBufferForCleanup(buffer);
429
430 memset(&root, 0, sizeof(DataPageDeleteStack));
431 root.leftBuffer = InvalidBuffer;
432 root.isRoot = true;
433
434 ginScanToDelete(gvs, rootBlkno, true, &root, InvalidOffsetNumber);
435
436 ptr = root.child;
437
438 while (ptr)
439 {
440 tmp = ptr->child;
441 pfree(ptr);
442 ptr = tmp;
443 }
444
445 UnlockReleaseBuffer(buffer);
446 }
447 }
448
449 /*
450 * returns modified page or NULL if page isn't modified.
451 * Function works with original page until first change is occurred,
452 * then page is copied into temporary one.
453 */
454 static Page
ginVacuumEntryPage(GinVacuumState * gvs,Buffer buffer,BlockNumber * roots,uint32 * nroot)455 ginVacuumEntryPage(GinVacuumState *gvs, Buffer buffer, BlockNumber *roots, uint32 *nroot)
456 {
457 Page origpage = BufferGetPage(buffer),
458 tmppage;
459 OffsetNumber i,
460 maxoff = PageGetMaxOffsetNumber(origpage);
461
462 tmppage = origpage;
463
464 *nroot = 0;
465
466 for (i = FirstOffsetNumber; i <= maxoff; i++)
467 {
468 IndexTuple itup = (IndexTuple) PageGetItem(tmppage, PageGetItemId(tmppage, i));
469
470 if (GinIsPostingTree(itup))
471 {
472 /*
473 * store posting tree's roots for further processing, we can't
474 * vacuum it just now due to risk of deadlocks with scans/inserts
475 */
476 roots[*nroot] = GinGetDownlink(itup);
477 (*nroot)++;
478 }
479 else if (GinGetNPosting(itup) > 0)
480 {
481 int nitems;
482 ItemPointer items_orig;
483 bool free_items_orig;
484 ItemPointer items;
485
486 /* Get list of item pointers from the tuple. */
487 if (GinItupIsCompressed(itup))
488 {
489 items_orig = ginPostingListDecode((GinPostingList *) GinGetPosting(itup), &nitems);
490 free_items_orig = true;
491 }
492 else
493 {
494 items_orig = (ItemPointer) GinGetPosting(itup);
495 nitems = GinGetNPosting(itup);
496 free_items_orig = false;
497 }
498
499 /* Remove any items from the list that need to be vacuumed. */
500 items = ginVacuumItemPointers(gvs, items_orig, nitems, &nitems);
501
502 if (free_items_orig)
503 pfree(items_orig);
504
505 /* If any item pointers were removed, recreate the tuple. */
506 if (items)
507 {
508 OffsetNumber attnum;
509 Datum key;
510 GinNullCategory category;
511 GinPostingList *plist;
512 int plistsize;
513
514 if (nitems > 0)
515 {
516 plist = ginCompressPostingList(items, nitems, GinMaxItemSize, NULL);
517 plistsize = SizeOfGinPostingList(plist);
518 }
519 else
520 {
521 plist = NULL;
522 plistsize = 0;
523 }
524
525 /*
526 * if we already created a temporary page, make changes in
527 * place
528 */
529 if (tmppage == origpage)
530 {
531 /*
532 * On first difference, create a temporary copy of the
533 * page and copy the tuple's posting list to it.
534 */
535 tmppage = PageGetTempPageCopy(origpage);
536
537 /* set itup pointer to new page */
538 itup = (IndexTuple) PageGetItem(tmppage, PageGetItemId(tmppage, i));
539 }
540
541 attnum = gintuple_get_attrnum(&gvs->ginstate, itup);
542 key = gintuple_get_key(&gvs->ginstate, itup, &category);
543 itup = GinFormTuple(&gvs->ginstate, attnum, key, category,
544 (char *) plist, plistsize,
545 nitems, true);
546 if (plist)
547 pfree(plist);
548 PageIndexTupleDelete(tmppage, i);
549
550 if (PageAddItem(tmppage, (Item) itup, IndexTupleSize(itup), i, false, false) != i)
551 elog(ERROR, "failed to add item to index page in \"%s\"",
552 RelationGetRelationName(gvs->index));
553
554 pfree(itup);
555 pfree(items);
556 }
557 }
558 }
559
560 return (tmppage == origpage) ? NULL : tmppage;
561 }
562
563 IndexBulkDeleteResult *
ginbulkdelete(IndexVacuumInfo * info,IndexBulkDeleteResult * stats,IndexBulkDeleteCallback callback,void * callback_state)564 ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
565 IndexBulkDeleteCallback callback, void *callback_state)
566 {
567 Relation index = info->index;
568 BlockNumber blkno = GIN_ROOT_BLKNO;
569 GinVacuumState gvs;
570 Buffer buffer;
571 BlockNumber rootOfPostingTree[BLCKSZ / (sizeof(IndexTupleData) + sizeof(ItemId))];
572 uint32 nRoot;
573
574 gvs.tmpCxt = AllocSetContextCreate(CurrentMemoryContext,
575 "Gin vacuum temporary context",
576 ALLOCSET_DEFAULT_SIZES);
577 gvs.index = index;
578 gvs.callback = callback;
579 gvs.callback_state = callback_state;
580 gvs.strategy = info->strategy;
581 initGinState(&gvs.ginstate, index);
582
583 /* first time through? */
584 if (stats == NULL)
585 {
586 /* Yes, so initialize stats to zeroes */
587 stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult));
588
589 /*
590 * and cleanup any pending inserts
591 */
592 ginInsertCleanup(&gvs.ginstate, !IsAutoVacuumWorkerProcess(),
593 false, true, stats);
594 }
595
596 /* we'll re-count the tuples each time */
597 stats->num_index_tuples = 0;
598 gvs.result = stats;
599
600 buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
601 RBM_NORMAL, info->strategy);
602
603 /* find leaf page */
604 for (;;)
605 {
606 Page page = BufferGetPage(buffer);
607 IndexTuple itup;
608
609 LockBuffer(buffer, GIN_SHARE);
610
611 Assert(!GinPageIsData(page));
612
613 if (GinPageIsLeaf(page))
614 {
615 LockBuffer(buffer, GIN_UNLOCK);
616 LockBuffer(buffer, GIN_EXCLUSIVE);
617
618 if (blkno == GIN_ROOT_BLKNO && !GinPageIsLeaf(page))
619 {
620 LockBuffer(buffer, GIN_UNLOCK);
621 continue; /* check it one more */
622 }
623 break;
624 }
625
626 Assert(PageGetMaxOffsetNumber(page) >= FirstOffsetNumber);
627
628 itup = (IndexTuple) PageGetItem(page, PageGetItemId(page, FirstOffsetNumber));
629 blkno = GinGetDownlink(itup);
630 Assert(blkno != InvalidBlockNumber);
631
632 UnlockReleaseBuffer(buffer);
633 buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
634 RBM_NORMAL, info->strategy);
635 }
636
637 /* right now we found leftmost page in entry's BTree */
638
639 for (;;)
640 {
641 Page page = BufferGetPage(buffer);
642 Page resPage;
643 uint32 i;
644
645 Assert(!GinPageIsData(page));
646
647 resPage = ginVacuumEntryPage(&gvs, buffer, rootOfPostingTree, &nRoot);
648
649 blkno = GinPageGetOpaque(page)->rightlink;
650
651 if (resPage)
652 {
653 START_CRIT_SECTION();
654 PageRestoreTempPage(resPage, page);
655 MarkBufferDirty(buffer);
656 xlogVacuumPage(gvs.index, buffer);
657 UnlockReleaseBuffer(buffer);
658 END_CRIT_SECTION();
659 }
660 else
661 {
662 UnlockReleaseBuffer(buffer);
663 }
664
665 vacuum_delay_point();
666
667 for (i = 0; i < nRoot; i++)
668 {
669 ginVacuumPostingTree(&gvs, rootOfPostingTree[i]);
670 vacuum_delay_point();
671 }
672
673 if (blkno == InvalidBlockNumber) /* rightmost page */
674 break;
675
676 buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
677 RBM_NORMAL, info->strategy);
678 LockBuffer(buffer, GIN_EXCLUSIVE);
679 }
680
681 MemoryContextDelete(gvs.tmpCxt);
682
683 return gvs.result;
684 }
685
686 IndexBulkDeleteResult *
ginvacuumcleanup(IndexVacuumInfo * info,IndexBulkDeleteResult * stats)687 ginvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
688 {
689 Relation index = info->index;
690 bool needLock;
691 BlockNumber npages,
692 blkno;
693 BlockNumber totFreePages;
694 GinState ginstate;
695 GinStatsData idxStat;
696
697 /*
698 * In an autovacuum analyze, we want to clean up pending insertions.
699 * Otherwise, an ANALYZE-only call is a no-op.
700 */
701 if (info->analyze_only)
702 {
703 if (IsAutoVacuumWorkerProcess())
704 {
705 initGinState(&ginstate, index);
706 ginInsertCleanup(&ginstate, false, true, true, stats);
707 }
708 return stats;
709 }
710
711 /*
712 * Set up all-zero stats and cleanup pending inserts if ginbulkdelete
713 * wasn't called
714 */
715 if (stats == NULL)
716 {
717 stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult));
718 initGinState(&ginstate, index);
719 ginInsertCleanup(&ginstate, !IsAutoVacuumWorkerProcess(),
720 false, true, stats);
721 }
722
723 memset(&idxStat, 0, sizeof(idxStat));
724
725 /*
726 * XXX we always report the heap tuple count as the number of index
727 * entries. This is bogus if the index is partial, but it's real hard to
728 * tell how many distinct heap entries are referenced by a GIN index.
729 */
730 stats->num_index_tuples = info->num_heap_tuples;
731 stats->estimated_count = info->estimated_count;
732
733 /*
734 * Need lock unless it's local to this backend.
735 */
736 needLock = !RELATION_IS_LOCAL(index);
737
738 if (needLock)
739 LockRelationForExtension(index, ExclusiveLock);
740 npages = RelationGetNumberOfBlocks(index);
741 if (needLock)
742 UnlockRelationForExtension(index, ExclusiveLock);
743
744 totFreePages = 0;
745
746 for (blkno = GIN_ROOT_BLKNO; blkno < npages; blkno++)
747 {
748 Buffer buffer;
749 Page page;
750
751 vacuum_delay_point();
752
753 buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
754 RBM_NORMAL, info->strategy);
755 LockBuffer(buffer, GIN_SHARE);
756 page = (Page) BufferGetPage(buffer);
757
758 if (GinPageIsRecyclable(page))
759 {
760 Assert(blkno != GIN_ROOT_BLKNO);
761 RecordFreeIndexPage(index, blkno);
762 totFreePages++;
763 }
764 else if (GinPageIsData(page))
765 {
766 idxStat.nDataPages++;
767 }
768 else if (!GinPageIsList(page))
769 {
770 idxStat.nEntryPages++;
771
772 if (GinPageIsLeaf(page))
773 idxStat.nEntries += PageGetMaxOffsetNumber(page);
774 }
775
776 UnlockReleaseBuffer(buffer);
777 }
778
779 /* Update the metapage with accurate page and entry counts */
780 idxStat.nTotalPages = npages;
781 ginUpdateStats(info->index, &idxStat, false);
782
783 /* Finally, vacuum the FSM */
784 IndexFreeSpaceMapVacuum(info->index);
785
786 stats->pages_free = totFreePages;
787
788 if (needLock)
789 LockRelationForExtension(index, ExclusiveLock);
790 stats->num_pages = RelationGetNumberOfBlocks(index);
791 if (needLock)
792 UnlockRelationForExtension(index, ExclusiveLock);
793
794 return stats;
795 }
796