<?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: Joe</title>
    <description>The latest articles on DEV Community by Joe (@altjoe).</description>
    <link>https://dev.to/altjoe</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%2F4090232%2Fa032ccb8-b2a3-473a-9703-f0fdf3316302.png</url>
      <title>DEV Community: Joe</title>
      <link>https://dev.to/altjoe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/altjoe"/>
    <language>en</language>
    <item>
      <title>Data Modelling, Relationships And Joins In Power BI</title>
      <dc:creator>Joe</dc:creator>
      <pubDate>Sun, 13 Sep 2026 12:49:16 +0000</pubDate>
      <link>https://dev.to/altjoe/data-modelling-relationships-and-joins-in-power-bi-5h6e</link>
      <guid>https://dev.to/altjoe/data-modelling-relationships-and-joins-in-power-bi-5h6e</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I used to think Power BI was mainly about dragging and dropping charts. If the data loaded successfully, I assumed the report would work.&lt;/p&gt;

&lt;p&gt;Then I built a dashboard that took 45 seconds to refresh, and my DAX formulas started returning unexpected results.&lt;/p&gt;

&lt;p&gt;That’s when I realized something important:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem wasn’t the charts. It was the model.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Data modelling in Power BI is about far more than simply “organizing tables.” The data model determines how filters flow through a report, how efficiently the model performs, and whether your calculations return reliable results.&lt;/p&gt;

&lt;p&gt;In this article, I’ll walk through what I learned while fixing my own data models, including schemas, fact and dimension tables, relationships, filter direction, and the often-confusing difference between Power Query joins and Power BI model relationships.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Modelling in Power BI
&lt;/h2&gt;

&lt;p&gt;At its core, data modelling is about deciding how your tables should interact.&lt;/p&gt;

&lt;p&gt;I was working with a retail dataset containing customers, products, and sales. My first instinct was to load everything into one large table.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;It quickly became a mess.&lt;/p&gt;

&lt;p&gt;“Alice” appeared thousands of times. “Nairobi” appeared thousands of times. The file became unnecessarily large, and writing DAX became more difficult because I had to work with repeated customer and location information just to calculate sales.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution
&lt;/h3&gt;

&lt;p&gt;I learned that a well-designed model separates &lt;strong&gt;context&lt;/strong&gt;—who, what, and where—from &lt;strong&gt;events&lt;/strong&gt;—how much or how many.&lt;/p&gt;

&lt;p&gt;That reduces redundancy, simplifies DAX, improves performance, and makes the model easier to scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flat Table
&lt;/h2&gt;

&lt;h3&gt;
  
  
  My First Attempt: The “Excel” Approach
&lt;/h3&gt;

&lt;p&gt;I initially combined everything into one wide table:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Customer ID | Date | Customer | Product | Sales | Category | Location&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer ID | Date       | Customer | Product | Category    | Location | Quantity | Sales
1001        | 01/09/2026 | Alice    | Laptop  | Electronics | Nairobi  | 1        | 80,000
1002        | 01/09/2026 | John     | Phone   | Electronics | Kisumu   | 2        | 60,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Verdict
&lt;/h3&gt;

&lt;p&gt;It was easy to import, but difficult to maintain.&lt;/p&gt;

&lt;p&gt;If Alice moved from Nairobi to Mombasa, for example, I could potentially need to update thousands of rows containing her information.&lt;/p&gt;

&lt;p&gt;A flat table can work well for small and simple datasets, but as the model grows, duplication quickly becomes a problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Star Schema
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Fix
&lt;/h3&gt;

&lt;p&gt;I switched to a &lt;strong&gt;Star Schema&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of keeping everything in one table, I placed a &lt;strong&gt;Fact Table&lt;/strong&gt; containing the measurable events at the centre and surrounded it with &lt;strong&gt;Dimension Tables&lt;/strong&gt; containing the descriptive context.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fact Sales:&lt;/strong&gt; &lt;code&gt;Customer Key&lt;/code&gt;, &lt;code&gt;Product Key&lt;/code&gt;, &lt;code&gt;Sales Amount&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dim Customer:&lt;/strong&gt; &lt;code&gt;Customer Key&lt;/code&gt;, &lt;code&gt;Name&lt;/code&gt;, &lt;code&gt;City&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dim Product:&lt;/strong&gt; &lt;code&gt;Product Key&lt;/code&gt;, &lt;code&gt;Name&lt;/code&gt;, &lt;code&gt;Category&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why It Worked
&lt;/h3&gt;

