<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: kitchen_code</title>
    <description>The latest articles on DEV Community by kitchen_code (@kitchen_code).</description>
    <link>https://dev.to/kitchen_code</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4036824%2Fd9c2be44-1cfe-4773-a9ea-589ff2b7323c.png</url>
      <title>DEV Community: kitchen_code</title>
      <link>https://dev.to/kitchen_code</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kitchen_code"/>
    <language>en</language>
    <item>
      <title>Incremental Relational Normalization of Semi-Structured API Product Data: An ELT Pipeline Case Study</title>
      <dc:creator>kitchen_code</dc:creator>
      <pubDate>Mon, 27 Jul 2026 01:50:15 +0000</pubDate>
      <link>https://dev.to/kitchen_code/incremental-relational-normalization-of-semi-structured-api-product-data-an-elt-pipeline-case-1o0m</link>
      <guid>https://dev.to/kitchen_code/incremental-relational-normalization-of-semi-structured-api-product-data-an-elt-pipeline-case-1o0m</guid>
      <description>&lt;p&gt;Check out my &lt;a href="https://dev.to/kitchen_code/incremental-relational-normalization-of-semi-structured-api-product-data-an-elt-pipeline-case-study-54l1"&gt;second article&lt;/a&gt;. I would be really happy to have your feedback.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>postgres</category>
      <category>restapi</category>
      <category>sql</category>
    </item>
    <item>
      <title>Incremental Relational Normalization of Semi-Structured API Product Data: An ELT Pipeline Case Study</title>
      <dc:creator>kitchen_code</dc:creator>
      <pubDate>Sat, 25 Jul 2026 23:51:12 +0000</pubDate>
      <link>https://dev.to/kitchen_code/incremental-relational-normalization-of-semi-structured-api-product-data-an-elt-pipeline-case-study-54l1</link>
      <guid>https://dev.to/kitchen_code/incremental-relational-normalization-of-semi-structured-api-product-data-an-elt-pipeline-case-study-54l1</guid>
      <description>&lt;p&gt;In my previous case study, I built an ETL pipeline using CSV files as the data source. Since CSV is a structured format and the dataset was relatively clean, the transformation stage was more predictable, allowing me to focus on understanding the overall ETL workflow rather than exploring each stage in great technical depth.&lt;/p&gt;

&lt;p&gt;For this new case study, I wanted to explore a different type of challenge by building an ELT pipeline using semi-structured data. At the same time, I was exploring relational database design and normalization principles, and I realized that they would naturally fit into the transformation phase.&lt;/p&gt;

&lt;p&gt;This article presents the development of an ELT pipeline that combines these objectives: ingesting nested JSON data from a REST API using a Python application, loading the raw data into a PostgreSQL database, and incrementally transforming it into a normalized relational data model through SQL-based transformations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Methods
&lt;/h2&gt;

&lt;h3&gt;
  
  
  System architecture
&lt;/h3&gt;

&lt;p&gt;Figure 1 illustrates the high-level architecture of the ELT pipeline implemented in this case study.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7qzgkcgptri93ep8o75c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7qzgkcgptri93ep8o75c.png" alt="Figure 1. High-level architecture of the data ELT pipeline." width="800" height="391"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 1. High-level architecture of the data ELT pipeline.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  REST API Endpoint
&lt;/h3&gt;

&lt;p&gt;The dataset was obtained from the public DummyJSON REST API, which is released under the MIT license. The API exposes multiple resources; however, but this exercise focuses on the &lt;code&gt;/products&lt;/code&gt; endpoint. &lt;/p&gt;

&lt;p&gt;To perform the extraction and loading stages of the pipeline, I developed a Python application within a virtual environment (&lt;code&gt;venv&lt;/code&gt;). Following the Separation of Concerns (SoC) principle, I implemented each stage in a separate module to improve modularity.&lt;/p&gt;
&lt;h4&gt;
  
  
  Extraction Stage
&lt;/h4&gt;

&lt;p&gt;First, I installed the &lt;code&gt;requests&lt;/code&gt; library to enable the application to send HTTP requests to the DummyJSON REST API and retrieve JSON data from the &lt;code&gt;/products&lt;/code&gt; endpoint. &lt;/p&gt;

&lt;p&gt;The response was parsed into a Python dictionary, from which the &lt;code&gt;products&lt;/code&gt; array was extracted. This allowed the application to iterate over each product individually before the loading stage.&lt;/p&gt;
&lt;h4&gt;
  
  
  Loading Stage
&lt;/h4&gt;

