<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>pganalyze Blog - Articles by Ryan Booz</title><description>Monitoring Postgres and tuning query performance</description><link>https://pganalyze.com/blog</link><generator>Astro</generator><lastBuildDate>Thu, 13 Aug 2026 01:41:04 GMT</lastBuildDate><atom:link href="https://pganalyze.com/feed/ryan-booz.xml" rel="self" type="application/rss+xml"/><item><title>Postgres in Production Special Series: Diagnosing High Cardinality Workloads in pg_stat_statements (Part 6)</title><link>https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-6</link><guid isPermaLink="false">https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-6</guid><description>In Part 6 of this special Postgres in Production deep dive series, Ryan Booz asks a question that determines how useful pg_stat_statements can be for you at all: do you have a high cardinality workload? This episode covers what that actually means, why ORMs, dynamic SQL, and AI-assisted development tools generate more unique queries than you might expect, a side by side demo of the same workload on Postgres 17 and Postgres 18, and the concrete checks that tell you whether pg_stat_statements is…</description><pubDate>Thu, 13 Aug 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In Part 6 of this special &lt;a href=&quot;https://pganalyze.com/blog/category/monitoring-observability&quot;&gt;&lt;strong&gt;Postgres in Production&lt;/strong&gt;&lt;/a&gt; deep dive series, Ryan Booz asks a question that determines how useful &lt;code&gt;pg_stat_statements&lt;/code&gt; can be for you at all: do you have a high cardinality workload? This episode covers what that actually means, why ORMs, dynamic SQL, and AI-assisted development tools generate more unique queries than you might expect, a side by side demo of the same workload on Postgres 17 and Postgres 18, and the concrete checks that tell you whether &lt;code&gt;pg_stat_statements&lt;/code&gt; is losing the data you need for query tuning.&lt;/p&gt;
&lt;iframe width=&quot;750&quot; height=&quot;421&quot; src=&quot;https://www.youtube-nocookie.com/embed/AD_mSu6o-AI&quot; frameborder=&quot;0&quot; modestbranding=&quot;1&quot; controls allownetworking=&quot;internal&quot; allow=&quot;autoplay; encrypted-media&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;&lt;strong&gt;Share this episode:&lt;/strong&gt; Click here to share this episode &lt;a href=&quot;https://www.linkedin.com/shareArticle?mini=true&amp;amp;url=https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-6&amp;amp;title=Deep%20Dive%20into%20pg_stat_statements%20Episode%206&amp;amp;source=LinkedIn&quot; target=&quot;_blank&quot;&gt;on LinkedIn&lt;/a&gt;. Feel free to &lt;a href=&quot;https://pganalyze.com/newsletter&quot; target=&quot;_blank&quot;&gt;sign up for our newsletter&lt;/a&gt; and &lt;a href=&quot;https://www.youtube.com/channel/UCDV_1Dz2Ixgl1nT_3DUZVFw&quot; target=&quot;_blank&quot;&gt;subscribe to our YouTube channel&lt;/a&gt;.&lt;/p&gt;
&lt;div &gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#a-quick-recap&quot;&gt;A quick recap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-a-high-cardinality-workload-means&quot;&gt;What a high cardinality workload means&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#where-the-unique-queries-come-from&quot;&gt;Where the unique queries come from&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#a-demo-overwhelming-pg_stat_statements-with-bluebox&quot;&gt;A demo: overwhelming pg_stat_statements with Bluebox&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#our-databases-are-hidden-behind-our-tools&quot;&gt;Our databases are hidden behind our tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#how-to-tell-if-you-have-a-high-cardinality-workload&quot;&gt;How to tell if you have a high cardinality workload&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#taking-action&quot;&gt;Taking action&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-happens-if-you-dont&quot;&gt;What happens if you don’t&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#key-takeaways&quot;&gt;Key takeaways&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#whats-coming-next&quot;&gt;What’s coming next&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-we-discussed&quot;&gt;What we discussed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr/&gt;
&lt;p&gt;&lt;strong&gt;Transcript&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;a-quick-recap&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#a-quick-recap&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;A quick recap&lt;/h2&gt;
&lt;p&gt;Over the first five episodes we covered what &lt;code&gt;pg_stat_statements&lt;/code&gt; is and the metrics it stores (&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-1&quot;&gt;Part 1&lt;/a&gt;), what makes a statement “unique” through normalization (&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-2&quot;&gt;Part 2&lt;/a&gt;), where the query texts live on disk (&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-3&quot;&gt;Part 3&lt;/a&gt;), how new metrics get stored and old ones get deallocated (&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-4&quot;&gt;Part 4&lt;/a&gt;), and the configuration settings that control all of it (&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-5&quot;&gt;Part 5&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Through all of that, I have probably mentioned the &lt;code&gt;pg_stat_statements.max&lt;/code&gt; setting at least 100 times, because it’s so crucial to understanding how effective the data is that you have. This episode is about the workloads that consistently outrun that setting, and being able to identify whether you’re in that situation is really helpful to determining if &lt;code&gt;pg_stat_statements&lt;/code&gt; can help you do the query tuning and optimization that you need it to.&lt;/p&gt;
&lt;h2 id=&quot;what-a-high-cardinality-workload-means&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#what-a-high-cardinality-workload-means&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What a high cardinality workload means&lt;/h2&gt;
&lt;p&gt;When I say high cardinality, what does it really mean? To me, it means something like this: if you have a workload that’s consistently generating more unique normalized queries than the &lt;code&gt;pg_stat_statements.max&lt;/code&gt; capacity, then you really have a high cardinality workload from a &lt;code&gt;pg_stat_statements&lt;/code&gt; perspective. The extension isn’t able to consistently retain the metrics that can help you find the queries that are most in need of being optimized.&lt;/p&gt;
&lt;p&gt;Now, I can hear a lot of you say, and quite honestly, I’ve said this myself: “My application isn’t that complex. There’s no way I’m suffering from something like this. The data must be there somewhere, I just can’t find it.” Well, that’s not always the case.&lt;/p&gt;
&lt;p&gt;It’s often the tooling we’re using that hides the SQL that’s being generated behind the scenes. We know that the tool is abstracting the SQL for us, but we might not fully understand the effect it’s having on our query metrics.&lt;/p&gt;
&lt;h2 id=&quot;where-the-unique-queries-come-from&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#where-the-unique-queries-come-from&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Where the unique queries come from&lt;/h2&gt;
&lt;p&gt;It might be the ORM and the way it generates unique SQL statements for different clients, customers, and tenants. Maybe it’s custom dynamic SQL that you yourself have written. I’ve done this often in stored procedures, and if I’m running with &lt;code&gt;pg_stat_statements.track = all&lt;/code&gt;, that dynamic SQL produces unique statements over and over again.&lt;/p&gt;
&lt;p&gt;Maybe it’s ad hoc reporting. All of us are connecting more and more tools to our database, including AI systems, and those tools generate a plethora of queries as they attempt to navigate the information needed to satisfy the request. Lots of ad hoc querying can fill up &lt;code&gt;pg_stat_statements&lt;/code&gt; very quickly.&lt;/p&gt;
&lt;p&gt;And we’ve talked often in this series about variable length &lt;code&gt;IN&lt;/code&gt; lists in Postgres 17 and below, not to mention dynamic column select lists with otherwise similar queries.&lt;/p&gt;
&lt;h2 id=&quot;a-demo-overwhelming-pg_stat_statements-with-bluebox&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#a-demo-overwhelming-pg_stat_statements-with-bluebox&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;A demo: overwhelming &lt;code&gt;pg_stat_statements&lt;/code&gt; with Bluebox&lt;/h2&gt;
&lt;p&gt;I want to show you a brief demo of exactly how easy it is to overwhelm &lt;code&gt;pg_stat_statements&lt;/code&gt;, with a tool you could actually test this with yourself. I’ll use the load generation tool included with the &lt;a href=&quot;https://github.com/ryanbooz/bluebox&quot; target=&quot;_blank&quot;&gt;Bluebox sample database&lt;/a&gt;, a sample database I’ve created and made available open source to the community for testing and demonstrations like this.&lt;/p&gt;
&lt;p&gt;About an hour before recording this video, I started this load generation with the exact same settings on a Postgres 17 database and a Postgres 18 database. The load testing project has about 30 different scenarios, each created for very specific kinds of query shapes and data requests. Most of them have one or two SQL parameterized statements included, executed directly on the database without the use of an ORM. In total, there are fewer than 50 actually unique text statements in the entire project.&lt;/p&gt;
&lt;p&gt;But a couple of them, like &lt;code&gt;batch_film_lookup&lt;/code&gt;, take a list of anywhere from one to 200 film IDs and look them up. On Postgres 17 or below, that becomes really easy to turn into a lot of unique load: all it takes is a &lt;code&gt;WHERE IN&lt;/code&gt; clause and lots of unique lists of film IDs.&lt;/p&gt;
&lt;p&gt;After about an hour of running, Postgres 17 has 671 unique statements inside of &lt;code&gt;pg_stat_statements&lt;/code&gt;. The exact same workload on Postgres 18 has 120 unique statements. That is one of the improvements we’ve talked about a number of times through this series: in Postgres 18 and above, those &lt;code&gt;IN&lt;/code&gt; lists are normalized down to one statement.&lt;/p&gt;
&lt;p&gt;Another way to visualize this is to look at the queries with those &lt;code&gt;IN&lt;/code&gt; lists. One example load query looks up data based on lists of customer IDs. On Postgres 17, we can see dozens of the same query except for the &lt;code&gt;WHERE IN&lt;/code&gt; clause that shows a unique list each time: one has many customer IDs, another has four or five, and so on. On Postgres 18, the same query produces only two entries.&lt;/p&gt;
&lt;p&gt;You might ask why two, if Postgres 18 collapses all of this. Some of it has to do with the tooling: the first time it sends a parameterized query, it also has to send the value types, and it often sends a generic type like &lt;code&gt;numeric&lt;/code&gt; when it isn’t 100% sure what the type is. Once it gets a response back, the tooling can sometimes adjust and from that point forward sends the correct data type, for instance an &lt;code&gt;integer&lt;/code&gt; instead of &lt;code&gt;numeric&lt;/code&gt;. This is often why you’ll see a few extra entries for a query text even on Postgres 18.&lt;/p&gt;
&lt;p&gt;The sample load test is a really easy way to see the impact of a seemingly simple query workload on &lt;code&gt;pg_stat_statements&lt;/code&gt;, and you can use the tool to test it yourself. Grab &lt;a href=&quot;https://github.com/ryanbooz/bluebox&quot; target=&quot;_blank&quot;&gt;Bluebox&lt;/a&gt;, read the instructions on how to start the load testing, and configure it against one of your sample Bluebox databases.&lt;/p&gt;
&lt;h2 id=&quot;our-databases-are-hidden-behind-our-tools&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#our-databases-are-hidden-behind-our-tools&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Our databases are hidden behind our tools&lt;/h2&gt;
&lt;p&gt;More than ever, I think we’re experiencing higher and higher cardinality query workloads because our databases are hidden and abstracted from our day to day developer work. The reason is staring us right in the face in this modern age: ORMs and data access tools let us use our own objects and object notation to get the data we want, letting the framework write the queries, hopefully as effectively as possible. But we’ve all experienced time and again how inefficient they can be as our applications and databases get more complex.&lt;/p&gt;
&lt;p&gt;Additionally, we now have dozens of AI-assisted development tools that freely query the database based on a schema, in ways they decide are best. Even in my own development sessions using a number of these tools, I see significant numbers of &lt;code&gt;pg_stat_statements&lt;/code&gt; data being created, because the tooling combs for specific bits of information with unique one-off queries. If it can’t find what it needs, it will keep iterating and changing the queries to get to the next step of its task as quickly as possible.&lt;/p&gt;
&lt;h2 id=&quot;how-to-tell-if-you-have-a-high-cardinality-workload&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#how-to-tell-if-you-have-a-high-cardinality-workload&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;How to tell if you have a high cardinality workload&lt;/h2&gt;
&lt;p&gt;So how do you diagnose it? First, you have to know what &lt;code&gt;pg_stat_statements.max&lt;/code&gt; is set to. I’m sorry to mention it yet again, but it really is the most essential setting in &lt;code&gt;pg_stat_statements&lt;/code&gt; for how effectively you can find the data you need, dependent on your workload.&lt;/p&gt;
&lt;p&gt;Second, how close are you to actually hitting that limit? If your statement count is always at the 95% threshold, it’s a pretty sure bet that deallocations have been going on. You could try resetting &lt;code&gt;pg_stat_statements&lt;/code&gt; and watching whether it fills back up quickly. I’ve worked with clients that have it set to 5,000 or even 10,000, and when they reset the metrics, the hash table refills in less than 30 minutes. That’s a really good indicator of how large your workload is and how quickly you can use that information before it’s gone again.&lt;/p&gt;
&lt;p&gt;Third, is the deallocation counter climbing? When you query &lt;code&gt;pg_stat_statements_info&lt;/code&gt;, do you see that number going up every so often, even once or twice a day, let alone once or twice an hour?&lt;/p&gt;
&lt;p&gt;Fourth, how often do you search for query text that doesn’t exist in &lt;code&gt;pg_stat_statements&lt;/code&gt;? I often hear people ask: “I know I have a query that’s running in my code. Why isn’t it in &lt;code&gt;pg_stat_statements&lt;/code&gt;?” The short and simple answer is, “It’s probably getting lost”. If you consistently try to find a statement you know the text of, querying by the first few lines, and you can’t find it, it’s being deallocated more quickly than you can get the metrics for it. That is a high cardinality workload.&lt;/p&gt;
&lt;p&gt;And one last test: your top queries. What you consider a top query might change depending on what you’re trying to achieve, but often we start with total execution time, how much total time a query has taken in execution since &lt;code&gt;pg_stat_statements&lt;/code&gt; started tracking it. If you order by that and the top 10 changes pretty frequently, while you believe your workload is pretty consistent, then you probably have a high cardinality workload.&lt;/p&gt;
&lt;h2 id=&quot;taking-action&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#taking-action&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Taking action&lt;/h2&gt;
&lt;p&gt;If you do have a high cardinality workload, it really is time to take action. You can’t just hope this is going to fix itself.&lt;/p&gt;
&lt;p&gt;First, go back and watch &lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-5&quot;&gt;episode five&lt;/a&gt;, where we reviewed all of the settings in &lt;code&gt;pg_stat_statements&lt;/code&gt;, the ones you can change on the fly and the ones that require a restart. Next, consider upgrading to Postgres 18, which was one of the suggestions I gave at the end of that episode.&lt;/p&gt;
&lt;p&gt;Then there are the things to look at in your ORM and AI tooling. Talk about it with your development teams: help them understand that when a query suggestion comes out of these tools, there are patterns to look for that might be problematic.&lt;/p&gt;
&lt;p&gt;And honestly, I suggest you keep notes of when &lt;code&gt;pg_stat_statements&lt;/code&gt; seemed to lack the information you needed. Is there any consistency to it? Maybe every Monday morning you go to find queries and you can’t, because big data transformation jobs ran over the weekend and flooded &lt;code&gt;pg_stat_statements&lt;/code&gt;, pushing out information from the week before that you wanted for identifying problems.&lt;/p&gt;
&lt;h2 id=&quot;what-happens-if-you-dont&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#what-happens-if-you-dont&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What happens if you don’t&lt;/h2&gt;
&lt;p&gt;Ultimately, if you don’t take action, tuning just becomes a lot harder in Postgres. You don’t have all the information you need to find problematic queries. Your history becomes shorter, so even if you think you’ve fixed something, it’s hard to identify whether the fix had the intended outcome. And the optimization decisions you make become a little less trustworthy over time, because you’re never sure they really had the intended impact.&lt;/p&gt;
&lt;h2 id=&quot;key-takeaways&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#key-takeaways&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Key takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A high cardinality workload means your queries outrun your capacity.&lt;/strong&gt; If your workload consistently generates more unique normalized queries than &lt;code&gt;pg_stat_statements.max&lt;/code&gt; can hold, &lt;code&gt;pg_stat_statements&lt;/code&gt; can’t retain the metrics you need for tuning.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The uniqueness usually comes from tooling.&lt;/strong&gt; ORMs generating per-tenant statements, custom dynamic SQL (especially with &lt;code&gt;track = all&lt;/code&gt;), ad hoc reporting, AI-assisted development tools, and &lt;code&gt;IN&lt;/code&gt; lists on Postgres 17 and below.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Postgres 18 makes a measurable difference.&lt;/strong&gt; The same one hour Bluebox workload produced 671 unique statements on Postgres 17 and 120 on Postgres 18, thanks to &lt;code&gt;IN&lt;/code&gt; list normalization.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Five checks diagnose it.&lt;/strong&gt; Know your &lt;code&gt;max&lt;/code&gt;; watch how fast the table refills after a reset; watch the deallocation counter in &lt;code&gt;pg_stat_statements_info&lt;/code&gt;; check whether queries you know are running go missing; and watch whether your top 10 by total execution time churns.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Settings buy relief, but the cause lives in your application.&lt;/strong&gt; Revisit the episode 5 settings, consider Postgres 18, and work with your development teams on the query patterns your tools produce.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;whats-coming-next&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#whats-coming-next&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What’s coming next&lt;/h2&gt;
&lt;p&gt;In our last episode we’ll talk about a couple of ways to use &lt;code&gt;pg_stat_statements&lt;/code&gt;, including some of the queries we recommend, so that you can more effectively find the queries that are most impacting your workload. Please join us next time for episode seven of this deep dive into &lt;code&gt;pg_stat_statements&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I hope this series helps you better understand one of the most essential tools in the Postgres ecosystem. Feel free to &lt;a href=&quot;https://www.youtube.com/channel/UCDV_1Dz2Ixgl1nT_3DUZVFw&quot; target=&quot;_blank&quot;&gt;subscribe to our YouTube channel&lt;/a&gt;, &lt;a href=&quot;https://pganalyze.com/newsletter&quot; target=&quot;_blank&quot;&gt;sign up for our newsletter&lt;/a&gt; or &lt;a href=&quot;https://www.linkedin.com/company/pganalyze/&quot; target=&quot;_blank&quot;&gt;follow us on LinkedIn&lt;/a&gt; to get updates about new episodes!&lt;/p&gt;
&lt;h2 id=&quot;what-we-discussed&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#what-we-discussed&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What we discussed&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/pgstatstatements.html&quot;&gt;&lt;code&gt;pg_stat_statements&lt;/code&gt; documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/pgstatstatements.html#PGSTATSTATEMENTS-PG-STAT-STATEMENTS-INFO&quot;&gt;The pg_stat_statements_info view&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ryanbooz/bluebox&quot;&gt;Bluebox sample database&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Earlier parts in the pg_stat_statements deep dive series:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-1&quot;&gt;Part 1: What it is and what it isn’t&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-2&quot;&gt;Part 2: What makes a statement unique&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-3&quot;&gt;Part 3: Where Your Query Text Actually Lives&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-4&quot;&gt;Part 4: How &lt;code&gt;pg_stat_statements&lt;/code&gt; Decides What to Keep&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-5&quot;&gt;Part 5: Configuring &lt;code&gt;pg_stat_statements&lt;/code&gt; to Reduce Deallocations&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Ryan Booz</dc:creator></item><item><title>Postgres in Production Special Series: Configuring pg_stat_statements to Reduce Deallocations (Part 5)</title><link>https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-5</link><guid isPermaLink="false">https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-5</guid><description>In Part 5 of this special Postgres in Production deep dive series, Ryan Booz turns to configuration. There are only a handful of settings that control how pg_stat_statements behaves, but they decide how much data you keep and how much you lose. This episode covers how to see when you’re losing data through deallocations, what each setting actually does, and the changes (in settings and in your application) that reduce data loss over time. Share this episode: Click here to share this episode on…</description><pubDate>Tue, 30 Jun 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In Part 5 of this special &lt;a href=&quot;https://pganalyze.com/blog/category/monitoring-observability&quot;&gt;&lt;strong&gt;Postgres in Production&lt;/strong&gt;&lt;/a&gt; deep dive series, Ryan Booz turns to configuration. There are only a handful of settings that control how pg_stat_statements behaves, but they decide how much data you keep and how much you lose. This episode covers how to see when you’re losing data through deallocations, what each setting actually does, and the changes (in settings and in your application) that reduce data loss over time.&lt;/p&gt;
&lt;iframe width=&quot;750&quot; height=&quot;421&quot; src=&quot;https://www.youtube-nocookie.com/embed/kjE4jP59oJ4&quot; frameborder=&quot;0&quot; modestbranding=&quot;1&quot; controls allownetworking=&quot;internal&quot; allow=&quot;autoplay; encrypted-media&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;&lt;strong&gt;Share this episode:&lt;/strong&gt; Click here to share this episode &lt;a href=&quot;https://www.linkedin.com/shareArticle?mini=true&amp;amp;url=https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-5&amp;amp;title=Deep%20Dive%20into%20pg_stat_statements%20Episode%205&amp;amp;source=LinkedIn&quot; target=&quot;_blank&quot;&gt;on LinkedIn&lt;/a&gt;. Feel free to &lt;a href=&quot;https://pganalyze.com/newsletter&quot; target=&quot;_blank&quot;&gt;sign up for our newsletter&lt;/a&gt; and &lt;a href=&quot;https://www.youtube.com/channel/UCDV_1Dz2Ixgl1nT_3DUZVFw&quot; target=&quot;_blank&quot;&gt;subscribe to our YouTube channel&lt;/a&gt;.&lt;/p&gt;
&lt;div &gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#a-quick-recap&quot;&gt;A quick recap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#why-deallocations-matter&quot;&gt;Why deallocations matter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#seeing-deallocations-the-pg_stat_statements_info-view&quot;&gt;Seeing deallocations: the pg_stat_statements_info view&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sampling-the-deallocation-counter-the-10-minute-window&quot;&gt;Sampling the deallocation counter (the 10-minute window)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-the-deallocation-numbers-mean&quot;&gt;What the deallocation numbers mean&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#the-configuration-settings&quot;&gt;The configuration settings&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#pg_stat_statementsmax&quot;&gt;pg_stat_statements.max&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#pg_stat_statementstrack&quot;&gt;pg_stat_statements.track&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#track_utility&quot;&gt;track_utility&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#track_planning&quot;&gt;track_planning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#save&quot;&gt;save&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#reducing-deallocations-through-settings&quot;&gt;Reducing deallocations through settings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#reducing-deallocations-in-your-application&quot;&gt;Reducing deallocations in your application&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#key-takeaways&quot;&gt;Key takeaways&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#whats-coming-next&quot;&gt;What’s coming next&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-we-discussed-in-this-episode&quot;&gt;What we discussed in this episode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr/&gt;
&lt;p&gt;&lt;strong&gt;Transcript&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;a-quick-recap&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#a-quick-recap&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;A quick recap&lt;/h2&gt;
&lt;p&gt;In the first four episodes we covered what pg_stat_statements is and the metrics it stores (&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-1&quot;&gt;Part 1&lt;/a&gt;), what makes a statement “unique” through normalization of the query text (&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-2&quot;&gt;Part 2&lt;/a&gt;), and where those query texts actually live on disk (&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-3&quot;&gt;Part 3&lt;/a&gt;). In &lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-4&quot;&gt;Part 4&lt;/a&gt; we walked through the entire process of storing new incoming metrics.&lt;/p&gt;
&lt;p&gt;pg_stat_statements is our main view into what’s happening with completed query executions, and we’ve gotten a lot of value from it. But we also discussed an issue many people aren’t aware of when they analyze this data: deallocations. This episode is about the configuration that controls them.&lt;/p&gt;
&lt;h2 id=&quot;why-deallocations-matter&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#why-deallocations-matter&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Why deallocations matter&lt;/h2&gt;
&lt;p&gt;In order to store all the information coming in, pg_stat_statements sometimes has to remove previously stored data to make room for new information. One of the extension’s settings determines how often and when that happens.&lt;/p&gt;
&lt;p&gt;If you aren’t tracking this, you have no idea how much data you could be losing over time. The good news is that the loss is measurable.&lt;/p&gt;
&lt;h2 id=&quot;seeing-deallocations-the-pg_stat_statements_info-view&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#seeing-deallocations-the-pg_stat_statements_info-view&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Seeing deallocations: the pg_stat_statements_info view&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;pg_stat_statements_info&lt;/code&gt; view exposes a deallocation counter. In one example server, since the server started (or the stats were last reset), pg_stat_statements had deallocated 46 times. With the default &lt;code&gt;pg_stat_statements.max&lt;/code&gt; of 5,000, each deallocation drops 5% of the table, or 250 rows. So 250 rows of metrics had been dropped 46 times since that server began.&lt;/p&gt;
&lt;p&gt;If you aren’t tracking it yourself, your monitoring tool might be. In pganalyze we show this to you over a 10-minute window, with a counter for each server. In one example, over a 10-minute window a server (again at the default max of 5,000) deallocated twice, which means roughly every five minutes 250 rows of metrics were dropped to make room for new ones.&lt;/p&gt;
&lt;h2 id=&quot;sampling-the-deallocation-counter-the-10-minute-window&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#sampling-the-deallocation-counter-the-10-minute-window&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Sampling the deallocation counter (the 10-minute window)&lt;/h2&gt;
&lt;p&gt;Taking a sample of the deallocation counter is easy, and something you should do periodically. You query &lt;code&gt;pg_stat_statements_info&lt;/code&gt; and read the deallocation counter. A 10-minute window is a good timeframe because it makes the per-second math easy to reason about.&lt;/p&gt;
&lt;p&gt;If you query the view, wait 10 minutes, query it again, and the counter hasn’t moved, you’re in a good state. Most reasonable monitoring tools query pg_stat_statements at least once every 10 minutes, so you’re likely retaining as much data as you can.&lt;/p&gt;
&lt;h2 id=&quot;what-the-deallocation-numbers-mean&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#what-the-deallocation-numbers-mean&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What the deallocation numbers mean&lt;/h2&gt;
&lt;p&gt;Here’s how to read the counter over a 10-minute window. If it’s gone up by about 10, that means roughly every minute you’re losing 5% of the rows in the hash table. That’s a little concerning, especially if your monitoring tool isn’t querying often enough to catch the data before it’s dropped. Once you reach about 100 deallocations per 10 minutes, you’re losing 5% of your rows every six seconds or so, which is a lot of contention and really concerning. And believe it or not, I’ve seen numbers at 600 and above. At that point pg_stat_statements is trying to deallocate enough rows roughly every second to make room for new information, which is a lot of locking and effort on your metrics table, and you’re very likely losing information that would help you tune your database.&lt;/p&gt;
&lt;h2 id=&quot;the-configuration-settings&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#the-configuration-settings&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;The configuration settings&lt;/h2&gt;
&lt;p&gt;The settings below come straight out of the &lt;a href=&quot;https://www.postgresql.org/docs/current/pgstatstatements.html#PGSTATSTATEMENTS-CONFIGURATION-PARAMETERS&quot;&gt;Postgres documentation&lt;/a&gt;, and you can read about them there yourself.&lt;/p&gt;
&lt;h3 id=&quot;pg_stat_statementsmax&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#pg_stat_statementsmax&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;pg_stat_statements.max&lt;/h3&gt;
&lt;p&gt;We’ve talked about this one in every episode, so I’ll keep it short, with two things worth repeating. The first is that a restart is required: if you decide to raise this value because you have a lot of deallocations, you have to plan downtime for it, and there’s no workaround, which is a real concern if you’re already having a problem. The second is that it reserves memory even if tracking is off. As long as the extension is loaded, it reserves enough shared memory for the maximum number of rows, because the schema is fixed and it knows how much it needs. That’s usually not much, a few to tens of megabytes, but it is reserved.&lt;/p&gt;
&lt;h3 id=&quot;pg_stat_statementstrack&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#pg_stat_statementstrack&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;pg_stat_statements.track&lt;/h3&gt;
&lt;p&gt;This one can be changed with a configuration reload, and it has three options. By default it’s set to &lt;code&gt;top&lt;/code&gt;, which tracks only top-level queries. Setting it to &lt;code&gt;all&lt;/code&gt; also tracks queries inside functions and stored procedures, which will likely increase the number of rows, and remember there’s no correlation between those non-top-level queries and the top-level function that called them. Setting it to &lt;code&gt;none&lt;/code&gt; turns off tracking while leaving the extension loaded. That last option is a handy shortcut: if pg_stat_statements is causing locking issues because of churn, you can reload with &lt;code&gt;none&lt;/code&gt; to stop tracking for a while and get relief.&lt;/p&gt;
&lt;h3 id=&quot;track_utility&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#track_utility&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;track_utility&lt;/h3&gt;
&lt;p&gt;This can also be changed with a reload, no restart required. It’s &lt;strong&gt;on by default&lt;/strong&gt;, which means pg_stat_statements tracks essentially all statements, including utility statements like &lt;code&gt;PREPARE&lt;/code&gt;, commits, and rollbacks. Those individual statements take up space in the table.&lt;/p&gt;
&lt;p&gt;If your priority is tracking DML operations (&lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, &lt;code&gt;MERGE&lt;/code&gt;), you can turn &lt;code&gt;track_utility&lt;/code&gt; off and reload to get instant relief, often saving a meaningful number of rows.&lt;/p&gt;
&lt;h3 id=&quot;track_planning&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#track_planning&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;track_planning&lt;/h3&gt;
&lt;p&gt;This can be changed with a reload. It adds planning values to your statements. The default is &lt;strong&gt;off&lt;/strong&gt;, because it incurs additional overhead from the timing work during the planning phase.&lt;/p&gt;
&lt;h3 id=&quot;save&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#save&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;save&lt;/h3&gt;
&lt;p&gt;Like &lt;code&gt;max&lt;/code&gt;, this one can only be changed with a restart. It saves the statistics to a file on a normal shutdown so they can be reloaded when the server restarts. The default is &lt;strong&gt;on&lt;/strong&gt;, and that’s almost certainly what you want: after a restart, pg_stat_statements picks up where it left off instead of rebuilding all the statistics over time.&lt;/p&gt;
&lt;h2 id=&quot;reducing-deallocations-through-settings&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#reducing-deallocations-through-settings&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Reducing deallocations through settings&lt;/h2&gt;
&lt;p&gt;If you’re seeing a lot of deallocations, there are two settings-based levers. The first is to raise &lt;code&gt;pg_stat_statements.max&lt;/code&gt;. We often recommend starting at 10,000 rather than the default 5,000, which is usually a good fit for one primary server with one application database; the more databases you have, the fewer slots there are per database, so the setting may need to go higher. Use the 10-minute window to gauge how often deallocations happen relative to your setting, and remember each one drops 5% of &lt;code&gt;max&lt;/code&gt;. Once you think you need beyond roughly 50,000 to 100,000 entries, you likely need other mitigation too. The second lever is to turn off &lt;code&gt;track_utility&lt;/code&gt;, a good, cheap way to reduce the number of entries while keeping the application-query data you actually want for tuning.&lt;/p&gt;
&lt;h2 id=&quot;reducing-deallocations-in-your-application&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#reducing-deallocations-in-your-application&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Reducing deallocations in your application&lt;/h2&gt;
&lt;p&gt;The fix for deallocations is almost never just tuning these values; it’s also about your application. The first thing to look at is query uniqueness, and the biggest culprit is &lt;code&gt;WHERE IN&lt;/code&gt; clauses: on Postgres 17 and below, many &lt;code&gt;IN&lt;/code&gt; lists of different lengths produce many individual entries in pg_stat_statements, so consider switching to an array value (your ORM may have a helper for this), and watch for queries built through string concatenation, which is another source of unnecessary uniqueness. Second, you can reduce the number of databases per server, since &lt;code&gt;pg_stat_statements.max&lt;/code&gt; is server-wide and lots of activity across many databases means they all contend for the same slots; at some point you may need to break off into more servers with fewer databases. And finally, avoid querying with multiple roles unless necessary, since the same SQL statement executed by different roles produces separate entries in pg_stat_statements.&lt;/p&gt;
&lt;h2 id=&quot;key-takeaways&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#key-takeaways&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Key takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Deallocations are measurable.&lt;/strong&gt; The deallocation counter in &lt;code&gt;pg_stat_statements_info&lt;/code&gt; tells you how often pg_stat_statements is dropping 5% of the table to make room for new entries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use a 10-minute sampling window.&lt;/strong&gt; No change is healthy; ~10 per window means losing 5% per minute; ~100 means every six seconds; 600+ means roughly every second, with serious locking and data loss.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Know the five settings.&lt;/strong&gt; &lt;code&gt;max&lt;/code&gt; (restart, reserves memory even when tracking is off), &lt;code&gt;track&lt;/code&gt; (top/all/none, reload), &lt;code&gt;track_utility&lt;/code&gt; (on by default, turn off to drop utility statements), &lt;code&gt;track_planning&lt;/code&gt; (off by default, adds overhead), and &lt;code&gt;save&lt;/code&gt; (restart, default on, persists stats across restarts).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Settings buy relief, the application fixes the cause.&lt;/strong&gt; Raising &lt;code&gt;max&lt;/code&gt; to 10,000 and turning off &lt;code&gt;track_utility&lt;/code&gt; help, but reducing query uniqueness (&lt;code&gt;WHERE IN&lt;/code&gt; lists, concatenation), fewer databases per server, and avoiding redundant roles address the root cause.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;whats-coming-next&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#whats-coming-next&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What’s coming next&lt;/h2&gt;
&lt;p&gt;In the next episode we’ll dig into high-cardinality workloads specifically, and look at a couple of concrete ways to minimize deallocations and the loss of data over time. We’re getting close to finishing this series on pg_stat_statements, so I hope to see you back for it.&lt;/p&gt;
&lt;p&gt;I hope this series helps you better understand one of the most essential tools in the Postgres ecosystem. Feel free to &lt;a href=&quot;https://www.youtube.com/channel/UCDV_1Dz2Ixgl1nT_3DUZVFw&quot; target=&quot;_blank&quot;&gt;subscribe to our YouTube channel&lt;/a&gt;, &lt;a href=&quot;https://pganalyze.com/newsletter&quot; target=&quot;_blank&quot;&gt;sign up for our newsletter&lt;/a&gt; or &lt;a href=&quot;https://www.linkedin.com/company/pganalyze/&quot; target=&quot;_blank&quot;&gt;follow us on LinkedIn&lt;/a&gt; to get updates about new episodes!&lt;/p&gt;
&lt;h2 id=&quot;what-we-discussed-in-this-episode&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#what-we-discussed-in-this-episode&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What we discussed in this episode&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/pgstatstatements.html&quot;&gt;pg_stat_statements documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/pgstatstatements.html#PGSTATSTATEMENTS-CONFIGURATION-PARAMETERS&quot;&gt;pg_stat_statements configuration parameters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/pgstatstatements.html#PGSTATSTATEMENTS-PG-STAT-STATEMENTS-INFO&quot;&gt;The pg_stat_statements_info view&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-1&quot;&gt;Part 1: What it is and what it isn’t&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-2&quot;&gt;Part 2: What makes a statement unique&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-3&quot;&gt;Part 3: Where Your Query Text Actually Lives&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-4&quot;&gt;Part 4: How pg_stat_statements Decides What to Keep&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Ryan Booz</dc:creator></item><item><title>Postgres in Production Special Series: How pg_stat_statements Decides What to Keep (Part 4)</title><link>https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-4</link><guid isPermaLink="false">https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-4</guid><description>In Part 4 of this special “Postgres in Production” deep dive series, Ryan Booz takes you into the pg_stat_statements source code to show how it decides what to keep when the hash table fills up. There’s a per-query counter you never see in the view, eviction sorts every entry on every deallocation, only completed executions get stored at all, and a higher pg_stat_statements.max is not free. Share this episode: Click here to share this episode on LinkedIn . Feel free to sign up for our…</description><pubDate>Tue, 02 Jun 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In Part 4 of this special “Postgres in Production” deep dive series, Ryan Booz takes you into the pg_stat_statements source code to show how it decides what to keep when the hash table fills up. There’s a per-query counter you never see in the view, eviction sorts every entry on every deallocation, only completed executions get stored at all, and a higher &lt;code&gt;pg_stat_statements.max&lt;/code&gt; is not free.&lt;/p&gt;
&lt;iframe width=&quot;750&quot; height=&quot;421&quot; src=&quot;https://www.youtube-nocookie.com/embed/MVAKQCrb8Xw?si=ZMYPfF6CxipWnXBw&quot; frameborder=&quot;0&quot; modestbranding=&quot;1&quot; controls allownetworking=&quot;internal&quot; allow=&quot;autoplay; encrypted-media&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;&lt;strong&gt;Share this episode:&lt;/strong&gt; Click here to share this episode &lt;a href=&quot;https://www.linkedin.com/shareArticle?mini=true&amp;amp;url=https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-4&amp;amp;title=Deep%20Dive%20into%20pg_stat_statements%20Episode%204&amp;amp;source=LinkedIn&quot; target=&quot;_blank&quot;&gt;on LinkedIn&lt;/a&gt;. Feel free to &lt;a href=&quot;https://pganalyze.com/newsletter&quot; target=&quot;_blank&quot;&gt;sign up for our newsletter&lt;/a&gt; and &lt;a href=&quot;https://www.youtube.com/channel/UCDV_1Dz2Ixgl1nT_3DUZVFw&quot; target=&quot;_blank&quot;&gt;subscribe to our YouTube channel&lt;/a&gt;.&lt;/p&gt;
&lt;div &gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#a-quick-recap&quot;&gt;A quick recap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-the-source-code-reveals&quot;&gt;What the source code reveals&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#the-three-values-that-drive-allocation&quot;&gt;The three values that drive allocation&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#1-pg_stat_statementsmax&quot;&gt;1. pg_stat_statements.max&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2-the-hidden-usage-counter&quot;&gt;2. The hidden usage counter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#3-mean_query_length&quot;&gt;3. mean_query_length&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#only-completed-executions-get-stored&quot;&gt;Only completed executions get stored&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#the-three-functions-that-do-the-work&quot;&gt;The three functions that do the work&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#the-fast-path-when-the-entry-already-exists&quot;&gt;The fast path: when the entry already exists&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#the-slow-path-when-the-entry-doesnt-exist&quot;&gt;The slow path: when the entry doesn’t exist&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#why-a-larger-pg_stat_statementsmax-is-not-free&quot;&gt;Why a larger pg_stat_statements.max is not free&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#key-takeaways&quot;&gt;Key takeaways&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#whats-coming-next&quot;&gt;What’s coming next&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-we-discussed-in-this-episode&quot;&gt;What we discussed in this episode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr/&gt;
&lt;p&gt;&lt;strong&gt;Transcript&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;a-quick-recap&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#a-quick-recap&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;A quick recap&lt;/h2&gt;
&lt;p&gt;In &lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-1&quot;&gt;Part 1&lt;/a&gt; we looked at the in-memory hash table that backs pg_stat_statements and the metrics it tracks. In &lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-2&quot;&gt;Part 2&lt;/a&gt; we dug into what makes an entry “unique” — the queryid, derived from the structural shape of the query. And in &lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-3&quot;&gt;Part 3&lt;/a&gt; we walked through where the query text actually lives: a separate file on disk, sized at 2× the mean query length × &lt;code&gt;pg_stat_statements.max&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This episode is honestly the one I’m most excited about. We’re going to go look at the source code itself — the structs, the functions, the workflow — and answer the question that’s been lurking under everything so far: &lt;strong&gt;when the table is full and a new query comes in, what gets kept and what gets dropped?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I’ll warn you upfront that this episode is a little longer than the others. There’s a lot to cover. The good news is the actual source is small: take out the comments and pg_stat_statements is only about 1,800 lines of code. Most of that is defensive — making sure the right things get stored at the right times. The story underneath is honestly pretty simple, once you can see it.&lt;/p&gt;
&lt;h2 id=&quot;what-the-source-code-reveals&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#what-the-source-code-reveals&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What the source code reveals&lt;/h2&gt;
&lt;p&gt;When you look at the C structs in the source, you start to see things we’ve already discussed. The hash table key is what you’d expect: user ID, database ID, queryid, and the top-level boolean that says whether the query was a top-level statement or executed inside a function.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;pgssEntry&lt;/code&gt; struct holds a &lt;code&gt;counters&lt;/code&gt; array. That’s where the metrics live — number of calls, total time, mean time, JIT counters, buffers read, all of it. From &lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-3&quot;&gt;Part 3&lt;/a&gt; you’ll also see the offset and length pointing into the external query text file.&lt;/p&gt;
&lt;p&gt;Then there’s a shared state struct with a handful of variables that the workers need across processes. One of those, &lt;code&gt;mean_query_length&lt;/code&gt;, is going to matter a lot in a minute.&lt;/p&gt;
&lt;h2 id=&quot;the-three-values-that-drive-allocation&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#the-three-values-that-drive-allocation&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;The three values that drive allocation&lt;/h2&gt;
&lt;p&gt;As I’ve spent more time in this code over the last year, I’ve come up with three things that I think are really necessary to understand about how pg_stat_statements decides what to keep. There’s more going on than just these three, but if you understand these, the rest of the workflow makes sense.&lt;/p&gt;
&lt;h3 id=&quot;1-pg_stat_statementsmax&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#1-pg_stat_statementsmax&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. pg_stat_statements.max&lt;/h3&gt;
&lt;p&gt;We’ve talked about this throughout the series and it works exactly as you’d expect. When a brand-new query comes in and we’ve already reached &lt;code&gt;max&lt;/code&gt; entries, pg_stat_statements has no other path — it has to deallocate something to make room, and may also have to truncate the external text file before it can store the new entry.&lt;/p&gt;
&lt;h3 id=&quot;2-the-hidden-usage-counter&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#2-the-hidden-usage-counter&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. The hidden usage counter&lt;/h3&gt;
&lt;p&gt;This is the one most people don’t know about. Every hash table entry has a per-query &lt;code&gt;usage&lt;/code&gt; value stored alongside the regular metrics. It’s not returned in the &lt;code&gt;pg_stat_statements&lt;/code&gt; view. You never see it directly. It exists solely to help decide which entries get dropped when deallocation has to run.&lt;/p&gt;
&lt;p&gt;It’s incremented by one every time a query finishes executing. Interestingly, the function that does the incrementing is called &lt;code&gt;USAGE_EXEC()&lt;/code&gt; and it takes total time as a parameter — but if you read the code, the value being added is statically typed to &lt;code&gt;1&lt;/code&gt;. The total time goes nowhere. There’s actually a note in the source asking whether this is the right approach, or whether time or buffer usage would be a better proxy for “which queries do we want to keep around longest?” That discussion has been ongoing in the community for a while.&lt;/p&gt;
&lt;p&gt;The mechanics during deallocation are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Decrease every entry’s usage by 1%.&lt;/li&gt;
&lt;li&gt;Sort all entries by usage.&lt;/li&gt;
&lt;li&gt;Drop the bottom 5%.&lt;/li&gt;
&lt;li&gt;Increment the deallocation counter.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;One interesting observation: while pg_stat_statements is filling up and no deallocation has happened yet, &lt;code&gt;calls&lt;/code&gt; and &lt;code&gt;usage&lt;/code&gt; will be the same number for every entry — they’re both going up by one per execution. They only diverge after the first deallocation.&lt;/p&gt;
&lt;h3 id=&quot;3-mean_query_length&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#3-mean_query_length&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. mean_query_length&lt;/h3&gt;
&lt;p&gt;This is the mean length of all the query texts currently in the hash table. It’s only updated during deallocation, right before the bottom 5% gets dropped. Once it’s recomputed, pg_stat_statements uses it to decide whether the external text file is now more than twice the expected length. If it is, the file gets rewritten (the “garbage collection” step we’ll cover below).&lt;/p&gt;
&lt;h2 id=&quot;only-completed-executions-get-stored&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#only-completed-executions-get-stored&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Only completed executions get stored&lt;/h2&gt;
&lt;p&gt;Before we go further, one thing worth being absolutely clear on: pg_stat_statements only stores metrics for &lt;strong&gt;completed executions&lt;/strong&gt;. It’s called from the &lt;code&gt;ExecutorEnd&lt;/code&gt; hook, after planning and after everything else has run.&lt;/p&gt;
&lt;p&gt;That means &lt;strong&gt;no aborted or timed-out query metrics are stored&lt;/strong&gt;. If you have a &lt;code&gt;statement_timeout&lt;/code&gt; of 30 seconds and a query hits that timeout after reading hundreds of thousands of buffers, those buffers don’t show up in pg_stat_statements at all. The query effectively didn’t happen, as far as this extension is concerned.&lt;/p&gt;
&lt;p&gt;I want to flag this directly because if you ask an AI about pg_stat_statements in mid-2026 it will often tell you the opposite. I queried this recently and got confidently wrong answers. The source is unambiguous: nothing in the code stores metrics for timed-out queries. This is also one of the problems the community is actively trying to solve — how do we get visibility into how often a query is timing out, and how much work it did before it died? Right now, we don’t.&lt;/p&gt;
&lt;h2 id=&quot;the-three-functions-that-do-the-work&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#the-three-functions-that-do-the-work&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;The three functions that do the work&lt;/h2&gt;
&lt;p&gt;When you actually read the code, there are three primary functions for storing data:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;pgss_store()&lt;/code&gt;&lt;/strong&gt; — every completed execution goes through this. It looks up the hash, and if it’s there, just updates the counters. Done.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;entry_alloc()&lt;/code&gt;&lt;/strong&gt; — called when the query is new and the entry needs to be allocated in the hash table.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;entry_dealloc()&lt;/code&gt;&lt;/strong&gt; — called from inside &lt;code&gt;entry_alloc&lt;/code&gt; when the table is full and we need to make room.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Two secondary functions handle the external query text file:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;need_gc_qtexts()&lt;/code&gt;&lt;/strong&gt; — checks whether the file is more than 2× expected size and flags that garbage collection is needed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;gc_qtexts()&lt;/code&gt;&lt;/strong&gt; — does the actual garbage collection: rewrites the file with the texts of whatever’s still in the (now smaller) hash table.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-fast-path-when-the-entry-already-exists&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#the-fast-path-when-the-entry-already-exists&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;The fast path: when the entry already exists&lt;/h2&gt;
&lt;p&gt;This is the path you want most of your workload to be on. &lt;code&gt;pgss_store()&lt;/code&gt; gets called at the end of execution, looks up the queryid in the hash table, finds it, takes a &lt;strong&gt;shared lock&lt;/strong&gt;, updates the counters, and releases all locks. That’s it. It’s fast, it’s cheap, and many backends can do it concurrently.&lt;/p&gt;
&lt;p&gt;If you’re in a steady-state workload where pg_stat_statements has captured your common queries and isn’t churning, this is what’s happening for the vast majority of executions.&lt;/p&gt;
&lt;h2 id=&quot;the-slow-path-when-the-entry-doesnt-exist&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#the-slow-path-when-the-entry-doesnt-exist&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;The slow path: when the entry doesn’t exist&lt;/h2&gt;
&lt;p&gt;This is where the work piles up. &lt;code&gt;pgss_store()&lt;/code&gt; looks up the queryid, doesn’t find it, and now we have to do real work under an &lt;strong&gt;exclusive lock&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Switch from shared to exclusive lock.&lt;/li&gt;
&lt;li&gt;Check whether the external text file needs garbage collection later.&lt;/li&gt;
&lt;li&gt;Check whether we’ve hit &lt;code&gt;pg_stat_statements.max&lt;/code&gt;. If yes, deallocate:
&lt;ul&gt;
&lt;li&gt;Decrease all usage values by 1% (or 50% for sticky entries).&lt;/li&gt;
&lt;li&gt;Sort every entry by usage.&lt;/li&gt;
&lt;li&gt;Drop the bottom 5%.&lt;/li&gt;
&lt;li&gt;Increment the deallocation counter.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;entry_alloc()&lt;/code&gt;, which &lt;strong&gt;searches one more time&lt;/strong&gt; before allocating — another backend might have inserted the same queryid in the gap between our locks.&lt;/li&gt;
&lt;li&gt;If garbage collection was flagged, load the entire query text file into memory, then overwrite it in place (not a copy + delete — pg_stat_statements actually overwrites the file and truncates the tail with a C &lt;code&gt;truncate&lt;/code&gt; call).&lt;/li&gt;
&lt;li&gt;Allocate the new entry, initialize its counters, write the text into the file, release the exclusive lock.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The longer this path is and the more often you’re on it, the more time your queries spend waiting for the exclusive lock.&lt;/p&gt;
&lt;h2 id=&quot;why-a-larger-pg_stat_statementsmax-is-not-free&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#why-a-larger-pg_stat_statementsmax-is-not-free&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Why a larger pg_stat_statements.max is not free&lt;/h2&gt;
&lt;p&gt;Look at the deallocation step again: every time we deallocate, we &lt;strong&gt;sort all entries by usage&lt;/strong&gt; and drop the bottom 5%. That sort is linear in the number of entries.&lt;/p&gt;
&lt;p&gt;If you’ve bumped &lt;code&gt;max&lt;/code&gt; from the default 5,000 to 10,000, 50,000, or 100,000 — which is tempting when you’re losing queries to eviction — then every deallocation has to touch and sort that many rows, all under an exclusive lock. You haven’t eliminated the deallocation cost; you’ve made each one more expensive, while still potentially deallocating frequently if your workload’s cardinality is high enough.&lt;/p&gt;
&lt;p&gt;There’s a dance here. A higher &lt;code&gt;max&lt;/code&gt; reduces deallocation frequency, but each deallocation costs more. A larger external text file means garbage collection rewrites more data when it triggers. If you only allocate more space without addressing the underlying cardinality, you pay the cost somewhere else.&lt;/p&gt;
&lt;p&gt;We’ll get into the configuration choices that actually help in the next couple of episodes.&lt;/p&gt;
&lt;h2 id=&quot;key-takeaways&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#key-takeaways&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Key takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;There’s a hidden per-query &lt;code&gt;usage&lt;/code&gt; counter&lt;/strong&gt; stored alongside the metrics. It’s not in the &lt;code&gt;pg_stat_statements&lt;/code&gt; view but it determines which queries get evicted.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Eviction sorts the whole table.&lt;/strong&gt; Every deallocation decreases all usage values by 1%, sorts every entry, and drops the bottom 5%.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Only completed executions are stored.&lt;/strong&gt; Aborted and timed-out queries leave no record in pg_stat_statements — regardless of what an AI tells you.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Three primary functions do all the work:&lt;/strong&gt; &lt;code&gt;pgss_store&lt;/code&gt;, &lt;code&gt;entry_alloc&lt;/code&gt;, &lt;code&gt;entry_dealloc&lt;/code&gt;. The fast path is shared-lock counter updates. The slow path runs deallocation, garbage collection of the external text file, and entry allocation under exclusive lock.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A higher &lt;code&gt;pg_stat_statements.max&lt;/code&gt; is not free.&lt;/strong&gt; The deallocation sort is linear in &lt;code&gt;max&lt;/code&gt;, so a bigger table means each eviction touches more rows under exclusive lock.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;whats-coming-next&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#whats-coming-next&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What’s coming next&lt;/h2&gt;
&lt;p&gt;Now that we’ve seen how pg_stat_statements decides what to keep, the last couple of episodes turn to what you can actually do about it. We’ll look at the configuration that matters most, what high-cardinality workloads really look like under this allocation/deallocation pressure (and how Postgres 18’s improvements to &lt;code&gt;WHERE IN&lt;/code&gt; help), and concrete strategies for using pg_stat_statements safely at scale.&lt;/p&gt;
&lt;p&gt;I hope this series helps you better understand one of the most essential tools in the Postgres ecosystem. Feel free to &lt;a href=&quot;https://www.youtube.com/channel/UCDV_1Dz2Ixgl1nT_3DUZVFw&quot; target=&quot;_blank&quot;&gt;subscribe to our YouTube channel&lt;/a&gt;, &lt;a href=&quot;https://pganalyze.com/newsletter&quot; target=&quot;_blank&quot;&gt;sign up for our newsletter&lt;/a&gt; or &lt;a href=&quot;https://www.linkedin.com/company/pganalyze/&quot; target=&quot;_blank&quot;&gt;follow us on LinkedIn&lt;/a&gt; to get updates about new episodes!&lt;/p&gt;
&lt;h2 id=&quot;what-we-discussed-in-this-episode&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#what-we-discussed-in-this-episode&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What we discussed in this episode&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/pgstatstatements.html&quot;&gt;pg_stat_statements documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/pgstatstatements.html#PGSTATSTATEMENTS-CONFIGURATION-PARAMETERS&quot;&gt;pg_stat_statements configuration parameters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/postgres/postgres/tree/master/contrib/pg_stat_statements&quot;&gt;pg_stat_statements source code on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-1&quot;&gt;Part 1: What it is and what it isn’t&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-2&quot;&gt;Part 2: What makes a statement unique&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-3&quot;&gt;Part 3: Where Your Query Text Actually Lives&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Ryan Booz</dc:creator></item><item><title>Postgres in Production Special Series: Where Your Query Text Actually Lives (Part 3)</title><link>https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-3</link><guid isPermaLink="false">https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-3</guid><description>In Part 3 of this special “Postgres in Production” deep dive series, Ryan Booz walks through where pg_stat_statements actually stores your query text. Spoiler: it’s not in the in-memory hash table you might expect. He covers the file’s location on disk, how it grows, the pointer mechanism that ties hash table entries back to your SQL, the quirky consequence that the same query text can appear multiple times, and why ORM-heavy workloads can balloon this file to hundreds of megabytes. Share this…</description><pubDate>Tue, 19 May 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In Part 3 of this special “Postgres in Production” deep dive series, Ryan Booz walks through where pg_stat_statements actually stores your query text. Spoiler: it’s not in the in-memory hash table you might expect. He covers the file’s location on disk, how it grows, the pointer mechanism that ties hash table entries back to your SQL, the quirky consequence that the same query text can appear multiple times, and why ORM-heavy workloads can balloon this file to hundreds of megabytes.&lt;/p&gt;
&lt;iframe width=&quot;750&quot; height=&quot;421&quot; src=&quot;https://www.youtube-nocookie.com/embed/5MGy7drBMCk?si=TpDXmPY6oxmTZSTI&quot; frameborder=&quot;0&quot; modestbranding=&quot;1&quot; controls allownetworking=&quot;internal&quot; allow=&quot;autoplay; encrypted-media&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;&lt;strong&gt;Share this episode:&lt;/strong&gt; Click here to share this episode &lt;a href=&quot;https://www.linkedin.com/shareArticle?mini=true&amp;amp;url=https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-3&amp;amp;title=Deep%20Dive%20into%20pg_stat_statements%20Episode%203&amp;amp;source=LinkedIn&quot;&gt;on LinkedIn&lt;/a&gt;. Feel free to &lt;a href=&quot;https://pganalyze.com/newsletter&quot;&gt;sign up for our newsletter&lt;/a&gt; and &lt;a href=&quot;https://www.youtube.com/channel/UCDV_1Dz2Ixgl1nT_3DUZVFw&quot;&gt;subscribe to our YouTube channel&lt;/a&gt;.&lt;/p&gt;
&lt;div &gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#a-quick-recap&quot;&gt;A quick recap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#where-the-query-text-actually-lives&quot;&gt;Where the query text actually lives&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#locating-the-query-text-file&quot;&gt;Locating the query text file&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#how-the-file-grows-append-only-with-a-2-size-ceiling&quot;&gt;How the file grows: append-only with a 2× size ceiling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#the-pointer-mechanism-offset-and-length-per-entry&quot;&gt;The pointer mechanism: offset and length per entry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#a-performance-note-when-the-file-actually-gets-read&quot;&gt;A performance note: when the file actually gets read&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#no-memory-of-previously-tracked-queries&quot;&gt;No memory of previously tracked queries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#the-rewrite-cycle-when-the-file-fills-up&quot;&gt;The rewrite cycle when the file fills up&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#a-real-world-size-example&quot;&gt;A real-world size example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#why-orms-make-this-worse&quot;&gt;Why ORMs make this worse&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#key-takeaways&quot;&gt;Key takeaways&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#whats-coming-next&quot;&gt;What’s coming next&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-we-discussed-in-this-episode&quot;&gt;What we discussed in this episode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr/&gt;
&lt;p&gt;&lt;strong&gt;Transcript&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;a-quick-recap&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#a-quick-recap&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;A quick recap&lt;/h2&gt;
&lt;p&gt;In &lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-1&quot;&gt;the first episode&lt;/a&gt;, we looked at the in-memory hash table that backs pg_stat_statements and the metrics it tracks. In &lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-2&quot;&gt;Part 2&lt;/a&gt;, we dug into what actually makes an entry “unique” — the queryid, which is derived from the structure of the query rather than its text, combined with the role, the database, and whether the query was top-level.&lt;/p&gt;
&lt;p&gt;That recap matters because of a question I haven’t answered yet: if the hash table is keyed on a structural hash, &lt;strong&gt;where does the actual query text live?&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;where-the-query-text-actually-lives&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#where-the-query-text-actually-lives&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Where the query text actually lives&lt;/h2&gt;
&lt;p&gt;This is the part that surprises most people the first time they hear it. The query text is &lt;strong&gt;not stored in the in-memory hash table&lt;/strong&gt;. It lives in a separate file on disk, inside the Postgres data directory.&lt;/p&gt;
&lt;p&gt;The hash table only holds the metrics, the queryid, and a few small bookkeeping fields. When you ask for the query text, Postgres has to go read it from this file.&lt;/p&gt;
&lt;h2 id=&quot;locating-the-query-text-file&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#locating-the-query-text-file&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Locating the query text file&lt;/h2&gt;
&lt;p&gt;In most installations, the file lives under the &lt;code&gt;global&lt;/code&gt; directory inside the Postgres data directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$PGDATA/global/pgss_query_texts.stat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In some Docker images and packaged installations, you’ll find it in &lt;code&gt;$PGDATA/pg_stat_tmp/&lt;/code&gt; instead. Either way, the file is there somewhere, and it’s worth poking around once just to see it exists.&lt;/p&gt;
&lt;h2 id=&quot;how-the-file-grows-append-only-with-a-2-size-ceiling&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#how-the-file-grows-append-only-with-a-2-size-ceiling&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;How the file grows: append-only with a 2× size ceiling&lt;/h2&gt;
&lt;p&gt;Two things to know about how this file behaves:&lt;/p&gt;
&lt;p&gt;First, it’s &lt;strong&gt;append-only&lt;/strong&gt; for new entries. Every time a brand-new, normalized query shows up in pg_stat_statements, the text of that query gets written to the end of the file.&lt;/p&gt;
&lt;p&gt;Second, it has a size ceiling. The file is allowed to grow to roughly &lt;strong&gt;twice the mean length of all currently tracked queries&lt;/strong&gt;, multiplied by the maximum number of entries pg_stat_statements is configured to hold. pg_stat_statements updates the mean query length during deallocation, so it changes over time, before the query file is truncated.&lt;/p&gt;
&lt;h2 id=&quot;the-pointer-mechanism-offset-and-length-per-entry&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#the-pointer-mechanism-offset-and-length-per-entry&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;The pointer mechanism: offset and length per entry&lt;/h2&gt;
&lt;p&gt;The way the hash table and the file stay in sync is straightforward. For every entry in the in-memory hash table, pg_stat_statements stores a pointer into the text file — really, an &lt;strong&gt;offset&lt;/strong&gt; and a &lt;strong&gt;length&lt;/strong&gt;. When you ask for the query text, Postgres opens the file, seeks to the offset, reads that many bytes, and hands you back the SQL.&lt;/p&gt;
&lt;p&gt;The file is otherwise hidden from you. You’ll never interact with it directly. But understanding that the pointer dance is happening explains a few quirks you’ll see in production.&lt;/p&gt;
&lt;h2 id=&quot;a-performance-note-when-the-file-actually-gets-read&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#a-performance-note-when-the-file-actually-gets-read&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;A performance note: when the file actually gets read&lt;/h2&gt;
&lt;p&gt;Because reading the file is a real I/O operation, pg_stat_statements is careful about when it opens it.&lt;/p&gt;
&lt;p&gt;If you call the function form, &lt;code&gt;pg_stat_statements(true)&lt;/code&gt;, you’ll get the query text in the results. Pass &lt;code&gt;false&lt;/code&gt; instead and pg_stat_statements skips the file entirely:&lt;/p&gt;
&lt;pre&gt;&lt;code &gt;&lt;span &gt;-- Reads the on-disk text file:&lt;/span&gt;
&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;*&lt;/span&gt; &lt;span &gt;FROM&lt;/span&gt; pg_stat_statements(&lt;span &gt;true&lt;/span&gt;);

&lt;span &gt;-- Does not read the file:&lt;/span&gt;
&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;*&lt;/span&gt; &lt;span &gt;FROM&lt;/span&gt; pg_stat_statements(&lt;span &gt;false&lt;/span&gt;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unfortunately the same does not apply to the view. Regardless of whether or not you select the &lt;code&gt;query&lt;/code&gt; column, the view always opens the query text file and retrieves the query text. On a busy system where you’re just charting metrics, using pg_stat_statements(false) is a good habit to get into, only retrieving query texts when necessary.&lt;/p&gt;
&lt;h2 id=&quot;no-memory-of-previously-tracked-queries&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#no-memory-of-previously-tracked-queries&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;No memory of previously tracked queries&lt;/h2&gt;
&lt;p&gt;Here’s the quirky consequence of the pointer mechanism. &lt;strong&gt;pg_stat_statements has no memory of previously tracked queryids.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Say your &lt;code&gt;pg_stat_statements.max&lt;/code&gt; is 5,000 and you fill it. A new query comes in, an old one gets evicted, and the evicted entry’s metrics are gone. Now imagine that same evicted query runs again later. As far as the in-memory hash table is concerned, this is a brand-new entry. Postgres writes the text to the file again, at a fresh offset, with a fresh pointer.&lt;/p&gt;
&lt;p&gt;The practical result: &lt;strong&gt;the same query text can appear multiple times in the file&lt;/strong&gt;. It’s not a bug, it’s a direct consequence of the hash table not remembering what it has evicted.&lt;/p&gt;
&lt;h2 id=&quot;the-rewrite-cycle-when-the-file-fills-up&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#the-rewrite-cycle-when-the-file-fills-up&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;The rewrite cycle when the file fills up&lt;/h2&gt;
&lt;p&gt;When the file hits its size ceiling, pg_stat_statements doesn’t garbage-collect in place. Instead, it rewrites the entire file based on what’s currently active in the hash table, writes that to a temporary file, drops the old file, and renames the temp file into its place.&lt;/p&gt;
&lt;p&gt;There’s a brief moment of locking during the swap. On a small file that’s invisible. On a large file, it isn’t.&lt;/p&gt;
&lt;h2 id=&quot;a-real-world-size-example&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#a-real-world-size-example&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;A real-world size example&lt;/h2&gt;
&lt;p&gt;This file can grow more than you’d expect. As a quick illustration, I checked a local instance with &lt;code&gt;pg_stat_statements.max = 5000&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;About 5,000 currently tracked queries&lt;/li&gt;
&lt;li&gt;Mean query text length: about 900 bytes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That works out to roughly &lt;code&gt;5000 × 900 × 2 ≈ 9 MB&lt;/code&gt; before pg_stat_statements rewrites the file. That’s modest.&lt;/p&gt;
&lt;p&gt;But the multipliers run away on you fast. Raise &lt;code&gt;max&lt;/code&gt; to 20,000 or 50,000, or work with a system that generates very long SQL on average, and the file can comfortably reach &lt;strong&gt;hundreds of megabytes&lt;/strong&gt;. At that scale, the rewrite cycle is a real disk and lock event, not a footnote.&lt;/p&gt;
&lt;h2 id=&quot;why-orms-make-this-worse&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#why-orms-make-this-worse&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Why ORMs make this worse&lt;/h2&gt;
&lt;p&gt;ORMs are the most common reason a real-world &lt;code&gt;pgss_query_texts.stat&lt;/code&gt; blows up.&lt;/p&gt;
&lt;p&gt;ORMs don’t optimize for the structural shape of the SQL they emit. They optimize for translating your application code into something Postgres can parse. The result is verbose, comment-laden, sometimes deeply nested SQL — and the texts are &lt;em&gt;long&lt;/em&gt;. When the mean query length on your system is 8,000 bytes instead of 900, the size ceiling for the text file shifts by an order of magnitude.&lt;/p&gt;
&lt;p&gt;Combined with the high cardinality problem from &lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-2&quot;&gt;Part 2&lt;/a&gt; (where ORMs create lots of distinct queryids), this is the worst-case profile: many entries, each one large, file at its maximum size, frequent rewrites under lock.&lt;/p&gt;
&lt;p&gt;If you’re seeing intermittent latency spikes on a system with a heavy ORM workload and a large &lt;code&gt;pg_stat_statements.max&lt;/code&gt;, this is worth a look.&lt;/p&gt;
&lt;h2 id=&quot;key-takeaways&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#key-takeaways&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Key takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The query text isn’t in the hash table.&lt;/strong&gt; It’s in a separate file on disk in the Postgres data directory.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Each hash table entry holds a pointer (offset + length) into that file&lt;/strong&gt;, not the text itself.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The file is append-only for new entries&lt;/strong&gt;, and grows to roughly 2× the mean query length × &lt;code&gt;pg_stat_statements.max&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pg_stat_statements has no memory of previously evicted queryids.&lt;/strong&gt; Evicted queries that come back are treated as new, and their text gets written to the file again. The same text can appear multiple times.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;When the file hits its size ceiling, Postgres rewrites it from scratch.&lt;/strong&gt; A temp file gets written, the old file is dropped, the temp is renamed in. There’s a brief moment of locking during the swap.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ORM-heavy workloads are the most common reason the file balloons.&lt;/strong&gt; Long SQL × many distinct queryids = a large file and frequent rewrites.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reading the file is optional.&lt;/strong&gt; &lt;code&gt;pg_stat_statements(false)&lt;/code&gt; skips reading the file from disk.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;whats-coming-next&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#whats-coming-next&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What’s coming next&lt;/h2&gt;
&lt;p&gt;Now that we’ve covered where the query text lives, the next episode takes us to the Postgres source. We’ll look at how &lt;code&gt;pg_stat_statements.max&lt;/code&gt; is enforced, how the 2× file size ceiling is implemented, what eviction actually does, and what gets &lt;em&gt;kept&lt;/em&gt; when entries are evicted. That’s where the practical “is the data I need actually still in pg_stat_statements?” question gets answered.&lt;/p&gt;
&lt;p&gt;I hope this series helps you better understand one of the most essential tools in the Postgres ecosystem. Feel free to &lt;a href=&quot;https://www.youtube.com/channel/UCDV_1Dz2Ixgl1nT_3DUZVFw&quot;&gt;subscribe to our YouTube channel&lt;/a&gt;, &lt;a href=&quot;https://pganalyze.com/newsletter&quot;&gt;sign up for our newsletter&lt;/a&gt; or &lt;a href=&quot;https://www.linkedin.com/company/pganalyze/&quot;&gt;follow us on LinkedIn&lt;/a&gt; to get updates about new episodes!&lt;/p&gt;
&lt;h2 id=&quot;what-we-discussed-in-this-episode&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#what-we-discussed-in-this-episode&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What we discussed in this episode&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/pgstatstatements.html&quot;&gt;pg_stat_statements documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/pgstatstatements.html#PGSTATSTATEMENTS-CONFIGURATION-PARAMETERS&quot;&gt;pg_stat_statements configuration parameters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-1&quot;&gt;Part 1: What it is and what it isn’t&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-2&quot;&gt;Part 2: What makes a statement unique&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Ryan Booz</dc:creator></item><item><title>Postgres in Production Special Series: What Makes a Statement Unique (Part 2)</title><link>https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-2</link><guid isPermaLink="false">https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-2</guid><description>In Part 2 of this special “Postgres in Production” deep dive series, Ryan Booz walks through what actually makes an entry in pg_stat_statements “unique”, why uniqueness is structural rather than textual, and how a few common patterns (ORMs, dynamic SQL, multi-database setups) can quietly fill up the in-memory hash table and cost you visibility into the queries that matter most. Share this episode: Click here to share this episode on LinkedIn . Feel free to sign up for our newsletter and…</description><pubDate>Wed, 06 May 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In Part 2 of this special “Postgres in Production” deep dive series, Ryan Booz walks through what actually makes an entry in pg_stat_statements “unique”, why uniqueness is structural rather than textual, and how a few common patterns (ORMs, dynamic SQL, multi-database setups) can quietly fill up the in-memory hash table and cost you visibility into the queries that matter most.&lt;/p&gt;
&lt;iframe width=&quot;750&quot; height=&quot;421&quot; src=&quot;https://www.youtube-nocookie.com/embed/gjNxObDi6Dw?si=hjCntqImuwGAPnWh&quot; frameborder=&quot;0&quot; modestbranding=&quot;1&quot; controls allownetworking=&quot;internal&quot; allow=&quot;autoplay; encrypted-media&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;&lt;strong&gt;Share this episode:&lt;/strong&gt; Click here to share this episode &lt;a href=&quot;https://www.linkedin.com/shareArticle?mini=true&amp;amp;url=https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-2&amp;amp;title=Deep%20Dive%20into%20pg_stat_statements%20Episode%202&amp;amp;source=LinkedIn&quot;&gt;on LinkedIn&lt;/a&gt;. Feel free to &lt;a href=&quot;https://pganalyze.com/newsletter&quot;&gt;sign up for our newsletter&lt;/a&gt; and &lt;a href=&quot;https://www.youtube.com/channel/UCDV_1Dz2Ixgl1nT_3DUZVFw&quot;&gt;subscribe to our YouTube channel&lt;/a&gt;.&lt;/p&gt;
&lt;div &gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#a-quick-recap&quot;&gt;A quick recap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-makes-an-entry-unique&quot;&gt;What makes an entry unique&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#a-note-on-top-level-queries&quot;&gt;A note on top-level queries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#how-quickly-entries-multiply&quot;&gt;How quickly entries multiply&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#uniqueness-is-structural-not-textual&quot;&gt;Uniqueness is structural, not textual&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-gets-normalized-away&quot;&gt;What gets normalized away&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#why-sql-comment-tags-can-disappear&quot;&gt;Why SQL comment tags can disappear&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-doesnt-get-normalized&quot;&gt;What doesn’t get normalized&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#the-in-clause-explosion-and-how-postgres-18-fixes-it&quot;&gt;The IN-clause explosion (and how Postgres 18 fixes it)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#structure-vs-semantics&quot;&gt;Structure vs. semantics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-happens-when-pg_stat_statements-fills-up&quot;&gt;What happens when pg_stat_statements fills up&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#key-takeaways&quot;&gt;Key takeaways&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#whats-coming-next&quot;&gt;What’s coming next&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-we-discussed-in-this-episode&quot;&gt;What we discussed in this episode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr/&gt;
&lt;p&gt;&lt;strong&gt;Transcript&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;a-quick-recap&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#a-quick-recap&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;A quick recap&lt;/h2&gt;
&lt;p&gt;In the first episode, we looked at the in-memory hash table that backs pg_stat_statements and the columns it exposes — about 45 of them in Postgres 18, with more likely to come in future versions. In this episode, we dig into what actually makes an entry in that table “unique”, and why getting that wrong can quietly cost you visibility into your most important queries.&lt;/p&gt;
&lt;h2 id=&quot;what-makes-an-entry-unique&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#what-makes-an-entry-unique&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What makes an entry unique&lt;/h2&gt;
&lt;p&gt;Each entry in pg_stat_statements is keyed on four things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;role&lt;/strong&gt; that ran the query&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;database&lt;/strong&gt; it ran in&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;queryid&lt;/strong&gt;, a hash derived from the structure of the query&lt;/li&gt;
&lt;li&gt;Whether the query was &lt;strong&gt;top-level&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last one deserves a brief explanation, because it’s controlled by a setting that’s easy to miss.&lt;/p&gt;
&lt;h2 id=&quot;a-note-on-top-level-queries&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#a-note-on-top-level-queries&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;A note on top-level queries&lt;/h2&gt;
&lt;p&gt;By default, &lt;code&gt;pg_stat_statements.track&lt;/code&gt; is set to &lt;code&gt;top&lt;/code&gt;. A top-level query is the one you initiated directly from a session or transaction. For example, running &lt;code&gt;SELECT title FROM ...&lt;/code&gt; from your application is a top-level query.&lt;/p&gt;
&lt;p&gt;If that query in turn triggers other queries, typically by calling a function or stored procedure, those internal queries are &lt;em&gt;not&lt;/em&gt; top-level. They were initiated by Postgres on your behalf, not by you.&lt;/p&gt;
&lt;p&gt;With the default setting, you’ll see the function call itself in pg_stat_statements, but not the queries it runs internally. If you change the setting to &lt;code&gt;all&lt;/code&gt;, those internal statements get tracked too. The catch: there is no column anywhere in pg_stat_statements that ties an internal statement back to the parent that called it. You can see the metrics, but you can’t reconstruct the call chain. As of Postgres 18, that’s a known limitation, and nothing on the horizon for 19 or beyond changes it.&lt;/p&gt;
&lt;h2 id=&quot;how-quickly-entries-multiply&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#how-quickly-entries-multiply&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;How quickly entries multiply&lt;/h2&gt;
&lt;p&gt;Even a simple workload can produce a surprising number of entries. Imagine a single query, &lt;code&gt;SELECT title FROM film WHERE id = ?&lt;/code&gt;. Now imagine running it across:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Three databases on the same server (for example, one per customer, all with the same schema)&lt;/li&gt;
&lt;li&gt;A handful of roles per database&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You’ll get one entry per &lt;code&gt;(role, database, queryid)&lt;/code&gt; combination. In a setup with three databases, three users, and one application user running the query in each database, you can land at five entries from a single logical query. Change the SELECT clause to &lt;code&gt;SELECT title, rating FROM film WHERE id = ?&lt;/code&gt; and you get a brand-new queryid, doubling the count to ten.&lt;/p&gt;
&lt;p&gt;Now scale that up across an ORM that emits dozens of variants of every query, and you can see how the entry count climbs fast.&lt;/p&gt;
&lt;p&gt;This matters because pg_stat_statements has a hard ceiling. The &lt;code&gt;pg_stat_statements.max&lt;/code&gt; setting controls how many unique entries it will track at once, and the default is &lt;strong&gt;5,000&lt;/strong&gt;. You can raise or lower it, but at any moment in time, that’s the upper limit. If you go over, something has to be evicted.&lt;/p&gt;
&lt;h2 id=&quot;uniqueness-is-structural-not-textual&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#uniqueness-is-structural-not-textual&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Uniqueness is structural, not textual&lt;/h2&gt;
&lt;p&gt;This is the part most people get wrong, myself included. The instinctive assumption is that uniqueness is based on the query &lt;em&gt;text&lt;/em&gt;, what you wrote or what your ORM generated. It’s not. It’s based on the query’s &lt;em&gt;structure&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Postgres parses the query, walks the parse tree, strips out things that don’t affect the structure (like literal values), serializes what’s left, and hashes the result. That hash is the queryid.&lt;/p&gt;
&lt;p&gt;The internal name for this process is &lt;strong&gt;jumbling&lt;/strong&gt;, handled by a function called &lt;code&gt;JumbleQuery()&lt;/code&gt;. You’ll hear the term in mailing list discussions and in the source code, so it’s worth knowing.&lt;/p&gt;
&lt;p&gt;The practical upshot: this query, run with three different parameter values:&lt;/p&gt;
&lt;pre&gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt; title &lt;span &gt;FROM&lt;/span&gt; film &lt;span &gt;WHERE&lt;/span&gt; id &lt;span &gt;=&lt;/span&gt; &lt;span &gt;1&lt;/span&gt;;
&lt;span &gt;SELECT&lt;/span&gt; title &lt;span &gt;FROM&lt;/span&gt; film &lt;span &gt;WHERE&lt;/span&gt; id &lt;span &gt;=&lt;/span&gt; &lt;span &gt;2&lt;/span&gt;;
&lt;span &gt;SELECT&lt;/span&gt; title &lt;span &gt;FROM&lt;/span&gt; film &lt;span &gt;WHERE&lt;/span&gt; id &lt;span &gt;=&lt;/span&gt; &lt;span &gt;3&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;produces a single entry in pg_stat_statements. You could run it a thousand times with a thousand different IDs and you’d still get one entry.&lt;/p&gt;
&lt;h2 id=&quot;what-gets-normalized-away&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#what-gets-normalized-away&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What gets normalized away&lt;/h2&gt;
&lt;p&gt;Anything that doesn’t change the &lt;em&gt;shape&lt;/em&gt; of the query gets normalized:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Literals and constants&lt;/strong&gt; (numbers, strings, dates)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parameter values&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Whitespace and formatting&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Comments&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last one trips people up. A common pattern is to inject comments into queries to track which application function generated them, something like &lt;code&gt;/* user_service.fetch_profile */ SELECT ...&lt;/code&gt;. Comments are stripped during jumbling, so two structurally identical queries with different comments collapse into one entry.&lt;/p&gt;
&lt;p&gt;There’s a subtle quirk here: when an entry is first created, pg_stat_statements stores the &lt;em&gt;exact text&lt;/em&gt; of the first execution it saw. Every subsequent execution with the same queryid contributes its metrics to that entry, but the stored text doesn’t change. So if your first execution happened to carry one comment and the next thousand carried different ones, the view will only ever show you that first comment. The metrics are accurate; the text is misleading.&lt;/p&gt;
&lt;h2 id=&quot;why-sql-comment-tags-can-disappear&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#why-sql-comment-tags-can-disappear&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Why SQL comment tags can disappear&lt;/h2&gt;
&lt;p&gt;The same thing applies to &lt;strong&gt;SQLcommenter&lt;/strong&gt;-style tags, which ORMs and observability tools often use to attach metadata (route, controller, feature flag) to a query. They’re just comments, so jumbling treats them the same way.&lt;/p&gt;
&lt;p&gt;I see this come up regularly: a customer is staring at an OpenTelemetry span with a SQLcommenter tag, trying to find the matching execution in pg_stat_statements, and they can’t. The tag they’re looking for isn’t in the stored text, because some other execution (with a different tag, or no tag at all) got there first.&lt;/p&gt;
&lt;h2 id=&quot;what-doesnt-get-normalized&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#what-doesnt-get-normalized&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What doesn’t get normalized&lt;/h2&gt;
&lt;p&gt;Anything that affects the structure of the query &lt;em&gt;does&lt;/em&gt; affect the queryid:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The number of columns selected&lt;/li&gt;
&lt;li&gt;The number of items in an &lt;code&gt;IN&lt;/code&gt; list&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ORDER BY&lt;/code&gt; clauses&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JOIN&lt;/code&gt; structure&lt;/li&gt;
&lt;li&gt;The number of &lt;code&gt;WHERE&lt;/code&gt; conditions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Take a long, complex query with multiple CTEs. If the only thing that changes between executions is the set of &lt;code&gt;WHERE&lt;/code&gt; conditions at the end, each variant gets its own entry.&lt;/p&gt;
&lt;h2 id=&quot;the-in-clause-explosion-and-how-postgres-18-fixes-it&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#the-in-clause-explosion-and-how-postgres-18-fixes-it&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;The IN-clause explosion (and how Postgres 18 fixes it)&lt;/h2&gt;
&lt;p&gt;This one is worth singling out, because it’s a common foot-gun and it explains a lot of “why is my pg_stat_statements full?” investigations.&lt;/p&gt;
&lt;p&gt;In Postgres 17 and below, every distinct length of an &lt;code&gt;IN&lt;/code&gt; list produces a unique queryid:&lt;/p&gt;
&lt;pre&gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;*&lt;/span&gt; &lt;span &gt;FROM&lt;/span&gt; film &lt;span &gt;WHERE&lt;/span&gt; id &lt;span &gt;IN&lt;/span&gt; (&lt;span &gt;1&lt;/span&gt;);
&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;*&lt;/span&gt; &lt;span &gt;FROM&lt;/span&gt; film &lt;span &gt;WHERE&lt;/span&gt; id &lt;span &gt;IN&lt;/span&gt; (&lt;span &gt;1&lt;/span&gt;, &lt;span &gt;2&lt;/span&gt;);
&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;*&lt;/span&gt; &lt;span &gt;FROM&lt;/span&gt; film &lt;span &gt;WHERE&lt;/span&gt; id &lt;span &gt;IN&lt;/span&gt; (&lt;span &gt;1&lt;/span&gt;, &lt;span &gt;2&lt;/span&gt;, &lt;span &gt;3&lt;/span&gt;);
&lt;span &gt;-- ... and so on&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your ORM cheerfully sends &lt;code&gt;IN&lt;/code&gt; lists of every possible length, you can blow through hundreds of entries from a single logical query.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Postgres 18 finally fixes this.&lt;/strong&gt; It rewrites &lt;code&gt;WHERE id IN (1, 2, 3)&lt;/code&gt; into &lt;code&gt;WHERE id = ANY(&amp;#39;{1, 2, 3}&amp;#39;)&lt;/code&gt;, which is structurally a single query regardless of the array length. The result: one entry in pg_stat_statements no matter how many values you pass. If you’ve ever stared at a pg_stat_statements view that was 90% IN-clause variants, this alone is a strong reason to look at upgrading.&lt;/p&gt;
&lt;h2 id=&quot;structure-vs-semantics&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#structure-vs-semantics&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Structure vs. semantics&lt;/h2&gt;
&lt;p&gt;It’s tempting to assume that semantically equivalent queries get the same queryid. They don’t. pg_stat_statements cares about structure, not meaning.&lt;/p&gt;
&lt;p&gt;These two queries produce identical query plans:&lt;/p&gt;
&lt;pre&gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;*&lt;/span&gt; &lt;span &gt;FROM&lt;/span&gt; film &lt;span &gt;WHERE&lt;/span&gt; film_id &lt;span &gt;=&lt;/span&gt; &lt;span &gt;557&lt;/span&gt;;
&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;*&lt;/span&gt; &lt;span &gt;FROM&lt;/span&gt; film &lt;span &gt;WHERE&lt;/span&gt; &lt;span &gt;557&lt;/span&gt; &lt;span &gt;=&lt;/span&gt; film_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But they’re structurally different, so they’re two entries. The same goes for:&lt;/p&gt;
&lt;pre&gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;*&lt;/span&gt; &lt;span &gt;FROM&lt;/span&gt; film &lt;span &gt;WHERE&lt;/span&gt; film_id &lt;span &gt;IN&lt;/span&gt; (&lt;span &gt;1&lt;/span&gt;, &lt;span &gt;2&lt;/span&gt;, &lt;span &gt;3&lt;/span&gt;);
&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;*&lt;/span&gt; &lt;span &gt;FROM&lt;/span&gt; film &lt;span &gt;WHERE&lt;/span&gt; film_id &lt;span &gt;=&lt;/span&gt; &lt;span &gt;ANY&lt;/span&gt;(&lt;span &gt;&amp;#39;{1, 2, 3}&amp;#39;&lt;/span&gt;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Logically and operationally identical. Two entries.&lt;/p&gt;
&lt;h2 id=&quot;what-happens-when-pg_stat_statements-fills-up&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#what-happens-when-pg_stat_statements-fills-up&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What happens when pg_stat_statements fills up&lt;/h2&gt;
&lt;p&gt;This is the reason any of this matters. When the number of tracked entries hits &lt;code&gt;pg_stat_statements.max&lt;/code&gt;, the next new query coming along has to displace something. Postgres deallocates an existing entry, and the metrics for that query are gone until the query runs again and a fresh entry gets created.&lt;/p&gt;
&lt;p&gt;Raising &lt;code&gt;pg_stat_statements.max&lt;/code&gt; helps, but it’s not a fix. If your workload is generating thousands of &lt;em&gt;unintentionally&lt;/em&gt; unique queries, you’ll fill up whatever ceiling you set. The better move is to understand where the structural variation is coming from and reduce it at the source.&lt;/p&gt;
&lt;h2 id=&quot;key-takeaways&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#key-takeaways&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Key takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;High cardinality is the enemy.&lt;/strong&gt; Every unique queryid eats a slot, and slots are finite.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ORMs are a major source of cardinality.&lt;/strong&gt; Different argument shapes, optional clauses, and dynamic column lists all create new queryids.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dynamic SQL has the same problem.&lt;/strong&gt; If you build queries by string concatenation, every variation is its own entry.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-database and multi-role setups multiply everything.&lt;/strong&gt; pg_stat_statements is per-instance, but its key includes role and database.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Eviction is silent.&lt;/strong&gt; You won’t get an error or a warning. Entries just disappear, and so do their metrics.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;whats-coming-next&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#whats-coming-next&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What’s coming next&lt;/h2&gt;
&lt;p&gt;Now that we’ve covered what makes an entry unique, the next episode looks at where the &lt;em&gt;query text itself&lt;/em&gt; is actually stored. Quick spoiler: it’s not in the in-memory hash table, and the way Postgres handles it has real consequences for how reliably you can identify queries after the fact. We’ll dig into that in Part 3.&lt;/p&gt;
&lt;p&gt;I hope this series helps you better understand one of the most essential tools in the Postgres ecosystem. Feel free to &lt;a href=&quot;https://www.youtube.com/channel/UCDV_1Dz2Ixgl1nT_3DUZVFw&quot;&gt;subscribe to our YouTube channel&lt;/a&gt;, &lt;a href=&quot;https://pganalyze.com/newsletter&quot;&gt;sign up for our newsletter&lt;/a&gt; or &lt;a href=&quot;https://www.linkedin.com/company/pganalyze/&quot;&gt;follow us on LinkedIn&lt;/a&gt; to get updates about new episodes!&lt;/p&gt;
&lt;h2 id=&quot;what-we-discussed-in-this-episode&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#what-we-discussed-in-this-episode&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What we discussed in this episode&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/pgstatstatements.html&quot;&gt;pg_stat_statements documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/pgstatstatements.html#PGSTATSTATEMENTS-CONFIGURATION-PARAMETERS&quot;&gt;pg_stat_statements configuration parameters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/18/release-18.html&quot;&gt;Postgres 18 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://google.github.io/sqlcommenter/&quot;&gt;SQLcommenter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Ryan Booz</dc:creator></item><item><title>Postgres in Production Special Series: What pg_stat_statements Is and Isn&apos;t (Part 1)</title><link>https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-1</link><guid isPermaLink="false">https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-1</guid><description>In this special series of “Postgres in Production”, we take a deep dive into pg_stat_statements — the essential Postgres extension for tracking query performance. In Part 1, Ryan Booz covers what pg_stat_statements is, how to enable it, what it tracks, and critically, what it doesn’t track and why that matters for your monitoring setup. Share this episode: Click here to share this episode on LinkedIn . Feel free to sign up for our newsletter and subscribe to our YouTube channel . What is…</description><pubDate>Mon, 13 Apr 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In this special series of “Postgres in Production”, we take a deep dive into pg_stat_statements — the essential Postgres extension for tracking query performance. In Part 1, Ryan Booz covers what pg_stat_statements is, how to enable it, what it tracks, and critically, what it doesn’t track and why that matters for your monitoring setup.&lt;/p&gt;
&lt;iframe width=&quot;750&quot; height=&quot;421&quot; src=&quot;https://www.youtube-nocookie.com/embed/U8UGTyJp-K4?si=S4EgZr5weI5FTNAK&quot; frameborder=&quot;0&quot; modestbranding=&quot;1&quot; controls allownetworking=&quot;internal&quot; allow=&quot;autoplay; encrypted-media&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;&lt;strong&gt;Share this episode:&lt;/strong&gt; Click here to share this episode &lt;a href=&quot;https://www.linkedin.com/shareArticle?mini=true&amp;amp;url=https://pganalyze.com/blog/postgres-in-production-pg-stat-statements-deep-dive-part-1&amp;amp;title=Deep%20Dive%20into%20pg_stat_statements%20Episode%201&amp;amp;source=LinkedIn&quot;&gt;on LinkedIn&lt;/a&gt;. Feel free to &lt;a href=&quot;https://pganalyze.com/newsletter&quot;&gt;sign up for our newsletter&lt;/a&gt; and &lt;a href=&quot;https://www.youtube.com/channel/UCDV_1Dz2Ixgl1nT_3DUZVFw&quot;&gt;subscribe to our YouTube channel&lt;/a&gt;.&lt;/p&gt;
&lt;div &gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#what-is-pg_stat_statements&quot;&gt;What is pg_stat_statements?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#how-pg_stat_statements-stores-data&quot;&gt;How pg_stat_statements stores data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#enabling-pg_stat_statements&quot;&gt;Enabling pg_stat_statements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-does-it-track&quot;&gt;What does it track?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#the-cumulative-metrics-problem&quot;&gt;The cumulative metrics problem&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-it-doesnt-track&quot;&gt;What it doesn’t track&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#why-this-matters-for-monitoring&quot;&gt;Why this matters for monitoring&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#whats-coming-in-this-series&quot;&gt;What’s coming in this series&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-we-discussed-in-this-episode&quot;&gt;What we discussed in this episode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr/&gt;
&lt;p&gt;&lt;strong&gt;Transcript&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-is-pg_stat_statements&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#what-is-pg_stat_statements&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What is pg_stat_statements?&lt;/h2&gt;
&lt;p&gt;Have you ever found yourself asking questions like: “My database is slow right now — which query is using all the resources?” Or maybe, “Over the last 24 hours, which queries on average take more than a second to execute?” What about, “Some of these webpages are loading slowly — can you show me which queries are called a lot, but take a couple hundred milliseconds on average?”&lt;/p&gt;
&lt;p&gt;These are common questions for database engineers, DBAs, and developers trying to make their applications work well with Postgres. And the answer has almost always been: &lt;strong&gt;go use pg_stat_statements.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;pg_stat_statements is an included extension, part of the contrib modules that ship with the Postgres open source project. It’s available wherever you install Postgres — whether that’s a cloud provider, a Docker container, or a bare metal install. It’s there, it just needs to be activated.&lt;/p&gt;
&lt;h2 id=&quot;how-pg_stat_statements-stores-data&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#how-pg_stat_statements-stores-data&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;How pg_stat_statements stores data&lt;/h2&gt;
&lt;p&gt;The metrics that pg_stat_statements holds are stored in an &lt;strong&gt;in-memory hash table per Postgres instance&lt;/strong&gt; — not per database. There’s one instance of this hash table across all databases within a Postgres instance, and it tracks per-execution query metrics.&lt;/p&gt;
&lt;h2 id=&quot;enabling-pg_stat_statements&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#enabling-pg_stat_statements&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Enabling pg_stat_statements&lt;/h2&gt;
&lt;p&gt;To enable pg_stat_statements, two steps are required:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Configure it within the &lt;code&gt;shared_preload_libraries&lt;/code&gt; setting in &lt;code&gt;postgresql.conf&lt;/code&gt;. If you’re using a cloud environment, some providers enable this by default, and all give you a way to edit this setting.&lt;/li&gt;
&lt;li&gt;Install the extension on at least one database using the &lt;code&gt;CREATE EXTENSION&lt;/code&gt; command.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once that’s done, you can begin querying the data.&lt;/p&gt;
&lt;h2 id=&quot;what-does-it-track&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#what-does-it-track&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What does it track?&lt;/h2&gt;
&lt;p&gt;As of Postgres 18, there are at least &lt;strong&gt;45 columns&lt;/strong&gt; in this view, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A unique query ID for every normalized query text&lt;/li&gt;
&lt;li&gt;The actual query text&lt;/li&gt;
&lt;li&gt;Call counts, total time, and mean run time&lt;/li&gt;
&lt;li&gt;Rows selected&lt;/li&gt;
&lt;li&gt;Lock and block information&lt;/li&gt;
&lt;li&gt;Planning metrics&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There’s a lot of data, and it can be incredibly helpful for finding queries that need your attention.&lt;/p&gt;
&lt;h2 id=&quot;the-cumulative-metrics-problem&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#the-cumulative-metrics-problem&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;The cumulative metrics problem&lt;/h2&gt;
&lt;p&gt;One of the key things to understand is that all metrics in pg_stat_statements are &lt;strong&gt;cumulative&lt;/strong&gt; — but only since the last time a query was tracked.&lt;/p&gt;
&lt;p&gt;Up until recently, the common understanding was that metrics persist until the server restarts or until you explicitly reset them with &lt;code&gt;pg_stat_statements_reset()&lt;/code&gt;. For years, the workflow looked like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Server is slow? Run &lt;code&gt;pg_stat_statements_reset()&lt;/code&gt; to clear all metrics&lt;/li&gt;
&lt;li&gt;Wait a few minutes, then query the view to find which queries are using the most resources&lt;/li&gt;
&lt;li&gt;Identify and optimize the problematic query&lt;/li&gt;
&lt;li&gt;Reset metrics again and continue monitoring&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;pg_stat_statements wasn’t originally intended to be sampled over and over for days, weeks, or months to track long-term query metric performance.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This was an aha moment — many of us assumed that if a query has run at any point during the lifetime of a Postgres instance, there would be data for it in pg_stat_statements. &lt;strong&gt;That’s just not true.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-it-doesnt-track&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#what-it-doesnt-track&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What it doesn’t track&lt;/h2&gt;
&lt;p&gt;At a high level, pg_stat_statements does &lt;strong&gt;not&lt;/strong&gt; provide:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Per-execution history&lt;/strong&gt; — There’s no record saying “this query was executed two months ago and here are the metrics for that specific execution”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Time-windowed data&lt;/strong&gt; — It’s not a per-second or per-minute view. Monitoring tools have to calculate deltas from the cumulative metrics themselves&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every query forever&lt;/strong&gt; — At any one time, it can only track up to &lt;code&gt;pg_stat_statements.max&lt;/code&gt; unique query texts (default: &lt;strong&gt;5,000&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The real issue is what happens when it reaches that 5,000 unique query limit — something we’ll explore in depth throughout this series.&lt;/p&gt;
&lt;h2 id=&quot;why-this-matters-for-monitoring&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#why-this-matters-for-monitoring&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Why this matters for monitoring&lt;/h2&gt;
&lt;p&gt;When we treat pg_stat_statements as the single source of truth for query performance history, we run into real problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Every monitoring tool&lt;/strong&gt; that tracks query performance over time samples from pg_stat_statements — it’s the only query metrics view available everywhere in Postgres&lt;/li&gt;
&lt;li&gt;If you don’t understand when pg_stat_statements can fail or when settings need to be modified, you could be &lt;strong&gt;flying blind when it matters most&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;You might not be able to find a problematic query or its historical data in either pg_stat_statements or your monitoring tool&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;whats-coming-in-this-series&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#whats-coming-in-this-series&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What’s coming in this series&lt;/h2&gt;
&lt;p&gt;Over the next episodes in this series, we’ll cover:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What makes a “unique” statement&lt;/strong&gt; — How normalized query texts work, and what “normalize” really means&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Source code deep dive&lt;/strong&gt; — Uncovering misconceptions by looking at the actual Postgres source&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Query text truncation&lt;/strong&gt; — Why you can’t always find the full query text&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configuration tuning&lt;/strong&gt; — How to better configure pg_stat_statements for your workload&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;High cardinality workloads&lt;/strong&gt; — Why they’re problematic and what you can do about it&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Long-term strategies&lt;/strong&gt; — Better ways to utilize pg_stat_statements as your Postgres environment grows&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Stay tuned for Part 2, where we’ll dig into what makes a statement “unique” in pg_stat_statements and look at the source code to uncover some longstanding misconceptions.&lt;/p&gt;
&lt;p&gt;I hope this series helps you better understand one of the most essential tools in the Postgres ecosystem. Feel free to &lt;a href=&quot;https://www.youtube.com/channel/UCDV_1Dz2Ixgl1nT_3DUZVFw&quot;&gt;subscribe to our YouTube channel&lt;/a&gt;, &lt;a href=&quot;https://pganalyze.com/newsletter&quot;&gt;sign up for our newsletter&lt;/a&gt; or &lt;a href=&quot;https://www.linkedin.com/company/pganalyze/&quot;&gt;follow us on LinkedIn&lt;/a&gt; to get updates about new episodes!&lt;/p&gt;
&lt;h2 id=&quot;what-we-discussed-in-this-episode&quot;&gt;&lt;a data-anchor=&quot;true&quot; href=&quot;#what-we-discussed-in-this-episode&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 576 512&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot;&gt;&lt;path fill=&quot;currentColor&quot; d=&quot;M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What we discussed in this episode&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/pgstatstatements.html&quot;&gt;pg_stat_statements documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/contrib.html&quot;&gt;PostgreSQL contrib modules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-SHARED-PRELOAD-LIBRARIES&quot;&gt;shared_preload_libraries configuration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Ryan Booz</dc:creator></item></channel></rss>