&lt;p&gt;The structure was easier to understand, easier to maintain, and better suited to analysis in Power BI.&lt;/p&gt;

&lt;p&gt;Instead of repeatedly storing customer and product information alongside every transaction, I could store that descriptive information once and connect it to the sales data through relationships.&lt;/p&gt;

&lt;p&gt;The result was a cleaner model and simpler DAX.&lt;/p&gt;

&lt;h2&gt;
  
  
  Snowflake Schema
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Over-Engineering Trap
&lt;/h3&gt;

&lt;p&gt;At one point, I tried to be “too proper” by further splitting my dimension tables.&lt;/p&gt;

&lt;p&gt;For example, instead of keeping the product category within the product dimension, I created a separate &lt;code&gt;DimCategory&lt;/code&gt; table that connected to &lt;code&gt;DimProduct&lt;/code&gt;, which then connected to &lt;code&gt;FactSales&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The structure worked, but it introduced additional relationships and made the model more complicated.&lt;/p&gt;

&lt;p&gt;For my use case, the Star Schema was simpler and easier to work with.&lt;/p&gt;

&lt;p&gt;The lesson I took away was simple: &lt;strong&gt;don’t introduce additional layers of normalization unless there is a clear reason to do so.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Fact Tables and Dimension Tables
&lt;/h2&gt;

&lt;p&gt;Once I separated my tables, I needed to understand the role each type of table played.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fact Tables
&lt;/h3&gt;

&lt;p&gt;Fact tables contain the &lt;strong&gt;events or transactions&lt;/strong&gt; being analysed.&lt;/p&gt;

&lt;p&gt;They typically contain measurable values such as Sales and Quantity, along with the foreign keys needed to connect those transactions to dimension tables.&lt;/p&gt;

&lt;p&gt;They answer questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How much?&lt;/li&gt;
&lt;li&gt;How many?&lt;/li&gt;
&lt;li&gt;How often?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, &lt;code&gt;FactSales&lt;/code&gt; might contain:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Customer Key | Product Key | Date Key | Quantity | Sales Amount&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Dimension Tables
&lt;/h3&gt;

&lt;p&gt;Dimension tables provide the &lt;strong&gt;descriptive context&lt;/strong&gt; used to analyse the facts.&lt;/p&gt;

&lt;p&gt;They typically contain attributes such as names, categories, locations, and dates.&lt;/p&gt;

&lt;p&gt;They answer questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who?&lt;/li&gt;
&lt;li&gt;What?&lt;/li&gt;
&lt;li&gt;Where?&lt;/li&gt;
&lt;li&gt;When?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, &lt;code&gt;DimCustomer&lt;/code&gt; might contain:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Customer Key | Customer Name | City&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This separation between facts and dimensions is one of the foundations of a good Power BI model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Grain or Granularity of a Fact Table
&lt;/h3&gt;

&lt;p&gt;This was one of the concepts I found most important to understand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grain means defining exactly what one row in a fact table represents.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In my &lt;code&gt;FactSales&lt;/code&gt; table, for example, I might define the grain as:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One row = one product sold in one order.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That definition matters.&lt;/p&gt;

&lt;p&gt;If I start mixing rows representing “one product sold” with rows representing “one entire order,” my calculations can become inconsistent and potentially produce incorrect results.&lt;/p&gt;

&lt;p&gt;The key lesson is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Define the grain clearly and keep it consistent.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Relationships in Power BI
&lt;/h2&gt;

&lt;p&gt;Once I had separated my tables, I needed a way for them to work together.&lt;/p&gt;

&lt;p&gt;That’s where relationships came in.&lt;/p&gt;

&lt;h3&gt;
  
  
  One-to-One
&lt;/h3&gt;

&lt;p&gt;One-to-one relationships are relatively uncommon in typical Power BI models.&lt;/p&gt;

&lt;p&gt;They can be useful in specific situations, such as when a table is intentionally split into two related tables.&lt;/p&gt;

&lt;h3&gt;
  
  
  One-to-Many: The Standard Relationship
&lt;/h3&gt;

&lt;p&gt;This is the relationship pattern I use most often.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dim Customer (1)&lt;/strong&gt; connects to &lt;strong&gt;Fact Sales (*)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;One customer can have many sales transactions.&lt;/li&gt;
&lt;li&gt;The “one” side—the dimension—must contain unique keys.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This relationship allows a selection such as a customer, city, or other dimension attribute to filter the corresponding sales records.&lt;/p&gt;

