Branch data Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : *
3 : : * ginvacuum.c
4 : : * delete & vacuum routines for the postgres GIN
5 : : *
6 : : *
7 : : * Portions Copyright (c) 1996-2026, 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 "storage/indexfsm.h"
23 : : #include "storage/lmgr.h"
24 : : #include "storage/predicate.h"
25 : : #include "storage/read_stream.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
48 : 168168 : ginVacuumItemPointers(GinVacuumState *gvs, ItemPointerData *items,
49 : : int nitem, int *nremaining)
50 : : {
51 : : int i,
52 : 168168 : remaining = 0;
53 : 168168 : ItemPointer tmpitems = NULL;
54 : :
55 : : /*
56 : : * Iterate over TIDs array
57 : : */
58 [ + + ]: 833015 : for (i = 0; i < nitem; i++)
59 : : {
60 [ + + ]: 664847 : if (gvs->callback(items + i, gvs->callback_state))
61 : : {
62 : 517065 : gvs->result->tuples_removed += 1;
63 [ + + ]: 517065 : if (!tmpitems)
64 : : {
65 : : /*
66 : : * First TID to be deleted: allocate memory to hold the
67 : : * remaining items.
68 : : */
69 : 160704 : tmpitems = palloc_array(ItemPointerData, nitem);
70 : 160704 : memcpy(tmpitems, items, sizeof(ItemPointerData) * i);
71 : : }
72 : : }
73 : : else
74 : : {
75 : 147782 : gvs->result->num_index_tuples += 1;
76 [ + + ]: 147782 : if (tmpitems)
77 : 1592 : tmpitems[remaining] = items[i];
78 : 147782 : remaining++;
79 : : }
80 : : }
81 : :
82 : 168168 : *nremaining = remaining;
83 : 168168 : return tmpitems;
84 : : }
85 : :
86 : : /*
87 : : * Create a WAL record for vacuuming entry tree leaf page.
88 : : */
89 : : static void
90 : 1148 : xlogVacuumPage(Relation index, Buffer buffer)
91 : : {
92 : 1148 : 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 [ - + - - : 1148 : if (!RelationNeedsWAL(index))
- - - - ]
100 : 1148 : 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 : 0 : XLogBeginInsert();
107 : 0 : XLogRegisterBuffer(0, buffer, REGBUF_FORCE_IMAGE | REGBUF_STANDARD);
108 : :
109 : 0 : recptr = XLogInsert(RM_GIN_ID, XLOG_GIN_VACUUM_PAGE);
110 : 0 : PageSetLSN(page, recptr);
111 : : }
112 : :
113 : :
114 : : /*
115 : : * Stack entry used during posting tree empty-page deletion scan.
116 : : *
117 : : * One DataPageDeleteStack entry is allocated per tree level. As
118 : : * ginScanPostingTreeToDelete() recurses down the tree, each entry tracks
119 : : * the buffer of the page currently being visited at that level ('buffer'),
120 : : * and the buffer of its left sibling ('leftBuffer'). The left page is kept
121 : : * pinned and exclusively locked because ginDeletePostingPage() needs it to
122 : : * update the sibling chain; acquiring it later could deadlock with
123 : : * ginStepRight(), which locks pages left-to-right.
124 : : */
125 : : typedef struct DataPageDeleteStack
126 : : {
127 : : struct DataPageDeleteStack *child;
128 : : struct DataPageDeleteStack *parent;
129 : :
130 : : Buffer buffer; /* buffer for the page being visited at this
131 : : * tree level */
132 : : Buffer leftBuffer; /* pinned and locked rightmost non-deleted
133 : : * sibling to the left of the current page */
134 : : OffsetNumber myoff; /* offset of this page's downlink in the
135 : : * parent */
136 : : bool isRoot;
137 : : } DataPageDeleteStack;
138 : :
139 : :
140 : : /*
141 : : * Delete a posting tree page.
142 : : *
143 : : * Removes the page identified by dBuffer from the posting tree by updating
144 : : * the left sibling's rightlink (in lBuffer) to skip over the deleted page,
145 : : * and removing the downlink from the parent page (in pBuffer). All three
146 : : * buffers must already have been pinned and exclusively locked by the caller.
147 : : *
148 : : * The buffers are NOT released nor unlocked here; the caller is responsible
149 : : * for this.
150 : : */
151 : : static void
152 : 9 : ginDeletePostingPage(GinVacuumState *gvs, Buffer dBuffer, Buffer lBuffer,
153 : : Buffer pBuffer, OffsetNumber myoff, bool isParentRoot)
154 : : {
155 : : Page page,
156 : : parentPage;
157 : : BlockNumber rightlink;
158 : 9 : BlockNumber deleteBlkno = BufferGetBlockNumber(dBuffer);
159 : :
160 : : /*
161 : : * This function MUST be called only if someone of parent pages hold
162 : : * exclusive cleanup lock. This guarantees that no insertions currently
163 : : * happen in this subtree. Caller also acquires Exclusive locks on
164 : : * deletable, parent and left pages.
165 : : */
166 : :
167 : 9 : page = BufferGetPage(dBuffer);
168 : 9 : rightlink = GinPageGetOpaque(page)->rightlink;
169 : :
170 : : Assert(GinPageGetOpaque(BufferGetPage(lBuffer))->rightlink == deleteBlkno);
171 : :
172 : : /*
173 : : * Any insert which would have gone on the leaf block will now go to its
174 : : * right sibling.
175 : : */
176 : 9 : PredicateLockPageCombine(gvs->index, deleteBlkno, rightlink);
177 : :
178 : 9 : START_CRIT_SECTION();
179 : :
180 : : /* Unlink the page by changing left sibling's rightlink */
181 : 9 : page = BufferGetPage(lBuffer);
182 : 9 : GinPageGetOpaque(page)->rightlink = rightlink;
183 : :
184 : : /* Delete downlink from parent */
185 : 9 : parentPage = BufferGetPage(pBuffer);
186 : : #ifdef USE_ASSERT_CHECKING
187 : : do
188 : : {
189 : : PostingItem *tod = GinDataPageGetPostingItem(parentPage, myoff);
190 : :
191 : : Assert(PostingItemGetBlockNumber(tod) == deleteBlkno);
192 : : } while (0);
193 : : #endif
194 : 9 : GinPageDeletePostingItem(parentPage, myoff);
195 : :
196 : 9 : page = BufferGetPage(dBuffer);
197 : :
198 : : /*
199 : : * we shouldn't change rightlink field to save workability of running
200 : : * search scan
201 : : */
202 : :
203 : : /*
204 : : * Mark page as deleted, and remember last xid which could know its
205 : : * address.
206 : : */
207 : 9 : GinPageSetDeleted(page);
208 : 9 : GinPageSetDeleteXid(page, ReadNextTransactionId());
209 : :
210 : 9 : MarkBufferDirty(pBuffer);
211 : 9 : MarkBufferDirty(lBuffer);
212 : 9 : MarkBufferDirty(dBuffer);
213 : :
214 [ - + - - : 9 : if (RelationNeedsWAL(gvs->index))
- - - - ]
215 : : {
216 : : XLogRecPtr recptr;
217 : : ginxlogDeletePage data;
218 : :
219 : : /*
220 : : * We can't pass REGBUF_STANDARD for the deleted page, because we
221 : : * didn't set pd_lower on pre-9.4 versions. The page might've been
222 : : * binary-upgraded from an older version, and hence not have pd_lower
223 : : * set correctly. Ditto for the left page, but removing the item from
224 : : * the parent updated its pd_lower, so we know that's OK at this
225 : : * point.
226 : : */
227 : 0 : XLogBeginInsert();
228 : 0 : XLogRegisterBuffer(0, dBuffer, 0);
229 : 0 : XLogRegisterBuffer(1, pBuffer, REGBUF_STANDARD);
230 : 0 : XLogRegisterBuffer(2, lBuffer, 0);
231 : :
232 : 0 : data.parentOffset = myoff;
233 : 0 : data.rightLink = GinPageGetOpaque(page)->rightlink;
234 : 0 : data.deleteXid = GinPageGetDeleteXid(page);
235 : :
236 : 0 : XLogRegisterData(&data, sizeof(ginxlogDeletePage));
237 : :
238 : 0 : recptr = XLogInsert(RM_GIN_ID, XLOG_GIN_DELETE_PAGE);
239 : 0 : PageSetLSN(page, recptr);
240 : 0 : PageSetLSN(parentPage, recptr);
241 : 0 : PageSetLSN(BufferGetPage(lBuffer), recptr);
242 : : }
243 : :
244 : 9 : END_CRIT_SECTION();
245 : :
246 : 9 : gvs->result->pages_newly_deleted++;
247 : 9 : gvs->result->pages_deleted++;
248 : 9 : }
249 : :
250 : :
251 : : /*
252 : : * Scans a posting tree and deletes empty pages.
253 : : *
254 : : * The caller must hold a cleanup lock on the root page to prevent concurrent
255 : : * inserts. The entire path from the root down to the current page is kept
256 : : * exclusively locked throughout the scan. The left sibling at each level is
257 : : * also kept locked, because ginDeletePostingPage() needs it to update the
258 : : * rightlink of the left sibling; re-acquiring the left sibling lock later
259 : : * could deadlock with ginStepRight(), which acquires page locks
260 : : * left-to-right.
261 : : *
262 : : * All per-level state is carried in 'myStackItem': the buffer to process
263 : : * (must already be pinned and exclusively locked), the left sibling buffer,
264 : : * and this page's offset in the parent's downlink array. The root entry is
265 : : * set up by ginVacuumPostingTree(); child entries are populated here before
266 : : * recursing.
267 : : *
268 : : * Returns true if the page was deleted, false otherwise.
269 : : */
270 : : static bool
271 : 49 : ginScanPostingTreeToDelete(GinVacuumState *gvs, DataPageDeleteStack *myStackItem)
272 : : {
273 : 49 : Buffer buffer = myStackItem->buffer;
274 : : Page page;
275 : 49 : bool pageWasDeleted = false;
276 : : bool isempty;
277 : :
278 : 49 : page = BufferGetPage(buffer);
279 : :
280 : : Assert(GinPageIsData(page));
281 : :
282 [ + + ]: 49 : if (!GinPageIsLeaf(page))
283 : : {
284 : : OffsetNumber i;
285 : :
286 [ + + ]: 49 : for (i = FirstOffsetNumber; i <= GinPageGetOpaque(page)->maxoff;)
287 : : {
288 : 39 : PostingItem *pitem = GinDataPageGetPostingItem(page, i);
289 : : Buffer childBuffer;
290 : :
291 : 39 : childBuffer = ReadBufferExtended(gvs->index,
292 : : MAIN_FORKNUM,
293 : 39 : PostingItemGetBlockNumber(pitem),
294 : : RBM_NORMAL, gvs->strategy);
295 : 39 : LockBuffer(childBuffer, GIN_EXCLUSIVE);
296 : :
297 : : /* Allocate a child stack entry on first use; reuse thereafter */
298 [ + + ]: 39 : if (!myStackItem->child)
299 : : {
300 : 10 : myStackItem->child = palloc0_object(DataPageDeleteStack);
301 : 10 : myStackItem->child->parent = myStackItem;
302 : 10 : myStackItem->child->leftBuffer = InvalidBuffer;
303 : : }
304 : :
305 : 39 : myStackItem->child->buffer = childBuffer;
306 : 39 : myStackItem->child->isRoot = false;
307 : 39 : myStackItem->child->myoff = i;
308 : :
309 : : /*
310 : : * Recurse into child. If the child page was deleted, its
311 : : * downlink was removed from our page, so re-examine the same
312 : : * offset; otherwise advance to the next downlink.
313 : : */
314 [ + + ]: 39 : if (!ginScanPostingTreeToDelete(gvs, myStackItem->child))
315 : 30 : i++;
316 : : }
317 : 10 : myStackItem->buffer = InvalidBuffer;
318 : :
319 : : /*
320 : : * After processing all children at this level, release the child
321 : : * level's leftBuffer if we're at the rightmost page. There is no
322 : : * right sibling that could need it for deletion.
323 : : */
324 [ + - + - ]: 10 : if (GinPageRightMost(page) && BufferIsValid(myStackItem->child->leftBuffer))
325 : : {
326 : 10 : UnlockReleaseBuffer(myStackItem->child->leftBuffer);
327 : 10 : myStackItem->child->leftBuffer = InvalidBuffer;
328 : : }
329 : : }
330 : :
331 [ + + ]: 49 : if (GinPageIsLeaf(page))
332 [ + - ]: 39 : isempty = GinDataLeafPageIsEmpty(page);
333 : : else
334 : 10 : isempty = GinPageGetOpaque(page)->maxoff < FirstOffsetNumber;
335 : :
336 [ + + ]: 49 : if (isempty)
337 : : {
338 : : /*
339 : : * Proceed to the ginDeletePostingPage() if target page is not the
340 : : * leftmost or the rightmost page.
341 : : *
342 : : * leftBuffer is the target's left sibling according to the parent
343 : : * level, which is not necessarily its left sibling in the sibling
344 : : * link chain (the rightlinks stored on pages): the new right half of
345 : : * an incompletely split page is in the sibling chain, but has no
346 : : * downlink yet. ginDeletePostingPage isn't prepared to deal with
347 : : * that, so we must refuse to delete when either the target or its
348 : : * left sibling page is marked incompletely split.
349 : : */
350 [ + + + + ]: 22 : if (BufferIsValid(myStackItem->leftBuffer) && !GinPageRightMost(page) &&
351 [ + - ]: 10 : !GinPageIsIncompleteSplit(page) &&
352 [ + + ]: 10 : !GinPageIsIncompleteSplit(BufferGetPage(myStackItem->leftBuffer)))
353 : : {
354 : : Assert(!myStackItem->isRoot);
355 : 9 : ginDeletePostingPage(gvs, buffer, myStackItem->leftBuffer,
356 : 9 : myStackItem->parent->buffer,
357 : 9 : myStackItem->myoff,
358 : 9 : myStackItem->parent->isRoot);
359 : 9 : pageWasDeleted = true;
360 : : }
361 : : }
362 : :
363 [ + + ]: 49 : if (!pageWasDeleted)
364 : : {
365 : : /*
366 : : * Keep this page as the new leftBuffer for this level: the next
367 : : * sibling to the right might need it for deletion. Release any
368 : : * previously held left page first.
369 : : */
370 [ + + ]: 40 : if (BufferIsValid(myStackItem->leftBuffer))
371 : 20 : UnlockReleaseBuffer(myStackItem->leftBuffer);
372 : 40 : myStackItem->leftBuffer = buffer;
373 : : }
374 : : else
375 : : {
376 : : /*
377 : : * Page was deleted; release the buffer. leftBuffer remains the same.
378 : : */
379 : 9 : UnlockReleaseBuffer(buffer);
380 : : }
381 : :
382 : 49 : return pageWasDeleted;
383 : : }
384 : :
385 : :
386 : : /*
387 : : * Scan through posting tree leafs, delete empty tuples. Returns true if there
388 : : * is at least one empty page.
389 : : */
390 : : static bool
391 : 22 : ginVacuumPostingTreeLeaves(GinVacuumState *gvs, BlockNumber blkno)
392 : : {
393 : : Buffer buffer;
394 : : Page page;
395 : 22 : bool hasVoidPage = false;
396 : : MemoryContext oldCxt;
397 : :
398 : : /* Find leftmost leaf page of posting tree and lock it in exclusive mode */
399 : : while (true)
400 : 10 : {
401 : : PostingItem *pitem;
402 : :
403 : 32 : buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno,
404 : : RBM_NORMAL, gvs->strategy);
405 : 32 : LockBuffer(buffer, GIN_SHARE);
406 : 32 : page = BufferGetPage(buffer);
407 : :
408 : : Assert(GinPageIsData(page));
409 : :
410 [ + + ]: 32 : if (GinPageIsLeaf(page))
411 : : {
412 : 22 : LockBuffer(buffer, GIN_UNLOCK);
413 : 22 : LockBuffer(buffer, GIN_EXCLUSIVE);
414 : :
415 [ - + ]: 22 : if (!GinPageIsLeaf(page))
416 : : {
417 : : /*
418 : : * The root page was a leaf page, but became an internal page
419 : : * while no lock was held. Unlock and reacquire a share lock.
420 : : */
421 : 0 : UnlockReleaseBuffer(buffer);
422 : 0 : continue;
423 : : }
424 : 22 : break;
425 : : }
426 : :
427 : : Assert(PageGetMaxOffsetNumber(page) >= FirstOffsetNumber);
428 : :
429 : 10 : pitem = GinDataPageGetPostingItem(page, FirstOffsetNumber);
430 : 10 : blkno = PostingItemGetBlockNumber(pitem);
431 : : Assert(blkno != InvalidBlockNumber);
432 : :
433 : 10 : UnlockReleaseBuffer(buffer);
434 : : }
435 : :
436 : : /* Iterate all posting tree leaves using rightlinks and vacuum them */
437 : : while (true)
438 : : {
439 : 52 : oldCxt = MemoryContextSwitchTo(gvs->tmpCxt);
440 : 52 : ginVacuumPostingTreeLeaf(gvs->index, buffer, gvs);
441 : 52 : MemoryContextSwitchTo(oldCxt);
442 : 52 : MemoryContextReset(gvs->tmpCxt);
443 : :
444 [ + - + + ]: 52 : if (GinDataLeafPageIsEmpty(page))
445 : 22 : hasVoidPage = true;
446 : :
447 : 52 : blkno = GinPageGetOpaque(page)->rightlink;
448 : :
449 : 52 : UnlockReleaseBuffer(buffer);
450 : :
451 [ + + ]: 52 : if (blkno == InvalidBlockNumber)
452 : 22 : break;
453 : :
454 : : /*
455 : : * A safe point to delay/accept interrupts: the previous page has been
456 : : * unlocked and released, so we hold no buffer content lock (nor any
457 : : * other LWLock) here and CHECK_FOR_INTERRUPTS() can do its job.
458 : : */
459 : 30 : vacuum_delay_point(false);
460 : :
461 : 30 : buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno,
462 : : RBM_NORMAL, gvs->strategy);
463 : 30 : LockBuffer(buffer, GIN_EXCLUSIVE);
464 : 30 : page = BufferGetPage(buffer);
465 : : }
466 : :
467 : 22 : return hasVoidPage;
468 : : }
469 : :
470 : : static void
471 : 22 : ginVacuumPostingTree(GinVacuumState *gvs, BlockNumber rootBlkno)
472 : : {
473 [ + + ]: 22 : if (ginVacuumPostingTreeLeaves(gvs, rootBlkno))
474 : : {
475 : : /*
476 : : * There is at least one empty page. So we have to rescan the tree
477 : : * deleting empty pages.
478 : : */
479 : : Buffer buffer;
480 : : DataPageDeleteStack root,
481 : : *ptr,
482 : : *tmp;
483 : : bool deleted PG_USED_FOR_ASSERTS_ONLY;
484 : :
485 : 10 : buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, rootBlkno,
486 : : RBM_NORMAL, gvs->strategy);
487 : :
488 : : /*
489 : : * Lock posting tree root for cleanup to ensure there are no
490 : : * concurrent inserts.
491 : : */
492 : 10 : LockBufferForCleanup(buffer);
493 : :
494 : 10 : memset(&root, 0, sizeof(DataPageDeleteStack));
495 : 10 : root.buffer = buffer;
496 : 10 : root.leftBuffer = InvalidBuffer;
497 : 10 : root.myoff = InvalidOffsetNumber;
498 : 10 : root.isRoot = true;
499 : :
500 : 10 : deleted = ginScanPostingTreeToDelete(gvs, &root);
501 : : Assert(!deleted);
502 : :
503 : 10 : ptr = root.child;
504 : :
505 [ + + ]: 20 : while (ptr)
506 : : {
507 : 10 : tmp = ptr->child;
508 : 10 : pfree(ptr);
509 : 10 : ptr = tmp;
510 : : }
511 : :
512 : 10 : UnlockReleaseBuffer(buffer);
513 : : }
514 : 22 : }
515 : :
516 : : /*
517 : : * returns modified page or NULL if page isn't modified.
518 : : * Function works with original page until first change is occurred,
519 : : * then page is copied into temporary one.
520 : : */
521 : : static Page
522 : 1242 : ginVacuumEntryPage(GinVacuumState *gvs, Buffer buffer, BlockNumber *roots, uint32 *nroot)
523 : : {
524 : 1242 : Page origpage = BufferGetPage(buffer),
525 : : tmppage;
526 : : OffsetNumber i,
527 : 1242 : maxoff = PageGetMaxOffsetNumber(origpage);
528 : :
529 : 1242 : tmppage = origpage;
530 : :
531 : 1242 : *nroot = 0;
532 : :
533 [ + + ]: 168365 : for (i = FirstOffsetNumber; i <= maxoff; i++)
534 : : {
535 : 167123 : IndexTuple itup = (IndexTuple) PageGetItem(tmppage, PageGetItemId(tmppage, i));
536 : :
537 [ + + ]: 167123 : if (GinIsPostingTree(itup))
538 : : {
539 : : /*
540 : : * store posting tree's roots for further processing, we can't
541 : : * vacuum it just now due to risk of deadlocks with scans/inserts
542 : : */
543 : 22 : roots[*nroot] = GinGetDownlink(itup);
544 : 22 : (*nroot)++;
545 : : }
546 [ + - ]: 167101 : else if (GinGetNPosting(itup) > 0)
547 : : {
548 : : int nitems;
549 : : ItemPointer items_orig;
550 : : bool free_items_orig;
551 : : ItemPointer items;
552 : :
553 : : /* Get list of item pointers from the tuple. */
554 [ + - ]: 167101 : if (GinItupIsCompressed(itup))
555 : : {
556 : 167101 : items_orig = ginPostingListDecode((GinPostingList *) GinGetPosting(itup), &nitems);
557 : 167101 : free_items_orig = true;
558 : : }
559 : : else
560 : : {
561 : 0 : items_orig = (ItemPointer) GinGetPosting(itup);
562 : 0 : nitems = GinGetNPosting(itup);
563 : 0 : free_items_orig = false;
564 : : }
565 : :
566 : : /* Remove any items from the list that need to be vacuumed. */
567 : 167101 : items = ginVacuumItemPointers(gvs, items_orig, nitems, &nitems);
568 : :
569 [ + - ]: 167101 : if (free_items_orig)
570 : 167101 : pfree(items_orig);
571 : :
572 : : /* If any item pointers were removed, recreate the tuple. */
573 [ + + ]: 167101 : if (items)
574 : : {
575 : : OffsetNumber attnum;
576 : : Datum key;
577 : : GinNullCategory category;
578 : : GinPostingList *plist;
579 : : int plistsize;
580 : :
581 [ + + ]: 160000 : if (nitems > 0)
582 : : {
583 : 12 : plist = ginCompressPostingList(items, nitems, GinMaxItemSize, NULL);
584 : 12 : plistsize = SizeOfGinPostingList(plist);
585 : : }
586 : : else
587 : : {
588 : 159988 : plist = NULL;
589 : 159988 : plistsize = 0;
590 : : }
591 : :
592 : : /*
593 : : * if we already created a temporary page, make changes in
594 : : * place
595 : : */
596 [ + + ]: 160000 : if (tmppage == origpage)
597 : : {
598 : : /*
599 : : * On first difference, create a temporary copy of the
600 : : * page and copy the tuple's posting list to it.
601 : : */
602 : 1148 : tmppage = PageGetTempPageCopy(origpage);
603 : :
604 : : /* set itup pointer to new page */
605 : 1148 : itup = (IndexTuple) PageGetItem(tmppage, PageGetItemId(tmppage, i));
606 : : }
607 : :
608 : 160000 : attnum = gintuple_get_attrnum(&gvs->ginstate, itup);
609 : 160000 : key = gintuple_get_key(&gvs->ginstate, itup, &category);
610 : 160000 : itup = GinFormTuple(&gvs->ginstate, attnum, key, category,
611 : : (char *) plist, plistsize,
612 : : nitems, true);
613 [ + + ]: 160000 : if (plist)
614 : 12 : pfree(plist);
615 : 160000 : PageIndexTupleDelete(tmppage, i);
616 : :
617 [ - + ]: 160000 : if (PageAddItem(tmppage, itup, IndexTupleSize(itup), i, false, false) != i)
618 [ # # ]: 0 : elog(ERROR, "failed to add item to index page in \"%s\"",
619 : : RelationGetRelationName(gvs->index));
620 : :
621 : 160000 : pfree(itup);
622 : 160000 : pfree(items);
623 : : }
624 : : }
625 : : }
626 : :
627 [ + + ]: 1242 : return (tmppage == origpage) ? NULL : tmppage;
628 : : }
629 : :
630 : : IndexBulkDeleteResult *
631 : 50 : ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
632 : : IndexBulkDeleteCallback callback, void *callback_state)
633 : : {
634 : 50 : Relation index = info->index;
635 : 50 : BlockNumber blkno = GIN_ROOT_BLKNO;
636 : : GinVacuumState gvs;
637 : : Buffer buffer;
638 : : BlockNumber rootOfPostingTree[BLCKSZ / (sizeof(IndexTupleData) + sizeof(ItemId))];
639 : : uint32 nRoot;
640 : :
641 : 50 : gvs.tmpCxt = AllocSetContextCreate(CurrentMemoryContext,
642 : : "Gin vacuum temporary context",
643 : : ALLOCSET_DEFAULT_SIZES);
644 : 50 : gvs.index = index;
645 : 50 : gvs.callback = callback;
646 : 50 : gvs.callback_state = callback_state;
647 : 50 : gvs.strategy = info->strategy;
648 : 50 : initGinState(&gvs.ginstate, index);
649 : :
650 : : /* first time through? */
651 [ + - ]: 50 : if (stats == NULL)
652 : : {
653 : : /* Yes, so initialize stats to zeroes */
654 : 50 : stats = palloc0_object(IndexBulkDeleteResult);
655 : : }
656 : :
657 : : /*
658 : : * The pending list might have already-dead TIDs that VACUUM now requires
659 : : * us to remove from the index. We must force cleanup of the pending list
660 : : * now, before vacuuming proper begins, to make sure nothing is missed.
661 : : *
662 : : * When running in an autovacuum worker, we won't necessarily _fully_
663 : : * empty the pending list. This is still safe; concurrent inserters
664 : : * cannot insert new tuples whose TIDs VACUUM needs us to remove.
665 : : */
666 : 50 : ginInsertCleanup(&gvs.ginstate, !AmAutoVacuumWorkerProcess(),
667 : : false, true, stats);
668 : :
669 : : /* we'll re-count the tuples each time */
670 : 50 : stats->num_index_tuples = 0;
671 : 50 : gvs.result = stats;
672 : :
673 : 50 : buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
674 : : RBM_NORMAL, info->strategy);
675 : :
676 : : /* find leaf page */
677 : : for (;;)
678 : 8 : {
679 : 58 : Page page = BufferGetPage(buffer);
680 : : IndexTuple itup;
681 : :
682 : 58 : LockBuffer(buffer, GIN_SHARE);
683 : :
684 : : Assert(!GinPageIsData(page));
685 : :
686 [ + + ]: 58 : if (GinPageIsLeaf(page))
687 : : {
688 : 50 : LockBuffer(buffer, GIN_UNLOCK);
689 : 50 : LockBuffer(buffer, GIN_EXCLUSIVE);
690 : :
691 [ + + - + ]: 50 : if (blkno == GIN_ROOT_BLKNO && !GinPageIsLeaf(page))
692 : : {
693 : 0 : LockBuffer(buffer, GIN_UNLOCK);
694 : 0 : continue; /* check it one more */
695 : : }
696 : 50 : break;
697 : : }
698 : :
699 : : Assert(PageGetMaxOffsetNumber(page) >= FirstOffsetNumber);
700 : :
701 : 8 : itup = (IndexTuple) PageGetItem(page, PageGetItemId(page, FirstOffsetNumber));
702 : 8 : blkno = GinGetDownlink(itup);
703 : : Assert(blkno != InvalidBlockNumber);
704 : :
705 : 8 : UnlockReleaseBuffer(buffer);
706 : 8 : buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
707 : : RBM_NORMAL, info->strategy);
708 : : }
709 : :
710 : : /* right now we found leftmost page in entry's BTree */
711 : :
712 : : for (;;)
713 : 1192 : {
714 : 1242 : Page page = BufferGetPage(buffer);
715 : : Page resPage;
716 : : uint32 i;
717 : :
718 : : Assert(!GinPageIsData(page));
719 : :
720 : 1242 : resPage = ginVacuumEntryPage(&gvs, buffer, rootOfPostingTree, &nRoot);
721 : :
722 : 1242 : blkno = GinPageGetOpaque(page)->rightlink;
723 : :
724 [ + + ]: 1242 : if (resPage)
725 : : {
726 : 1148 : START_CRIT_SECTION();
727 : 1148 : PageRestoreTempPage(resPage, page);
728 : 1148 : MarkBufferDirty(buffer);
729 : 1148 : xlogVacuumPage(gvs.index, buffer);
730 : 1148 : END_CRIT_SECTION();
731 : 1148 : UnlockReleaseBuffer(buffer);
732 : : }
733 : : else
734 : : {
735 : 94 : UnlockReleaseBuffer(buffer);
736 : : }
737 : :
738 : 1242 : vacuum_delay_point(false);
739 : :
740 [ + + ]: 1264 : for (i = 0; i < nRoot; i++)
741 : : {
742 : 22 : ginVacuumPostingTree(&gvs, rootOfPostingTree[i]);
743 : 22 : vacuum_delay_point(false);
744 : : }
745 : :
746 [ + + ]: 1242 : if (blkno == InvalidBlockNumber) /* rightmost page */
747 : 50 : break;
748 : :
749 : 1192 : buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
750 : : RBM_NORMAL, info->strategy);
751 : 1192 : LockBuffer(buffer, GIN_EXCLUSIVE);
752 : : }
753 : :
754 : 50 : MemoryContextDelete(gvs.tmpCxt);
755 : :
756 : 50 : return gvs.result;
757 : : }
758 : :
759 : : IndexBulkDeleteResult *
760 : 52 : ginvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
761 : : {
762 : 52 : Relation index = info->index;
763 : : bool needLock;
764 : : BlockNumber npages,
765 : : blkno;
766 : : BlockNumber totFreePages;
767 : : GinState ginstate;
768 : : GinStatsData idxStat;
769 : : BlockRangeReadStreamPrivate p;
770 : : ReadStream *stream;
771 : :
772 : : /*
773 : : * In an autovacuum analyze, we want to clean up pending insertions.
774 : : * Otherwise, an ANALYZE-only call is a no-op.
775 : : */
776 [ + + ]: 52 : if (info->analyze_only)
777 : : {
778 [ + + ]: 13 : if (AmAutoVacuumWorkerProcess())
779 : : {
780 : 9 : initGinState(&ginstate, index);
781 : 9 : ginInsertCleanup(&ginstate, false, true, true, stats);
782 : : }
783 : 13 : return stats;
784 : : }
785 : :
786 : : /*
787 : : * Set up all-zero stats and cleanup pending inserts if ginbulkdelete
788 : : * wasn't called
789 : : */
790 [ + + ]: 39 : if (stats == NULL)
791 : : {
792 : 29 : stats = palloc0_object(IndexBulkDeleteResult);
793 : 29 : initGinState(&ginstate, index);
794 : 29 : ginInsertCleanup(&ginstate, !AmAutoVacuumWorkerProcess(),
795 : : false, true, stats);
796 : : }
797 : :
798 : 39 : memset(&idxStat, 0, sizeof(idxStat));
799 : :
800 : : /*
801 : : * XXX we always report the heap tuple count as the number of index
802 : : * entries. This is bogus if the index is partial, but it's real hard to
803 : : * tell how many distinct heap entries are referenced by a GIN index.
804 : : */
805 [ + - ]: 39 : stats->num_index_tuples = Max(info->num_heap_tuples, 0);
806 : 39 : stats->estimated_count = info->estimated_count;
807 : :
808 : : /*
809 : : * Need lock unless it's local to this backend.
810 : : */
811 [ + + + - ]: 39 : needLock = !RELATION_IS_LOCAL(index);
812 : :
813 [ + + ]: 39 : if (needLock)
814 : 33 : LockRelationForExtension(index, ExclusiveLock);
815 : 39 : npages = RelationGetNumberOfBlocks(index);
816 [ + + ]: 39 : if (needLock)
817 : 33 : UnlockRelationForExtension(index, ExclusiveLock);
818 : :
819 : 39 : totFreePages = 0;
820 : :
821 : : /* Scan all blocks starting from the root using streaming reads */
822 : 39 : p.current_blocknum = GIN_ROOT_BLKNO;
823 : 39 : p.last_exclusive = npages;
824 : :
825 : : /*
826 : : * It is safe to use batchmode as block_range_read_stream_cb takes no
827 : : * locks.
828 : : */
829 : 39 : stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
830 : : READ_STREAM_FULL |
831 : : READ_STREAM_USE_BATCHING,
832 : : info->strategy,
833 : : index,
834 : : MAIN_FORKNUM,
835 : : block_range_read_stream_cb,
836 : : &p,
837 : : 0);
838 : :
839 [ + + ]: 6165 : for (blkno = GIN_ROOT_BLKNO; blkno < npages; blkno++)
840 : : {
841 : : Buffer buffer;
842 : : Page page;
843 : :
844 : 6126 : vacuum_delay_point(false);
845 : :
846 : 6126 : buffer = read_stream_next_buffer(stream, NULL);
847 : :
848 : 6126 : LockBuffer(buffer, GIN_SHARE);
849 : 6126 : page = BufferGetPage(buffer);
850 : :
851 [ + + ]: 6126 : if (GinPageIsRecyclable(page))
852 : : {
853 : : Assert(blkno != GIN_ROOT_BLKNO);
854 : 3105 : RecordFreeIndexPage(index, blkno);
855 : 3105 : totFreePages++;
856 : : }
857 [ + + ]: 3021 : else if (GinPageIsData(page))
858 : : {
859 : 166 : idxStat.nDataPages++;
860 : : }
861 [ + - ]: 2855 : else if (!GinPageIsList(page))
862 : : {
863 : 2855 : idxStat.nEntryPages++;
864 : :
865 [ + + ]: 2855 : if (GinPageIsLeaf(page))
866 : 2835 : idxStat.nEntries += PageGetMaxOffsetNumber(page);
867 : : }
868 : :
869 : 6126 : UnlockReleaseBuffer(buffer);
870 : : }
871 : :
872 : : Assert(read_stream_next_buffer(stream, NULL) == InvalidBuffer);
873 : 39 : read_stream_end(stream);
874 : :
875 : : /* Update the metapage with accurate page and entry counts */
876 : 39 : idxStat.nTotalPages = npages;
877 : 39 : ginUpdateStats(info->index, &idxStat, false);
878 : :
879 : : /* Finally, vacuum the FSM */
880 : 39 : IndexFreeSpaceMapVacuum(info->index);
881 : :
882 : 39 : stats->pages_free = totFreePages;
883 : :
884 [ + + ]: 39 : if (needLock)
885 : 33 : LockRelationForExtension(index, ExclusiveLock);
886 : 39 : stats->num_pages = RelationGetNumberOfBlocks(index);
887 [ + + ]: 39 : if (needLock)
888 : 33 : UnlockRelationForExtension(index, ExclusiveLock);
889 : :
890 : 39 : return stats;
891 : : }
892 : :
893 : : /*
894 : : * Return whether Page can safely be recycled.
895 : : */
896 : : bool
897 : 6198 : GinPageIsRecyclable(Page page)
898 : : {
899 : : TransactionId delete_xid;
900 : :
901 [ - + ]: 6198 : if (PageIsNew(page))
902 : 0 : return true;
903 : :
904 [ + + ]: 6198 : if (!GinPageIsDeleted(page))
905 : 3012 : return false;
906 : :
907 : 3186 : delete_xid = GinPageGetDeleteXid(page);
908 : :
909 [ + + ]: 3186 : if (!TransactionIdIsValid(delete_xid))
910 : 3176 : return true;
911 : :
912 : : /*
913 : : * If no backend still could view delete_xid as in running, all scans
914 : : * concurrent with ginDeletePostingPage() must have finished.
915 : : */
916 : 10 : return GlobalVisCheckRemovableXid(NULL, delete_xid);
917 : : }
|