&lt;p&gt;Using &lt;code&gt;psycopg3&lt;/code&gt;, I established a connection between the application and the PostgreSQL database &lt;code&gt;product_catalog&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Each product record was loaded as a raw &lt;code&gt;JSONB&lt;/code&gt; document into the &lt;code&gt;raw_products_data&lt;/code&gt; table, preserving the original structure before transformation. Figure 2 presents the data model of &lt;code&gt;raw_products_data&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff173bp6xhc9us7not9dh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff173bp6xhc9us7not9dh.png" alt="Figure 2. Data model of the raw_products_data table used to store raw product records as  raw `JSONB` endraw  documents." width="149" height="94"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 2. Data model of the &lt;code&gt;raw_products_data&lt;/code&gt; table used to store raw product records as &lt;code&gt;JSONB&lt;/code&gt; documents.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Figure 3 demonstrates that &lt;code&gt;raw_products_data&lt;/code&gt; was successfully populated.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5r0pellbl6q2d5o039dv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5r0pellbl6q2d5o039dv.png" alt="Raw product records stored as  raw `JSONB` endraw  documents in  raw `raw_products_data` endraw  before any SQL-based transformations and normalization." width="800" height="420"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 3. Raw product records stored as &lt;code&gt;JSONB&lt;/code&gt; documents in the &lt;code&gt;raw_products_data&lt;/code&gt; table before any SQL-based transformations or normalization.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The next step is to transform the raw data into a normalized relational data model.&lt;/p&gt;
&lt;h4&gt;
  
  
  Transformation Stage
&lt;/h4&gt;

&lt;p&gt;This stage represents the main focus of this study. The transformation is centered on the application of normalization principles to convert the raw &lt;code&gt;JSONB&lt;/code&gt; documents into a structured relational data model.&lt;/p&gt;

&lt;p&gt;After establishing the initial relational model, higher normal forms are evaluated to determine whether further decomposition is required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First Normal Form (1NF):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since the &lt;code&gt;JSONB&lt;/code&gt; documents contain nested arrays and objects, their attributes are not represented in atomic values. This violates the requirements of First Normal Form, which states that each attribute must contain an atomic value. &lt;/p&gt;

&lt;p&gt;I created the &lt;code&gt;products_stage&lt;/code&gt; table to store the atomic attributes associated with each product. Top-level scalar attributes were mapped directly to columns, while nested objects were flattened into individual columns. The &lt;code&gt;tags&lt;/code&gt;, &lt;code&gt;images&lt;/code&gt;, and &lt;code&gt;reviews&lt;/code&gt; attributes were intentionally excluded because they represent multi-valued attributes or relationships requiring separate relational tables.&lt;/p&gt;

&lt;p&gt;The following SQL statement creates the &lt;code&gt;products_stage&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;products_stage&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="n"&gt;discount_percentage&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="n"&gt;rating&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;brand&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="n"&gt;dimension_width&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="n"&gt;dimension_height&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="n"&gt;dimension_depth&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="n"&gt;warranty_information&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;shipping_information&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;availability_status&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="n"&gt;return_policy&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;minimum_order_quantity&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;barcode&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;qr_code&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;thumbnail&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compared with &lt;code&gt;raw_products_data&lt;/code&gt; shown in Figure 2, Figure 4 illustrates the data model of the products_stage table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9q52s9w5dsdt1f4uhrw1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9q52s9w5dsdt1f4uhrw1.png" alt="Figure 4. Data model of the products_stage containing only atomic values." width="200" height="534"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 4. Data model of the &lt;code&gt;products_stage&lt;/code&gt; containing only atomic values.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After creating the table, the following &lt;code&gt;INSERT&lt;/code&gt; statement was executed to populate it with data extracted from the &lt;code&gt;JSONB&lt;/code&gt; documents.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;products_stage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;discount_percentage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rating&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dimension_width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;dimension_height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dimension_depth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;warranty_information&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shipping_information&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;availability_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;minimum_order_quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;barcode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qr_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thumbnail&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'description'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'price'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'discountPercentage'&lt;/span&gt;
  &lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'rating'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'stock'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'brand'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'sku'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'weight'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'dimensions'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'width'&lt;/span&gt;
  &lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'dimensions'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'height'&lt;/span&gt;
  &lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'dimensions'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'depth'&lt;/span&gt;
  &lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'warrantyInformation'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'shippingInformation'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'availabilityStatus'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'returnPolicy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'minimumOrderQuantity'&lt;/span&gt;
  &lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'meta'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'createdAt'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'meta'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'updatedAt'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'meta'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'barcode'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'meta'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'qrCode'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'thumbnail'&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; 
  &lt;span class="n"&gt;raw_products_data&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;INSERT&lt;/code&gt; statement uses &lt;a href="https://www.postgresql.org/docs/current/functions-json.html" rel="noopener noreferrer"&gt;PostgreSQL's JSONB operators&lt;/a&gt; (&lt;code&gt;-&amp;gt;&lt;/code&gt; and &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt;) to navigate the JSON structure, extract values, and cast them to the appropriate SQL data types.&lt;/p&gt;