&lt;h3&gt;
  
  
  Many-to-Many
&lt;/h3&gt;

&lt;p&gt;This is where things can become more complicated.&lt;/p&gt;

&lt;p&gt;I initially tried connecting &lt;code&gt;FactSales&lt;/code&gt; directly to &lt;code&gt;DimCategory&lt;/code&gt;, but the relationship did not behave the way I expected because the underlying data could involve multiple category associations.&lt;/p&gt;

&lt;p&gt;The solution was to introduce a &lt;strong&gt;Bridge Table&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of creating one direct many-to-many relationship, the bridge table allows the model to use two one-to-many relationships.&lt;/p&gt;

&lt;p&gt;This provides a clearer and more controlled relationship structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Referential Integrity
&lt;/h3&gt;

&lt;p&gt;I also learned the importance of referential integrity.&lt;/p&gt;

&lt;p&gt;For example, if &lt;code&gt;FactSales&lt;/code&gt; contains a &lt;code&gt;CustomerID&lt;/code&gt; that does not exist in &lt;code&gt;DimCustomer&lt;/code&gt;, Power BI may return a &lt;code&gt;(Blank)&lt;/code&gt; member when the relationship is used.&lt;/p&gt;

&lt;p&gt;That is a modelling issue, but it is also a data-quality issue.&lt;/p&gt;

&lt;p&gt;A good model therefore depends not only on how tables are structured, but also on the quality and consistency of the underlying keys.&lt;/p&gt;

&lt;h3&gt;
  
  
  Active and Inactive Relationships
&lt;/h3&gt;

&lt;p&gt;Another concept I had to understand was active versus inactive relationships.&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;FactSales&lt;/code&gt; table contained both &lt;code&gt;OrderDate&lt;/code&gt; and &lt;code&gt;ShipDate&lt;/code&gt;, while my model had a &lt;code&gt;DimDate&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;I could not simply make both relationships active at the same time in the way I wanted.&lt;/p&gt;

&lt;p&gt;Instead, I kept &lt;code&gt;OrderDate&lt;/code&gt; as the active relationship and used DAX when I needed to analyse sales based on &lt;code&gt;ShipDate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This allowed the same date dimension to support different date-based analyses without creating unnecessary complexity in the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filter Direction
&lt;/h2&gt;

&lt;p&gt;This was another area that explained why some of my slicers worked as expected while others did not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Single Direction
&lt;/h3&gt;

&lt;p&gt;With a single-direction relationship, filters generally flow from the &lt;strong&gt;Dimension → Fact&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For a standard Star Schema, this is usually the safest and simplest approach.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Dim Customer → Fact Sales&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Selecting a customer filters the relevant sales records.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bidirectional
&lt;/h3&gt;

&lt;p&gt;With bidirectional filtering, filters can flow in both directions between related tables.&lt;/p&gt;

&lt;p&gt;Although this can be useful in specific scenarios, I found that relying on it unnecessarily could introduce ambiguous filter paths and make the model harder to understand and troubleshoot.&lt;/p&gt;

&lt;p&gt;It could also affect performance.&lt;/p&gt;

&lt;h3&gt;
  
  
  My Rule
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use single-direction filtering by default, and introduce bidirectional filtering only when there is a clear modelling requirement for it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Joins in Power Query
&lt;/h2&gt;

&lt;p&gt;Sometimes I needed to change or combine data &lt;strong&gt;before&lt;/strong&gt; it entered the Power BI data model.&lt;/p&gt;

&lt;p&gt;That’s where Power Query Merges became useful.&lt;/p&gt;

&lt;p&gt;A merge allows me to combine queries based on matching columns.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Join Type&lt;/th&gt;
&lt;th&gt;Records Retained&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Left Outer&lt;/td&gt;
&lt;td&gt;All records from the left table plus matching records from the right&lt;/td&gt;
&lt;td&gt;Find all customers, including those without orders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Right Outer&lt;/td&gt;
&lt;td&gt;All records from the right table plus matching records from the left&lt;/td&gt;
&lt;td&gt;Find all orders, including those without matching customers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full Outer&lt;/td&gt;
&lt;td&gt;All records from both tables&lt;/td&gt;
&lt;td&gt;Identify matched and unmatched records from both sides&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inner&lt;/td&gt;
&lt;td&gt;Only matching records&lt;/td&gt;
&lt;td&gt;Find customers who placed orders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Left Anti&lt;/td&gt;
&lt;td&gt;Records in the left table with no match in the right&lt;/td&gt;
&lt;td&gt;Find customers who never placed an order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Right Anti&lt;/td&gt;
&lt;td&gt;Records in the right table with no match in the left&lt;/td&gt;
&lt;td&gt;Find orders without a matching customer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These joins are particularly useful when I need to &lt;strong&gt;transform or enrich the data before it is loaded into the model&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Power Query Joins vs. Power BI Relationships
&lt;/h2&gt;

