{"id":105915,"date":"2025-04-02T20:52:05","date_gmt":"2025-04-02T20:52:05","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=105915"},"modified":"2025-02-27T21:01:21","modified_gmt":"2025-02-27T21:01:21","slug":"multi-version-concurrency-control-mvcc-in-postgresql-learning-postgresql-with-grant","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/postgresql\/multi-version-concurrency-control-mvcc-in-postgresql-learning-postgresql-with-grant\/","title":{"rendered":"Multi-Version Concurrency Control (MVCC) in PostgreSQL: Learning PostgreSQL with Grant"},"content":{"rendered":"\n<p>It\u2019s a tale as old as time. You want to read data. Your mate wants to write data. You\u2019re stepping on each other&#8217;s toes, all the time. When we\u2019re talking about relational data stores, one aspect that makes them what they are is the need to comply with the ACID properties. These are:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Atomicity: A transaction fails or completes as a unit<\/li>\n\n\n\n<li>Consistency: Once a transaction completes, the database is in a valid, consistent, state<\/li>\n\n\n\n<li>Isolation: Each transaction occurs on its own and shouldn\u2019t interfere with the others<\/li>\n\n\n\n<li>Durability: Basically, writes are writes and will survive a system crash<\/li>\n<\/ul>\n<\/div>\n\n\n<p>A whole lot of effort is then made to build databases that both allow you to meet the necessary ACID properties while simultaneously letting lots of people into your database. PostgreSQL does this through the Multi-version Concurrency Control (MVCC). In this article we\u2019ll discuss what MVCC is and how PostgreSQL deals with concurrency in order to both meet ACID properties and provide a snappy performance profile. Along the way we\u2019ll also be talking once more about the <code>VACUUM<\/code> process in PostgreSQL (you can read my <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/postgresql\/learning-postgresql-with-grant-introducing-vacuum\/\">introduction to the VACUUM<\/a> here).<\/p>\n\n\n\n<p>Let me start by giving you the short version of what MVCC is, and then the rest of the article explains more details. Basically, PostgreSQL is focused on ensuring, as much as possible, that reads don\u2019t block writes and writes don\u2019t block reads. This is done by always, only, inserting rows (tuples). No updates to an existing row. No actual deletes or updates. Instead, it uses a logical delete mechanism, which we\u2019ll get into. This means that data in motion doesn\u2019t interfere with data at rest, meaning a write doesn\u2019t interfere with a read, therefore, less contention &amp; blocking. There\u2019s a lot to how all that works, so let\u2019s get into it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-concurrency-modes-in-postgresql\">Concurrency Modes in PostgreSQL<\/h2>\n\n\n\n<p>The world can be a messy place. If everything in a database were ordered, completely in series, including exactly who could access what and when they could access it, we\u2019d never really have to worry about concurrency. However, concurrency is all about simultaneous actions. Two people are going to want to perform two different actions to the same row (AKA, tuple). One person wants to read from it, the other wants to delete it. Or, both want to update it, but with different values. Before we get into describing MVCC, let\u2019s talk concurrency. The PostgreSQL database management system has three ways, isolation levels, for dealing with concurrency:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Read Committed<\/li>\n\n\n\n<li>Repeatable Read<\/li>\n\n\n\n<li>Serializable<\/li>\n<\/ul>\n<\/div>\n\n\n<p>Let\u2019s examine each in turn. But first, for those of you who come from SQL Server land, one is missing. That\u2019s right, PostgreSQL does not have a Read Uncommitted isolation level. Personally, I find this to be a feature, but we\u2019ll talk about it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-read-committed-isolation-level\">Read Committed Isolation Level<\/h3>\n\n\n\n<p>This is a pretty straightforward concurrency model. When you read from the database, you only want to see the data that has been committed. No data in flight. No data from open transactions. Easy as can be. Well, it quickly gets sticky.<\/p>\n\n\n\n<p>Basically, when you run a query against PostgreSQL, it gets a transaction id (and we\u2019ll be talking about this in more detail later). That transaction id is then used to ensure that as it reads data, it only gets data with transaction ids that are older. Effectively, a snapshot of the database is created, without actually moving data round. There is a lot more to it, but that\u2019s the gist of the behavior.<\/p>\n\n\n\n<p>As such, when you run a <code>SELECT<\/code>, you\u2019ll only see committed transactions, none that are in flight, based on your <code>transaction<\/code> <code>ID<\/code>. Now, if data gets committed before your <code>SELECT<\/code>, you\u2019ll see that committed data, even if the <code>ID<\/code> is different, because we\u2019re reading committed data. This works because, as was mentioned in my introduction to <code>VACUUM<\/code>, PostgreSQL doesn\u2019t delete or update rows, but instead, creates a new row and marks the old row as being replaced. While your transaction is open, it can still read the old row that was \u201clive\u201d when your transaction started because it\u2019s part of your snapshot.<\/p>\n\n\n\n<p>In Read Committed however, if you ran two identical <code>SELECT<\/code> statements, one after the other, you could see two different sets of data. This is because transactions may be committed between the start of running those two <code>SELECT<\/code> statements. Read Committed only worries about a single command at a time within a transaction. If you need consistent reads across commands within a transaction, you need to use the Repeatable Read Isolation Level<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-repeatable-read-isolation-level\">Repeatable Read Isolation Level<\/h3>\n\n\n\n<p>Repeatable Read is pretty similar in behavior to Read Committed. You get your transaction id at the start and that\u2019s used to make sure you don\u2019t see data in flight with newer or open transactions. However, it goes a little farther. Repeatable Read ensures that even if you have two <code>SELECT<\/code> statements, starting one after the other, the results will always be based on your transaction ID. No data committed after your transaction started will be shown.<\/p>\n\n\n\n<p>In terms of reading data then, this seems like a very attractive way to go, right? Well, sure, if all you\u2019re doing is a <code>SELECT<\/code>, that\u2019s easy. However, what if you\u2019re reading data in order to <code>UPDATE<\/code> or <code>DELETE<\/code> it? Ah, then the fact that another transaction has committed ahead of you becomes an issue. In this case, you will get an error stating:<\/p>\n\n\n\n<p><code>ERROR:  could not serialize access due to concurrent update<\/code><\/p>\n\n\n\n<p>This is because, while Repeatable Read ensures that reads are consistent across your transaction. ACID states that you can\u2019t modify data that was modified by another transaction. That requires resubmitting it, if nothing else to ensure that the change didn\u2019t exclude a given tuple from the result set you were going to modify.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-serializable-isolation-level\">Serializable Isolation Level<\/h3>\n\n\n\n<p>If you need to ensure that any given transaction sees a perfectly consistent view of the data, and that it has, more or less, exclusive control of that data, you need Serializable Isolation. In Serializable Isolation, PostgreSQL does what the name says, it makes sure that all transactions occur in a serialized fashion, one after the other, in order. For read only transactions, this has zero implications. They\u2019ll proceed the same way as Repeatable Read. The difference is in how writes are handled.<\/p>\n\n\n\n<p>The way this works is roughly the same as Repeatable Read. Further, you can, and may, see serializable errors in Serializable Isolation. The nature of PostgreSQL is such that it has to be able to support simultaneous transactions, otherwise, it would have to take exclusive locks on everything during a given, serialized, transaction, blocking all other transactions. Instead, Serializable adds a second kind of monitoring to prevent two transactions from doing things that would break the other. The monitor can catch when two transactions are doing something naughty to one another and will rollback one of the transactions with the following error:<\/p>\n\n\n\n<p><code>ERROR:  could not serialize access due to read\/write dependencies among transactions<\/code><\/p>\n\n\n\n<p>Serializable Isolation has a lot going for it in terms of ensuring absolutely consistent data, not only during a transaction, but at the end of that transaction. However, it comes with added overhead. Queries might perform slower as additional evaluations must take place to ensure that a serialized transaction isn\u2019t interfering with another. Further, since it won\u2019t be all that hard to hit read\/write dependencies, you need a much more robust error handling mechanism to retry transactions after an error is raised, obviously resulting in slower performance as a transaction starts a second time. There are a number of suggestions on how best to deal with this <a href=\"https:\/\/www.postgresql.org\/docs\/17\/transaction-iso.html&quot; \\l &quot;XACT-SERIALIZABLE\">in the PostgreSQL documentation<\/a>.<\/p>\n\n\n\n<p>There are more details, a lot more, to all three isolation levels, but as an introductory article, we\u2019ll leave it at that for the moment and talk next about transactions and transaction identifiers.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-transaction-id\">The Transaction ID<\/h2>\n\n\n\n<p>At the root of meeting ACID requirements is the concept of a transaction. It is a unit of work that will be atomic, meaning it completes successfully as a whole, or it does not, and anything it did gets rolled back. Further, the transaction is fundamental to isolation within the ACID requirements, ensuring that each unit of work is independent of the others. The two work together of course to meet the other ACID requirements of consistency (driven by our Isolation Level) and durability (it all got written to disk, yay).<\/p>\n\n\n\n<p>The way you explicitly define a transaction within PostgreSQL is through the use of BEGIN. You then complete a transaction with END. PostgreSQL takes care of every transaction in the event of an error, so there\u2019s no need in most cases for a ROLLBACK. A query within a transaction could look something like this:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">BEGIN;\n    INSERT INTO radio.antenna (\n        antenna_name,\n        manufacturer_id,\n        connectortype_id\n    ) VALUES (\n        'Stubby',\n        3,\n        2\n    );\nEND;<\/pre>\n\n\n\n<p>Executing this query will of course insert a row into the table. The <code>BEGIN<\/code> and <code>END<\/code> act as wrappers for the single statement transaction. Each transaction is assigned an identifier called a <code>VirtualTransactionId<\/code>. This value actually consists of two numbers, the process (also called backend) number for this query and a sequential identifier called <code>LocalXID<\/code>. The <code>VirtualTransactionId<\/code> is for tracking transactions within a specific process.<\/p>\n\n\n\n<p>Then, there is the <code>TransactionId<\/code>, mentioned earlier in the article. The primary driver for all the behaviors already described is that <code>TransactionId<\/code>, or <code>xid<\/code>, value. This is the value that is used to set the snapshot of data for the various isolation levels. We can see this value easily by querying PostgreSQL:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">SELECT txid_current();<\/pre>\n\n\n\n<p>That will return the highest transaction identifier (<code>xid<\/code>) at the moment:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"470\" height=\"186\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/02\/a-screenshot-of-a-computer-description-automatica-5.png\" alt=\"A screenshot of a computer\n\nDescription automatically generated\" class=\"wp-image-105916\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/02\/a-screenshot-of-a-computer-description-automatica-5.png 470w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/02\/a-screenshot-of-a-computer-description-automatica-5-300x119.png 300w\" sizes=\"auto, (max-width: 470px) 100vw, 470px\" \/><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>If I execute the <code>INSERT<\/code> statement and then rerun the query against <code>txid_current<\/code>, I get a new value:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"468\" height=\"182\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/02\/a-screenshot-of-a-computer-description-automatica-6.png\" alt=\"A screenshot of a computer\n\nDescription automatically generated\" class=\"wp-image-105917\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/02\/a-screenshot-of-a-computer-description-automatica-6.png 468w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/02\/a-screenshot-of-a-computer-description-automatica-6-300x117.png 300w\" sizes=\"auto, (max-width: 468px) 100vw, 468px\" \/><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>Worth noting, I\u2019m running on a test system without any other connections, so I won\u2019t see the transaction count jumping a whole lot, and even my act of running this SELECT statement adds to the transaction values. So, doing these experiments, you may not see exactly one value increments between checks.<\/p>\n\n\n\n<p>I\u2019m going to go ahead and <code>DELETE<\/code> the value I just inserted, so I can use the same query again:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">DELETE\nFROM\n\tradio.antenna\nWHERE\n\tantenna_name = 'Stubby';<\/pre>\n\n\n\n<p>Now, If go back to my <code>INSERT<\/code> statement in DBeaver and I highlight the query down to, but just shy of the END; statement, and run that, I have an open, active transaction. I can now query <code>pg_stat_activity<\/code>, from a second connection, to see queries in motion:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">SELECT\n\tpsa.pid,\n\tpsa.backend_xid,\n\tpsa.backend_xmin,\n\tpsa.state,\n\tpsa.query\nFROM\n\tpg_stat_activity AS psa\nWHERE\n\tpsa.state &lt;&gt; 'idle';<\/pre>\n\n\n\n<p>Which results in:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"1758\" height=\"238\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/02\/a-screenshot-of-a-computer-description-automatica-7.png\" alt=\"A screenshot of a computer\n\nDescription automatically generated\" class=\"wp-image-105918\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/02\/a-screenshot-of-a-computer-description-automatica-7.png 1758w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/02\/a-screenshot-of-a-computer-description-automatica-7-300x41.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/02\/a-screenshot-of-a-computer-description-automatica-7-1024x139.png 1024w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/02\/a-screenshot-of-a-computer-description-automatica-7-768x104.png 768w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/02\/a-screenshot-of-a-computer-description-automatica-7-1536x208.png 1536w\" sizes=\"auto, (max-width: 1758px) 100vw, 1758px\" \/><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>I filtered on the <code>psa.state <\/code>value in order to remove connections that aren\u2019t doing anything currently on the server. I got back two rows, one for the transaction that I have not yet completed and the other for this query itself, marked \u2018active.\u2019 The <code>pid<\/code> values 28,435 and 28,436 are the process numbers. The <code>xid<\/code> for my idle transaction is stored there in the <code>backend_xid<\/code> column. I included the <code>backend_xmin<\/code> so you can see it, but we\u2019ll address it in the next section.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-mvcc-and-vacuum\">MVCC and VACUUM<\/h3>\n\n\n\n<p>Now you have a pretty good understanding of what\u2019s going on with MVCC. Queries are assigned transaction identifier values. Those values are then used to snapshot the data, according to the isolation level of the backend. Then as new rows are added through what would otherwise be an <code>UPDATE<\/code> process, reads continue on the data that\u2019s there in a row that was marked for later deleting. Same things when running a <code>DELETE<\/code> statement. Read queries can still hit the row which has been marked for removal. With multiple versions of the rows, you get less locking and block. That\u2019s MVCC at work.<\/p>\n\n\n\n<p>So, a couple of questions. First, what cleans up the rows marked for removal? Second, assuming the xid is a data type, and we\u2019re incrementing that value once for every single transaction, can\u2019t we run out of values?<\/p>\n\n\n\n<p>Yeah, this is where we start talking about the <code>VACUUM<\/code> process. As I said right at the front of my introduction to <code>VACUUM<\/code> article (linked above), <code>VACUUM<\/code> is responsible for removing the rows that have been logically deleted from the database. It does this by taking advantage of the same thing that lets us have snapshots of data, the transaction identifier or xid value.<\/p>\n\n\n\n<p>As transactions open and close, the value for <code>backend_xmin<\/code> gets updated to the minimum value for open transactions. So, take the above image where we see the <code>backend_xid<\/code> for one process is 1343 and the <code>backend_xmin<\/code> is also 1343. Now, if a second transaction starts while our first one is still running, you\u2019ll see two things. First, the new <code>backend_xid<\/code> value will be an increment on the existing value. And you\u2019ll see the <code>backend_xmin<\/code> value stay the same. Why?<\/p>\n\n\n\n<p>Because that minimum value is the lowest value for rows marked for deletion. It can remove all rows before 1343, but no rows marked with the transaction ID value of 1343 or higher. That is, until and unless those transactions are also closed and the <code>backend_xmin<\/code> value gets updated to a new minimum.<\/p>\n\n\n\n<p>As to the transaction id (<code>xid<\/code>) it does have a data type. It\u2019s a 32-bit value, so over 4 billion transactions can take place before it runs out of room. When it runs out of room, it recycles, starting all over again at 1. Now conceivably that could lead to issues since there could be old values out there.<\/p>\n\n\n\n<p>For example, let\u2019s say the last transaction to create a new row was 32. Now, some period of time later, when the xid wraps around and starts again, we could see issues with concurrency, transactions, reads, who knows what as 32 is higher than 1. There\u2019s another process within <code>VACUUM<\/code> (told you in the introduction, <code>VACUUM<\/code> is complicated) that marks \u201cold\u201d transactions as frozen so that they\u2019re ignored for visibility checks as processes read data. This is calculated based on the <code>oldest_xmin<\/code> value, the minimum of the <code>backend_xmin<\/code> values, minus a configuration value, <code>vacuum_freeze_min_age<\/code>. It\u2019s a configurable value because some systems with an extremely high number of transactions may need a very fast freeze process to keep things moving, while others don\u2019t need to sweat it too much.<\/p>\n\n\n\n<p>Proper maintenance on a table is ultimately accomplished through a properly tuned autovacuum process, including freezing rows to avoid transaction ID reuse issues. Autovacuum is triggered to perform maintenance on a table based on a percentage of how many rows have been updated or deleted. By default this is set at 20% of rows. The larger a table gets, the more rows that need to be updated before vacuuming takes place, and the longer between vacuums.<\/p>\n\n\n\n<p>So, for larger, update or delete heavy tables, you shoud consider lowering the threshold percentage of rows before autovacuum kicks in. Although you can set this at the cluster level, it\u2019s usually best (and generally advised) to focus on tuning specific tables. You modify the threshold for autovacuum by altering the table. This example sets the scale factor to 10% of rows.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">ALTER TABLE my_large_table \n   SET (autovacuum_vacuum_scale_factor=0.1);<\/pre>\n\n\n\n<p>Setting the <code>vacuum_freeze_min_age<\/code> is one of those parameters that you may need to adjust. Too large a value could lead to:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>data corruption as the xid wraps around<\/li>\n\n\n\n<li>more disk usage as rows stick around through vacuum process longer<\/li>\n\n\n\n<li>longer vacuum times as it removes larger amounts of rows.<\/li>\n<\/ul>\n<\/div>\n\n\n<p>Of course, setting it too low can lead to things like:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><code>VACUUM<\/code> may run more frequently as it tries to deal with marking rows as frozen<\/li>\n\n\n\n<li>More CPU usage again because so many tuples are being checked and marked<\/li>\n\n\n\n<li>The possibility of lock contention as the <code>VACUUM<\/code> is trying to clean things up so frequently<\/li>\n<\/ul>\n<\/div>\n\n\n<p>Finally, of course there are many more details and nuances to how all of this works. For example, adding in replication to the mix changes how the <code>backend_xmin<\/code> values are determined and therefore what tuples can be cleaned during the <code>VACUUM<\/code> process. Suffice to say, <code>VACUUM<\/code> and MVCC, while they work extremely well to help reduce blocking within PostgreSQL, are very complex processes that can be messed up.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-conclusion\">Conclusion<\/h2>\n\n\n\n<p>While PostgreSQL satisfies the ACID requirements of a relational data store quite handily, you can now see that it does it in a way that\u2019s a bit dissimilar from other database systems. MVCC certainly does help read performance for most systems. However, within PostgreSQL, there are still locks taken out and there is still blocking that occurs.<\/p>\n\n\n\n<p>It\u2019s even possible to get a deadlock within PostgreSQL, so MVCC isn\u2019t magic. Overall, MVCC and VACUUM take a bit of getting used to, but as you understand them more, they actually make a great deal of sense.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>It\u2019s a tale as old as time. You want to read data. Your mate wants to write data. You\u2019re stepping on each other&#8217;s toes, all the time. When we\u2019re talking about relational data stores, one aspect that makes them what they are is the need to comply with the ACID properties. These are: A whole&#8230;&hellip;<\/p>\n","protected":false},"author":221792,"featured_media":105920,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[53,143534],"tags":[158977,158976,158978],"coauthors":[6785],"class_list":["post-105915","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-featured","category-postgresql","tag-learningpostgresqlwithgrant","tag-planetpostgresqlgrantfritchey","tag-postgresql"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/105915","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\/221792"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=105915"}],"version-history":[{"count":1,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/105915\/revisions"}],"predecessor-version":[{"id":105919,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/105915\/revisions\/105919"}],"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=105915"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=105915"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=105915"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=105915"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}