&lt;p&gt;Figure 5 presents a sample of the populated &lt;code&gt;products_stage&lt;/code&gt; table in PostgreSQL, showing a subset of its columns.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe5vbliysooereluf3a7o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe5vbliysooereluf3a7o.png" alt="Figure 5 represents a Sample output of the populated  raw `products_stage` endraw  table in PostgreSQL." width="800" height="328"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 5. Sample output of the populated &lt;code&gt;products_stage&lt;/code&gt; table in PostgreSQL.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Although the &lt;code&gt;products_stage&lt;/code&gt; table satisfies the requirements of the First Normal Form (1NF), the complete relational model is not yet finalized. The excluded attributes must still be extracted into dedicated tables to ensure that multi-valued attributes are properly represented withtin the relational model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create the &lt;code&gt;product_images&lt;/code&gt; table&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To normalize the multi-valued &lt;code&gt;images&lt;/code&gt; attribute, I created the &lt;code&gt;product_images&lt;/code&gt; table. &lt;/p&gt;

&lt;p&gt;Since a single product can have multiple images, the relationship between &lt;code&gt;products_stage&lt;/code&gt; and &lt;code&gt;product_images&lt;/code&gt; is one-to-many (1:N). Each image is therefore stored as a  separate row and associated with its corresponding product through a foreign key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;product_images&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;image_id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;image_url&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;fk_product_images&lt;/span&gt;
        &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;products_stage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Populate the &lt;code&gt;product_images&lt;/code&gt; table&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;product_images&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;jsonb_array_elements_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'images'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;raw_products_data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;jsonb_array_elements_text()&lt;/code&gt; function expands each element of the &lt;code&gt;images&lt;/code&gt; array into a separate row, allowing each image URL to be stored as an atomic value. &lt;/p&gt;

&lt;p&gt;The updated relational model is shown in Figure 6.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwyotdopct1ehk9uxugjl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwyotdopct1ehk9uxugjl.png" alt="Figure 6 illustrates the updated relational model showing the one-to-many (1:N) relationship between  raw `products_stage` endraw  and the  raw `product_images` endraw ." width="385" height="534"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 6. Updated relational model showing the one-to-many (1:N) relationship between &lt;code&gt;products_stage&lt;/code&gt; and &lt;code&gt;product_images&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create the &lt;code&gt;product_reviews&lt;/code&gt; table&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The relationship between &lt;code&gt;products_stage&lt;/code&gt; and the new &lt;code&gt;product_reviews&lt;/code&gt; table is also a one-to-many relationship since one product can have multiple reviews.&lt;/p&gt;

&lt;p&gt;The following SQL statement creates &lt;code&gt;product_reviews&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;product_reviews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;review_id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;review_rating&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;review_comment&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;review_date&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reviewer_name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;reviewer_email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;fk_product_reviews&lt;/span&gt;
        &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;products_stage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Populate &lt;code&gt;product_reviews&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;product_reviews&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;review_rating&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;review_comment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;review_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reviewer_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reviewer_email&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'rating'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'comment'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'date'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'reviewerName'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'reviewerEmail'&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;raw_products_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;jsonb_array_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'reviews'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;jsonb_array_elements()&lt;/code&gt; function was used to iterate over the &lt;code&gt;reviews&lt;/code&gt; array of JSON objects in each row of &lt;code&gt;raw_products_data&lt;/code&gt;. Each review object was expanded into a separate row, allowing its individual attributes to be extracted and stored as atomic values.&lt;/p&gt;

&lt;p&gt;The updated relational schema is shown in Figure 7.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fta02rpbokwjym073fgfj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fta02rpbokwjym073fgfj.png" alt="Figure 7 illustrates the updated model showing the one-to-many (1:N) relationship between  raw `products_stage` endraw  and  raw `product_reviews` endraw ." width="407" height="534"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 7. Updated relational model showing the one-to-many (1:N) relationship between the &lt;code&gt;products_stage&lt;/code&gt; table and the &lt;code&gt;product_reviews&lt;/code&gt; table.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create the &lt;code&gt;tags&lt;/code&gt; table&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unlike the &lt;code&gt;images&lt;/code&gt; and &lt;code&gt;reviews&lt;/code&gt; attributes, the relationship between products and &lt;code&gt;tags&lt;/code&gt; is many-to-many (M:N). A product can have multiple tags, and a tag can be associated with many products.&lt;/p&gt;