&lt;p&gt;This was probably one of the most confusing distinctions for me to understand.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;Power Query Merge&lt;/strong&gt; and a &lt;strong&gt;Power BI relationship&lt;/strong&gt; both connect data, but they serve different purposes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Power Query Merge
&lt;/h3&gt;

&lt;p&gt;A merge combines data during the &lt;strong&gt;data preparation stage&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example, I might merge a customer table with another source to bring additional attributes into the customer query.&lt;/p&gt;

&lt;p&gt;The resulting columns become part of the transformed data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Power BI Relationship
&lt;/h3&gt;

&lt;p&gt;A relationship connects tables within the &lt;strong&gt;data model&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of physically combining the tables, the relationship allows Power BI's model engine to understand how the tables are connected and how filters should propagate between them.&lt;/p&gt;

&lt;h3&gt;
  
  
  My Rule
&lt;/h3&gt;

&lt;p&gt;The distinction that helped me was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Merge for enrichment. Relate for analysis.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If I need to bring additional columns into a table as part of data preparation, a Power Query Merge may be appropriate.&lt;/p&gt;

&lt;p&gt;If I simply need separate tables to work together during analysis, a model relationship is usually the better approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Recommended Power BI Model
&lt;/h2&gt;

&lt;p&gt;After working through the different approaches—and making a few mistakes along the way—this is the architecture I generally recommend:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Schema:&lt;/strong&gt; Star Schema&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relationships:&lt;/strong&gt; One-to-Many (&lt;code&gt;1:*&lt;/code&gt;) from Dimensions to Facts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter Direction:&lt;/strong&gt; Single Direction&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Why?
&lt;/h3&gt;

&lt;p&gt;Because the model is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Easier to understand&lt;/li&gt;
&lt;li&gt;Easier to maintain&lt;/li&gt;
&lt;li&gt;More predictable when writing DAX&lt;/li&gt;
&lt;li&gt;Well suited to analytical reporting&lt;/li&gt;
&lt;li&gt;Less likely to develop unnecessary relationship complexity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dim Product  1 → *  Fact Sales
Dim Date     1 → *  Fact Sales
Dim Customer 1 → *  Fact Sales
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this structure, a straightforward measure such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SUM(FactSales[Sales Amount])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can work naturally with filters coming from the related dimension tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Data modelling isn’t just about making a Power BI report work.&lt;/p&gt;

&lt;p&gt;It’s about building a model that remains &lt;strong&gt;accurate, understandable, and scalable&lt;/strong&gt; as the data and reporting requirements grow.&lt;/p&gt;

&lt;p&gt;I started with a flat table because it seemed simple. I then experimented with more complex structures before understanding why the Star Schema is so widely used for analytical models.&lt;/p&gt;

&lt;p&gt;The biggest lesson I took away is this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A good Power BI report starts with a good data model.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When the model is structured properly, relationships become easier to manage, DAX becomes simpler, filters behave more predictably, and the entire report becomes easier to maintain.&lt;/p&gt;

&lt;p&gt;The charts are what users see.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model is what makes them work.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>data</category>
      <category>database</category>
      <category>performance</category>
    </item>
    <item>
      <title>Understanding the Git Workflow: Working Directory, Staging, Commit and Push</title>
      <dc:creator>Joe</dc:creator>
      <pubDate>Sun, 23 Aug 2026 02:58:05 +0000</pubDate>
      <link>https://dev.to/altjoe/understanding-the-git-workflow-working-directory-staging-commit-and-push-5fgo</link>
      <guid>https://dev.to/altjoe/understanding-the-git-workflow-working-directory-staging-commit-and-push-5fgo</guid>
      <description>&lt;p&gt;Working through a practical reference to moving a change through Git took me through the process of taking a single change from my local computer, committing it in Git, and pushing it to GitHub where others would be able to see it. This was written for complete Git Novices and so I approached it without any prior experience of using Git.&lt;/p&gt;

