Branch data Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : *
3 : : * pgstatapprox.c
4 : : * Bloat estimation functions
5 : : *
6 : : * Copyright (c) 2014-2026, PostgreSQL Global Development Group
7 : : *
8 : : * IDENTIFICATION
9 : : * contrib/pgstattuple/pgstatapprox.c
10 : : *
11 : : *-------------------------------------------------------------------------
12 : : */
13 : : #include "postgres.h"
14 : :
15 : : #include "access/heapam.h"
16 : : #include "access/htup_details.h"
17 : : #include "access/relation.h"
18 : : #include "access/visibilitymap.h"
19 : : #include "catalog/pg_am_d.h"
20 : : #include "commands/vacuum.h"
21 : : #include "funcapi.h"
22 : : #include "miscadmin.h"
23 : : #include "pgstattuple.h"
24 : : #include "storage/bufmgr.h"
25 : : #include "storage/freespace.h"
26 : : #include "storage/procarray.h"
27 : : #include "storage/read_stream.h"
28 : :
29 : 1 : PG_FUNCTION_INFO_V1(pgstattuple_approx);
30 : 2 : PG_FUNCTION_INFO_V1(pgstattuple_approx_v1_5);
31 : :
32 : : Datum pgstattuple_approx_internal(Oid relid, FunctionCallInfo fcinfo);
33 : :
34 : : typedef struct output_type
35 : : {
36 : : uint64 table_len;
37 : : double scanned_percent;
38 : : uint64 tuple_count;
39 : : uint64 tuple_len;
40 : : double tuple_percent;
41 : : uint64 dead_tuple_count;
42 : : uint64 dead_tuple_len;
43 : : double dead_tuple_percent;
44 : : uint64 free_space;
45 : : double free_percent;
46 : : } output_type;
47 : :
48 : : #define NUM_OUTPUT_COLUMNS 10
49 : :
50 : : /*
51 : : * Struct for statapprox_heap read stream callback.
52 : : */
53 : : typedef struct StatApproxReadStreamPrivate
54 : : {
55 : : Relation rel;
56 : : output_type *stat;
57 : : BlockNumber current_blocknum;
58 : : BlockNumber nblocks;
59 : : BlockNumber scanned; /* count of pages actually read */
60 : : Buffer vmbuffer; /* for VM lookups */
61 : : } StatApproxReadStreamPrivate;
62 : :
63 : : /*
64 : : * Read stream callback for statapprox_heap.
65 : : *
66 : : * This callback checks the visibility map for each block. If the block is
67 : : * all-visible, we can get the free space from the FSM without reading the
68 : : * actual page, and skip to the next block. Only the blocks that are not
69 : : * all-visible are returned for actual reading after being locked.
70 : : */
71 : : static BlockNumber
72 : 2 : statapprox_heap_read_stream_next(ReadStream *stream,
73 : : void *callback_private_data,
74 : : void *per_buffer_data)
75 : : {
76 : 2 : StatApproxReadStreamPrivate *p =
77 : : (StatApproxReadStreamPrivate *) callback_private_data;
78 : :
79 [ - + ]: 2 : while (p->current_blocknum < p->nblocks)
80 : : {
81 : 0 : BlockNumber blkno = p->current_blocknum++;
82 : : Size freespace;
83 : :
84 [ # # ]: 0 : CHECK_FOR_INTERRUPTS();
85 : :
86 : : /*
87 : : * If the page has only visible tuples, then we can find out the free
88 : : * space from the FSM and move on without reading the page.
89 : : */
90 [ # # ]: 0 : if (VM_ALL_VISIBLE(p->rel, blkno, &p->vmbuffer))
91 : : {
92 : 0 : freespace = GetRecordedFreeSpace(p->rel, blkno);
93 : 0 : p->stat->tuple_len += BLCKSZ - freespace;
94 : 0 : p->stat->free_space += freespace;
95 : 0 : continue;
96 : : }
97 : :
98 : : /* This block needs to be read */
99 : 0 : p->scanned++;
100 : 0 : return blkno;
101 : : }
102 : :
103 : 2 : return InvalidBlockNumber;
104 : : }
105 : :
106 : : /*
107 : : * This function takes an already open relation and scans its pages,
108 : : * skipping those that have the corresponding visibility map bit set.
109 : : * For pages we skip, we find the free space from the free space map
110 : : * and approximate tuple_len on that basis. For the others, we count
111 : : * the exact number of dead tuples etc.
112 : : *
113 : : * This scan is loosely based on vacuumlazy.c:lazy_scan_heap(), but
114 : : * we do not try to avoid skipping single pages.
115 : : */
116 : : static void
117 : 2 : statapprox_heap(Relation rel, output_type *stat)
118 : : {
119 : : BlockNumber nblocks;
120 : : BufferAccessStrategy bstrategy;
121 : : TransactionId OldestXmin;
122 : : StatApproxReadStreamPrivate p;
123 : : ReadStream *stream;
124 : :
125 : 2 : OldestXmin = GetOldestNonRemovableTransactionId(rel);
126 : 2 : bstrategy = GetAccessStrategy(BAS_BULKREAD);
127 : :
128 : 2 : nblocks = RelationGetNumberOfBlocks(rel);
129 : :
130 : : /* Initialize read stream private data */
131 : 2 : p.rel = rel;
132 : 2 : p.stat = stat;
133 : 2 : p.current_blocknum = 0;
134 : 2 : p.nblocks = nblocks;
135 : 2 : p.scanned = 0;
136 : 2 : p.vmbuffer = InvalidBuffer;
137 : :
138 : : /*
139 : : * Create the read stream. We don't use READ_STREAM_USE_BATCHING because
140 : : * the callback accesses the visibility map which may need to read VM
141 : : * pages. While this shouldn't cause deadlocks, we err on the side of
142 : : * caution.
143 : : */
144 : 2 : stream = read_stream_begin_relation(READ_STREAM_FULL,
145 : : bstrategy,
146 : : rel,
147 : : MAIN_FORKNUM,
148 : : statapprox_heap_read_stream_next,
149 : : &p,
150 : : 0);
151 : :
152 : : for (;;)
153 : 0 : {
154 : : Buffer buf;
155 : : Page page;
156 : : OffsetNumber offnum,
157 : : maxoff;
158 : : BlockNumber blkno;
159 : :
160 : 2 : buf = read_stream_next_buffer(stream, NULL);
161 [ + - ]: 2 : if (buf == InvalidBuffer)
162 : 2 : break;
163 : :
164 : 0 : LockBuffer(buf, BUFFER_LOCK_SHARE);
165 : :
166 : 0 : page = BufferGetPage(buf);
167 : 0 : blkno = BufferGetBlockNumber(buf);
168 : :
169 : 0 : stat->free_space += PageGetExactFreeSpace(page);
170 : :
171 [ # # # # ]: 0 : if (PageIsNew(page) || PageIsEmpty(page))
172 : : {
173 : 0 : UnlockReleaseBuffer(buf);
174 : 0 : continue;
175 : : }
176 : :
177 : : /*
178 : : * Look at each tuple on the page and decide whether it's live or
179 : : * dead, then count it and its size. Unlike lazy_scan_heap, we can
180 : : * afford to ignore problems and special cases.
181 : : */
182 : 0 : maxoff = PageGetMaxOffsetNumber(page);
183 : :
184 : 0 : for (offnum = FirstOffsetNumber;
185 [ # # ]: 0 : offnum <= maxoff;
186 : 0 : offnum = OffsetNumberNext(offnum))
187 : : {
188 : : ItemId itemid;
189 : : HeapTupleData tuple;
190 : :
191 : 0 : itemid = PageGetItemId(page, offnum);
192 : :
193 [ # # # # ]: 0 : if (!ItemIdIsUsed(itemid) || ItemIdIsRedirected(itemid) ||
194 [ # # ]: 0 : ItemIdIsDead(itemid))
195 : : {
196 : 0 : continue;
197 : : }
198 : :
199 : : Assert(ItemIdIsNormal(itemid));
200 : :
201 : 0 : ItemPointerSet(&(tuple.t_self), blkno, offnum);
202 : :
203 : 0 : tuple.t_data = (HeapTupleHeader) PageGetItem(page, itemid);
204 : 0 : tuple.t_len = ItemIdGetLength(itemid);
205 : 0 : tuple.t_tableOid = RelationGetRelid(rel);
206 : :
207 : : /*
208 : : * We follow VACUUM's lead in counting INSERT_IN_PROGRESS tuples
209 : : * as "dead" while DELETE_IN_PROGRESS tuples are "live". We don't
210 : : * bother distinguishing tuples inserted/deleted by our own
211 : : * transaction.
212 : : */
213 [ # # # ]: 0 : switch (HeapTupleSatisfiesVacuum(&tuple, OldestXmin, buf))
214 : : {
215 : 0 : case HEAPTUPLE_LIVE:
216 : : case HEAPTUPLE_DELETE_IN_PROGRESS:
217 : 0 : stat->tuple_len += tuple.t_len;
218 : 0 : stat->tuple_count++;
219 : 0 : break;
220 : 0 : case HEAPTUPLE_DEAD:
221 : : case HEAPTUPLE_RECENTLY_DEAD:
222 : : case HEAPTUPLE_INSERT_IN_PROGRESS:
223 : 0 : stat->dead_tuple_len += tuple.t_len;
224 : 0 : stat->dead_tuple_count++;
225 : 0 : break;
226 : 0 : default:
227 [ # # ]: 0 : elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result");
228 : : break;
229 : : }
230 : : }
231 : :
232 : 0 : UnlockReleaseBuffer(buf);
233 : : }
234 : :
235 : : Assert(p.current_blocknum == nblocks);
236 : 2 : read_stream_end(stream);
237 : :
238 : 2 : stat->table_len = (uint64) nblocks * BLCKSZ;
239 : :
240 : : /*
241 : : * We don't know how many tuples are in the pages we didn't scan, so
242 : : * extrapolate the live-tuple count to the whole table in the same way
243 : : * that VACUUM does. (Like VACUUM, we're not taking a random sample, so
244 : : * just extrapolating linearly seems unsafe.) There should be no dead
245 : : * tuples in all-visible pages, so no correction is needed for that, and
246 : : * we already accounted for the space in those pages, too.
247 : : */
248 : 4 : stat->tuple_count = vac_estimate_reltuples(rel, nblocks, p.scanned,
249 : 2 : stat->tuple_count);
250 : :
251 : : /* It's not clear if we could get -1 here, but be safe. */
252 : 2 : stat->tuple_count = Max(stat->tuple_count, 0);
253 : :
254 : : /*
255 : : * Calculate percentages if the relation has one or more pages.
256 : : */
257 [ - + ]: 2 : if (nblocks != 0)
258 : : {
259 : 0 : stat->scanned_percent = 100.0 * p.scanned / nblocks;
260 : 0 : stat->tuple_percent = 100.0 * stat->tuple_len / stat->table_len;
261 : 0 : stat->dead_tuple_percent = 100.0 * stat->dead_tuple_len / stat->table_len;
262 : 0 : stat->free_percent = 100.0 * stat->free_space / stat->table_len;
263 : : }
264 : :
265 [ - + ]: 2 : if (BufferIsValid(p.vmbuffer))
266 : : {
267 : 0 : ReleaseBuffer(p.vmbuffer);
268 : 0 : p.vmbuffer = InvalidBuffer;
269 : : }
270 : 2 : }
271 : :
272 : : /*
273 : : * Returns estimated live/dead tuple statistics for the given relid.
274 : : *
275 : : * The superuser() check here must be kept as the library might be upgraded
276 : : * without the extension being upgraded, meaning that in pre-1.5 installations
277 : : * these functions could be called by any user.
278 : : */
279 : : Datum
280 : 0 : pgstattuple_approx(PG_FUNCTION_ARGS)
281 : : {
282 : 0 : Oid relid = PG_GETARG_OID(0);
283 : :
284 [ # # ]: 0 : if (!superuser())
285 [ # # ]: 0 : ereport(ERROR,
286 : : (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
287 : : errmsg("must be superuser to use pgstattuple functions")));
288 : :
289 : 0 : PG_RETURN_DATUM(pgstattuple_approx_internal(relid, fcinfo));
290 : : }
291 : :
292 : : /*
293 : : * As of pgstattuple version 1.5, we no longer need to check if the user
294 : : * is a superuser because we REVOKE EXECUTE on the SQL function from PUBLIC.
295 : : * Users can then grant access to it based on their policies.
296 : : *
297 : : * Otherwise identical to pgstattuple_approx (above).
298 : : */
299 : : Datum
300 : 6 : pgstattuple_approx_v1_5(PG_FUNCTION_ARGS)
301 : : {
302 : 6 : Oid relid = PG_GETARG_OID(0);
303 : :
304 : 6 : PG_RETURN_DATUM(pgstattuple_approx_internal(relid, fcinfo));
305 : : }
306 : :
307 : : Datum
308 : 6 : pgstattuple_approx_internal(Oid relid, FunctionCallInfo fcinfo)
309 : : {
310 : : Relation rel;
311 : 6 : output_type stat = {0};
312 : : TupleDesc tupdesc;
313 : : bool nulls[NUM_OUTPUT_COLUMNS];
314 : : Datum values[NUM_OUTPUT_COLUMNS];
315 : : HeapTuple ret;
316 : 6 : int i = 0;
317 : :
318 [ - + ]: 6 : if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
319 [ # # ]: 0 : elog(ERROR, "return type must be a row type");
320 : :
321 [ - + ]: 6 : if (tupdesc->natts != NUM_OUTPUT_COLUMNS)
322 [ # # ]: 0 : elog(ERROR, "incorrect number of output arguments");
323 : :
324 : 6 : rel = relation_open(relid, AccessShareLock);
325 : :
326 : 6 : check_relation_supported(rel);
327 : :
328 : : /*
329 : : * We support only relation kinds with a visibility map and a free space
330 : : * map.
331 : : */
332 [ + + ]: 6 : if (!(rel->rd_rel->relkind == RELKIND_RELATION ||
333 [ + - ]: 5 : rel->rd_rel->relkind == RELKIND_MATVIEW ||
334 [ + + ]: 5 : rel->rd_rel->relkind == RELKIND_TOASTVALUE))
335 [ + - ]: 4 : ereport(ERROR,
336 : : (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
337 : : errmsg("relation \"%s\" is of wrong relation kind",
338 : : RelationGetRelationName(rel)),
339 : : errdetail_relkind_not_supported(rel->rd_rel->relkind)));
340 : :
341 [ - + ]: 2 : if (rel->rd_rel->relam != HEAP_TABLE_AM_OID)
342 [ # # ]: 0 : ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
343 : : errmsg("only heap AM is supported")));
344 : :
345 : 2 : statapprox_heap(rel, &stat);
346 : :
347 : 2 : relation_close(rel, AccessShareLock);
348 : :
349 : 2 : memset(nulls, 0, sizeof(nulls));
350 : :
351 : 2 : values[i++] = Int64GetDatum(stat.table_len);
352 : 2 : values[i++] = Float8GetDatum(stat.scanned_percent);
353 : 2 : values[i++] = Int64GetDatum(stat.tuple_count);
354 : 2 : values[i++] = Int64GetDatum(stat.tuple_len);
355 : 2 : values[i++] = Float8GetDatum(stat.tuple_percent);
356 : 2 : values[i++] = Int64GetDatum(stat.dead_tuple_count);
357 : 2 : values[i++] = Int64GetDatum(stat.dead_tuple_len);
358 : 2 : values[i++] = Float8GetDatum(stat.dead_tuple_percent);
359 : 2 : values[i++] = Int64GetDatum(stat.free_space);
360 : 2 : values[i++] = Float8GetDatum(stat.free_percent);
361 : :
362 : 2 : ret = heap_form_tuple(tupdesc, values, nulls);
363 : 2 : return HeapTupleGetDatum(ret);
364 : : }
|