&lt;p&gt;To model this relationship, I created a &lt;code&gt;tags&lt;/code&gt; table that stores each unique tag only once. The following SQL statement creates the &lt;code&gt;tags&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;tag_id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tag_name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Populate the &lt;code&gt;tags&lt;/code&gt; table&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt;
    &lt;span class="n"&gt;jsonb_array_elements_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'tags'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;raw_products_data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I created the junction table &lt;code&gt;product_tags&lt;/code&gt; to represent the many-to-many relationship between products with their corresponding tags through foreign keys. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create the &lt;code&gt;product_tags&lt;/code&gt; table&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;product_tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tag_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;fk_product_tags_product&lt;/span&gt;
        &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;products_stage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;fk_product_tags_tag&lt;/span&gt;
        &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Populate the &lt;code&gt;product_tags&lt;/code&gt; table&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;product_tags&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;raw_products_data&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;jsonb_array_elements_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'tags'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;tag_name&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tag_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;CROSS JOIN&lt;/code&gt; with &lt;code&gt;jsonb_array_elements_text()&lt;/code&gt; expands the &lt;code&gt;tags&lt;/code&gt; array of each product into individual rows, producing one row for each "product-tag" combination. These tag values are then matched with the corresponding records in the &lt;code&gt;tags&lt;/code&gt; table using an &lt;code&gt;INNER JOIN&lt;/code&gt;, allowing the &lt;code&gt;tag_id&lt;/code&gt; values to be retrieved. Finally, the &lt;code&gt;DISTINCT&lt;/code&gt; keyword ensures that each &lt;code&gt;(product_id, tag_id)&lt;/code&gt; pair is inserted only once into &lt;code&gt;product_tags&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The updated relational schema is shown in Figure 8.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffm8p9h8yyiw9mb1t0npc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffm8p9h8yyiw9mb1t0npc.png" alt="Figure 8 illustrates the updated relational model showing the many-to-many (M:N) relationship between  raw `products_stage` endraw  and  raw `tags` endraw , implemented through the  raw `product_tags` endraw  junction table." width="375" height="678"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 8. Updated relational model showing the many-to-many (M:N) relationship between &lt;code&gt;products_stage&lt;/code&gt; and &lt;code&gt;tags&lt;/code&gt;, implemented through the &lt;code&gt;product_tags&lt;/code&gt; junction table.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second Normal Form (2NF):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a relational model to satisfy the requirements of the Second Normal Form (2NF), it must first comply with the rules of the First Normal Form (1NF). &lt;/p&gt;

&lt;p&gt;In the previous section, all tables were normalized to satisfy this prerequisite.&lt;/p&gt;

&lt;p&gt;The next step is to examine whether each table in the 1NF relational model satisfies the second requirement of 2NF. This involves determining whether a non-key attribute within a table depends on only a subset of a composite primary key than the entire key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analyze &lt;code&gt;products_stage&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;products_stage&lt;/code&gt; complies with 2NF. Since the primary key of the table is composed of a single attribute &lt;code&gt;product_id&lt;/code&gt; (see Figure 4), partial dependencies cannot occur. Furthermore, all non-key attributes describe the product associated with &lt;code&gt;product_id&lt;/code&gt; and are therefore fully functionally dependent on the primary key. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analyze &lt;code&gt;product_images&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;product_images&lt;/code&gt; fulfills the conditions of 2NF. Since the primary key consists of a single attribute &lt;code&gt;image_id&lt;/code&gt; (see &lt;code&gt;product_images&lt;/code&gt; in Figure 6), partial dependencies cannot occur. In addition, the non-key attributes, &lt;code&gt;product_id&lt;/code&gt; and &lt;code&gt;image_url&lt;/code&gt;, describe the image identified by &lt;code&gt;image_id&lt;/code&gt; and are therefore fully functionally dependent on the primary key. &lt;/p&gt;

&lt;p&gt;A common misconception is to assume that &lt;code&gt;image_url&lt;/code&gt; depends on &lt;code&gt;product_id&lt;/code&gt; because a product can have multiple images. However, that is not how the table is designed.&lt;/p&gt;

&lt;p&gt;Each row represents a single image entity identified by &lt;code&gt;image_id&lt;/code&gt;, while &lt;code&gt;product_id&lt;/code&gt; simply establishes the relationship between the image and its corresponding product. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analyze &lt;code&gt;product_reviews&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;product_reviews&lt;/code&gt; satisfies the criteria for 2NF. Since the primary key consists of a single attribute &lt;code&gt;review_id&lt;/code&gt; (see &lt;code&gt;product_reviews&lt;/code&gt; in Figure 7), partial dependencies cannot occur. Furthermore, all non-key attributes are fully functionally dependent on the primary key.&lt;/p&gt;

