{"id":97196,"date":"2023-07-06T14:46:18","date_gmt":"2023-07-06T14:46:18","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=97196"},"modified":"2024-03-07T11:51:25","modified_gmt":"2024-03-07T11:51:25","slug":"when-parameter-tuning-is-not-the-answer","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/postgresql\/when-parameter-tuning-is-not-the-answer\/","title":{"rendered":"When PostgreSQL Parameter Tuning is not the Answer"},"content":{"rendered":"<p><em>So much about parameters tuning, but does it always help?<\/em><\/p>\n<p>Welcome to the third and final blog of the &#8220;magic of parameters&#8221; series. In two previous blogs, we discussed how tuning PostgreSQL parameters could help improve overall system performance. However, the very first paragraph of\u00a0<a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/postgresql\/are-postgresql-memory-parameters-magic\/\" target=\"_blank\" rel=\"noopener\">the very first blog<\/a> on this topic stated that:\u00a0<\/p>\n<p><em>Although some parameter tuning can indeed improve the database performance, we are usually talking about 10%, 20%, and in rare cases, up to 50% performance improvement if some parameters are set poorly. That might sound like impressive numbers, but individual query optimization routinely makes queries run several\u00a0<strong>times\u00a0<\/strong>faster, in some cases, ten or more times faster, and restructuring applications can improve overall system performance by\u00a0<strong>hundreds of times!<\/strong><\/em><\/p>\n<p><em>In this first blog, we didn&#8217;t provide any examples of the practical impact of parameters tuning on performance. Indeed, it is challenging to model such an impact on the training database.\u00a0<\/em><\/p>\n<p>In this blog, we will segue from discussing PostgreSQL system parameters best practices to other ways of performance tuning. Moreover, we will demonstrate that essential database performance tuning goes beyond choosing the appropriate parameters settings.<\/p>\n<h2>Measuring impact<\/h2>\n<p>With this in mind, let&#8217;s examine the impact of parameters modifications and alternative ways of performance tuning. To run our experiments, we will use a database that I have created for performance tuning examples: <a href=\"https:\/\/github.com\/hettie-d\/postgres_air&quot; \\t &quot;_blank\">postgres_air database<\/a>. If you want to repeat the experiments described in this article, download the latest version of this database (file <code>postges_air_2023.backup<\/code>) and restore it on a Postgres instance where you are going to run these experiments. We suggest that you do it on your personal device or any other instance where you have a complete control of what\u2019s going on, since you will need to restart the instance a couple of times during these experiments.\u00a0We ran the examples on version 15.2, however, they will work the same at a minimum on versions 13 and 14.<\/p>\n<p>The restore will create a schema <code>postgres_air<\/code> populated with data but without any indexes except for the ones which support primary\/unique constraints. To be able to replicate the examples, you will need to create a couple of additional indexes:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">SET search_path TO postgres_air;\r\n\r\nCREATE INDEX flight_departure_airport ON\r\n                   flight(departure_airport);\r\nCREATE INDEX flight_arrival_airport ON\r\n                   flight(arrival_airport);\r\n\r\nCREATE INDEX flight_scheduled_departure ON \r\n                  flight  (scheduled_departure);<\/pre>\n<p>Let&#8217;s examine the execution plan of the query which calculate the number of passengers on each flight:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">SELECT f.flight_no,\r\n       f.actual_departure,\r\n       count(passenger_id) passengers\r\n  FROM postgres_air.flight f\r\n       JOIN postgres_air.booking_leg bl \r\n          ON bl.flight_id = f.flight_id\r\n       JOIN postgres_air.passenger p \r\n        ON p.booking_id=bl.booking_id\r\n WHERE f.departure_airport = 'JFK'\r\n   AND f.arrival_airport = 'ORD'\r\n   AND f.actual_departure BETWEEN\r\n        '2023-08-08' and '2023-08-12'\r\nGROUP BY f.flight_id, f.actual_departure;<\/pre>\n<p>First, we will run it with default memory allocation parameters:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">shared_buffers=128MB\r\nwork_mem=4MB<\/pre>\n<p>Note: to check the parameters settings, you can run show all command for a complete list of parameters or show &lt;parameter_name&gt; for a specific parameter value. So executing <code>SHOW work_mem<\/code> will return the current setting of that parameter.<\/p>\n<p>To make sure we compute not only the execution plan itself, but also the actual runtimes and buffers usage, execute the <code>EXPLAIN<\/code> command with the following parameters:<\/p>\n<pre class=\"lang:c# theme:vs2012\">EXPLAIN (ANALYZE, BUFFERS, TIMING)\r\nSELECT \u2026<\/pre>\n<p>The execution plan for this query is presented in Figure 1. Your results may differ a bit, especially in some of the actual numbers, but should be very similar if you restored a copy of the <code>Air<\/code> database and added the indexes that were indicated. The query plan will show up in text as a result set in whatever tool you are using.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1429\" height=\"1650\" class=\"wp-image-97197\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/a-screenshot-of-a-computer-description-automatica-6.png\" alt=\"A screenshot of a computer\n\nDescription automatically generated with medium confidence\" \/><\/p>\n<p><strong>Figure 1. Execution plan with default memory allocations<\/strong><\/p>\n<p>Looking at this query plan, we can see that the optimizer uses two indexes on table flight,<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-97198\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/a-screenshot-of-a-computer-description-automatica-7.png\" alt=\"A screenshot of a computer\n\nDescription automatically generated with low confidence\" width=\"1094\" height=\"279\" \/><\/p>\n<p>and then scans all the blocks checking for the actual departure.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-97199\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/a-screenshot-of-a-phone-description-automatically.png\" alt=\"A screenshot of a phone\n\nDescription automatically generated with medium confidence\" width=\"1097\" height=\"138\" \/><\/p>\n<p>We can also notice that the number of shared buffers is insufficient (to achieve consistent results, run the same query or <code>EXPLAIN<\/code> <code>ANALYZE<\/code> more than once) and that PostgreSQL chooses to run two parallel workers to speed up the process. The total execution time is a little but over one second.<\/p>\n<p>Since we are not modelling the effect of multiple simultaneously running queries, the parameters which might make a material impact on the query execution time are <code>shared_buffers<\/code> (the change requires restart) and <code>work_mem<\/code> (can be modified in session). Let\u2019s first try to increase the size of <code>work_mem<\/code>, then change the shared_buffers parameter to 1 GB, restart the database cluster and repeat the work_mem changes.<\/p>\n<p>Changing the <code>work_mem<\/code> parameter does not require restart \u2013 you can change it locally for a current session and repeat the experiment. Gradually increasing <code>work_mem<\/code>\u00a0up to 1GB, we won\u2019t notice any significant changes in the execution plan and execution time. The limiting factor appears to be the insufficient <code>shared_buffers<\/code> size. We do not see any disk usage which means that there is enough <code>work_mem<\/code> from the start.<\/p>\n<p>Figure 2 presents the execution plan with 128MB <code>shared_buffers<\/code> and 500 MB <code>work_mem<\/code>. Total execution time is fluctuating around 1 sec on my computer for these current parameters. The plan after this change:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1340\" height=\"1590\" class=\"wp-image-97200\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/a-screenshot-of-a-computer-description-automatica-8.png\" alt=\"A screenshot of a computer\n\nDescription automatically generated with low confidence\" \/><\/p>\n<p><strong>Figure 2. Execution plan with increased work_mem and default shared_buffers<\/strong><\/p>\n<p>Now, let\u2019s increase <code>shared_buffers<\/code> to 1GB. TO do this, you will need to edit this parameter in the <code>postgresql.conf<\/code> file and restart our Postgres instance. Before we start measuring the execution time, we run this query a couple of times making sure that whatever can fit into the shared buffers is there.<\/p>\n<p>Unfortunately, the execution time decrease will be insignificant. The execution plan will stay the same with the only difference of slightly less reads due to increased shared_buffers (see Figure 3 for a portion of the execution plan)<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1428\" height=\"352\" class=\"wp-image-97201\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/a-screenshot-of-a-computer-description-automatica-9.png\" alt=\"A screenshot of a computer\n\nDescription automatically generated with low confidence\" \/><\/p>\n<p><strong>Figure 3. A part of the execution plan with increased shared_buffers which shows less reads than on Figure 2.<\/strong><\/p>\n<p>If we keep increasing the work_mem to 200MB, 500MB, 1 GB, we will notice slow decrease in the execution time, finally getting as low as 750ms. However, we need to be mindful that such increase in work_mem won\u2019t be possible on production server for multiple sessions simultaneously. Anyway, this query does not seem to be too complicated. Are there other ways to improve its performance?<\/p>\n<h2>Are there better ways to tune this query?<\/h2>\n<p>Looking at all the execution plans which were produced during our experiments, we can notice one major deficiency: Postgres has to read the heap (table rows) in order to find records where the actual departure is within the range from August 8 to August 12. That indicates that an index on this attribute might help. Let\u2019s go ahead and create the missing index:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">CREATE INDEX flight_actual_departure\r\n    ON postgres_air.flight (actual_departure);<\/pre>\n<p>Immediately, we notice the difference in the execution plan (see Figure 4)<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1429\" height=\"964\" class=\"wp-image-97202\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/a-screenshot-of-a-computer-description-automatica-10.png\" alt=\"A screenshot of a computer\n\nDescription automatically generated with medium confidence\" \/><\/p>\n<p><strong>Figure 4. Truncated execution plan with index on actual_departure column.<\/strong><\/p>\n<p>Also, we can see that the execution time finally started to decrease, and we are down to 0.5 sec execution time. However, the difference is still not so dramatic as we would like it to be. Continuing examining the execution plan we notice that Postgres performs a full scan on the passenger table (see Figure 5).<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1428\" height=\"290\" class=\"wp-image-97203\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/a-screenshot-of-a-computer-description-automatica-11.png\" alt=\"A screenshot of a computer\n\nDescription automatically generated with low confidence\" \/><\/p>\n<p><strong>Figure 5. A part of the execution plan with full table scan.<\/strong><\/p>\n<p>Looking at the join condition and at the foreign key constraint on the <code>booking_id<\/code> field, we can conclude that the index on <code>booking_id<\/code> might help. Indeed, if we create a new index:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">CREATE INDEX IF NOT EXISTS passenger_booking_id\r\n    ON postgres_air.passenger (booking_id);<\/pre>\n<p>The execution plan will change dramatically \u2013 see Figure 6. Now, the total execution time is just 10 ms, which is <strong>fifty times<\/strong> shorter than with best possible parameters tuning.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1431\" height=\"1149\" class=\"wp-image-97204\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/a-screenshot-of-a-computer-description-automatica-12.png\" alt=\"A screenshot of a computer\n\nDescription automatically generated with medium confidence\" \/><strong>Figure 6. Execution plan with two indexes.<\/strong><\/p>\n<p>More importantly, if we roll back the system parameters to the default settings and reduce the shared_buffers to 128MB, nothing will change either in the execution plan or in the execution time. No wonder \u2013 looking closely at the new execution plan in Figure 6, we can see that since we do not need massive table scans, we do not need that many shared buffers!<\/p>\n<h2>Other Limitations of Parameters Tuning<\/h2>\n<p>When we think about the meaning of the Postgres configuration parameters, it\u2019s important to remember that, in essence, all we are doing is communicating to the query planner what hardware resources it has available. Postgres is agnostic to what is the size of the RAM of the host it is running, what is the type or storage, how many cores are available, and what other systems are running on the same host. We provide some \u201cinput parameters\u201d for optimization by setting up parameters to specific values.<\/p>\n<p>For example, if we define <code>shared_buffers=128MB<\/code> while the host has 16 GB of RAM, the query planner won\u2019t be able to use most of the memory. However, it could choose another way to make queries faster, such as running it with multiple parallel processes, as we saw in Figures 1 \u2013 3.<\/p>\n<p>The opposite is also true. If we have the same 16 GB of RAM, and we allocate 4 GB for shared buffers, and at the same time set default <code>work_mem=200MB<\/code> and <code>max_connections=700<\/code>, there are high chance that Postgres will get an \u201cout of memory\u201d error from the operating system.<\/p>\n<p>Another example. The <code>random_page_cost<\/code> parameter helps the optimizer estimate the cost of index-based access. The default value of 4 in the older Postgres versions reflected the disk characteristics which were available ten years ago. Nowadays, keeping this value at 4 might stop the optimizer from choosing the best execution plan. However, if we reduce this value for the system which utilizes the slow disks, the result will be the opposite.<\/p>\n<h2>Conclusion<\/h2>\n<p>Although it is important to set up Postgres configuration parameters in the best possible way for a specific system, it is far from enough to achieve the best performance. As we demonstrated, parameters tuning usually helps to improve system performance by ten or twenty percent, and maybe in some cases even more. At the same time, the creation of missing indexes can make the queries run dozen times faster, and the same time reduce resource usage.<\/p>\n<p>In subsequent blog post series, we will talk about multiple techniques that make this optimization possible.<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>So much about parameters tuning, but does it always help? Welcome to the third and final blog of the &#8220;magic of parameters&#8221; series. In two previous blogs, we discussed how tuning PostgreSQL parameters could help improve overall system performance. However, the very first paragraph of\u00a0the very first blog on this topic stated that:\u00a0 Although some&#8230;&hellip;<\/p>\n","protected":false},"author":341426,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[53,143534],"tags":[159066],"coauthors":[158986],"class_list":["post-97196","post","type-post","status-publish","format-standard","hentry","category-featured","category-postgresql","tag-postgresql-101-webinar-sidebar"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/97196","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\/341426"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=97196"}],"version-history":[{"count":3,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/97196\/revisions"}],"predecessor-version":[{"id":97258,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/97196\/revisions\/97258"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=97196"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=97196"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=97196"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=97196"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}