Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1w0LJj-001pK9-30 for pgsql-hackers@arkaria.postgresql.org; Wed, 11 Mar 2026 15:17:48 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1w0LJi-009gLx-0g for pgsql-hackers@arkaria.postgresql.org; Wed, 11 Mar 2026 15:17:46 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1w0LJh-009gLo-2z for pgsql-hackers@lists.postgresql.org; Wed, 11 Mar 2026 15:17:46 +0000 Received: from fhigh-b6-smtp.messagingengine.com ([202.12.124.157]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1w0LJe-000000029bB-35Vn for pgsql-hackers@lists.postgresql.org; Wed, 11 Mar 2026 15:17:46 +0000 Received: from phl-compute-06.internal (phl-compute-06.internal [10.202.2.46]) by mailfhigh.stl.internal (Postfix) with ESMTP id 8277B7A0174; Wed, 11 Mar 2026 11:17:40 -0400 (EDT) Received: from phl-frontend-03 ([10.202.2.162]) by phl-compute-06.internal (MEProxy); Wed, 11 Mar 2026 11:17:40 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=anarazel.de; h= cc:cc:content-transfer-encoding:content-type:content-type:date :date:from:from:in-reply-to:in-reply-to:message-id:mime-version :references:reply-to:subject:subject:to:to; s=fm1; t=1773242260; x=1773328660; bh=lU5b76NtJPkOb4+Z0CWWxPzB0W0d9twCqyBMMBioYM4=; b= WkZytQvaLLSgH1KwQF05ytM8pgG0JU21/Ij4OCEUH+tYuMrmh3AqboEfbVZFnjyE ODqNYWtkvtsBQReIJiE0dQDxPG3Yvsj3RgmFptsTYMeA9MWysipOJ/D+3EOo09L9 AKzx/VGdOhAhVou65YDoBTsS89mRtmZbw+JiqYDLWuOmpSpB+of1Gl9iD93nrhTt LZ346uW9hxXtxE8n59OKOj+4RVR6uTJe9GBYAMtjK3xsk2/kBwV+CnGg8cWZRnYi NSmqj2lVlSYAQCUrpu7B1wfX4SK+aHY1/eVPbTKfbm4uBgzRnji/swWyo9kgK2Sy 5RX1rsEqjDLOUIXAU39SWg== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:cc:content-transfer-encoding :content-type:content-type:date:date:feedback-id:feedback-id :from:from:in-reply-to:in-reply-to:message-id:mime-version :references:reply-to:subject:subject:to:to:x-me-proxy :x-me-sender:x-me-sender:x-sasl-enc; s=fm1; t=1773242260; x= 1773328660; bh=lU5b76NtJPkOb4+Z0CWWxPzB0W0d9twCqyBMMBioYM4=; b=D 7VWFp6HOhWdjOwN3c5ABr3fXw2v4EpdXgLRou4RDfwGs1hNf6KX5rwABeHDlmAFQ aZxyvh+MgSv0jLgWgCsR1CJj3V/2bCiW4Dqi7LKALk5YaFR9RXJK92hYHlg8jYZv untBnw6Cp1WGGLBvZWhQ0q0wmXX5wwkXQr7rEKtDO3gNOTcs2Wu5cQrHA3c0casw DIzXkoBPkQWJtPV+l3bQUaUTr+za8w+8fq05Rltybn1+PnSB3IpGaFoB5quF+Roj DlRusfdpF4/Rv6zbhaujfx+Fx/Qc779y5yfjB0/HZEfucqeSguTYdbKJrAbYCVqd Dxsiza6E71+sRx8QlUJ1A== X-ME-Sender: X-ME-Received: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeefgedrtddtgddvkeegvdegucetufdoteggodetrf dotffvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfurfetoffkrfgpnffqhgenuceu rghilhhouhhtmecufedttdenucesvcftvggtihhpihgvnhhtshculddquddttddmnecujf gurhepfffhvfevuffkfhggtggugfgjsehtkefstddttdejnecuhfhrohhmpeetnhgurhgv shcuhfhrvghunhguuceorghnughrvghssegrnhgrrhgriigvlhdruggvqeenucggtffrrg htthgvrhhnpedtleelvdfgjedvffeiueekfeeuleffhfegfffhgfffkeevueehieehhfei gffhvdenucevlhhushhtvghrufhiiigvpedtnecurfgrrhgrmhepmhgrihhlfhhrohhmpe grnhgurhgvshesrghnrghrrgiivghlrdguvgdpnhgspghrtghpthhtohepfedpmhhouggv pehsmhhtphhouhhtpdhrtghpthhtoheptghhrhhishhtohhphhgvrdgtohhurhhtohhish esuggrlhhisghordgtohhmpdhrtghpthhtohepfhhrvgguvghrihgtrdihhhhuvghlsegu rghlihgsohdrtghomhdprhgtphhtthhopehpghhsqhhlqdhhrggtkhgvrhhssehlihhsth hsrdhpohhsthhgrhgvshhqlhdrohhrgh X-ME-Proxy: Feedback-ID: id4a34324:Fastmail Received: by mail.messagingengine.com (Postfix) with ESMTPA; Wed, 11 Mar 2026 11:17:39 -0400 (EDT) Date: Wed, 11 Mar 2026 11:17:39 -0400 From: Andres Freund To: =?utf-8?B?RnLDqWTDqXJpYw==?= Yhuel Cc: PostgreSQL Hackers , Christophe Courtois Subject: Re: n_dead_tup could be way off just after a vacuum Message-ID: References: MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Disposition: inline Content-Transfer-Encoding: 8bit In-Reply-To: List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk On 2026-03-10 18:24:18 +0100, Frédéric Yhuel wrote: > My colleague Christophe Courtois and I came across some surprising behavior. > After executing the following script, n_live_tup and n_dead_tup are way off. > We know they are not meant to be exact, but the table will be autovacuumed > for no reason afterwards. > > DROP TABLE IF EXISTS foo ; > CREATE TABLE foo (id int); > INSERT INTO foo (id) SELECT * FROM generate_series(1,1000); > DELETE FROM foo WHERE id <= 500; > > VACUUM foo ; > > SELECT pg_sleep(1); > > SELECT relname, n_tup_ins, n_tup_del, > n_live_tup, n_dead_tup, > last_autovacuum, last_autoanalyze, > last_vacuum, last_analyze > FROM pg_stat_user_tables WHERE relname='foo' \gx > -[ RECORD 1 ]----+------------------------------ > relname | foo > n_tup_ins | 1000 > n_tup_del | 500 > n_live_tup | 1000 <--- should be 500 after a VACUUM > n_dead_tup | 500 <--- should be 0 after a VACUUM > last_autovacuum | ∅ > last_autoanalyze | ∅ > last_vacuum | 2026-03-10 18:11:09.893913+01 > last_analyze | ∅ > > This issue appears on all tested versions from 9.6 to 18. > > If we sleep one second between the DELETE and the VACUUM, the problem > disappears, because (IIUC) pgstat_relation_flush_cb() gets executed before > the VACUUM. > > I wonder if this is a known issue, and if pg_stat_report_vacuum() should > clear the pending stats, or something. This is just because of the small gap between the DELETE and the VACUUM. The stats from queries are only merged into the shared state every now and then, as it'd be way too expensive to do so all the time. Because you issue the statements in quick successing, the report of the row insertion and deletions are only taken into account after the VACUUM. If you put a SELECT pg_stat_force_next_flush(); after the DELETE, you get the stats you expect: ┌─[ RECORD 1 ]─────┬───────────────────────────────┐ │ relname │ foo │ │ n_tup_ins │ 1000 │ │ n_tup_del │ 500 │ │ n_live_tup │ 500 │ │ n_dead_tup │ 0 │ │ last_autovacuum │ (null) │ │ last_autoanalyze │ (null) │ │ last_vacuum │ 2026-03-11 11:13:11.936146-04 │ │ last_analyze │ (null) │ └──────────────────┴───────────────────────────────┘ Note that you'd *also* get the good stats if you didn't do the pg_sleep(1) after the VACUUM, because after the VACUUM the stats actually *are* accurate. It's just the stats from the DELETE are merged later. I don't think this is a bug. It's just an engineering compromise between cost and accuracy. In real scenarios the effect of this is much smaller, because either the DELETE is only a smaller portion of the rows, or it takes longer to run and thus will trigger a stats flush during transaction commit. Greetings, Andres Freund