&lt;p&gt;Each row represents a single review entity identified by &lt;code&gt;review_id&lt;/code&gt;, while &lt;code&gt;product_id&lt;/code&gt; simply establishes the relationship between the review and its corresponding product. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analyze &lt;code&gt;tags&lt;/code&gt; and &lt;code&gt;product_tags&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tags&lt;/code&gt; satisfies 2NF. Since the primary key consists of a single attribute &lt;code&gt;tag_id&lt;/code&gt; (see &lt;code&gt;tags&lt;/code&gt; in Figure 8), partial dependencies cannot occur. Furthermore, the non-key attribute &lt;code&gt;tag_name&lt;/code&gt; is fully functionally dependent on the primary key. &lt;/p&gt;

&lt;p&gt;Unlike the previous tables, the &lt;code&gt;product_tags&lt;/code&gt; table uses a composite primary key composed of &lt;code&gt;product_id&lt;/code&gt; and &lt;code&gt;tag_id&lt;/code&gt; (see &lt;code&gt;product_tags&lt;/code&gt; in Figure 8).&lt;/p&gt;

&lt;p&gt;Each row represents the association between a product and a tag, uniquely identified by the combination of &lt;code&gt;product_id&lt;/code&gt; and &lt;code&gt;tag_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, the table contains no non-key attributes so no partial dependencies can exist. As a result, &lt;code&gt;product_tags&lt;/code&gt; also satisfies the requirements of 2NF.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third Normal Form (3NF):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a relational model to comply with the conditions of the Third Normal Form (3NF), it must first satisfy the rules of 2NF. &lt;/p&gt;

&lt;p&gt;In the previous section, all tables were normalized to satisfy this prerequisite.&lt;/p&gt;

&lt;p&gt;The next step is to determine whether each table in the 2NF relational model satisfies 3NF. &lt;/p&gt;

&lt;p&gt;During the analysis of each table, potential functional dependency candidates are identified and evaluated to determine whether they could introduce transitive dependencies.  In other words, the objective is to identify if any non-key attribute within a table is transitively dependent on another non-key attribute in the same table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analyze &lt;code&gt;products_stage&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;During the analysis, it was observed that products with &lt;code&gt;stock&lt;/code&gt; value lower than 8 were consistently assigned the value "Low stock" in their &lt;code&gt;availability_status&lt;/code&gt; attribute. Conversely, products with a &lt;code&gt;stock&lt;/code&gt; value of 8 or greater were consistently assigned "In stock". This suggests a possible dependency between &lt;code&gt;stock&lt;/code&gt; and &lt;code&gt;availability_status&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;However, this observation alone is insufficient to establish the existence of a functional dependency. As a result, the 3NF status of this table could not be determined.&lt;/p&gt;

&lt;p&gt;Although removing the &lt;code&gt;availability_status&lt;/code&gt; attribute would eliminate the potential dependency, it would alter the source data without sufficient proof that the attribute is reundundant.&lt;/p&gt;

&lt;p&gt;Since the objective in this case study is to normalize the source data rather than modify its underlying structure, the attribute &lt;code&gt;availability_status&lt;/code&gt; was retained in the table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analyze &lt;code&gt;product_images&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since a product may have multiple images, &lt;code&gt;product_id&lt;/code&gt; does not functionally determine &lt;code&gt;image_url&lt;/code&gt;; therefore, no transitive dependency exists between non-key attributes. Every non-key attribute depends directly on the primary key &lt;code&gt;image_id&lt;/code&gt;, thus satisfying 3NF. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analyze &lt;code&gt;product_reviews&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;During the examination, it was determined that each &lt;code&gt;reviewer_email&lt;/code&gt; value was consistently associated with one &lt;code&gt;reviewer_name&lt;/code&gt; suggesting a possible functional dependency between the two non-key attributes. However, this observation alone is insufficient to determine the existence of a functional dependency. As a result, the 3NF status of this table could not be determined.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analyze &lt;code&gt;tags&lt;/code&gt; and &lt;code&gt;product_tags&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;tags&lt;/code&gt; table satisfies 3NF. It contains only one non-key attribute &lt;code&gt;tag_name&lt;/code&gt;, which depends directly on the primary key &lt;code&gt;tag_id&lt;/code&gt;. Therefore, no transitive dependency can exist. The same reasoning applies to the &lt;code&gt;product_tags&lt;/code&gt; table. &lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;First Normal Form (1NF)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The raw product data was initially stored in &lt;code&gt;raw_products_data&lt;/code&gt; as &lt;code&gt;JSONB&lt;/code&gt; documents. &lt;/p&gt;

