{"id":101859,"date":"2024-04-08T18:57:06","date_gmt":"2024-04-08T18:57:06","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=101859"},"modified":"2026-03-16T10:59:50","modified_gmt":"2026-03-16T10:59:50","slug":"using-common-table-expressions-transforming-and-analyzing-data-in-postgresql-part-2","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/postgresql\/using-common-table-expressions-transforming-and-analyzing-data-in-postgresql-part-2\/","title":{"rendered":"Using Common Table Expressions: Transforming and Analyzing Data in PostgreSQL, Part 2"},"content":{"rendered":"<p>In the <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/postgresql\/transforming-and-analyzing-data-in-postgresql\/\">first article<\/a> in this transforming data series, I discussed how powerful PostgreSQL can be in ingesting and transforming data for analysis. Over the last few decades, this was traditionally done with a methodology called Extract-Transform-Load (ETL) which usually requires external tools. The goal of ETL is to do the transformation work outside of the database and only import the final form of data that is needed for further analysis and reporting.<\/p>\n<p>However, as databases have improved and matured, there are more capabilities to do much of the raw data transformation inside of the database. In doing so, we flip the process just slightly so that we Extract-Load-Transform (ELT), focusing on getting the raw data into the database and transforming it internally. In many circumstances this can dramatically improve the iteration of development because we can use SQL rather than external tools.<\/p>\n<p>While ELT won\u2019t be able to replace every transformation workload, understanding how to do the work can help improve many data transformation and analysis workloads.<\/p>\n<p>To demonstrate how SQL and PostgreSQL functions can be used to transform raw data directly in the database, in the first article I used sample data from the <a href=\"https:\/\/adventofcode.com\/2023\/day\/7\">Advent of Code 2023, Day 7<\/a>. By the end of the first article, I had demonstrated how to take the sample input and transform it into a usable table of data that could be queried and analyzed. If you haven\u2019t read that article first, it\u2019s best to start there because you\u2019ll be able to load the sample data, understand the puzzle we are trying to solve, and some of the unique PostgreSQL features that improve the process.<\/p>\n<p>To get setup so that you can follow along, this simple script will create the \u2018dec07\u2019 table we need and insert a few rows of sample data. In the first article, I demonstrated two ways to do this that are more practical when dealing with raw input. This is just intended to get you started quickly.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">CREATE TABLE dec07 (\n\tid integer generated by default as identity,\n\tlines text\n);\nINSERT INTO dec07 (lines) VALUES \n\t('32T3K 765'),\n('T55J5 684'),\n('KK677 28'),\n('KTJJT 220'),\n('QQQJA 483');<\/pre>\n<p>By the end of the first article, we had this query.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">SELECT id AS hand, \nt.card, \nt.position, \nbid \nFROM dec07, \n     string_to_table(split_part(lines,' ',1),null) \n\t\tWITH ORDINALITY t(card,position),\n     split_part(lines,' ',2) bid;<\/pre>\n<p>Which, if you execute will return the following:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">hand|card|postition|bid|\n----+----+---------+---+\n   1|3   |        1|765|\n   1|2   |        2|765|\n   1|T   |        3|765|\n   1|3   |        4|765|\n   1|K   |        5|765|\n   2|T   |        1|684|\n   2|5   |        2|684|\n   2|5   |        3|684|\n   2|J   |        4|684|\n   2|5   |        5|684|\n   3|K   |        1|28 |\n   3|K   |        2|28 |\n   3|6   |        3|28 |\n   3|7   |        4|28 |\n   3|7   |        5|28 |\n   \u2026|\u2026   |        \u2026|\u2026  |<\/pre>\n<p>Finally, throughout that first article, all the examples that relied on multiple transformations had to use a derived table because we hadn\u2019t discussed Common Table Expressions (CTE) and their usefulness as a tool in SQL to breakup steps in the query and analysis process.<\/p>\n<p>In fact, the next part of solving the Day 7 puzzle involves converting the face value of each card into a point value. Without using a CTE, I can think of at least three ways to take the table above and add a new column with the point value of each card in the hand.<\/p>\n<ul>\n<li>Joining multiple derived tables<\/li>\n<li>Joining to a static data table<\/li>\n<li>Creating a function that takes the card face and returns a point value<\/li>\n<\/ul>\n<p>Each of these methods are either more complicated to write than necessary or require maintaining additional, separate code and data.<\/p>\n<p>For example, if we tried to do this work with multiple derived tables, it might look something like this:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">SELECT * FROM \n\t(SELECT id AS hand, \nt.*, \nbid \n\tFROM dec07, \n\t\tstring_to_table(split_part(lines,' ',1),null) \n\t\t\tWITH ORDINALITY t(card,position),\n\t\tsplit_part(lines,' ',2) bid\n) cb\nJOIN ( \n\tSELECT * FROM (VALUES  ('2',2),\n\t\t\t('3',3),\n\t\t\t('4',4),\n\t\t\t('5',5),\n\t\t\t('6',6),\n\t\t\t('7',7),\n\t\t\t('8',8),\n\t\t\t('9',9),\n\t\t\t('T',10),\n\t\t\t('J',11),\n\t\t\t('Q',12),\n\t\t\t('K',13),\n\t\t\t('A',14)) AS t(card,value)\n) vals USING (card)\nORDER BY hand, position;<\/pre>\n<p>And executing this code, you would see the following truncated dataset:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">card|hand|position|bid|value|\n----+----+--------+---+-----+\n3   |   1|       1|765|    3|\n2   |   1|       2|765|    2|\nT   |   1|       3|765|   10|\n3   |   1|       4|765|    3|\nK   |   1|       5|765|   13|\nT   |   2|       1|684|   10|\n5   |   2|       2|684|    5|\n5   |   2|       3|684|    5|\nJ   |   2|       4|684|   11|\n5   |   2|       5|684|    5|\nK   |   3|       1|28 |   13|\nK   |   3|       2|28 |   13|\n6   |   3|       3|28 |    6|\n7   |   3|       4|28 |    7|\n7   |   3|       5|28 |    7|\n\u2026   |   \u2026|       \u2026|\u2026  |   \u2026 |<\/pre>\n<p>This does perform the next transformation we need, but the query is already becoming difficult to read and nearly impossible to reuse components if needed.<\/p>\n<h2>Common Table Expressions to the Rescue<\/h2>\n<p>Let me start by acknowledging a trap that I often fall into. Any time that I learn a new feature or skill in SQL (or any technology), I can quickly start to overuse it. For instance, when I first learned about the array datatype in PostgreSQL, I started seeing every transformation and analysis problem as an opportunity to utilize arrays. As the saying goes, \u201cWhen all you have is a hammer, everything looks like a nail.\u201d<\/p>\n<p>Sometimes, CTE\u2019s can become a hammer in search of something to do, even if some other tool would work better. While using them often improves the readability of a long, complicated query, they can degrade performance because of their implementation for each platform or because storing data in temporary tables, for instance, might have been a better solution.<\/p>\n<p>That said, I find CTE\u2019s incredibly helpful when iterating over a data analysis problem without committing to a schema that\u2019s bound to change through the process. And they help me see how each step will transform the data along the way.<\/p>\n<h2>CTE\u2019s: The Basics<\/h2>\n<p>The idea of a CTE is straightforward. Wrap the output of a query in a named object (essentially a temporary <code>VIEW<\/code>) that can be referenced as a table later in the query, either inside additional CTE\u2019s or the final statement. CTE\u2019s always have at least one named object and a final statement (ie. <code>SELECT<\/code>, <code>INSERT<\/code>, <code>DELETE<\/code>) that references the CTE. Also, a CTE always starts with the keyword <code>WITH<\/code>.<\/p>\n<p>A very simple example might look like the following:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">WITH sensors AS (\n\t SELECT * FROM (VALUES\n\t \t(1, 'sensor_a'),\n\t \t(2, 'sensor_b')) s(id, name)\n)\nSELECT * FROM sensors;<\/pre>\n<p>This returns:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">id|  name  |\n--+--------+\n 1|sensor_a|\n 2|sensor_b|<\/pre>\n<p>As you can see, the CTE is named <code>sensors<\/code> and then we can select from this named object later in the query. In this case, we reference it in a simple <code>SELECT<\/code> statement.<\/p>\n<p>What\u2019s more, you can have multiple CTE\u2019s that can be referenced anywhere further down the query. When you have multiple CTE\u2019s, they are separated by a comma, except for the last CTE before the actual query. For example, we could have two CTE\u2019s, one that returns static data and a second that returns readings and then joins them in the final query.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">WITH sensors AS (\n\t SELECT * FROM (VALUES\n\t \t(1, 'sensor_a'),\n\t \t(2, 'sensor_b')) s(id, name)\n),\nsensor_data AS (\n\tSELECT * FROM (VALUES\n\t \t(1, 55.5),\n\t \t(2, 34.7),\n\t \t(1, 57.3),\n\t \t(2, 33.8)) sd(id, temp_f)\n)\nSELECT * FROM sensors s\n\tJOIN sensor_data sd\tUSING (id);<\/pre>\n<p>Executing this will return:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">id|name    |temp_f|\n--+--------+------+\n 1|sensor_a|  55.5|\n 2|sensor_b|  34.7|\n 1|sensor_a|  57.3|\n 2|sensor_b|  33.8|<\/pre>\n<p>Remember, once declared, any CTE can be referenced as a table in any query that follows, either inside a following CTE or the final query.<\/p>\n<p>With those basics explained, let\u2019s move back to the query at the beginning of this article that took the sample Day 7 puzzle input and converted card faces into a numerical value. This time, we will wrap each query in a CTE, which has a name, and allows us to write a clean final <code>SELECT<\/code> statement.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">WITH given_hand AS (\n\tSELECT id AS hand, t.*, bid \n\tFROM dec07, \n\tstring_to_table(split_part(lines,' ',1),null) \n                          WITH ORDINALITY t(card,position),\n\tsplit_part(lines,' ',2) bid\n),\ncard_converter AS ( \n\tSELECT * FROM (\n\t\tVALUES  ('2',2),\n\t\t\t('3',3),\n\t\t\t('4',4),\n\t\t\t('5',5),\n\t\t\t('6',6),\n\t\t\t('7',7),\n\t\t\t('8',8),\n\t\t\t('9',9),\n\t\t\t('T',10),\n\t\t\t('J',11),\n\t\t\t('Q',12),\n\t\t\t('K',13),\n\t\t\t('A',14)) AS t(card,value)\n)\nSELECT * FROM given_hand\n\tJOIN card_converter USING (card);<\/pre>\n<p>Which returns the following truncated results:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">card|hand|position|bid|value|\n----+----+--------+---+-----+\n3   |   1|       1|765|    3|\n2   |   1|       2|765|    2|\nT   |   1|       3|765|   10|\n3   |   1|       4|765|    3|\nK   |   1|       5|765|   13|\nT   |   2|       1|684|   10|\n5   |   2|       2|684|    5|\n5   |   2|       3|684|    5|\nJ   |   2|       4|684|   11|\n5   |   2|       5|684|    5|\nK   |   3|       1|28 |   13|\nK   |   3|       2|28 |   13|\n6   |   3|       3|28 |    6|\n7   |   3|       4|28 |    7|\n7   |   3|       5|28 |    7|\n\u2026   |   \u2026|       \u2026|\u2026  |    \u2026|<\/pre>\n<p>Easy enough! There is one more change we can make to improve readability and maintenance of the CTE.<\/p>\n<p>When a CTE is defined we can preset the column name aliases outside of the query, rather than creating aliases in the query itself. This is particularly helpful when transforming data because it saves the effort of aliasing each column name or function output inside the query every time.<\/p>\n<p>CTE column aliases are defined inside parenthesis between the name and the \u2018AS\u2019 keyword.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">WITH given_hand (hand,card,position,bid) AS (\nSELECT id, t.*, bid \n\tFROM dec07, \n\tstring_to_table(split_part(lines,' ',1),null)\nWITH ORDINALITY t(card,position),\n\tsplit_part(lines,' ',2) bid\n),\ncard_converter (card,value) AS ( \n\t\tVALUES  ('2',2),\n\t\t\t('3',3),\n\t\t\t('4',4),\n\t\t\t('5',5),\n\t\t\t('6',6),\n\t\t\t('7',7),\n\t\t\t('8',8),\n\t\t\t('9',9),\n\t\t\t('T',10),\n\t\t\t('J',11),\n\t\t\t('Q',12),\n\t\t\t('K',13),\n\t\t\t('A',14)\n)\nSELECT * FROM given_hand\n\tJOIN card_converter USING (card);<\/pre>\n<p>Hopefully this demonstrates the main reason many people use CTE\u2019s; readability.<\/p>\n<p>Let me say one more time that readability doesn\u2019t mean performant. SQL Server, for instance, doesn\u2019t materialize (cache) the query result of a CTE. Instead, the SQL from the CTE is inlined into the query that references it. In PostgreSQL, CTE\u2019s can be inlined (the default starting with PostgreSQL 12) or materialized for easy and fast reusability. (This is chosen by the optimizer, unless you specifically need to force it. You can read more about this <a href=\"https:\/\/www.postgresql.org\/docs\/current\/queries-with.html#QUERIES-WITH-CTE-MATERIALIZATION\">here<\/a> in the documentation.)<\/p>\n<p>In many situations writing a complex query as a CTE can be very helpful and probably relatively performant. If you\u2019re doing a lot of querying across large datasets, just consider if there\u2019s an alternative approach once the CTE query doesn\u2019t perform as it used to.<\/p>\n<h2>Easier Debugging<\/h2>\n<p>The second reason I personally like to use CTE\u2019s when I\u2019m working on a data transformation is because I can always insert a <code>SELECT<\/code> statement after any CTE to see the output. Oftentimes as I\u2019m building a query through multiple transformations, something doesn\u2019t work, and I have to remind myself what the output of the source CTE is that I\u2019m referencing later in the query. It\u2019s also a great way to teach others how to build a query over multiple steps.<\/p>\n<p>The next step in solving the puzzle from Day 7 is to identify how many cards of the same face exist in each hand. The first hand in our sample data was \u201832T3K\u2019 which has two \u20183\u2019 cards and one of \u20182\u2019,\u2019T\u2019,\u2019K\u2019. These values will be used later to order the final output based on card values and groupings, high to low.<\/p>\n<p>To accomplish this, we\u2019ll add a third CTE called \u2018<code>card_counts<\/code>\u2019.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">WITH given_hand (hand,card,position,bid) AS (\n   SELECT id, t.*, bid \n\tFROM dec07, \n\tstring_to_table(split_part(lines,' ',1),null) \n                           WITH ORDINALITY t(card,position),\n\tsplit_part(lines,' ',2) bid\n),\ncard_converter (card,value) AS ( \n\t\tVALUES  ('2',2),\n\t\t\t('3',3),\n\t\t\t('4',4),\n\t\t\t('5',5),\n\t\t\t('6',6),\n\t\t\t('7',7),\n\t\t\t('8',8),\n\t\t\t('9',9),\n\t\t\t('T',10),\n\t\t\t('J',11),\n\t\t\t('Q',12),\n\t\t\t('K',13),\n\t\t\t('A',14)\n),\ncard_counts (card, hand, bid, value, POSITION, card_count) AS (\n\tSELECT card, hand, bid, value, position, \n\t\tcount(*) over(PARTITION BY hand, card) FROM given_hand\n\t\tJOIN card_converter USING (card)\n)\nSELECT * FROM card_counts;<\/pre>\n<p>This will return the following truncated results:<\/p>\n<pre class=\"\">card|hand|bid|value|position|card_count|\n----+----+---+-----+--------+----------+\n2\u00a0 \u00a0|\u00a0 \u00a01|765|\u00a0 \u00a0 2|\u00a0 \u00a0 \u00a0 \u00a02|\u00a0 \u00a0 \u00a0 \u00a0 \u00a01|\n3\u00a0 \u00a0|\u00a0 \u00a01|765|\u00a0 \u00a0 3| \u00a0 \u00a0 \u00a0 1|\u00a0 \u00a0 \u00a0 \u00a0 \u00a02|\n3\u00a0 \u00a0|\u00a0 \u00a01|765|\u00a0 \u00a0 3| \u00a0 \u00a0 \u00a0 4|\u00a0 \u00a0 \u00a0 \u00a0 \u00a02|\nK\u00a0 \u00a0|\u00a0 \u00a01|765|\u00a0 \u00a013| \u00a0 \u00a0 \u00a0 5|\u00a0 \u00a0 \u00a0 \u00a0 \u00a01|\nT\u00a0 \u00a0|\u00a0 \u00a01|765|\u00a0 \u00a010| \u00a0 \u00a0 \u00a0 3|\u00a0 \u00a0 \u00a0 \u00a0 \u00a01|\n5\u00a0 \u00a0|\u00a0 \u00a02|684|\u00a0 \u00a0 5|\u00a0 \u00a0 \u00a0 \u00a05|\u00a0 \u00a0 \u00a0 \u00a0 \u00a03|\n5\u00a0 \u00a0|\u00a0 \u00a02|684|\u00a0 \u00a0 5|\u00a0 \u00a0 \u00a0 \u00a02|\u00a0 \u00a0 \u00a0 \u00a0 \u00a03|\n5\u00a0 \u00a0|\u00a0 \u00a02|684|\u00a0 \u00a0 5|\u00a0 \u00a0 \u00a0 \u00a03|\u00a0 \u00a0 \u00a0 \u00a0 \u00a03|\nJ\u00a0 \u00a0|\u00a0 \u00a02|684|\u00a0 \u00a011|\u00a0 \u00a0 \u00a0 \u00a04|\u00a0 \u00a0 \u00a0 \u00a0 \u00a01|\nT\u00a0 \u00a0|\u00a0 \u00a02|684|\u00a0 \u00a010|\u00a0 \u00a0 \u00a0 \u00a01|\u00a0 \u00a0 \u00a0 \u00a0 \u00a01|\n\u2026\u00a0 \u00a0|\u00a0 \u00a0\u2026| \u2026 |\u00a0 \u00a0 \u2026|\u00a0 \u00a0 \u00a0 \u00a0\u2026|\u00a0 \u00a0 \u00a0 \u00a0 \u00a0\u2026|<\/pre>\n<p>Notice that the final <code>SELECT<\/code> statement only references the new \u2018<code>card_counts<\/code>\u2019 CTE. That\u2019s because the \u2018<code>card_counts<\/code>\u2019 CTE already references the other two CTE\u2019s in its query.<\/p>\n<p>Next, we need to aggregate the card counts with the card values to determine the order of the hands, lowest to highest. Notice, however, that I simply comment out the `<code>SELECT * FROM card_counts;<\/code>` statement and build the next query after that.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">WITH given_hand (hand,card,position,bid) AS (\n    SELECT id, t.*, bid \n\tFROM dec07, \n\tstring_to_table(split_part(lines,' ',1),null) \n                         WITH ORDINALITY t(card,position),\n\tsplit_part(lines,' ',2) bid\n)\n--SELECT * FROM given_hand;\n,card_converter (card,value) AS ( \n\t\tVALUES  ('2',2),\n\t\t\t('3',3),\n\t\t\t('4',4),\n\t\t\t('5',5),\n\t\t\t('6',6),\n\t\t\t('7',7),\n\t\t\t('8',8),\n\t\t\t('9',9),\n\t\t\t('T',10),\n\t\t\t('J',11),\n\t\t\t('Q',12),\n\t\t\t('K',13),\n\t\t\t('A',14)\n),\ncard_counts (card, hand, bid, value, POSITION, card_count) AS (\n\tSELECT card, hand, bid, value, position, \n\t\tcount(*) over(PARTITION BY hand, card) FROM given_hand\n\t\tJOIN card_converter USING (card)\n\tORDER BY hand,position\n)\n--SELECT * FROM card_counts;\n,ordered_hands AS (\n\tSELECT hand, bid,\n\t\tarray_agg(c1 ORDER BY c1 DESC, value desc) AS ordered_card_count,\n\t\tarray_agg(value) AS card_values\n\tFROM (\n\t\tSELECT gh.hand, gh.card, gh.bid, cc.value, cc.count c1\n\t\tFROM given_hand gh\n\t\t\tJOIN card_counts cc USING (hand, o)\n\t\tORDER BY hand, o\n\t) x\n\tGROUP BY hand, bid\n\tORDER BY ordered_card_count, card_values\n)\nSELECT * FROM ordered_hands;<\/pre>\n<p>This returns:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">hand|bid|ordered_card_count|card_values     |\n----+---+------------------+----------------+\n   1|765|{2,2,1,1,1}       |{3,2,10,3,13}   |\n   4|220|{2,2,2,2,1}       |{13,10,11,11,10}|\n   3|28 |{2,2,2,2,1}       |{13,13,6,7,7}   |\n   2|684|{3,3,3,1,1}       |{10,5,5,11,5}   |\n   5|483|{3,3,3,1,1}       |{12,12,12,11,14}|<\/pre>\n<p>The final step to solving this puzzle is to number the order of rows, multiply the row number by the bid amount, and then sum all those values.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk \">WITH given_hand (hand,card,position,bid) AS (\n    SELECT id, t.*, bid \n\tFROM dec07, \n\tstring_to_table(split_part(lines,' ',1),null) \n                        WITH ORDINALITY t(card,position),\n\tsplit_part(lines,' ',2) bid\n)\n--SELECT * FROM given_hand;\n,card_converter (card,value) AS ( \n\t\tVALUES  ('2',2),\n\t\t\t('3',3),\n\t\t\t('4',4),\n\t\t\t('5',5),\n\t\t\t('6',6),\n\t\t\t('7',7),\n\t\t\t('8',8),\n\t\t\t('9',9),\n\t\t\t('T',10),\n\t\t\t('J',11),\n\t\t\t('Q',12),\n\t\t\t('K',13),\n\t\t\t('A',14)\n),\ncard_counts (card, hand, bid, value, POSITION, card_count) AS (\n\tSELECT card, hand, bid, value, position, \n\t\tcount(*) over(PARTITION BY hand, card) FROM given_hand\n\t\tJOIN card_converter USING (card)\n\tORDER BY hand,position\n)\n--SELECT * FROM card_counts;\n,ordered_hands AS (\n\tSELECT hand, bid,\n\t\tarray_agg(c1 ORDER BY c1 DESC, value desc) AS ordered_card_count,\n\t\tarray_agg(value) AS card_values\n\tFROM (\n\t\tSELECT gh.hand, gh.card, gh.bid, cc.value, cc.count c1\n\t\tFROM given_hand gh\n\t\t\tJOIN card_counts cc USING (hand, o)\n\t\tORDER BY hand, o\n\t) x\n\tGROUP BY hand, bid\n\tORDER BY ordered_card_count, card_values\n)\n--SELECT * FROM ordered_hands;\nSELECT sum(bid*row_number) FROM (\n\tSELECT *, ROW_NUMBER() OVER() \n\tFROM ordered_hands\n) z;<\/pre>\n<p>And the output is just the one value!<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">sum |\n----+\n6440|<\/pre>\n<p>Again, notice that I have added <code>SELECT<\/code> statements after each CTE as I work. Most of the time they\u2019re commented out. However, when something about the output along the way doesn\u2019t seem correct, I can insert (or uncomment) a `<code>SELECT * FROM\u2026<\/code>` after any CTE to check the results at each step.<\/p>\n<p>Being able to do this kind of incremental debugging in a static query with many derived tables would be almost impossible. CTE\u2019s simplify the process.<\/p>\n<h2>Conclusion<\/h2>\n<p>In this article, I have shown you the basics of how CTEs work and how they make coding easier by letting you format data in one query without storing data step by step.<\/p>\n<p>In the final entry in this series, I will show you how to expand the use of CTEs to allow recursive queries. This allows you to work with data that needs to be iterated over, for examples hierarchies.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In the first article in this transforming data series, I discussed how powerful PostgreSQL can be in ingesting and transforming data for analysis. Over the last few decades, this was traditionally done with a methodology called Extract-Transform-Load (ETL) which usually requires external tools. The goal of ETL is to do the transformation work outside of&#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-101859","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\/101859","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=101859"}],"version-history":[{"count":4,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/101859\/revisions"}],"predecessor-version":[{"id":109207,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/101859\/revisions\/109207"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=101859"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=101859"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=101859"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=101859"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}