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