&lt;p&gt;During the 1NF transformation, the semi-structured data was converted into a set of relational tables: &lt;code&gt;products_stage&lt;/code&gt;, &lt;code&gt;product_images&lt;/code&gt;, &lt;code&gt;product_reviews&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt; and &lt;code&gt;product_tags&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Nested arrays and objects were eliminated and each attribute was stored as an atomic value while preserving the relationships between the entities through primary and foreign keys. &lt;/p&gt;

&lt;p&gt;The resulting relational model satisfied the First Normal Form (1NF). Figure 9 presents the complete relational model obtained after the 1NF transformation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcdxadcollabaodoqxcn4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcdxadcollabaodoqxcn4.png" alt="Figure 9. Final relational model satisfying the requirements of 1NF, derived from the raw JSONB product data." width="407" height="690"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 9. Final relational schema satisfying the requirements of the First Normal Form 1NF, derived from the raw JSONB product data.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second Normal Form (2NF)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The analysis showed that no partial dependencies exist within the relational model. Tables containing single-attribute primary keys (&lt;code&gt;products_stage&lt;/code&gt;, &lt;code&gt;product_images&lt;/code&gt;, &lt;code&gt;product_reviews&lt;/code&gt;, and &lt;code&gt;tags&lt;/code&gt;) cannot exhibit partial dependencies because every non-key attribute is fully functionally dependent on its primary key. &lt;/p&gt;

&lt;p&gt;Although the &lt;code&gt;product_tags&lt;/code&gt; table contains a composite primary key (&lt;code&gt;product_id&lt;/code&gt;, &lt;code&gt;tag_id&lt;/code&gt;), it stores no non-key attributes. Consequently, no partial dependencies can exist, and the table therefore does not violate the Second Normal Form (2NF) requirements.&lt;/p&gt;

&lt;p&gt;No modifications were required ater the 2NF evaluation. The relational data model remains identical to the one shown in Figure 9.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third Normal Form (3NF)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The 3NF analysis identified two categories of tables within the relational model. &lt;/p&gt;

&lt;p&gt;The first category consists of the &lt;code&gt;product_images&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt;, and &lt;code&gt;product_tags&lt;/code&gt;, for which no transitive dependencies were identified. These tables comply with 3NF.&lt;/p&gt;

&lt;p&gt;The second category consists of &lt;code&gt;products_stage&lt;/code&gt; and &lt;code&gt;product_reviews&lt;/code&gt;, where potential functional dependencies were observed. However, the available data was insufficient to conclusively establish these dependencies. Therefore, their compliance with 3NF could not be conclusively established.&lt;/p&gt;

&lt;p&gt;As a result no modifications to the relational model were required following the 3NF analysis. The final relational model therefore remains identical to the model presented in Figure 9.&lt;/p&gt;

&lt;p&gt;The resulting relational model was normalized to 2NF. Although 3NF was established for some tables, the 3NF status of the complete relational model could not be determined.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discussion
&lt;/h2&gt;

&lt;p&gt;This case study set out to explore an ELT approach for handling semi-structured data and transforming it into a relational data model. Beyond this objective, the study also strengthened both my practical and theoretical skills.&lt;/p&gt;

&lt;p&gt;The practical side involves performing HTTP requests with the &lt;code&gt;requests&lt;/code&gt; library to consume a REST API in Python. It also involves working extensively with SQL in PostgreSQL by using &lt;code&gt;JSONB&lt;/code&gt; operators and set-returning functions. &lt;/p&gt;

&lt;p&gt;On the theorical side, the exercise provided hands-on application of normalization principles requiring the evaluation of the data models across the normal forms. In addition, it deepened my understanding of data modeling, particularly how relationships (1:N and M:N) are represented through foreign keys and junction tables as the model evolves. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/k1ssa1/api-to-postgresql-elt-pipeline" rel="noopener noreferrer"&gt;GiHub repository of the Python application&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dummyjson.com/" rel="noopener noreferrer"&gt;Product semi-structured dataset: DummyJSON official website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dbeaver.io/" rel="noopener noreferrer"&gt;Data visualisation tool for the ER diagrams: DBeaver&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.diagrams.net/" rel="noopener noreferrer"&gt;Pipeline architecture design tool: draw.io&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>elt</category>
      <category>normalization</category>
      <category>sql</category>
      <category>restapi</category>
    </item>
    <item>
      <title>Building my first ETL Pipeline: A Healthcare Management System case study</title>
      <dc:creator>kitchen_code</dc:creator>
      <pubDate>Mon, 20 Jul 2026 00:26:02 +0000</pubDate>
      <link>https://dev.to/kitchen_code/building-my-first-etl-pipeline-a-healthcare-management-system-case-study-2ba3</link>
      <guid>https://dev.to/kitchen_code/building-my-first-etl-pipeline-a-healthcare-management-system-case-study-2ba3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Data engineering has long been a field of interest to me. Drawing upon my software engineering background, particularly in web development, I decided to undertake a structured learning path to master its core concepts. This article presents my first hands-on exercise in data engineering: the development of an ETL pipeline for a healthcare management system.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For this introductory case study, I chose to build an ETL pipeline because it provides a straightforward way to understand the three fundamental stages of the process: extraction, transformation and loading. By contrast, ELT pipelines are typically used within data warehouse or data lakehouse environments, adding another layer of concepts beyond the scope of this introductory exercise. &lt;/p&gt;

