Age Owner Branch data TLA 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
4600 heikki.linnakangas@i 48 :CBC 167896 : ginVacuumItemPointers(GinVacuumState *gvs, ItemPointerData *items,
49 : : int nitem, int *nremaining)
50 : : {
51 : : int i,
52 : 167896 : remaining = 0;
4496 bruce@momjian.us 53 : 167896 : ItemPointer tmpitems = NULL;
54 : :
55 : : /*
56 : : * Iterate over TIDs array
57 : : */
7267 58 [ + + ]: 813374 : for (i = 0; i < nitem; i++)
59 : : {
60 [ + + ]: 645478 : if (gvs->callback(items + i, gvs->callback_state))
61 : : {
7422 teodor@sigaev.ru 62 : 519981 : gvs->result->tuples_removed += 1;
4600 heikki.linnakangas@i 63 [ + + ]: 519981 : if (!tmpitems)
64 : : {
65 : : /*
66 : : * First TID to be deleted: allocate memory to hold the
67 : : * remaining items.
68 : : */
260 michael@paquier.xyz 69 : 160707 : tmpitems = palloc_array(ItemPointerData, nitem);
4600 heikki.linnakangas@i 70 : 160707 : memcpy(tmpitems, items, sizeof(ItemPointerData) * i);
71 : : }
72 : : }
73 : : else
74 : : {
7422 teodor@sigaev.ru 75 : 125497 : gvs->result->num_index_tuples += 1;
4600 heikki.linnakangas@i 76 [ + + ]: 125497 : if (tmpitems)
77 : 4292 : tmpitems[remaining] = items[i];
78 : 125497 : remaining++;
79 : : }
80 : : }
81 : :
82 : 167896 : *nremaining = remaining;
83 : 167896 : return tmpitems;
84 : : }
85 : :
86 : : /*
87 : : * Create a WAL record for vacuuming entry tree leaf page.
88 : : */
89 : : static void
7267 bruce@momjian.us 90 : 1149 : xlogVacuumPage(Relation index, Buffer buffer)
91 : : {
3781 kgrittn@postgresql.o 92 : 1149 : Page page = BufferGetPage(buffer);
93 : : XLogRecPtr recptr;
94 : :
95 : : /* This is only used for entry tree leaf pages. */
4600 heikki.linnakangas@i 96 [ - + ]: 1149 : Assert(!GinPageIsData(page));
7267 bruce@momjian.us 97 [ - + ]: 1149 : Assert(GinPageIsLeaf(page));
98 : :
5736 rhaas@postgresql.org 99 [ + + - + : 1149 : if (!RelationNeedsWAL(index))
- - - - ]
7267 bruce@momjian.us 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 : : */
4298 heikki.linnakangas@i 106 : 1 : XLogBeginInsert();
107 : 1 : XLogRegisterBuffer(0, buffer, REGBUF_FORCE_IMAGE | REGBUF_STANDARD);
108 : :
109 : 1 : recptr = XLogInsert(RM_GIN_ID, XLOG_GIN_VACUUM_PAGE);
7422 teodor@sigaev.ru 110 : 1 : 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
187 akorotkov@postgresql 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 : :
3072 teodor@sigaev.ru 167 : 9 : page = BufferGetPage(dBuffer);
168 : 9 : rightlink = GinPageGetOpaque(page)->rightlink;
169 : :
5 pg@bowt.ie 170 [ - + ]: 9 : 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 : : */
3072 teodor@sigaev.ru 176 : 9 : PredicateLockPageCombine(gvs->index, deleteBlkno, rightlink);
177 : :
7422 178 : 9 : START_CRIT_SECTION();
179 : :
180 : : /* Unlink the page by changing left sibling's rightlink */
3781 kgrittn@postgresql.o 181 : 9 : page = BufferGetPage(lBuffer);
4675 heikki.linnakangas@i 182 : 9 : GinPageGetOpaque(page)->rightlink = rightlink;
183 : :
184 : : /* Delete downlink from parent */
3781 kgrittn@postgresql.o 185 : 9 : parentPage = BufferGetPage(pBuffer);
186 : : #ifdef USE_ASSERT_CHECKING
187 : : do
188 : : {
4711 heikki.linnakangas@i 189 : 9 : PostingItem *tod = GinDataPageGetPostingItem(parentPage, myoff);
190 : :
6860 bruce@momjian.us 191 [ - + ]: 9 : Assert(PostingItemGetBlockNumber(tod) == deleteBlkno);
192 : : } while (0);
193 : : #endif
5793 tgl@sss.pgh.pa.us 194 : 9 : GinPageDeletePostingItem(parentPage, myoff);
195 : :
3781 kgrittn@postgresql.o 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 : : */
2473 akorotkov@postgresql 207 : 9 : GinPageSetDeleted(page);
2019 tmunro@postgresql.or 208 : 9 : GinPageSetDeleteXid(page, ReadNextTransactionId());
209 : :
7023 teodor@sigaev.ru 210 : 9 : MarkBufferDirty(pBuffer);
4518 heikki.linnakangas@i 211 : 9 : MarkBufferDirty(lBuffer);
7023 teodor@sigaev.ru 212 : 9 : MarkBufferDirty(dBuffer);
213 : :
5736 rhaas@postgresql.org 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 : : */
4298 heikki.linnakangas@i 227 :UBC 0 : XLogBeginInsert();
228 : 0 : XLogRegisterBuffer(0, dBuffer, 0);
229 : 0 : XLogRegisterBuffer(1, pBuffer, REGBUF_STANDARD);
230 : 0 : XLogRegisterBuffer(2, lBuffer, 0);
231 : :
7422 teodor@sigaev.ru 232 : 0 : data.parentOffset = myoff;
7267 bruce@momjian.us 233 : 0 : data.rightLink = GinPageGetOpaque(page)->rightlink;
2814 akorotkov@postgresql 234 : 0 : data.deleteXid = GinPageGetDeleteXid(page);
235 : :
562 peter@eisentraut.org 236 : 0 : XLogRegisterData(&data, sizeof(ginxlogDeletePage));
237 : :
4298 heikki.linnakangas@i 238 : 0 : recptr = XLogInsert(RM_GIN_ID, XLOG_GIN_DELETE_PAGE);
7422 teodor@sigaev.ru 239 : 0 : PageSetLSN(page, recptr);
240 : 0 : PageSetLSN(parentPage, recptr);
3781 kgrittn@postgresql.o 241 : 0 : PageSetLSN(BufferGetPage(lBuffer), recptr);
242 : : }
243 : :
189 michael@paquier.xyz 244 [ - + ]:CBC 9 : END_CRIT_SECTION();
245 : :
2009 pg@bowt.ie 246 : 9 : gvs->result->pages_newly_deleted++;
7422 teodor@sigaev.ru 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
187 akorotkov@postgresql 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 : :
3781 kgrittn@postgresql.o 278 : 49 : page = BufferGetPage(buffer);
279 : :
7267 bruce@momjian.us 280 [ - + ]: 49 : Assert(GinPageIsData(page));
281 : :
282 [ + + ]: 49 : if (!GinPageIsLeaf(page))
283 : : {
284 : : OffsetNumber i;
285 : :
187 akorotkov@postgresql 286 [ + + ]: 49 : for (i = FirstOffsetNumber; i <= GinPageGetOpaque(page)->maxoff;)
287 : : {
4711 heikki.linnakangas@i 288 : 39 : PostingItem *pitem = GinDataPageGetPostingItem(page, i);
289 : : Buffer childBuffer;
290 : :
187 akorotkov@postgresql 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 : :
4600 heikki.linnakangas@i 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 : : */
5 pg@bowt.ie 350 [ + + + + ]: 22 : if (BufferIsValid(myStackItem->leftBuffer) && !GinPageRightMost(page) &&
351 [ + - ]: 10 : !GinPageIsIncompleteSplit(page) &&
352 [ + + ]: 10 : !GinPageIsIncompleteSplit(BufferGetPage(myStackItem->leftBuffer)))
353 : : {
187 akorotkov@postgresql 354 [ - + ]: 9 : 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
2814 391 : 19 : ginVacuumPostingTreeLeaves(GinVacuumState *gvs, BlockNumber blkno)
392 : : {
393 : : Buffer buffer;
394 : : Page page;
3298 peter_e@gmx.net 395 : 19 : bool hasVoidPage = false;
396 : : MemoryContext oldCxt;
397 : :
398 : : /* Find leftmost leaf page of posting tree and lock it in exclusive mode */
399 : : while (true)
2814 akorotkov@postgresql 400 : 10 : {
401 : : PostingItem *pitem;
402 : :
403 : 29 : buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno,
404 : : RBM_NORMAL, gvs->strategy);
405 : 29 : LockBuffer(buffer, GIN_SHARE);
406 : 29 : page = BufferGetPage(buffer);
407 : :
408 [ - + ]: 29 : Assert(GinPageIsData(page));
409 : :
410 [ + + ]: 29 : if (GinPageIsLeaf(page))
411 : : {
412 : 19 : LockBuffer(buffer, GIN_UNLOCK);
413 : 19 : LockBuffer(buffer, GIN_EXCLUSIVE);
414 : :
8 pg@bowt.ie 415 [ - + ]: 19 : 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 : : */
8 pg@bowt.ie 421 :UBC 0 : UnlockReleaseBuffer(buffer);
422 : 0 : continue;
423 : : }
2814 akorotkov@postgresql 424 :CBC 19 : break;
425 : : }
426 : :
427 [ - + ]: 10 : Assert(PageGetMaxOffsetNumber(page) >= FirstOffsetNumber);
428 : :
429 : 10 : pitem = GinDataPageGetPostingItem(page, FirstOffsetNumber);
430 : 10 : blkno = PostingItemGetBlockNumber(pitem);
431 [ - + ]: 10 : 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 : : {
3444 teodor@sigaev.ru 439 : 49 : oldCxt = MemoryContextSwitchTo(gvs->tmpCxt);
440 : 49 : ginVacuumPostingTreeLeaf(gvs->index, buffer, gvs);
441 : 49 : MemoryContextSwitchTo(oldCxt);
442 : 49 : MemoryContextReset(gvs->tmpCxt);
443 : :
444 [ + - + + ]: 49 : if (GinDataLeafPageIsEmpty(page))
3298 peter_e@gmx.net 445 : 22 : hasVoidPage = true;
446 : :
2814 akorotkov@postgresql 447 : 49 : blkno = GinPageGetOpaque(page)->rightlink;
448 : :
3444 teodor@sigaev.ru 449 : 49 : UnlockReleaseBuffer(buffer);
450 : :
2814 akorotkov@postgresql 451 [ + + ]: 49 : if (blkno == InvalidBlockNumber)
452 : 19 : 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 : : */
30 459 : 30 : vacuum_delay_point(false);
460 : :
2814 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 : 19 : return hasVoidPage;
468 : : }
469 : :
470 : : static void
471 : 19 : ginVacuumPostingTree(GinVacuumState *gvs, BlockNumber rootBlkno)
472 : : {
473 [ + + ]: 19 : 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));
187 495 : 10 : root.buffer = buffer;
2473 496 : 10 : root.leftBuffer = InvalidBuffer;
187 497 : 10 : root.myoff = InvalidOffsetNumber;
2814 498 : 10 : root.isRoot = true;
499 : :
187 500 : 10 : deleted = ginScanPostingTreeToDelete(gvs, &root);
501 [ - + ]: 10 : Assert(!deleted);
502 : :
2814 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 : : }
3444 teodor@sigaev.ru 514 : 19 : }
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
7267 bruce@momjian.us 522 : 1222 : ginVacuumEntryPage(GinVacuumState *gvs, Buffer buffer, BlockNumber *roots, uint32 *nroot)
523 : : {
3781 kgrittn@postgresql.o 524 : 1222 : Page origpage = BufferGetPage(buffer),
525 : : tmppage;
526 : : OffsetNumber i,
7267 bruce@momjian.us 527 : 1222 : maxoff = PageGetMaxOffsetNumber(origpage);
528 : :
7422 teodor@sigaev.ru 529 : 1222 : tmppage = origpage;
530 : :
7267 bruce@momjian.us 531 : 1222 : *nroot = 0;
532 : :
533 [ + + ]: 168097 : for (i = FirstOffsetNumber; i <= maxoff; i++)
534 : : {
535 : 166875 : IndexTuple itup = (IndexTuple) PageGetItem(tmppage, PageGetItemId(tmppage, i));
536 : :
537 [ + + ]: 166875 : 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 : : */
5711 tgl@sss.pgh.pa.us 543 : 19 : roots[*nroot] = GinGetDownlink(itup);
7422 teodor@sigaev.ru 544 : 19 : (*nroot)++;
545 : : }
7267 bruce@momjian.us 546 [ + - ]: 166856 : 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. */
4600 heikki.linnakangas@i 554 [ + - ]: 166856 : if (GinItupIsCompressed(itup))
555 : : {
4186 556 : 166856 : items_orig = ginPostingListDecode((GinPostingList *) GinGetPosting(itup), &nitems);
557 : 166856 : free_items_orig = true;
558 : : }
559 : : else
560 : : {
4186 heikki.linnakangas@i 561 :UBC 0 : items_orig = (ItemPointer) GinGetPosting(itup);
4600 562 : 0 : nitems = GinGetNPosting(itup);
4186 563 : 0 : free_items_orig = false;
564 : : }
565 : :
566 : : /* Remove any items from the list that need to be vacuumed. */
4186 heikki.linnakangas@i 567 :CBC 166856 : items = ginVacuumItemPointers(gvs, items_orig, nitems, &nitems);
568 : :
569 [ + - ]: 166856 : if (free_items_orig)
570 : 166856 : pfree(items_orig);
571 : :
572 : : /* If any item pointers were removed, recreate the tuple. */
573 [ + + ]: 166856 : if (items)
574 : : {
575 : : OffsetNumber attnum;
576 : : Datum key;
577 : : GinNullCategory category;
578 : : GinPostingList *plist;
579 : : int plistsize;
580 : :
4600 581 [ + + ]: 160003 : if (nitems > 0)
582 : : {
4186 583 : 15 : plist = ginCompressPostingList(items, nitems, GinMaxItemSize, NULL);
4600 584 : 15 : 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 : : */
7267 bruce@momjian.us 596 [ + + ]: 160003 : 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 : : */
6506 tgl@sss.pgh.pa.us 602 : 1149 : tmppage = PageGetTempPageCopy(origpage);
603 : :
604 : : /* set itup pointer to new page */
7422 teodor@sigaev.ru 605 : 1149 : itup = (IndexTuple) PageGetItem(tmppage, PageGetItemId(tmppage, i));
606 : : }
607 : :
6621 tgl@sss.pgh.pa.us 608 : 160003 : attnum = gintuple_get_attrnum(&gvs->ginstate, itup);
5711 609 : 160003 : key = gintuple_get_key(&gvs->ginstate, itup, &category);
610 : 160003 : itup = GinFormTuple(&gvs->ginstate, attnum, key, category,
611 : : (char *) plist, plistsize,
612 : : nitems, true);
4600 heikki.linnakangas@i 613 [ + + ]: 160003 : if (plist)
614 : 15 : pfree(plist);
7422 teodor@sigaev.ru 615 : 160003 : PageIndexTupleDelete(tmppage, i);
616 : :
304 peter@eisentraut.org 617 [ - + ]: 160003 : if (PageAddItem(tmppage, itup, IndexTupleSize(itup), i, false, false) != i)
7267 bruce@momjian.us 618 [ # # ]:UBC 0 : elog(ERROR, "failed to add item to index page in \"%s\"",
619 : : RelationGetRelationName(gvs->index));
620 : :
7267 bruce@momjian.us 621 :CBC 160003 : pfree(itup);
4186 heikki.linnakangas@i 622 : 160003 : pfree(items);
623 : : }
624 : : }
625 : : }
626 : :
7267 bruce@momjian.us 627 [ + + ]: 1222 : return (tmppage == origpage) ? NULL : tmppage;
628 : : }
629 : :
630 : : IndexBulkDeleteResult *
3875 tgl@sss.pgh.pa.us 631 : 30 : ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
632 : : IndexBulkDeleteCallback callback, void *callback_state)
633 : : {
7422 634 : 30 : Relation index = info->index;
7267 bruce@momjian.us 635 : 30 : BlockNumber blkno = GIN_ROOT_BLKNO;
636 : : GinVacuumState gvs;
637 : : Buffer buffer;
638 : : BlockNumber rootOfPostingTree[BLCKSZ / (sizeof(IndexTupleData) + sizeof(ItemId))];
639 : : uint32 nRoot;
640 : :
4600 heikki.linnakangas@i 641 : 30 : gvs.tmpCxt = AllocSetContextCreate(CurrentMemoryContext,
642 : : "Gin vacuum temporary context",
643 : : ALLOCSET_DEFAULT_SIZES);
6365 tgl@sss.pgh.pa.us 644 : 30 : gvs.index = index;
645 : 30 : gvs.callback = callback;
646 : 30 : gvs.callback_state = callback_state;
647 : 30 : gvs.strategy = info->strategy;
648 : 30 : initGinState(&gvs.ginstate, index);
649 : :
650 : : /* first time through? */
7422 651 [ + - ]: 30 : if (stats == NULL)
652 : : {
653 : : /* Yes, so initialize stats to zeroes */
260 michael@paquier.xyz 654 : 30 : 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 : : */
8 pg@bowt.ie 666 : 30 : ginInsertCleanup(&gvs.ginstate, !AmAutoVacuumWorkerProcess(),
667 : : false, true, stats);
668 : :
669 : : /* we'll re-count the tuples each time */
7422 tgl@sss.pgh.pa.us 670 : 30 : stats->num_index_tuples = 0;
671 : 30 : gvs.result = stats;
672 : :
6509 heikki.linnakangas@i 673 : 30 : buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
674 : : RBM_NORMAL, info->strategy);
675 : :
676 : : /* find leaf page */
677 : : for (;;)
7267 bruce@momjian.us 678 : 8 : {
3781 kgrittn@postgresql.o 679 : 38 : Page page = BufferGetPage(buffer);
680 : : IndexTuple itup;
681 : :
7267 bruce@momjian.us 682 : 38 : LockBuffer(buffer, GIN_SHARE);
683 : :
684 [ - + ]: 38 : Assert(!GinPageIsData(page));
685 : :
686 [ + + ]: 38 : if (GinPageIsLeaf(page))
687 : : {
688 : 30 : LockBuffer(buffer, GIN_UNLOCK);
689 : 30 : LockBuffer(buffer, GIN_EXCLUSIVE);
690 : :
691 [ + + - + ]: 30 : if (blkno == GIN_ROOT_BLKNO && !GinPageIsLeaf(page))
692 : : {
7267 bruce@momjian.us 693 :UBC 0 : LockBuffer(buffer, GIN_UNLOCK);
694 : 0 : continue; /* check it one more */
695 : : }
7267 bruce@momjian.us 696 :CBC 30 : break;
697 : : }
698 : :
699 [ - + ]: 8 : Assert(PageGetMaxOffsetNumber(page) >= FirstOffsetNumber);
700 : :
7422 teodor@sigaev.ru 701 : 8 : itup = (IndexTuple) PageGetItem(page, PageGetItemId(page, FirstOffsetNumber));
5711 tgl@sss.pgh.pa.us 702 : 8 : blkno = GinGetDownlink(itup);
7267 bruce@momjian.us 703 [ - + ]: 8 : Assert(blkno != InvalidBlockNumber);
704 : :
7028 teodor@sigaev.ru 705 : 8 : UnlockReleaseBuffer(buffer);
6509 heikki.linnakangas@i 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 (;;)
7267 bruce@momjian.us 713 : 1192 : {
3781 kgrittn@postgresql.o 714 : 1222 : Page page = BufferGetPage(buffer);
715 : : Page resPage;
716 : : uint32 i;
717 : :
7267 bruce@momjian.us 718 [ - + ]: 1222 : Assert(!GinPageIsData(page));
719 : :
7422 teodor@sigaev.ru 720 : 1222 : resPage = ginVacuumEntryPage(&gvs, buffer, rootOfPostingTree, &nRoot);
721 : :
7267 bruce@momjian.us 722 : 1222 : blkno = GinPageGetOpaque(page)->rightlink;
723 : :
724 [ + + ]: 1222 : if (resPage)
725 : : {
7422 teodor@sigaev.ru 726 : 1149 : START_CRIT_SECTION();
7267 bruce@momjian.us 727 : 1149 : PageRestoreTempPage(resPage, page);
728 : 1149 : MarkBufferDirty(buffer);
7023 teodor@sigaev.ru 729 : 1149 : xlogVacuumPage(gvs.index, buffer);
7422 730 [ - + ]: 1149 : END_CRIT_SECTION();
189 michael@paquier.xyz 731 : 1149 : UnlockReleaseBuffer(buffer);
732 : : }
733 : : else
734 : : {
7422 teodor@sigaev.ru 735 : 73 : UnlockReleaseBuffer(buffer);
736 : : }
737 : :
562 nathan@postgresql.or 738 : 1222 : vacuum_delay_point(false);
739 : :
7267 bruce@momjian.us 740 [ + + ]: 1241 : for (i = 0; i < nRoot; i++)
741 : : {
742 : 19 : ginVacuumPostingTree(&gvs, rootOfPostingTree[i]);
562 nathan@postgresql.or 743 : 19 : vacuum_delay_point(false);
744 : : }
745 : :
3354 tgl@sss.pgh.pa.us 746 [ + + ]: 1222 : if (blkno == InvalidBlockNumber) /* rightmost page */
7422 teodor@sigaev.ru 747 : 30 : break;
748 : :
6509 heikki.linnakangas@i 749 : 1192 : buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
750 : : RBM_NORMAL, info->strategy);
7267 bruce@momjian.us 751 : 1192 : LockBuffer(buffer, GIN_EXCLUSIVE);
752 : : }
753 : :
4600 heikki.linnakangas@i 754 : 30 : MemoryContextDelete(gvs.tmpCxt);
755 : :
3875 tgl@sss.pgh.pa.us 756 : 30 : return gvs.result;
757 : : }
758 : :
759 : : IndexBulkDeleteResult *
760 : 54 : ginvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
761 : : {
7267 bruce@momjian.us 762 : 54 : 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 : : */
6365 tgl@sss.pgh.pa.us 776 [ + + ]: 54 : if (info->analyze_only)
777 : : {
906 heikki.linnakangas@i 778 [ + + ]: 15 : if (AmAutoVacuumWorkerProcess())
779 : : {
6365 tgl@sss.pgh.pa.us 780 : 11 : initGinState(&ginstate, index);
3206 rhaas@postgresql.org 781 : 11 : ginInsertCleanup(&ginstate, false, true, true, stats);
782 : : }
3875 tgl@sss.pgh.pa.us 783 : 15 : return stats;
784 : : }
785 : :
786 : : /*
787 : : * Set up all-zero stats and cleanup pending inserts if ginbulkdelete
788 : : * wasn't called
789 : : */
7422 790 [ + + ]: 39 : if (stats == NULL)
791 : : {
260 michael@paquier.xyz 792 : 29 : stats = palloc0_object(IndexBulkDeleteResult);
6365 tgl@sss.pgh.pa.us 793 : 29 : initGinState(&ginstate, index);
906 heikki.linnakangas@i 794 : 29 : ginInsertCleanup(&ginstate, !AmAutoVacuumWorkerProcess(),
795 : : false, true, stats);
796 : : }
797 : :
5793 tgl@sss.pgh.pa.us 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 : : */
2188 805 [ + - ]: 39 : stats->num_index_tuples = Max(info->num_heap_tuples, 0);
6291 806 : 39 : stats->estimated_count = info->estimated_count;
807 : :
808 : : /*
809 : : * Need lock unless it's local to this backend.
810 : : */
6044 811 [ + + + - ]: 39 : needLock = !RELATION_IS_LOCAL(index);
812 : :
7422 teodor@sigaev.ru 813 [ + + ]: 39 : if (needLock)
814 : 33 : LockRelationForExtension(index, ExclusiveLock);
815 : 39 : npages = RelationGetNumberOfBlocks(index);
816 [ + + ]: 39 : if (needLock)
817 : 33 : UnlockRelationForExtension(index, ExclusiveLock);
818 : :
6286 bruce@momjian.us 819 : 39 : totFreePages = 0;
820 : :
821 : : /* Scan all blocks starting from the root using streaming reads */
168 michael@paquier.xyz 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 : :
5793 tgl@sss.pgh.pa.us 839 [ + + ]: 6162 : for (blkno = GIN_ROOT_BLKNO; blkno < npages; blkno++)
840 : : {
841 : : Buffer buffer;
842 : : Page page;
843 : :
562 nathan@postgresql.or 844 : 6123 : vacuum_delay_point(false);
845 : :
168 michael@paquier.xyz 846 : 6123 : buffer = read_stream_next_buffer(stream, NULL);
847 : :
7422 teodor@sigaev.ru 848 : 6123 : LockBuffer(buffer, GIN_SHARE);
363 peter@eisentraut.org 849 : 6123 : page = BufferGetPage(buffer);
850 : :
2814 akorotkov@postgresql 851 [ + + ]: 6123 : if (GinPageIsRecyclable(page))
852 : : {
5793 tgl@sss.pgh.pa.us 853 [ - + ]: 3105 : Assert(blkno != GIN_ROOT_BLKNO);
6540 heikki.linnakangas@i 854 : 3105 : RecordFreeIndexPage(index, blkno);
7280 tgl@sss.pgh.pa.us 855 : 3105 : totFreePages++;
856 : : }
5793 857 [ + + ]: 3018 : else if (GinPageIsData(page))
858 : : {
859 : 163 : idxStat.nDataPages++;
860 : : }
861 [ + - ]: 2855 : else if (!GinPageIsList(page))
862 : : {
863 : 2855 : idxStat.nEntryPages++;
864 : :
5618 bruce@momjian.us 865 [ + + ]: 2855 : if (GinPageIsLeaf(page))
5793 tgl@sss.pgh.pa.us 866 : 2835 : idxStat.nEntries += PageGetMaxOffsetNumber(page);
867 : : }
868 : :
7422 teodor@sigaev.ru 869 : 6123 : UnlockReleaseBuffer(buffer);
870 : : }
871 : :
168 michael@paquier.xyz 872 [ - + ]: 39 : 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 */
5793 tgl@sss.pgh.pa.us 876 : 39 : idxStat.nTotalPages = npages;
2703 heikki.linnakangas@i 877 : 39 : ginUpdateStats(info->index, &idxStat, false);
878 : :
879 : : /* Finally, vacuum the FSM */
6534 880 : 39 : IndexFreeSpaceMapVacuum(info->index);
881 : :
7280 tgl@sss.pgh.pa.us 882 : 39 : stats->pages_free = totFreePages;
883 : :
7422 teodor@sigaev.ru 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 : :
3875 tgl@sss.pgh.pa.us 890 : 39 : return stats;
891 : : }
892 : :
893 : : /*
894 : : * Return whether Page can safely be recycled.
895 : : */
896 : : bool
2206 andres@anarazel.de 897 : 6194 : GinPageIsRecyclable(Page page)
898 : : {
899 : : TransactionId delete_xid;
900 : :
901 [ - + ]: 6194 : if (PageIsNew(page))
2206 andres@anarazel.de 902 :UBC 0 : return true;
903 : :
2206 andres@anarazel.de 904 [ + + ]:CBC 6194 : if (!GinPageIsDeleted(page))
905 : 3009 : return false;
906 : :
907 : 3185 : delete_xid = GinPageGetDeleteXid(page);
908 : :
909 [ + + ]: 3185 : if (!TransactionIdIsValid(delete_xid))
910 : 3175 : 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 : : }
|