{"id":101922,"date":"2024-04-22T01:12:09","date_gmt":"2024-04-22T01:12:09","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=101922"},"modified":"2026-03-16T11:00:28","modified_gmt":"2026-03-16T11:00:28","slug":"recursive-ctes-transforming-and-analyzing-data-in-postgresql-part-3","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/postgresql\/recursive-ctes-transforming-and-analyzing-data-in-postgresql-part-3\/","title":{"rendered":"Recursive CTEs: Transforming and Analyzing Data in PostgreSQL, Part 3"},"content":{"rendered":"<p>The first two articles in this series demonstrated how PostgreSQL is a capable tool for ELT &#8211; taking raw input and transforming it into usable data for querying and analyzing. We used sample data from the Advent of Code 2023 to demonstrate some of the ELT techniques in PostgreSQL.<\/p>\n<p>In the first article, we discussed functions and features of SQL in PostgreSQL that can be used together to transform and analyze data. In the second article, we introduced Common Table Expressions (CTE) as a method to build a transformation query one step at a time to improve readability and debugging.<\/p>\n<p>In this final article, I\u2019ll tackle one last feature of SQL that allows us to process data in a loop, referencing previous iterations to perform complex calculations: Recursive CTE\u2019s.<\/p>\n<h2>SQL is Set-based<\/h2>\n<p>SQL is primarily a set-based, declarative language. Using standard ANSII SQL and platform-specific functions, a SQL developer declares the desired outcome of a query, not the process by which the database should retrieve and process the data. The query planner typically uses statistics about the distribution of data to determine the best plan to get the desired result and return the full set of rows. While CTE\u2019s and <code>LATERAL<\/code> joins make it feel like we can use the output of one query to impact another, those are always a one-shot opportunity. As a set-based language, it\u2019s impossible to do algorithmic calculations, the ability to use the output of a query as input and control to another in a loop.<\/p>\n<p>Stated another way, early versions of the SQL standard did not have procedural capabilities. To do that, most database platforms use their own superset of SQL that provides procedural capabilities. By default, this is T-SQL in SQL Server and pl\/pgsql in PostgreSQL.<\/p>\n<p>That changed with the SQL:1999 standard. With this new feature, implemented by all major databases, SQL became a <a href=\"https:\/\/en.wikipedia.org\/wiki\/Turing_completeness\">Turing-complete language<\/a> that can solve complex calculations in a single query.<\/p>\n<h2>Recursive Common Table Expressions (aka. Hierarchical Queries)<\/h2>\n<p>Recursive Common Table Expressions take the same form as a regular CTE. Each named query can be referred to, just like a<code>VIEW<\/code> in a later part of the query \u2013 with one major difference. Any named query can refer to back to itself in a loop, reusing the output of each iteration as input to the next until some condition is satisfied.<\/p>\n<p>What does this really mean? Let\u2019s look at a simple example.<\/p>\n<p>Let\u2019s say you wanted to return a set of data that started at number one (1) and \u201ccounted\u201d to ten (10). In what ways might you do that using your database platform without using a recursive CTE? A few possibilities that come to mind in PostgreSQL include:<\/p>\n<ul>\n<li>Returning a <code>VALUES<\/code> clause with all the numbers from 1 to 10 as separate rows.<\/li>\n<li>Create a set-returning function that takes a starting and ending value as input and then uses a procedural language like pl\/pgsql to loop over the data until the ending condition is met.<\/li>\n<li>Write ten different named CTE blocks, each selecting from the previous and adding 1 to the highest current value, likely with a <code>UNION<\/code> query.<\/li>\n<\/ul>\n<p>Oi Vey! Notice that all these solutions are either very manual or require a superset of SQL.<\/p>\n<p>Recursive CTE\u2019s fix this.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">-- start at 1 and count to 10\nWITH RECURSIVE count_value AS (\n    SELECT 1 AS value\n    UNION ALL\n    SELECT value + 1 FROM count_value\n    WHERE value &lt; 10\n)\nSELECT * FROM count_value;<\/pre>\n<p>This will return:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">value|\n-----+\n    1|\n    2|\n    3|\n    4|\n    5|\n    6|\n    7|\n    8|\n    9|\n   10|<\/pre>\n<p>What just happened there? I\u2019m glad you asked.<\/p>\n<h2>Recursive CTE Explained<\/h2>\n<p>Recursive CTE\u2019s are straightforward to explain but often challenging to implement with real data, especially the more complex calculations you have. The example above starts with static data and builds from there. There is no table access. (we\u2019ll get to an example of that in a bit)<\/p>\n<p>To create a recursive CTE, the following rules must be satisfied:<\/p>\n<ul>\n<li>The keyword <code>RECURSIVE<\/code> must be added to the initial <code>WITH<\/code> keyword. This doesn\u2019t mean the first named query in the CTE has to be recursive. Adding it indicates to the query planner that one (or more) of the named queries will refer back to itself.<\/li>\n<li>The named query must emit a set of one or more rows that is used as the starting data for the next iteration. This is referred to as the initial query.<\/li>\n<li>The second query, however complex, must reference the name of the current CTE to access the output from the previous iteration.<\/li>\n<li>A <code>UNION<\/code>\/<code>UNION ALL<\/code> combines the output of the first result and each iteration thereafter.<\/li>\n<li>This process continues until a condition is met or the recursive query returns no data. Otherwise, it will perform an infinite loop and hopefully, the process will be stopped by the server to avoid crashing.<\/li>\n<\/ul>\n<p>The results of each iteration are stored in a \u201cworking table\u201d which is accessed by referencing the name of the named recursive CTE. The <code>UNION<\/code> of all the iterations is returned as the final result.<\/p>\n<p>That probably feels a little abstract if you\u2019re new to recursive CTE\u2019s, so let\u2019s take our first example and follow it step-by-step.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">-- start at 1 and count to 10\nWITH RECURSIVE count_value AS (\n    SELECT 1 AS value -\u2013 iteration 1, initial output query\n    UNION ALL\n    SELECT value + 1 FROM count_value\n    WHERE value &lt; 10\n)\nSELECT * \nFROM count_value;<\/pre>\n<p>This returns:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-101938\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/04\/a-close-up-of-a-working-table-description-automat.png\" alt=\"A close-up of a working table\n\nDescription automatically generated\" width=\"831\" height=\"129\" \/><\/p>\n<p>The initial query output sets the stage for the remainder of the recursive query. The hidden working table contains the output of the initial query, and the <code>UNION<\/code> output has the same data to start.<\/p>\n<p>We now move to the recursive query after the <code>UNION ALL<\/code>. Because it refers back to the CTE by name (\u201c<code>count_value<\/code>\u201d), it can access all of the rows in the working table from the previous iteration. In our example, when the second query is executed for the first time, the working table contains one row with the \u201cvalue\u201d of 1.<\/p>\n<p>As written, the query increments the \u2018value\u2019 column of all rows from the working table by one and then adds that result to the <code>UNION<\/code> output. In our case, the working table for each iteration will only have one row, so only one value will be emitted to the working table for the next iteration.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-101939\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/04\/a-table-with-black-text-description-automatically.png\" alt=\"A table with black text\n\nDescription automatically generated\" width=\"822\" height=\"129\" \/><\/p>\n<p>And we can keep repeating this while the value of the working table is less than 10.<\/p>\n<p>.<img loading=\"lazy\" decoding=\"async\" class=\"wp-image-101940\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/04\/a-white-sheet-of-paper-with-black-text-descriptio.png\" alt=\"A white sheet of paper with black text\n\nDescription automatically generated\" width=\"812\" height=\"858\" \/><\/p>\n<p>Recursive queries allow us to perform algorithmic calculations that otherwise wouldn\u2019t be possible with standard SQL.<\/p>\n<h2>Hierarchical Recursive Queries<\/h2>\n<p>That first example didn\u2019t select any data from a real database table. While it hopefully provided a clear, if basic, understanding of how a recursive CTE works, we need to look at a slightly more complex example.<\/p>\n<p>A common use case for recursive queries is to return hierarchical data sets based on a parent\/child relationship.<\/p>\n<ul>\n<li>Manager\/Employee<\/li>\n<li>Region\/Division\/Territory<\/li>\n<li>Disk\/Folder\/Files<\/li>\n<li>Etc.<\/li>\n<\/ul>\n<p>In each of these cases, the recursive query starts with an initial query that represents the top-level parent(s) and then with each iteration, selects the children for each parent. It will continue until there are no more children to select (or a certain depth of the hierarchy is reached).<\/p>\n<p>To demonstrate this, let\u2019s look at an example disk folder and file hierarchy.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">\/*\n * Setup for initial Recursive query example\n * \n * Folders: size = NULL\n * Files: size != NULL\n * Top-level parents: \u2018parent_folder\u2019=NULL\n * Child folder &amp; files: parent_folder != NULL\n * \n *\/\nCREATE TABLE files_on_disk (\n    name TEXT,\n    parent_folder TEXT,\n    SIZE bigint\n);\nINSERT INTO files_on_disk VALUES\n    ('Folder_A',NULL,NULL),\n    ('Folder_A_1','Folder_A',NULL),\n    ('Folder_B','Folder_A',NULL),\n    ('Folder_A_2','Folder_A',NULL),\n    ('Folder_B_1','Folder_B',NULL),\n    ('File_A1.txt','Folder_A',1234),\n    ('File_A2.txt','Folder_A',6789),\n    ('File_B1.txt','Folder_B',4567);<\/pre>\n<p>The objective is to initiate the recursive query with all top-level parent folders with the initial query. The output of the initial query can then be referenced to select the first level of children folders and files. That output is stored in the working table and used during the next iteration to find any children folders or files. Rinse and repeat.<\/p>\n<h3>Recursive CTE Example 1<\/h3>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk \">WITH recursive files AS (\n    -- this is the initial, non-recursive query that gets \n    -- things started and fills the \"working table\" with the \n    -- first set of data\n    SELECT name, parent_folder, \"size\" FROM files_on_disk\n    WHERE parent_folder IS NULL\n    UNION\n    -- this is the recursive query. For each iteration, \n    -- we join the \u201cname\u201d of all rows to the \u201cparent_folder\u201d \n    -- of the table which selects any children for each row of \n    -- the previous working table\n    SELECT fid.name, fid.parent_folder AS parent_path, \n        fid.\"size\" FROM files_on_disk fid\n        INNER JOIN files f ON fid.parent_folder = f.name\n)\nSELECT * FROM files;<\/pre>\n<p>The process as it iterates through the data is as follows:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone  wp-image-101946\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/04\/Screenshot-2024-03-27-145109.png\" alt=\"\" width=\"810\" height=\"526\" \/><\/p>\n<p>In this example, the working table sometimes had one row like the first example, but multiple rows after other iterations. The <code>JOIN<\/code> clause works like any other join. The only difference is that the contents of one table in the join (the working table) changes with each iteration.<\/p>\n<h2>Recursive CTE Example 2<\/h2>\n<p>There are a lot of ways to modify the data within a recursive CTE query to transform the final result. As long as the columns and datatypes match between the initial query and the recursive query, the sky is the limit for how the working table data is used for filtering, data modification, and more.<\/p>\n<p>To demonstrate this, let\u2019s make one small change to the query above. Notice that under the \u201c<code>parent_path<\/code>\u201d column the string doesn\u2019t look like a path, just the parent folder name. However, \u201c<code>Folder_B<\/code>\u201d is a child of \u201c<code>Folder_A<\/code>\u201d. Showing that in the output would make things clearer.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">WITH recursive files AS (\n    -- The same output as the first example \n    SELECT name, COALESCE(parent_folder,'') parent_path, SIZE \n    FROM files_on_disk\n    WHERE parent_folder IS NULL\n    UNION\n    -- This time, prepend the \u201cparent_path\u201d from the \n    -- working table to the \u201cparent_folder\u201d in the \n    -- \u201cfiles_on_disk\u201d table, adding a \u201c\\\u201d in between\n    SELECT fid.name, \n       f.parent_path || '\\' || fid.parent_folder AS parent_path, \n       fid.SIZE \n    FROM files_on_disk fid\n        INNER JOIN files f ON fid.parent_folder = f.name\n)\nSELECT * FROM files;<\/pre>\n<p>This returns the following hierarchy:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">name       |parent_path       |size|\n-----------+------------------+----+\nFolder_A   |                  |    |\nFolder_A_1 |\\Folder_A         |    |\nFolder_B   |\\Folder_A         |    |\nFolder_A_2 |\\Folder_A         |    |\nFile_A1.txt|\\Folder_A         |1234|\nFile_A2.txt|\\Folder_A         |6789|\nFolder_B_1 |\\Folder_A\\Folder_B|    |\nFile_B1.txt|\\Folder_A\\Folder_B|4567|<\/pre>\n<p>Cool, huh?<\/p>\n<h2>Putting It All Together<\/h2>\n<p>Over these three articles, we\u2019ve discussed how PostgreSQL can be used as a powerful ELT tool. We\u2019ve shown how various functions and features like<code>CROSS JOIN LATERAL<\/code> can be used to transform data after it\u2019s loaded into the database. We then demonstrated how Common Table Expressions help you build a query over time and often make it more readable. Finally, we introduced and demonstrated how to use a recursive CTE in this article.<\/p>\n<p>To put it all together, I\u2019m going to use data from the <a href=\"https:\/\/adventofcode.com\/2022\/day\/7\">Advent of Code 2022, Dec 7<\/a> puzzle.<\/p>\n<p>For this puzzle, the input is a list of commands for walking through a file system. Each line either has a command like change directory (cd) or list directory (ls), or a directory name or file.<\/p>\n<p>The object of the puzzle is to identify folders on the drive that can be deleted to make room for a \u201csystem update\u201d file to fix the device. To do this, we need to identify the hierarchy of the filesystem by following the commands from beginning to end, identifying all the directories a file is a child of. Finally, the total size of files in each directory (inclusive of all children folders and files) needs to be calculated to find directories that can be deleted to create enough space for the update file to be loaded onto the device.<\/p>\n<p>While that probably sounds a bit complicated, we can break up the steps using a recursive CTE and slowly build our solution.<\/p>\n<h3>Puzzle Input<\/h3>\n<p>The sample data, shown below, has 22 lines. Files are shown with the numerical size first followed by the name\u2014directories identified after a \u2018dir\u2019 keyword.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">$ cd \/\n$ ls\ndir a\n14848514 b.txt\n8504156 c.dat\ndir d\n$ cd a\n$ ls\ndir e\n29116 f\n2557 g\n62596 h.lst\n$ cd e\n$ ls\n584 i\n$ cd ..\n$ cd ..\n$ cd d\n$ ls\n4060174 j\n8033020 d.log\n5626152 d.ext\n7214296 k<\/pre>\n<p><strong>Step 1: <\/strong>Identify directories and files. Everything else is noise.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">with recursive commands as (\n    select id,\n        regexp_match(file_system, '\\$ cd (.*)') as dir,\n        regexp_match(file_system,'([\\d]+) (.*)') as file \n     from dec07\n)\nSELECT * FROM commands;<\/pre>\n<p>This returns:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">id|dir |file            |\n--+----+----------------+\n 1|{\/} |NULL            |\n 2|NULL|NULL            |\n 3|NULL|NULL            |\n 4|NULL|{14848514,b.txt}|\n 5|NULL|{8504156,c.dat} |\n 6|NULL|NULL            |\n 7|{a} |NULL            |\n 8|NULL|NULL            |\n 9|NULL|NULL            |\n10|NULL|{29116,f}       |\n11|NULL|{2557,g}        |\n12|NULL|{62596,h.lst}   |\n13|{e} |NULL            |\n14|NULL|NULL            |\n15|NULL|{584,i}         |\n16|{..}|NULL            |\n17|{..}|NULL            |\n18|{d} |NULL            |\n19|NULL|NULL            |\n20|NULL|{4060174,j}     |\n21|NULL|{8033020,d.log} |\n22|NULL|{5626152,d.ext} |\n23|NULL|{7214296,k}     |<\/pre>\n<p><strong>Step 2:<\/strong> Recursively step through the output of the first CTE, modifying the directory array as we go down or move up through the directories. Also, break out the file and size into separate columns. Note that we simply commented out the initial <code>SELECT<\/code> statement and added a second (recursive) CTE.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">with recursive commands as (\n    select id,\n        regexp_match(file_system, '\\$ cd (.*)') as dir,\n        regexp_match(file_system,'([\\d]+) (.*)') as file from dec07\n)\n--SELECT * FROM commands;\n,\nwalk_tree as (\n    --Initial query, just static data to get started\n    select 1::int id,\n        '{}'::text[] as dir, \n        null::text file,\n        null::int fsize    \n    union all\n    -- Check the first element of the directory array.\n-- \n-- If it is not \u2018..\u2019 and not NULL, append the directory \n-- to the array of directories.\n-- \n-- If it is \u2018..\u2019, remove the last element from the \n-- array because we moved up a directory.\n    select c.id+1, \n        case when c.dir[1] != '..' and c.dir is not null \n            then wt.dir || c.dir[1]\n        when c.dir[1] = '..' THEN \n-- remove the last element in the array\n            trim_array(wt.dir,1)\n        else wt.dir end dir,\n        c.file[2], c.file[1]::int\n    from commands c\n        join walk_tree wt on c.id = wt.id\n)\nSELECT * FROM walk_tree;<\/pre>\n<p>And this returns:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">id|dir    |file |fsize   |\n--+-------+-----+--------+\n 1|{}     |     |        |\n 2|{\/}    |     |        |\n 3|{\/}    |     |        |\n 4|{\/}    |     |        |\n 5|{\/}    |b.txt|14848514|\n 6|{\/}    |c.dat| 8504156|\n 7|{\/}    |     |        |\n 8|{\/,a}  |     |        |\n 9|{\/,a}  |     |        |\n10|{\/,a}  |     |        |\n11|{\/,a}  |f    |   29116|\n12|{\/,a}  |g    |    2557|\n13|{\/,a}  |h.lst|   62596|\n14|{\/,a,e}|     |        |\n15|{\/,a,e}|     |        |\n16|{\/,a,e}|i    |     584|\n17|{\/,a}  |     |        |\n18|{\/}    |     |        |\n19|{\/,d}  |     |        |\n20|{\/,d}  |     |        |\n21|{\/,d}  |j    | 4060174|\n22|{\/,d}  |d.log| 8033020|\n23|{\/,d}  |d.ext| 5626152|\n24|{\/,d}  |k    | 7214296|<\/pre>\n<p><strong>Step 3:<\/strong> Get a distinct set of values from the \u2018<code>dir<\/code>\u2019 column and then sum the total size of each directory, all child files included.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">with recursive commands as (\n    select id,\n        regexp_match(file_system, '\\$ cd (.*)') as dir,\n        regexp_match(file_system,'([\\d]+) (.*)') as file from dec07\n)\n--SELECT * FROM commands;\n,\nwalk_tree as (\n    select 1::int id,\n        '{}'::text[] as dir, \n        null::text file,\n        null::int fsize    \n    union all\n    select c.id+1, \n        case when c.dir[1] != '..' and c.dir is not null \n            then wt.dir || c.dir[1]\n        when c.dir[1] = '..' THEN \n            trim_array(wt.dir,1)\n        else wt.dir end dir,\n        c.file[2], c.file[1]::int\n    from commands c\n        join walk_tree wt on c.id = wt.id\n)\n--SELECT * FROM walk_tree;\n,\npaths (dir) as (\n    select distinct dir\n    from walk_tree \n    where CARDINALITY(dir) &gt; 0\n)\n--SELECT dir, cardinality(dir) FROM paths;\n,\nsizes(dir, size) as (\n    select p.dir, sum(fsize)\n    from paths as p\n    join walk_tree wt on wt.dir[:CARDINALITY(p.dir)] = p.dir\n    group by p.dir\n)\nSELECT * FROM sizes;<\/pre>\n<p>Returns:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">dir    |size    |\n-------+--------+\n{\/,a}  |   94853|\n{\/}    |48381165|\n{\/,d}  |24933642|\n{\/,a,e}|     584|<\/pre>\n<p><strong>Step 4:<\/strong> Find all directories that are less than 100,000 in total size and add them up. These are the directories that will be deleted, and we want to know how much total space will be freed.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">with recursive commands as (\n    select id,\n        regexp_match(file_system, '\\$ cd (.*)') as dir,\n        regexp_match(file_system,'([\\d]+) (.*)') as file from dec07\n)\n--SELECT * FROM commands;\n,\nwalk_tree as (\n    select 1::int id,\n        '{}'::text[] as dir, \n        null::text file,\n        null::int fsize    \n    union all\n    select c.id+1, \n        case when c.dir[1] != '..' and c.dir is not null \n            then wt.dir || c.dir[1]\n        when c.dir[1] = '..' THEN \n            trim_array(wt.dir,1)\n        else wt.dir end dir,\n        c.file[2], c.file[1]::int\n    from commands c\n        join walk_tree wt on c.id = wt.id\n)\n--SELECT * FROM walk_tree;\n,\npaths (dir) as (\n    select distinct dir\n    from walk_tree \n    where CARDINALITY(dir) &gt; 0\n)\n--SELECT dir, cardinality(dir) FROM paths;\n,\nsizes(dir, size) as (\n    select p.dir, sum(fsize)\n    from paths as p\n    join walk_tree wt on wt.dir[:CARDINALITY(p.dir)] = p.dir\n    group by p.dir\n)\n--SELECT * FROM sizes;\nselect sum(size) as first_star\nfrom sizes\nwhere size &lt; 100000;<\/pre>\n<p>Finally returning<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">space_freed|\n-----------+\n      95437|<\/pre>\n<p>Data transformation, building a query with CTE\u2019s, and a recursive CTE to build the filesystem structure.<\/p>\n<h2>Conclusion<\/h2>\n<p>These three articles are just the tip of the iceberg for what is possible using SQL features and PostgreSQL functions. Transforming and analyzing data close to the database reduces dependencies on external tools and allows for easy modifications and updates as your process improves.<\/p>\n<p>I\u2019d recommend trying to solve puzzles from sites like the Advent of Code, and search for solutions that other SQL developers have posted to learn new tips and techniques. Each little step will improve your knowledge and hone your skills for working with data using SQL and PostgreSQL.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The first two articles in this series demonstrated how PostgreSQL is a capable tool for ELT &#8211; taking raw input and transforming it into usable data for querying and analyzing. We used sample data from the Advent of Code 2023 to demonstrate some of the ELT techniques in PostgreSQL. In the first article, we discussed&#8230;&hellip;<\/p>\n","protected":false},"author":341037,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[53,143534],"tags":[158982,158978],"coauthors":[158981],"class_list":["post-101922","post","type-post","status-publish","format-standard","hentry","category-featured","category-postgresql","tag-planetpostgresqlryanbooz","tag-postgresql"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/101922","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\/341037"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=101922"}],"version-history":[{"count":21,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/101922\/revisions"}],"predecessor-version":[{"id":109208,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/101922\/revisions\/109208"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=101922"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=101922"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=101922"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=101922"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}