&lt;p&gt;To support this case study, I selected the publicly available Healthcare Management System dataset published on Kaggle by Anouska Abhisikta. The dataset consists of five CSV files representing a healthcare management system: Appointment, Billing, Doctor, Medical Procedure and Patient. Its relational structure is particularly suitable for a introductory ETL pipeline practical demonstration. In addition, I deliberately selected a CSV-based dataset to avoid the additional complexity of formats such as JSON and Parquet files, or other data sources that would introduce distractions from the core ETL concepts explored in this exercise.&lt;/p&gt;

&lt;p&gt;The objective is to build an ETL pipeline that extracts the data from these CSV files, applies data transformations, and loads the transformed data into a local PostgreSQL database. &lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;METHOD:&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The ETL pipeline was developed as a batch processing application using Python. A dedicated virtual environment (venv) was created in order to isolate project dependencies, with Pandas serving as the library for data manipulation. The source CSV files were stored in a data directory within the project and served as the input source throughout the pipeline. &lt;/p&gt;

&lt;p&gt;The Python application follows the principle of separation of concerns, with each stage of the ETL pipeline being implemented in a separate module to improve readability, while main.py coordinates the execution of the pipeline (figure 1).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1wq3m9aeumsdrgk4lbsz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1wq3m9aeumsdrgk4lbsz.png" alt="Figure 1 - Python Batch ETL project structure" width="336" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STAGE 1: EXTRACTION&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;During the extraction stage, each CSV file is independently read in its own dedicated module and the extraction function returns a Pandas DataFrame, marking the first stage of the ETL pipeline (Figure 2).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffrswsc5eotvvzhyhplmq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffrswsc5eotvvzhyhplmq.png" alt="Figure 2 - Example of the implementation of the extraction phase on the Appointment CSV dataset" width="800" height="272"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STAGE 2: TRANSFORMATION&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The transformation stage demonstrates basic data processing techniques using the Python library Pandas. Given that this is a beginner case study, this data transformation is intentionally simple and is intended to understand the Transform phase rather than performing complex data manipulation strategies. Depending on the use case, different transformation rules may be applied to the same dataset. &lt;/p&gt;

&lt;p&gt;Figure 3 illustrates an example of the data transformation performed on the Appointment dataset. This includes duplicate removal (drop_duplicates()), data type conversion, column renaming (rename()), and the creation of surrogate primary keys. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8i0tns54eao04uzice0k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8i0tns54eao04uzice0k.png" alt="Figure 3 - Example of the implementation of the transformation phase on the Appointment DataFrame" width="800" height="292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STAGE 3: LOAD&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To load the transformed data, the Psycopg2 library was used to establish a connection between my Python application and a local PostgreSQL database. To centralize the database connection logic, a database.py file was created as shown in figure 4.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6tnp4kn0zvtewrvz1iu6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6tnp4kn0zvtewrvz1iu6.png" alt="Figure 4 - Establishing the connection to the PostgreSQL database using the library Psyocpg2" width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As an additional security layer, environment variables were used to avoid hardcoding credentials in the source code. Accordingly, the python-dotenv library was used to load these environment variables into the application at runtime.&lt;/p&gt;

&lt;p&gt;Once the database connection is established, dedicated loading functions were implemented for each transformed dataset. These functions follow the same workflow. First, a database cursor is created to execute SQL statements. Then, the corresponding table is created if it does not already exist, and the transaction is committed before the insertion phase. This ensures that the table is successfully created and permanently saved, even if an error occurs during data insertion. &lt;/p&gt;