&lt;p&gt;By the end, you will be able to move a change through all four Git stages and confirm it's visible on GitHub with a clear commit history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who This Is For
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Anyone that want to work with Git in the terminal&lt;/li&gt;
&lt;li&gt;Users who find &lt;code&gt;git add&lt;/code&gt;, &lt;code&gt;git commit&lt;/code&gt;, and &lt;code&gt;git push&lt;/code&gt; unclear&lt;/li&gt;
&lt;li&gt;No prior Git experience required&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Git installed (&lt;code&gt;git --version&lt;/code&gt; to check)&lt;/li&gt;
&lt;li&gt;A terminal or command-line application&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://github.com/login" rel="noopener noreferrer"&gt;GitHub account&lt;/a&gt; (create one if needed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll use a small &lt;strong&gt;sales data&lt;/strong&gt; project as a running example. You do &lt;strong&gt;not&lt;/strong&gt; need Python or Pandas installeda as we will be only tracking files, not running code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Four-Stage Flow
&lt;/h2&gt;

&lt;p&gt;Every change travels in one direction:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project Directory → Staging Area → Local Git Repository → Remote  Github Repository&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Project directory&lt;/strong&gt;: where everything is worked on&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staging area&lt;/strong&gt;: choosing what goes in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local Git Repository(Commit)&lt;/strong&gt;: your historical record for everything worked on&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote Github Repository(Push)&lt;/strong&gt;: share it so collaborators can see it&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setup: Initialize a Repository
&lt;/h2&gt;

&lt;p&gt;Create a project folder and make it a Git repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;monthly-sales
&lt;span class="nb"&gt;cd &lt;/span&gt;monthly-sales
git init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initialized empty Git repository in /path/to/monthly-sales/.git/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;git init&lt;/code&gt; creates a hidden &lt;code&gt;.git&lt;/code&gt; folder where Git stores the entire history. From now on, Git watches this folder.&lt;/p&gt;

&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Working Directory (Untracked Files)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create a raw data file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"date,region,revenue"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; sales_data.csv
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"2026-01-01,East,1200"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; sales_data.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check its status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;On branch main
No commits yet
Untracked files:
  (use "git add &amp;lt;file&amp;gt;..." to include in what will be committed)
        sales_data.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;em&gt;Untracked&lt;/em&gt;&lt;/strong&gt; means Git can see the file but isn't following it yet. This is the default for new files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Staging Area (Choose What to Commit)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Stage the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add sales_data.csv
git status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;On branch main
No commits yet
Changes to be committed:
  (use "git rm --cached &amp;lt;file&amp;gt;..." to unstage)
        new file:   sales_data.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The label changes from Untracked files to &lt;strong&gt;&lt;em&gt;Changes to be committed&lt;/em&gt;&lt;/strong&gt;. The file is staged but not yet saved to history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PRO TIP&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you edit two unrelated files (e.g., raw data and a cleaning script), you don't have to commit them together. Stage and commit one, then come back for the other. This keeps your history readable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;git add .&lt;/code&gt; stages everything at once.&lt;br&gt;
Always run &lt;code&gt;git status&lt;/code&gt; first so you know exactly what that includes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;3. Commit (Local Snapshot)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create a commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Add raw January sales data"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;View the history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;commit 6f3a1e2c9d8b4a17f0e3c5d9a2b1e8f7c3d4a5b6
Author: Some Rando &amp;lt;somerando@example.com&amp;gt;
Date:   Sat Aug 22 10:15:03 2026 +0000

    Add raw January sales data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;&lt;em&gt;commit&lt;/em&gt;&lt;/strong&gt; is a permanent snapshot of everything staged, saved with a message, timestamp, and author details. It's local until you push.Commit messages have two readers: your future self and collaborators.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PRO TIP&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Writing Good Commit Messages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the imperative mood ("Add", "Fix", "Update")&lt;/li&gt;
&lt;li&gt;Say specifically what changed&lt;/li&gt;
&lt;li&gt;Explain why when it isn't obvious&lt;/li&gt;
&lt;li&gt;Cover one logical change&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Weak&lt;/th&gt;
&lt;th&gt;Strong&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;update&lt;/td&gt;
&lt;td&gt;Add February revenue row to raw sales data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fix&lt;/td&gt;
&lt;td&gt;Fix currency parsing for negative revenue values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;changes&lt;/td&gt;
&lt;td&gt;Drop rows with missing region before export&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wipe&lt;/td&gt;
&lt;td&gt;Add first draft of sales cleaning script&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For longer commit messages, run &lt;code&gt;git commit&lt;/code&gt; without &lt;code&gt;-m&lt;/code&gt; to open your editor. Use a short summary line, a blank line, then a detailed body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fix currency parsing for negative revenue values

