{"id":109489,"date":"2026-04-29T13:00:00","date_gmt":"2026-04-29T13:00:00","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=109489"},"modified":"2026-04-28T20:24:57","modified_gmt":"2026-04-28T20:24:57","slug":"fillfactor-in-postgresql-what-it-means-and-when-to-adjust-it","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/postgresql\/fillfactor-in-postgresql-what-it-means-and-when-to-adjust-it\/","title":{"rendered":"What is fillfactor in PostgreSQL &#8211; and when should you adjust it?"},"content":{"rendered":"\n<p><strong><code>fillfactor<\/code> in PostgreSQL controls how full table or index pages are. The lower the <code>fillfactor<\/code>, the better the performance on update-heavy tables. In this article, learn everything you need to know about <code>fillfactor<\/code> in PostgreSQL &#8211; from what it is, to exactly when (and how)  you should adjust it.<\/strong><\/p>\n\n\n\n<p><a href=\"https:\/\/www.red-gate.com\/simple-talk\/collections\/series-learning-postgresql-with-grant\/\" target=\"_blank\" rel=\"noreferrer noopener\">PostgreSQL<\/a> comes pre-configured with hundreds of various settings. We&#8217;ll never want to change (or are even aware of) most of them, but there is one you <em>definitely<\/em> should be familiar with: <code>fillfactor<\/code>.<\/p>\n\n\n\n<p>First, some background. All <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/\" target=\"_blank\" rel=\"noreferrer noopener\">database management systems<\/a> (DBMS&#8217;) implement ways to avoid conflicts between users simultaneously modifying and accessing the same data. PostgreSQL uses a system called <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/postgresql\/multi-version-concurrency-control-mvcc-in-postgresql-learning-postgresql-with-grant\/\" target=\"_blank\" rel=\"noreferrer noopener\">MVCC \u2013 multi-version concurrency control<\/a>. <\/p>\n\n\n\n<p>MVCC generally outperforms competing systems like <a href=\"https:\/\/www.geeksforgeeks.org\/dbms\/lock-based-concurrency-control-protocol-in-dbms\/\" target=\"_blank\" rel=\"noreferrer noopener\">lock-based<\/a> protocols, especially in reading or inserting new data. But there is one use case for which MVCC often struggles: heavily-updated tables.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-how-table-rows-are-updated-in-postgresql-and-where-fillfactor-comes-in\">How table rows are updated in PostgreSQL (and where fillfactor comes in)<\/h2>\n\n\n\n<p>Some people are surprised to learn that PostgreSQL never directly updates a <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/postgresql\/creating-a-database-and-tables-in-postgresql-learning-postgresql-with-grant\/\" target=\"_blank\" rel=\"noreferrer noopener\">table row<\/a>. Instead, it writes an entirely new version of the row, leaving the original untouched. Later, when no running queries are referencing the old version, PostgreSQL will delete it. Effectively, the <code>UPDATE<\/code> works like an <code>INSERT<\/code>, followed by a later <code>DELETE<\/code>. <\/p>\n\n\n\n<p>When updates are frequent or <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/postgresql\/learning-postgresql-with-grant-introducing-vacuum\/\" target=\"_blank\" rel=\"noreferrer noopener\">vacuum<\/a> operators are far apart, there may be several different versions of a row still in the table. And because every <code>UPDATE<\/code> is inserting a new row, we must also update the table&#8217;s indexes &#8211; even when we&#8217;re not updating any indexed columns. Treating every update as an <code>INSERT<\/code> is expensive and leads to index fragmentation. This means performance in update-heavy workloads can suffer, sometimes badly &#8211; but there&#8217;s a solution.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-hot-updates-in-postgresql\">&#8216;HOT&#8217; updates in PostgreSQL<\/h3>\n\n\n\n<p>PostgreSQL contains an optimization that can help: a <a href=\"https:\/\/www.postgresql.org\/docs\/current\/storage-hot.html\" target=\"_blank\" rel=\"noreferrer noopener\">&#8220;HOT update&#8221;<\/a>. <code>HOT<\/code> &#8212; which stands for &#8220;Heap-Only Tuple&#8221; &#8212; is a type of update in which the new row is inserted on the same data page as the existing row, then &#8220;chained&#8221; to the earlier copy. This allows PostgreSQL to skip updating the table&#8217;s indexes. Also, future row accesses can delete the old copies (the entire &#8220;HOT chain&#8221;) without having to perform a vacuum.<\/p>\n\n\n\n<p>For a <code>HOT<\/code> update to happen, none of the columns being altered can be indexed. And, most importantly, there must be room in the data page to store the new row. This is where <code>fillfactor<\/code> comes in. By default, PostgreSQL fills a table&#8217;s data pages to 100%, leaving no free space (fill factor = 100). But we can lower this default setting on a per-table basis. If we set a value of 80, then 20% of every data page is available for <code>HOT<\/code> updates.<\/p>\n\n\n\n<section id=\"my-first-block-block_173764fae35338480ced23d6edded94f\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Get started with PostgreSQL &#8211; free book download<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            &#8216;Introduction to PostgreSQL for the data professional&#8217;, written by Grant Fritchey and Ryan Booz, covers all the basics of how to get started with PostgreSQL.                                    <\/div>\n            <\/div>\n                            <a href=\"https:\/\/www.red-gate.com\/hub\/books\/introduction-to-postgresql-for-the-data-professional\/\" class=\"btn btn--secondary btn--lg\">Download your free copy<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<h2 class=\"wp-block-heading\" id=\"h-how-much-free-space-is-best-for-hot-updates-in-postgresql\">How much free space is best for HOT updates in PostgreSQL?<\/h2>\n\n\n\n<p>If a lack of free space prevents <code>HOT<\/code> updates from happening in PostgreSQL, why not set the amount of free space even higher? Wouldn&#8217;t more room be better? Well, no. Not usually. <\/p>\n\n\n\n<p>A fill factor of 80 means 20% of the table&#8217;s data pages are effectively empty, making your table <em>that<\/em> much larger. This impacts on all operations on that table: reads and inserts included. This is why PostgreSQL defaults to a 100% <code>fillfactor<\/code>: most tables experience many more selects and inserts than they do updates.<\/p>\n\n\n\n<p>A <code>fillfactor<\/code> below 70 only pays off in the rare case when there are several times more updates against a table than inserts and selects combined. And remember &#8211; a <code>SELECT<\/code> that returns 100 rows is more load than one that returns a single row.<\/p>\n\n\n\n<p>There are also other factors. The more indexes on a table, the more it can benefit by avoiding a <code>HOT<\/code> update. However, if the updates are modifying <em>indexed <\/em>columns, the <code>HOT<\/code> cannot happen. To complicate matters further, <code>HOT<\/code> update rates are affected by row size and the frequency of vacuum operations (among other considerations.)<\/p>\n\n\n\n<p>The interplay of all these factors makes coming up with a &#8220;rule of thumb&#8221; for setting the <code>fillfactor<\/code> nearly impossible. Only testing can reveal the best value for each table. Still, it&#8217;s not incorrect to say that most frequently-updated tables will benefit from lowering the <code>fillfactor<\/code>.<\/p>\n\n\n\n<p>One misconception is that <code>fillfactor<\/code> operates like the balance on a scale: lowering it improves update speed only at the cost of reduced select performance. While this is often true, when <code>HOT<\/code> updates are bogging down a server is when tuning the <code>fillfactor<\/code> can also improve <code>SELECT<\/code> performance. Additionally, more <code>HOT<\/code> updates means less index fragmentation, which also helps <code>SELECT<\/code> performance.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-how-does-postgresql-store-a-database-table\">How does PostgreSQL store a database table?<\/h2>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1007\" height=\"532\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/04\/image-5.png\" alt=\"An image showing how PostgreSQL stores a database table.\" class=\"wp-image-109491\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/04\/image-5.png 1007w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/04\/image-5-300x158.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/04\/image-5-768x406.png 768w\" sizes=\"auto, (max-width: 1007px) 100vw, 1007px\" \/><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-how-to-alter-a-table-s-fillfactor-in-postgresql\">How to alter a table&#8217;s fillfactor in PostgreSQL<\/h4>\n\n\n\n<p><code>fillfactor<\/code> is a table-specific setting. Altering it is easy: to adjust the factor to 80 for table &#8216;sales&#8217;, for instance, execute this statement:<\/p>\n\n\n\n<p><code>ALTER TABLE sales SET (fillfactor = 80);<\/code><\/p>\n\n\n\n<p>Do note that this <em>only<\/em> alters the value for new data &#8211; not data already in the table.&nbsp;If you want to see immediate results, you&#8217;ll need to dump and reload the table&#8217;s data. Otherwise, you&#8217;ll only see the results gradually over time (as existing data pages get updated.) For a large table, this may take days or weeks, so bear this in mind when testing your results.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-how-to-check-the-effectiveness-of-fillfactor-in-postgresql\">How to check the effectiveness of fillfactor in PostgreSQL<\/h4>\n\n\n\n<p>The following query will tell you the total number of updates made to a table, the number of which qualified for a <code>HOT<\/code> update, and the <code>HOT<\/code> update percentage (the ratio of the first two):<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT\n\trelname AS \"Table name\",\n\tn_tup_upd AS \"Total updates\",\n\tn_tup_hot_upd AS \"HOT updates\",\n\tROUND(100.0 * n_tup_hot_upd \/ NULLIF(n_tup_upd,0), 1) AS \"HOT update %\"\nFROM\n\tpg_stat_user_tables\nWHERE\n\trelname = {table_name};<\/pre><\/div>\n\n\n\n<p>Unless you&#8217;ve previously cleared the statistics counters, these values start from when the table was created. It&#8217;s therefore recommended you execute the system function <code>pg_stat_reset()<\/code> after resetting the <code>fillfactor<\/code>. Otherwise, this query will contain mostly data from before the setting was changed.<\/p>\n\n\n\n<p>If you reloaded your table data after lowering the <code>fillfactor<\/code>, you should see an immediate improvement in hit rates. Otherwise, you should expect to see the hit rate climb slowly over time. I can&#8217;t stress this latter point enough. For large tables, it may take several weeks before the <code>HOT<\/code> hit rate climbs appreciably.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-when-and-how-to-use-fillfactor-for-indexes-in-postgresql\">When (and how) to use fillfactor for indexes in PostgreSQL<\/h4>\n\n\n\n<p>Every <a href=\"https:\/\/www.red-gate.com\/simple-talk\/featured\/postgresql-indexes-what-they-are-and-how-they-help\/\" target=\"_blank\" rel=\"noreferrer noopener\">index in PostgreSQL<\/a> has a <code>fillfactor<\/code> setting. Unlike tables, which default to 100%, indexes default to 90%. The setting affects indexes differently than tables, and the criteria for changing it is also different. <\/p>\n\n\n\n<p>A lower index fill factor reduces expensive page splits for the index. These splits can occur on both inserts and updates &#8211; but only for updates that alter an indexed column. So, when choosing indexes to potentially lower the <code>fillfactor<\/code> on, the ratio we&#8217;re interested in is (total insert + index-involved updates) vs .(total select + non-index updates). As before, we measure each value based on the number of rows affected, not just the transaction count.<\/p>\n\n\n\n<p>Also note that indexes are even more sensitive than tables to the performance implications of lowering the <code>fillfactor<\/code>. In practice, you&#8217;ll rarely want to set an index <code>fillfactor<\/code> lower than 80. The syntax for doing this is similar to that of tables: for an index called<code> idx_sales<\/code>, for instance, you would use this command:<\/p>\n\n\n\n<p><code>ALTER INDEX idx_sales SET (fillfactor = 80);<\/code><\/p>\n\n\n\n<p>Note: PostgreSQL contains an optimization for so-called &#8220;monotonic inserts&#8221;, such as a sequentially-increasing <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/learn\/primary-key-primer-for-sql-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">primary key<\/a>. This drastically reduces the number of <a href=\"https:\/\/www.red-gate.com\/simple-talk\/blogs\/how-to-identify-the-source-of-page-splits-in-a-database\/\" target=\"_blank\" rel=\"noreferrer noopener\">page splits<\/a>, so you&#8217;ll rarely want to consider these indexes for adjustment. Focus on indexes with values that are inserted or updated on a random basis.<\/p>\n\n\n\n<p>If your table is static (no inserts or updates to indexed columns), you can set its index <code>fillfactor<\/code> to 100 for maximum performance. This packs the index as tightly as possible, removing all wasted space. It even works for tables that receive updates (as long as those updates don&#8217;t alter indexed columns.)<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-further-optimization-using-toasting-in-postgresql\">Further optimization: using TOASTing in PostgreSQL<\/h2>\n\n\n\n<p>If your table has text columns holding lengthy strings, but not quite long enough for the rows to be <a href=\"https:\/\/www.postgresql.org\/docs\/current\/storage-toast.html\" target=\"_blank\" rel=\"noreferrer noopener\">TOASTed<\/a> (moved by PostgreSQL into separate storage), this can make maintaining free space on a data page difficult. If these text columns aren&#8217;t often referenced, you can further improve performance by lowering the table setting &#8216;<code>toast_tuple_target<\/code>&#8216;.<\/p>\n\n\n\n<p>Feel free to share your thoughts about PostgreSQL&#8217;s <code>fillfactor<\/code> in the comments below!<\/p>\n\n\n\n<section id=\"my-first-block-block_416959daed136ab2555ee95e63fe9ecf\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Simple Talk is brought to you by Redgate Software<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            Take control of your databases with the trusted Database DevOps solutions provider. Automate with confidence, scale securely, and unlock growth through AI.                                    <\/div>\n            <\/div>\n                            <a href=\"https:\/\/www.red-gate.com\/solutions\/overview\/\" class=\"btn btn--secondary btn--lg\">Discover how Redgate can help you<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<section id=\"faq\" class=\"faq-block my-5xl\">\n    <h2>FAQs: fillfactor in PostgreSQL<\/h2>\n\n                        <h3 class=\"mt-4xl\">1. What is fillfactor in PostgreSQL?<\/h3>\n            <div class=\"faq-answer\">\n                <p data-start=\"121\" data-end=\"218\"><code>fillfactor<\/code> in PostgreSQL controls how full table or index pages are. Lower values leave free space for updates.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">2. What is the default fillfactor in PostgreSQL?<\/h3>\n            <div class=\"faq-answer\">\n                <ul>\n<li data-section-id=\"1xv1gry\" data-start=\"265\" data-end=\"280\">Tables: 100<\/li>\n<li data-section-id=\"1bl8jmb\" data-start=\"281\" data-end=\"296\">Indexes: 90<\/li>\n<\/ul>\n            <\/div>\n                    <h3 class=\"mt-4xl\">3. Why change fillfactor in PostgreSQL?<\/h3>\n            <div class=\"faq-answer\">\n                <p data-start=\"334\" data-end=\"450\">Lowering <code>fillfactor<\/code> improves performance on update-heavy tables by enabling HOT updates and reducing index overhead.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">4. What are HOT updates in PostgreSQL?<\/h3>\n            <div class=\"faq-answer\">\n                <p data-start=\"487\" data-end=\"607\">HOT (Heap-Only Tuple) updates store new row versions on the same page, avoiding index updates and improving performance.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">5. When should I lower fillfactor in PostgreSQL?<\/h3>\n            <div class=\"faq-answer\">\n                <ul>\n<li data-section-id=\"1bryg50\" data-start=\"654\" data-end=\"693\">Try ~80 for frequently updated tables<\/li>\n<li data-section-id=\"3v58ba\" data-start=\"694\" data-end=\"721\">Stick to 100 for rare updates<\/li>\n<\/ul>\n            <\/div>\n                    <h3 class=\"mt-4xl\">6. Can lower fillfactor hurt performance in PostgreSQL??<\/h3>\n            <div class=\"faq-answer\">\n                <p data-start=\"775\" data-end=\"847\">Yes. It increases table size and can slow reads and inserts if overused.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">7. How do I change fillfactor in PostgreSQL?<\/h3>\n            <div class=\"faq-answer\">\n                <p>SQL code:<\/p>\n<p><code>ALTER TABLE table_name SET (fillfactor = 80);<\/code><\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">8. Does fillfactor in PostgreSQL apply to existing data?<\/h3>\n            <div class=\"faq-answer\">\n                <p data-start=\"1001\" data-end=\"1070\">No. It only affects new or updated rows unless you rebuild the table.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">9. How do I measure results?<\/h3>\n            <div class=\"faq-answer\">\n                <p data-start=\"1111\" data-end=\"1162\">Check HOT update rates using <code data-start=\"1140\" data-end=\"1161\">pg_stat_user_tables<\/code>.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">10. Should I change index fillfactor in PostgreSQL?<\/h3>\n            <div class=\"faq-answer\">\n                <p data-start=\"1211\" data-end=\"1290\">Sometimes. You can lower it (e.g., to 80\u201390) for indexes with frequent inserts or updates.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">11. What\u2019s the best PostgreSQL fillfactor value?<\/h3>\n            <div class=\"faq-answer\">\n                <p data-start=\"1339\" data-end=\"1394\">There\u2019s no universal &#8216;best&#8217; value &#8211; simply test based on your workload.<\/p>\n            <\/div>\n            <\/section>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Learn how to optimize PostgreSQL performance with fillfactor &#8211; including how it affects HOT updates, indexing, and update-heavy workloads.&hellip;<\/p>\n","protected":false},"author":343663,"featured_media":105920,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143523,53,143534],"tags":[4168,158978],"coauthors":[159110],"class_list":["post-109489","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-databases","category-featured","category-postgresql","tag-database","tag-postgresql"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/109489","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/343663"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=109489"}],"version-history":[{"count":15,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/109489\/revisions"}],"predecessor-version":[{"id":110188,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/109489\/revisions\/110188"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media\/105920"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=109489"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=109489"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=109489"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=109489"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}