&lt;p&gt;The transformed dataset is then iterated row by row so that each record is inserted into the corresponding table in the local PostgreSQL database. The transaction is committed again to permanently save the inserted data and the cursor is closed to release the associated resources. Figure 5 presents an example of the loading phase implementation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5wec7gjn5vqd23w2vcok.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5wec7gjn5vqd23w2vcok.png" alt="Figure 5 - Example of the implementation of the loading phase of the transformed Appointment data" width="800" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OPTIONAL:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As an introduction to containerization concepts, the Python application was packaged using Docker. A Dockerfile was created to define the instructions required to build the Docker image as illustrated in figure 6.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo9ylcepflh1sjhy8tuly.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo9ylcepflh1sjhy8tuly.png" alt="Figure 6 - Defining the instructions in the Dockerfile for the image creation" width="714" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Only the Python application was containerized, while the PostgreSQL database is kept running locally outside the image. Figure 7 presents the global architecture of this exercise.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fodqdrryvrujt8993oa93.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fodqdrryvrujt8993oa93.png" alt="Figure 7 - the global architecture of the exercise" width="800" height="671"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;RESULT&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The main.py file plays a central role in coordinating the execution of the ETL pipeline. It acts as the entry point of the application by orchestrating the sequential execution of the different stages for each dataset entity.&lt;/p&gt;

&lt;p&gt;The complete implementation of this file is shown in the code block below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;extract.patient&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;extract_patient_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transform.patient&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transform_patient_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;load.patient&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_patient_data&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;extract.doctor&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;extract_doctor_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transform.doctor&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transform_doctor_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;load.doctor&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_doctor_data&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;extract.appointment&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;extract_appointment_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transform.appointment&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transform_appointment_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;load.appointment&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_appointment_data&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;extract.billing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;extract_billing_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transform.billing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transform_billing_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;load.billing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_billing_data&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;extract.medical_proecdure&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;extract_med_procedure_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transform.medical_procedure&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transform_med_procedure_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;load.medical_procedure&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_med_procedure_data&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;database&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_connection&lt;/span&gt;

&lt;span class="n"&gt;patient_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_patient_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;patient_table_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transform_patient_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;patient_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;doctor_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_doctor_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;doctor_table_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transform_doctor_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doctor_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;appointment_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_appointment_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;appointment_table_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transform_appointment_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;appointment_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;billing_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_billing_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;billing_data_table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transform_billing_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;billing_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;procedure_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_med_procedure_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;procedure_data_table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transform_med_procedure_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;procedure_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_connection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Connected to database&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;load_patient_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;patient_table_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;load_doctor_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doctor_table_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;load_appointment_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;appointment_table_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;load_billing_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;billing_data_table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;load_med_procedure_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;procedure_data_table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to connect to database&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the execution of the main.py file, the pipeline successfully extracted data from the CSV files, applied the predefined transformations and loaded the transformed data into the PostgreSQL database. All relational tables were created as shown in Figure 8 and 9.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5nw5lg60g8vb9s0x8ijx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5nw5lg60g8vb9s0x8ijx.png" alt="Figure 8 - Successful creation the tables on PostgreSQL" width="343" height="680"&gt;&lt;/a&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsak1l4wolcc2b53kxn38.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsak1l4wolcc2b53kxn38.png" alt="Figure 9 - Successful insertion of the data into the tables" width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;In conclusion, this study provided a practial experience with the fundamental concepts involved in developing a batch ETL pipeline. It enbales you to gain experience in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structuring Python projects with separation of concerns (modular packages per ETL stage).&lt;/li&gt;
&lt;li&gt;Environment isolation with venv and dependency management with Pip.&lt;/li&gt;
&lt;li&gt;Hands-on pandas transformations.&lt;/li&gt;
&lt;li&gt;Connection Python applications to a local PostgreSQL database, with psycopg2 and safe credential handling using environment variables.&lt;/li&gt;
&lt;li&gt;Basic Docker fundamentals (Dockerfile, building image, running containers).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As of now, the pipeline does not include an orchestration tool for scheduling or automating executions, and the PostgreSQL database is not containerized so the setup isn’t fully portable. These design choices are made to keep this exercise focused on understanding core ETL concepts while adding a basic application of Docker features. Future work will focus on different data sources such as Json, Parquet or perhaps a REST API and eventually explore ELT patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Resources&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.kaggle.com/datasets/anouskaabhisikta/healthcare-management-system" rel="noopener noreferrer"&gt;Kaggle Healthcare Management System dataset&lt;/a&gt; (under the Apache 2.0 license)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/k1ssa1/healthcare-management-system-etl-pipeline-miniproject" rel="noopener noreferrer"&gt;Github repository of this exercise&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dataengineering</category>
      <category>etl</category>
      <category>docker</category>
      <category>python</category>
    </item>
  </channel>
</rss>