Regions reporting refunds submit revenue as "-120.00", which the
previous parser rejected as invalid. This adds a check for a
leading minus sign before the numeric conversion.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Demonstration: Separate Commits for Separate Changes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Make two changes—one to an existing file and one new file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"2026-01-02,West,950"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; sales_data.csv
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"import pandas as pd"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; clean_sales.py
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"df = pd.read_csv('sales_data.csv')"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; clean_sales.py
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"df.to_csv('clean_sales.csv', index=False)"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; clean_sales.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Changes not staged for commit:
        modified:   sales_data.csv

Untracked files:
        clean_sales.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stage and commit them separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add sales_data.csv
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Add February revenue row to raw sales data"&lt;/span&gt;

git add clean_sales.py
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Add script to clean and export sales data"&lt;/span&gt;

git log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;git log&lt;/code&gt; now shows three commits, each describing one specific change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Push (GitHub)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Connect your local repository to an empty GitHub repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote add origin https://github.com/your-username/monthly-sales.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Push:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Enumerating objects: 9, done.
Writing objects: 100% (9/9), done.
To https://github.com/your-username/monthly-sales.git
 * [new branch]      main -&amp;gt; main
branch 'main' set up to track 'origin/main'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;origin&lt;/code&gt; is the nickname for your GitHub URL.&lt;br&gt;
&lt;code&gt;main&lt;/code&gt; is the branch being pushed.&lt;br&gt;
&lt;code&gt;-u&lt;/code&gt; links your local &lt;code&gt;main&lt;/code&gt; to &lt;code&gt;origin/main&lt;/code&gt;, so future pushes can just be &lt;code&gt;git push&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Verify on GitHub:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sales_data.csv&lt;/code&gt; and &lt;code&gt;clean_sales.py&lt;/code&gt; are present&lt;/li&gt;
&lt;li&gt;The commit history shows three separate, clearly labeled commits&lt;/li&gt;
&lt;li&gt;Locally, &lt;code&gt;git status&lt;/code&gt; reports nothing to commit (working tree clean)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the success signal: a local change, visible on GitHub, with a history that explains itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Troubleshooting&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Cause and Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;git commit&lt;/code&gt; runs, but GitHub shows nothing&lt;/td&gt;
&lt;td&gt;Commits are local only. Run &lt;code&gt;git push&lt;/code&gt; to upload.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nothing to commit, working tree clean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Changes must be staged. Run &lt;code&gt;git add &amp;lt;file&amp;gt;&lt;/code&gt;, then &lt;code&gt;git status&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;git add .&lt;/code&gt; staged files you didn't want&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;git status&lt;/code&gt; first. Use a &lt;code&gt;.gitignore&lt;/code&gt; to exclude secrets, &lt;code&gt;.env&lt;/code&gt;, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets committed by mistake&lt;/td&gt;
&lt;td&gt;Treat them as compromised and rotate. Removing them later doesn't erase history.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fatal: No configured push destination&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No remote is set. Run &lt;code&gt;git remote add origin &amp;lt;url&amp;gt;&lt;/code&gt; before pushing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vague commit messages ("update", "fix stuff")&lt;/td&gt;
&lt;td&gt;Not an error, but unreadable history. Follow the commit message guidelines.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Quick Reference&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Working Directory&lt;/td&gt;
&lt;td&gt;create/edit a file&lt;/td&gt;
&lt;td&gt;Untracked or modified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staging Area&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git add &amp;lt;file&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"Changes to be committed"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local Repository&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git commit -m "msg"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Permanent local snapshot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote Repository&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git push&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Visible on GitHub&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When in doubt, always run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Try It Yourself
&lt;/h3&gt;

&lt;p&gt;In a new, empty folder:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Repeat all four stages with a data file or script of your own.&lt;/li&gt;
&lt;li&gt;Edit two unrelated files in the same session.&lt;/li&gt;
&lt;li&gt;Stage and commit them as two separate commits, each with a specific, imperative-mood message.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;git log&lt;/code&gt; and confirm each commit describes exactly one change clearly enough that a teammate wouldn't need to ask what it means.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>beginners</category>
      <category>git</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
