{"id":4386,"date":"2018-08-13T14:59:14","date_gmt":"2018-08-13T04:59:14","guid":{"rendered":"https:\/\/www.flamingspork.com\/blog\/?p=4386"},"modified":"2018-08-13T14:59:27","modified_gmt":"2018-08-13T04:59:27","slug":"optimizing-database-access-in-django-a-patchwork-story","status":"publish","type":"post","link":"https:\/\/www.flamingspork.com\/blog\/2018\/08\/13\/optimizing-database-access-in-django-a-patchwork-story\/","title":{"rendered":"Optimizing database access in Django: A patchwork story"},"content":{"rendered":"\n<p><strong>tl;dr:<\/strong> I made Patchwork a lot faster by looking at what database queries were being generated and optimizing them either by making Django produce better queries or by adding better indexes.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Introduction to Patchwork<\/h2>\n\n\n\n<p class=\"has-drop-cap\">One of the key bits of infrastructure a bunch of maintainers of Open Source Software use is a tool called <a href=\"http:\/\/jk.ozlabs.org\/projects\/patchwork\/\">Patchwork<\/a>. We use it for a bunch of OpenPOWER firmware development, several Linux subsystems use it as well as freedesktop.org.<\/p>\n\n\n\n<p>The purpose of Patchwork is to supplement the patches-to-a-mailing-list development work flow. It allows a maintainer to see all the patches that have been posted on the list, How many Acked-by\/Reviewed-by\/Tested-by replies they have, delegate responsibility for the patch to a co-maintainer, track (and change) the state of the patch (e.g. to &#8220;Under Review&#8221;, &#8220;Changes Requested&#8221;, or &#8220;Accepted&#8221;), and create bundles of patches to help in review and testing.<\/p>\n\n\n\n<p>Since patchwork is an open source project itself, there&#8217;s several instances of it out there in common use. One of the main instances is <a href=\"https:\/\/patchwork.ozlabs.org\/\">https:\/\/patchwork.ozlabs.org\/<\/a> which is (funnily enough) used by a bunch of people connected to OzLabs for projects that are somewhat connected to OzLabs. e.g. the linuxppc-dev project and the skiboot and petitboot projects. There&#8217;s also a kernel.org instance, which is used by some kernel subsystems.<\/p>\n\n\n\n<p>Recent versions of Patchwork have added some pretty cool features such as the ability to integrate with CI systems such as <a href=\"https:\/\/developer.ibm.com\/code\/open\/projects\/snowpatch\/\">Snowpatch<\/a> which helps maintainers see if patches submitted are likely to break things.<\/p>\n\n\n\n<p>Unfortunately, there&#8217;s also been some complaints that recent version of patchwork have gotten slower than previous ones. This may well be the case, or it could just be that the volume of patches is much higher and there&#8217;s load on the database. Anyway, after asking a few questions about what the size and scope was of the patchwork database on ozlabs.org, I went &#8220;hrm&#8230; this sounds like it shouldn&#8217;t really be a problem&#8230; perhaps I should look into this&#8221;.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Attacking the problem&#8230;<\/h2>\n\n\n\n<p>Every so often it is revealed that I know a little bit about databases.<\/p>\n\n\n\n<p>Getting a development environment up for Patchwork is amazingly easy thanks to Docker and the great work of the Patchwork maintainers. The only thing you need to load in is an example dataset. I started by importing mail from a few mailing lists I&#8217;m subscribed to, which was Good Enough(TM) for an initial look.<\/p>\n\n\n\n<p>Due to how Django forces us to design a database schema though, the suggested method of getting a sample data set will <strong>not<\/strong> mirror what occurs in a production system with multiple lists. It&#8217;s for this reason that I ended up using a copy of a live dataset for much of my work rather than constructing an artificial one.<\/p>\n\n\n\n<p>Patchwork supports both a MySQL and PostgreSQL database backend. Since the one on ozlabs.org is backed by PostgreSQL, I ended up loading a large dataset into PostgreSQL for most of my work, although I also did some testing with MySQL.<\/p>\n\n\n\n<p>The current patchwork.ozlabs.org instance has a database of around 13GB in side, with about a million patches. You may think this is big, my database brain goes &#8220;no, this is actually quite small and everything should be a lot faster than it is even on quite limited hardware&#8221;<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The problem with ORMs<\/h2>\n\n\n\n<p>It turns out that Patchwork is written in Django, an ORM (Object-Relational Mapping) framework in Python &#8211; and thus something that pretty effectively obfuscates application code from the SQL being run.<\/p>\n\n\n\n<p>There is one thing that Django misses that could be a pretty big general performance boost to many applications: it doesn&#8217;t support composite primary keys. For some databases (e.g. MySQL&#8217;s InnoDB engine) the PRIMARY KEY is a clustered index &#8211; that is, the physical layout of the rows on disk reflect primary key order. You can use this feature to your advantage and have much higher cache hits of your database pages.<\/p>\n\n\n\n<p>Unfortunately though, we cannot do that with Django, so we lose a bunch of possible performance because of it (especially for queries that are going to have to bring in data from disk). In fact, we&#8217;re <strong>forced<\/strong> to use an <strong>ID <\/strong>field that&#8217;ll scatter our rows all over the place rather than do something efficient. You can somewhat get back some of the performance by creating covering indexes, but this costs in terms of index maintenance and disk space.<\/p>\n\n\n\n<p>It should be noted that PostgreSQL doesn&#8217;t have a similar concept, although there is a (locking) CLUSTER statement that can (as an offline operation for the table) re-arrange existing rows to be in index order. In my testing, this can give a bit of a boost to performance of some of the Patchwork queries.<\/p>\n\n\n\n<p>With MySQL, you&#8217;d look at a bunch of statistics on what pages are being brought in and paged out of the InnoDB buffer pool. With PostgreSQL it&#8217;s a bit more complex as it relies heavily on the OS page cache.<\/p>\n\n\n\n<p>My main experience is with MySQL like environment, so I&#8217;ve had to re-learn a bunch of PostgreSQL things in this work which was kind of fun. It may be &#8220;because of my upbringing&#8221; but it seems as if there&#8217;s a lot more resources and documentation out in the wild about optimizing MySQL environments than PostgreSQL ones, especially when it comes to documentation around a bunch of things inside the database server. A <strong>lot<\/strong> of credit should go to the MySQL Documentation team &#8211; I wish the PostgreSQL documentation was up to the same standard.<\/p>\n\n\n\n<p>Another issue is that fetching BLOBs is generally an <strong>expensive<\/strong> operation that you want to avoid unless you&#8217;re going to use them. Thus, fetching the whole &#8220;object&#8221; at once isn&#8217;t always optimal. The Django query generation appears to be somewhat buggy when it comes to &#8220;hey, don&#8217;t fetch these columns, I don&#8217;t need them&#8221;, so you do have to watch what query is <strong>produced<\/strong> not just what query you <strong>expect<\/strong> to be produced. For example,  <a href=\"https:\/\/patchwork.ozlabs.org\/patch\/956046\/\"> [01\/11] Improve patch listing performance (~3x).<\/a><br \/><\/p>\n\n\n\n<p>Another issue with Django is how you go from your Python code to an actual SQL query, especially when the produced SQL query is needlessly complex or inefficient. I&#8217;ve seen Django <strong>always<\/strong> produce an ORDER BY for one table, even when not needed, I&#8217;ve also seen it <strong>always<\/strong> join tables even when you&#8217;re getting no columns from one of them and there&#8217;s no way you&#8217;re asking for it. In fact, I had to revert to raw SQL for one of my performance improvements as I just couldn&#8217;t beat it into submission:  <a href=\"https:\/\/patchwork.ozlabs.org\/patch\/956051\/\"> [10\/11] Be sensible computing project patch counts<\/a>.<\/p>\n\n\n\n<p>An ORM can be great for getting apps out quickly, or programming in a familiar way. But like many things, an understanding of what is going on underneath is key for extracting maximum performance.<\/p>\n\n\n\n<p>Also, if you ever hear something like &#8220;ORM $x doesn&#8217;t scale&#8221; then maybe that person just hasn&#8217;t looked at how to use the ORM better. The same goes for if they say &#8220;database $y doesn&#8217;t scale&#8221;- especially if it&#8217;s a long existing relational database such as MySQL or PostgreSQL.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Speeding up listing current patches for a project<\/h2>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"alignright\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"190\" height=\"59\" data-attachment-id=\"4388\" data-permalink=\"https:\/\/www.flamingspork.com\/blog\/2018\/08\/13\/optimizing-database-access-in-django-a-patchwork-story\/screenshot-from-2018-08-13-12-20-35\/\" data-orig-file=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-12-20-35.png?fit=190%2C59&amp;ssl=1\" data-orig-size=\"190,59\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"17 SQL queries in 4477ms\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-12-20-35.png?fit=190%2C59&amp;ssl=1\" src=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-12-20-35.png?resize=190%2C59&#038;ssl=1\" alt=\"17 SQL queries in 4477ms\" class=\"wp-image-4388\" \/><figcaption>More than 4 seconds in the database<br \/>does not make page load time great.<\/figcaption><\/figure>\n<\/div>\n\n\n<p>Fortunately though, the Django development environment lets you <strong>really<\/strong> easily dive into what queries are being generated and (at least roughly) where they&#8217;re being generated from. There&#8217;s a sidebar in your browser that shows how many SQL queries were needed to generate the page and how long they took. The key to making your application go faster is to run fewer queries in less time.<\/p>\n\n\n\n<p>I was incredibly impressed with how easy it was to see what queries were run, where they were run from, and the EXPLAIN output for them.<\/p>\n\n\n\n<p>By clicking on that SQL button on the right side of your browser, you get this <strong>wonderful<\/strong> chart of what queries were executed, when, and how long they took. From this, it is <strong>incredibly<\/strong> obvious which query is the most problematic: the one that took <strong>more than four seconds!<\/strong><\/p>\n\n\n\n<p>In the dim dark days of web development, you&#8217;d have to turn on a Slow Query Log on the database server and then grep through your source code or some other miserable activity. I am <strong>so glad<\/strong> I didn&#8217;t have to do that.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"584\" height=\"359\" data-attachment-id=\"4389\" data-permalink=\"https:\/\/www.flamingspork.com\/blog\/2018\/08\/13\/optimizing-database-access-in-django-a-patchwork-story\/screenshot-from-2018-08-13-12-22-36\/\" data-orig-file=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-12-22-36.png?fit=1468%2C903&amp;ssl=1\" data-orig-size=\"1468,903\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"Screenshot-from-2018-08-13-12-22-36\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-12-22-36.png?fit=584%2C359&amp;ssl=1\" src=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-12-22-36.png?resize=584%2C359&#038;ssl=1\" alt=\"\" class=\"wp-image-4389\" srcset=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-12-22-36.png?w=1468&amp;ssl=1 1468w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-12-22-36.png?resize=300%2C185&amp;ssl=1 300w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-12-22-36.png?resize=768%2C472&amp;ssl=1 768w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-12-22-36.png?resize=1024%2C630&amp;ssl=1 1024w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-12-22-36.png?resize=488%2C300&amp;ssl=1 488w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-12-22-36.png?w=1168&amp;ssl=1 1168w\" sizes=\"auto, (max-width: 584px) 100vw, 584px\" \/><figcaption>More than four seconds for a single database query does not make for a nice UX.<\/figcaption><\/figure>\n\n\n\n<p>This particular query was a real hairy one, the EXPLAIN output from PostgreSQL (and MySQL) was certainly long and involved and would most certainly <strong>not<\/strong> feature in the first half of an &#8220;Introduction to query optimization&#8221; day long workshop. If you haven&#8217;t brushed up on various bits of documentation on understanding EXPLAIN, you should! The MySQL EXPLAIN FORMAT=JSON is especially fantastic for getting deep details as to what&#8217;s going on with query execution.<\/p>\n\n\n\n<p>The big performance gain here was to have the database be able to execute the query in a much more efficient way by creating two covering indexes for part of the query. To work out what indexes to create, one has to look at the EXPLAIN output and work out <strong>why<\/strong> the database is choosing to do either a sequential scan of a large table, or use an index that doesn&#8217;t exclude that many rows. In this case, I tweaked the code to slightly change the query that was generated as well as adding a covering index. What we ended up with is something that is <strong>dramatically<\/strong> faster.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"584\" height=\"134\" data-attachment-id=\"4390\" data-permalink=\"https:\/\/www.flamingspork.com\/blog\/2018\/08\/13\/optimizing-database-access-in-django-a-patchwork-story\/screenshot-from-2018-08-13-12-29-14\/\" data-orig-file=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-12-29-14.png?fit=1373%2C314&amp;ssl=1\" data-orig-size=\"1373,314\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"Screenshot-from-2018-08-13-12-29-14\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-12-29-14.png?fit=584%2C133&amp;ssl=1\" src=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-12-29-14.png?resize=584%2C134&#038;ssl=1\" alt=\"\" class=\"wp-image-4390\" srcset=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-12-29-14.png?w=1373&amp;ssl=1 1373w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-12-29-14.png?resize=300%2C69&amp;ssl=1 300w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-12-29-14.png?resize=768%2C176&amp;ssl=1 768w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-12-29-14.png?resize=1024%2C234&amp;ssl=1 1024w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-12-29-14.png?resize=500%2C114&amp;ssl=1 500w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-12-29-14.png?w=1168&amp;ssl=1 1168w\" sizes=\"auto, (max-width: 584px) 100vw, 584px\" \/><figcaption>The main query is ~350x faster than before<\/figcaption><\/figure>\n\n\n\n<p>You&#8217;ll notice that it <strong>appears<\/strong> that the first query there takes a lot more time but it doesn&#8217;t, it just takes a lot more time <strong>relative to<\/strong> the main query.<\/p>\n\n\n\n<p>In fact, this particular page is one that people have mentioned at being really, really slow to load. With the main query now about 350 times faster than it was originally, it shouldn&#8217;t be a problem anymore.<\/p>\n\n\n\n<p>A future improvement would be to cache the COUNT() for the common case, as it&#8217;s pretty easily computed when new patches come in or states change.<\/p>\n\n\n\n<p>The patches that help this particular page have been submitted upstream here:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li> <a href=\"https:\/\/patchwork.ozlabs.org\/patch\/956046\/\"> [01\/11] Improve patch listing performance (~3x) <\/a> <\/li><li> <a href=\"https:\/\/patchwork.ozlabs.org\/patch\/956049\/\"> [05\/11] Add covering index for \/list\/ query <\/a> <\/li><li> <a href=\"https:\/\/patchwork.ozlabs.org\/patch\/956052\/\"> [06\/11] check distinct(user) based on just user_id <\/a> <\/li><li> <a href=\"https:\/\/patchwork.ozlabs.org\/patch\/956045\/\"> [08\/11] Add covering index to patchwork_submissions for \/list\/ queries<\/a><\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Making viewing a patch with comments faster<\/h2>\n\n\n\n<p>Now that we can list patches faster, can we make other pages that Patchwork has quicker?<\/p>\n\n\n\n<p>One such page is viewing a patch (or cover letter) that has a lot of comments on it. One feature of Patchwork is that it will display all the email replies to a patch or cover letter in the Web UI. But&#8230; this seemed slow<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"alignright\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"193\" height=\"58\" data-attachment-id=\"4391\" data-permalink=\"https:\/\/www.flamingspork.com\/blog\/2018\/08\/13\/optimizing-database-access-in-django-a-patchwork-story\/screenshot-from-2018-08-13-14-25-04\/\" data-orig-file=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-25-04.png?fit=193%2C58&amp;ssl=1\" data-orig-size=\"193,58\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"Screenshot-from-2018-08-13-14-25-04\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-25-04.png?fit=193%2C58&amp;ssl=1\" src=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-25-04.png?resize=193%2C58&#038;ssl=1\" alt=\"\" class=\"wp-image-4391\" \/><\/figure>\n<\/div>\n\n\n<p>On one of the most commented patches I could find, we ended up executing <strong>one hundred and seventy seven SQL queries<\/strong> to view it! If we dove into it, a bunch of the queries looked really really similar&#8230;<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"584\" height=\"311\" data-attachment-id=\"4392\" data-permalink=\"https:\/\/www.flamingspork.com\/blog\/2018\/08\/13\/optimizing-database-access-in-django-a-patchwork-story\/screenshot-from-2018-08-13-14-24-40\/\" data-orig-file=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-24-40.png?fit=1451%2C773&amp;ssl=1\" data-orig-size=\"1451,773\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"Screenshot-from-2018-08-13-14-24-40\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-24-40.png?fit=584%2C311&amp;ssl=1\" src=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-24-40.png?resize=584%2C311&#038;ssl=1\" alt=\"\" class=\"wp-image-4392\" srcset=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-24-40.png?w=1451&amp;ssl=1 1451w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-24-40.png?resize=300%2C160&amp;ssl=1 300w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-24-40.png?resize=768%2C409&amp;ssl=1 768w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-24-40.png?resize=1024%2C546&amp;ssl=1 1024w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-24-40.png?resize=500%2C266&amp;ssl=1 500w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-24-40.png?w=1168&amp;ssl=1 1168w\" sizes=\"auto, (max-width: 584px) 100vw, 584px\" \/><figcaption>I&#8217;ve got 99 queries where I only need 1.<\/figcaption><\/figure>\n\n\n\n<p>The problem here is that the Patchwork UI is wanting to find out the <strong>name<\/strong> of each person who submitted a comment, and is doing that by querying the ID from a table. What it <strong>should<\/strong> be doing instead is a <strong>SQL JOIN<\/strong> on the original query and just fetching all that information in one go: make the database server do the work, it&#8217;s really good at it.<\/p>\n\n\n\n<p>My patch  <a href=\"https:\/\/patchwork.ozlabs.org\/patch\/956040\/\"> [02\/11] 4x performance improvement for viewing patch with many comments <\/a> \u00c2\u00a0 does just that by using the <em>select_related()<\/em> method correctly, as well as being explicit about what information we want to retrieve.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"584\" height=\"149\" data-attachment-id=\"4394\" data-permalink=\"https:\/\/www.flamingspork.com\/blog\/2018\/08\/13\/optimizing-database-access-in-django-a-patchwork-story\/screenshot-from-2018-08-13-14-32-28\/\" data-orig-file=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-32-28.png?fit=1380%2C352&amp;ssl=1\" data-orig-size=\"1380,352\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"Screenshot-from-2018-08-13-14-32-28\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-32-28.png?fit=584%2C149&amp;ssl=1\" src=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-32-28.png?resize=584%2C149&#038;ssl=1\" alt=\"\" class=\"wp-image-4394\" srcset=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-32-28.png?w=1380&amp;ssl=1 1380w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-32-28.png?resize=300%2C77&amp;ssl=1 300w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-32-28.png?resize=768%2C196&amp;ssl=1 768w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-32-28.png?resize=1024%2C261&amp;ssl=1 1024w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-32-28.png?resize=500%2C128&amp;ssl=1 500w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-32-28.png?w=1168&amp;ssl=1 1168w\" sizes=\"auto, (max-width: 584px) 100vw, 584px\" \/><figcaption>We&#8217;re now only a few milliseconds to grab all the comments<\/figcaption><\/figure>\n\n\n\n<p>With that patch, we&#8217;re down to a constant number of queries and around a 3x-7x faster time executing them depending if we have a warm cache or not.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The one time I had to use raw SQL<\/h2>\n\n\n\n<p>When viewing a project page (such as <a href=\"https:\/\/patchwork.ozlabs.org\/project\/qemu-devel\/\">https:\/\/patchwork.ozlabs.org\/project\/qemu-devel\/<\/a> ) it displays the number of patches (archived and not archived) for the project. By looking at what SQL queries are executed to collect these numbers, you&#8217;ll notice two things. First, here are the queries:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"584\" height=\"111\" data-attachment-id=\"4393\" data-permalink=\"https:\/\/www.flamingspork.com\/blog\/2018\/08\/13\/optimizing-database-access-in-django-a-patchwork-story\/screenshot-from-2018-08-13-14-36-26\/\" data-orig-file=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-36-26.png?fit=1467%2C280&amp;ssl=1\" data-orig-size=\"1467,280\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"Screenshot-from-2018-08-13-14-36-26\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-36-26.png?fit=584%2C111&amp;ssl=1\" src=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-36-26.png?resize=584%2C111&#038;ssl=1\" alt=\"\" class=\"wp-image-4393\" srcset=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-36-26.png?w=1467&amp;ssl=1 1467w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-36-26.png?resize=300%2C57&amp;ssl=1 300w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-36-26.png?resize=768%2C147&amp;ssl=1 768w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-36-26.png?resize=1024%2C195&amp;ssl=1 1024w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-36-26.png?resize=500%2C95&amp;ssl=1 500w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-36-26.png?w=1168&amp;ssl=1 1168w\" sizes=\"auto, (max-width: 584px) 100vw, 584px\" \/><figcaption>COUNT() queries can be expensive<\/figcaption><\/figure>\n\n\n\n<p>First thing you&#8217;ll notice is that they took a <strong>loooooong<\/strong> time to execute (more than a second <strong>each<\/strong>). The second thing, if you look closer, is that they contain a join which is <strong>completely unneeded.<\/strong><\/p>\n\n\n\n<p>I spent a good long while trying to make Django behave, and I just could not. I believe it&#8217;s due to the model having some inheritance in it. Writing the query by hand ended up being the best solution, and it gave a <strong>significant<\/strong> performance improvement:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"584\" height=\"48\" data-attachment-id=\"4395\" data-permalink=\"https:\/\/www.flamingspork.com\/blog\/2018\/08\/13\/optimizing-database-access-in-django-a-patchwork-story\/screenshot-from-2018-08-13-14-46-47\/\" data-orig-file=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-46-47.png?fit=1401%2C116&amp;ssl=1\" data-orig-size=\"1401,116\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"Screenshot-from-2018-08-13-14-46-47\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-46-47.png?fit=584%2C48&amp;ssl=1\" src=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-46-47.png?resize=584%2C48&#038;ssl=1\" alt=\"\" class=\"wp-image-4395\" srcset=\"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-46-47.png?w=1401&amp;ssl=1 1401w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-46-47.png?resize=300%2C25&amp;ssl=1 300w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-46-47.png?resize=768%2C64&amp;ssl=1 768w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-46-47.png?resize=1024%2C85&amp;ssl=1 1024w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-46-47.png?resize=500%2C41&amp;ssl=1 500w, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-13-14-46-47.png?w=1168&amp;ssl=1 1168w\" sizes=\"auto, (max-width: 584px) 100vw, 584px\" \/><figcaption>Unfortunately, only 4x faster.<\/figcaption><\/figure>\n\n\n\n<p>Arguably, a better way would be to precompute the count for the archived\/non-archived patches and just display them. I (or someone else who knows more about Django) may want to look at that for a future improvement.<br \/><\/p>\n\n\n\n<p><strong><\/strong> <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion and final thoughts<\/h2>\n\n\n\n<p>There&#8217;s a few more places where there could be some optimizations, but currently I cannot get any single page to take more than between 40-400ms in the database when running on my laptop &#8211; and that&#8217;s Good Enough(TM) for now.<\/p>\n\n\n\n<p>The next steps are getting these patches through a round or two of review, and then getting them into a Patchwork release and deployed out on patchwork.ozlabs.org and see if people can find any new ways to make things slow.<\/p>\n\n\n\n<p>If you&#8217;re interested, the full patchset with cover letter is here: <a href=\"https:\/\/patchwork.ozlabs.org\/cover\/956041\/\">[00\/11] Performance for ALL THE THINGS!<\/a><\/p>\n\n\n\n<p>The diffstat is interesting, as most of the added code is auto-generated by Django for database migrations (adding of indexes). <br \/><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"> ...\/migrations\/0027_add_comment_date_index.py | 23 +++++++++++++++++\n ...\/0028_add_list_covering_index.py           | 19 ++++++++++++++\n ...\/0029_add_submission_covering_index.py     | 19 ++++++++++++++\n patchwork\/models.py                           | 21 ++++++++++++++--\n patchwork\/templates\/patchwork\/submission.html | 16 ++++++------\n patchwork\/views\/__init__.py                   |  8 +++++-\n patchwork\/views\/cover.py                      |  5 ++++\n patchwork\/views\/patch.py                      |  7 ++++++\n patchwork\/views\/project.py                    | 25 ++++++++++++++++---\n 9 files changed, 128 insertions(+), 15 deletions(-)\n create mode 100644 patchwork\/migrations\/0027_add_comment_date_index.py\n create mode 100644 patchwork\/migrations\/0028_add_list_covering_index.py\n create mode 100644 patchwork\/migrations\/0029_add_submission_covering_index.py<\/pre>\n\n\n\n<p>I think the lesson is that making <strong>dramatic<\/strong> improvements to performance of your Django based app does <strong>not<\/strong> mean you have to write a lot of code or revert to raw SQL or abandon your ORM. In fact, use it properly and you can get a <strong>looong<\/strong> way. It&#8217;s just that to use it properly, you&#8217;re going to have to understand the layer below the ORM, and not just treat the database as a magic black box.<br \/><\/p>\n","protected":false},"excerpt":{"rendered":"<p>tl;dr: I made Patchwork a lot faster by looking at what database queries were being generated and optimizing them either by making Django produce better queries or by adding better indexes. Introduction to Patchwork One of the key bits of &hellip; <a href=\"https:\/\/www.flamingspork.com\/blog\/2018\/08\/13\/optimizing-database-access-in-django-a-patchwork-story\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2},"jetpack_post_was_ever_published":false},"categories":[76,570],"tags":[295,735,734,733,579,554],"class_list":["post-4386","post","type-post","status-publish","format-standard","hentry","category-code","category-ibm-work-et-al","tag-database","tag-django","tag-optimization","tag-patchwork","tag-performance","tag-python"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/p5a6n8-18K","jetpack-related-posts":[{"id":4397,"url":"https:\/\/www.flamingspork.com\/blog\/2018\/08\/22\/pwnm-sync-synchronizing-patchwork-and-notmuch\/","url_meta":{"origin":4386,"position":0},"title":"pwnm-sync: Synchronizing Patchwork and Notmuch","author":"Stewart Smith","date":"2018-08-22","format":false,"excerpt":"One of the core bits of infrastructure I use as a maintainer is Patchwork (I wrote about making it faster recently). Patchwork tracks patches sent to a mailing list, allowing me as a maintainer to track the state of them (New|Under Review|Changes Requested|Accepted etc), combine them into patch bundles, look\u2026","rel":"","context":"In &quot;General&quot;","block_context":{"text":"General","link":"https:\/\/www.flamingspork.com\/blog\/category\/general\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2018\/08\/Screenshot-from-2018-08-22-17-28-25-1.png?resize=350%2C200&ssl=1","width":350,"height":200},"classes":[]},{"id":636,"url":"https:\/\/www.flamingspork.com\/blog\/2006\/04\/05\/totally-quivering-over-phpbms\/","url_meta":{"origin":4386,"position":1},"title":"totally quivering over phpBMS","author":"Stewart Smith","date":"2006-04-05","format":false,"excerpt":"phpBMS Basically I want something to generate invoices for me. This should greatly help in a bunch of things - namely not being a retard and fucking it up every month. Primarily I want to just be able to *not* have a whole bunch of spreadsheet files (one for each\u2026","rel":"","context":"In &quot;mysql&quot;","block_context":{"text":"mysql","link":"https:\/\/www.flamingspork.com\/blog\/category\/work-et-al\/mysql\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":218,"url":"https:\/\/www.flamingspork.com\/blog\/2004\/02\/18\/memberdb-01\/","url_meta":{"origin":4386,"position":2},"title":"memberdb 0.1!","author":"Stewart Smith","date":"2004-02-18","format":false,"excerpt":"Oh yes, time for me to abuse my position and spam a bunch of lists! :) memberdb is the membership database software i've been hacking off and on for a while now, and is being used by Linux Australia. In it's current form, it's visually not very pretty - but\u2026","rel":"","context":"In &quot;linux-aus&quot;","block_context":{"text":"linux-aus","link":"https:\/\/www.flamingspork.com\/blog\/category\/linux-aus\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":3705,"url":"https:\/\/www.flamingspork.com\/blog\/2014\/03\/17\/ghosts-of-mysql-past-part-11-why-are-you-happy-about-this\/","url_meta":{"origin":4386,"position":3},"title":"Ghosts of MySQL Past, part 11: Why are you happy about this?","author":"Stewart Smith","date":"2014-03-17","format":false,"excerpt":"This is part 11 in what's shaping up to be the best part of a 6 week series (Part 1, 2, 3, 4, 5, 6, 7, 7.1, 8, 8.1, 9 and 10) on various history bits of MySQL, somewhat following my LCA2014 talk (video here). One of my favorite MySQL\u2026","rel":"","context":"In &quot;mysql&quot;","block_context":{"text":"mysql","link":"https:\/\/www.flamingspork.com\/blog\/category\/work-et-al\/mysql\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":3051,"url":"https:\/\/www.flamingspork.com\/blog\/2012\/06\/28\/new-jenkins-bazaar-plugin-release-1-18\/","url_meta":{"origin":4386,"position":4},"title":"New Jenkins Bazaar plugin release! 1.18","author":"Stewart Smith","date":"2012-06-28","format":false,"excerpt":"From the desk of your new Bazaar plugin for Jenkins maintainer, I give you Version 1.18. This release has two good bug fixes: UI fix for checkout option (JENKINS-12261) Auto-recover from corrupt BZR branches (e.g. bzr branch\/checkout killed at inopportune moment) by cleaning the workspace and trying again (this is\u2026","rel":"","context":"In &quot;code&quot;","block_context":{"text":"code","link":"https:\/\/www.flamingspork.com\/blog\/category\/code\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":1906,"url":"https:\/\/www.flamingspork.com\/blog\/2010\/04\/22\/the-rotating-blades-database-benchmark\/","url_meta":{"origin":4386,"position":5},"title":"The rotating blades database benchmark","author":"Stewart Smith","date":"2010-04-22","format":false,"excerpt":"(and before you ask, yes \"rotating blades\" comes from \"become a fan\") I'm forming the ideas here first and then we can go and implement it. Feedback is much appreciated. Two tables. Table one looks like this: CREATE TABLE fan_of ( user_id BIGINT, item_id BIGINT, PRIMARY KEY (user_id, item_id), INDEX\u2026","rel":"","context":"In &quot;drizzle&quot;","block_context":{"text":"drizzle","link":"https:\/\/www.flamingspork.com\/blog\/category\/work-et-al\/drizzle-work-et-al\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]}],"jetpack_likes_enabled":true,"_links":{"self":[{"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/posts\/4386","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/comments?post=4386"}],"version-history":[{"count":1,"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/posts\/4386\/revisions"}],"predecessor-version":[{"id":4396,"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/posts\/4386\/revisions\/4396"}],"wp:attachment":[{"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/media?parent=4386"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/categories?post=4386"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/tags?post=4386"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}