Who this is for
- Analytics Engineers and BI Developers collaborating on SQL models in Git.
- Anyone merging feature branches that touch the same SQL files (dbt models, views, stored procedures).
- Teams formalizing code review, CI, and stable releases for analytics.
Prerequisites
- Basic Git workflow: clone, branch, commit, merge, pull, push.
- Comfort reading and editing SQL (SELECT, JOIN, WHERE, CTEs).
- Familiarity with your project structure (e.g., models directory, macros, seeds).
Why this matters
- Conflicts happen when multiple people change the same SQL model. Resolving them quickly keeps releases moving.
- Incorrect conflict resolutions cause silent logic bugs (double filters, missing columns) that break dashboards.
- A clear process reduces rework and production fire drills.
Concept explained simply
A merge conflict occurs when Git cannot automatically combine two edits to the same lines. In SQL models, this often means different filters, column aliases, or CTE structures.
Mental model: The 3-way comparison
- Base: the original version both branches started from.
- Ours: your branch changes.
- Theirs: the branch you're merging in (or main during rebase).
Your job: choose the correct logic from each side and create a single, consistent SQL result. Think "compose the right query", not just "delete conflict markers".
Workflow to resolve conflicts quickly
-
Open the conflicted file. Look for markers:
<<<<<<< HEAD -- ours ======= -- theirs >>>>>>> branch_name -
Understand intent.
Fast intent checklist
- What business rule changed? (filters, joins, aggregations)
- Which columns/aliases are canonical?
- Any downstream models depending on these names?
-
Compose the right SQL.
- Pick or merge filters (prefer inclusive but correct logic).
- Unify column names and expressions (avoid duplicates).
- Keep consistent style (aliases, indentation, CTE order).
-
Test locally.
- Run the query or build the model.
- Spot-check row counts and a few sample rows.
-
Stage and commit the resolution:
git add path/to/model.sql git commit -m "Resolve conflict in model: unify filters and column aliases" - Push and have a teammate review the logic.
Worked examples
Example 1: Filter and column expression conflict
Conflict:
SELECT
o.order_id,
o.customer_id,
<<<<<<< HEAD
0.0 AS discount,
o.amount AS order_total
=======
COALESCE(o.discount, 0) AS discount,
o.amount AS order_total
>>>>>>> feature/discount
FROM raw.orders o
WHERE
<<<<<<< HEAD
o.status = 'completed'
=======
o.status IN ('completed','shipped')
>>>>>>> feature/discount
Resolution: Keep the safer expression and the broader but correct filter.
SELECT
o.order_id,
o.customer_id,
COALESCE(o.discount, 0) AS discount,
o.amount AS order_total
FROM raw.orders o
WHERE o.status IN ('completed','shipped')
Example 2: Alias and join change conflict
Conflict:
WITH customers AS (
SELECT id, email FROM raw.customers
),
orders AS (
SELECT customer_id, amount FROM raw.orders
)
SELECT
<<<<<<< HEAD
c.id AS customer_id,
o.amount
=======
customers.id AS customer_id,
orders.amount
>>>>>>> feature/alias_cleanup
FROM customers c
JOIN orders o ON o.customer_id = c.id
Resolution: Use short aliases consistently.
WITH customers AS (
SELECT id, email FROM raw.customers
),
orders AS (
SELECT customer_id, amount FROM raw.orders
)
SELECT
c.id AS customer_id,
o.amount
FROM customers c
JOIN orders o ON o.customer_id = c.id
Example 3: CTE restructure conflict
Conflict:
WITH base AS (
SELECT * FROM raw.events
),
<<<<<<< HEAD
filtered AS (
SELECT * FROM base WHERE event_type = 'purchase'
),
agg AS (
SELECT user_id, COUNT(*) AS purchases FROM filtered GROUP BY 1
)
=======
agg AS (
SELECT user_id, COUNT(*) AS purchases FROM base WHERE event_type = 'purchase' GROUP BY 1
)
>>>>>>> feature/ctes
SELECT * FROM agg
Resolution: Prefer fewer CTEs if readability stays high.
WITH base AS (
SELECT * FROM raw.events
),
agg AS (
SELECT user_id, COUNT(*) AS purchases
FROM base
WHERE event_type = 'purchase'
GROUP BY 1
)
SELECT * FROM agg
Common mistakes and how to self-check
- Keeping both conflicting lines (duplicate columns). Self-check: run the query; look for duplicate column names or ambiguous references.
- Losing business logic by accepting one side blindly. Self-check: compare the resolved SQL to both sides; did you keep all intended conditions/columns?
- Inconsistent aliases causing downstream errors. Self-check: ensure a single alias per table within the query.
- Style-only conflicts because of formatting. Self-check: run a formatter before committing to reduce noisy diffs.
- Forgetting to run the model. Self-check: build or run the SQL locally and verify row counts and sample rows.
Practical projects
- Create a demo repo with two branches changing the same SQL model; practice resolving conflicts and writing clear commit messages.
- Introduce a project-wide SQL style guide (aliases, capitalization, indentation) and reformat models to reduce future conflicts.
- Add a lightweight checklist to your PR template: filters, aggregates, aliases, duplicates, downstream impacts.
Exercises
Do these in order. They mirror the interactive exercises below.
Exercise 1: Resolve a filter and expression conflict
Goal: Merge discount expression and broaden filter correctly. After resolving, stage and commit.
- Resolve to COALESCE(o.discount, 0) and status IN ('completed','shipped').
- Keep alias o consistently.
- Commit with a clear message.
Exercise 2: Resolve a rename vs new column conflict
Goal: Keep the standardized column name and preserve the new column.
- Adopt order_total as the canonical name for the amount field.
- Retain tax as a separate column; ensure expressions compile.
- Commit with a message describing both decisions.
Checklist before you commit
- [ ] No conflict markers remain.
- [ ] No duplicate or ambiguous column names.
- [ ] Aliases are consistent throughout the query.
- [ ] Filters reflect intended business logic from both sides.
- [ ] Query runs and returns expected row counts.
Learning path
- Refresh core Git: branching, merging, rebasing, resolving conflicts.
- Adopt a SQL style guide and a formatter to minimize conflicts.
- Practice resolving realistic SQL conflicts (filters, joins, CTEs).
- Add reviews: a second set of eyes on logic after conflict resolution.
- Automate basic checks (build model, lint, simple data assertions) in CI.
Next steps
- Practice on a small sandbox repo until you can resolve typical conflicts in minutes.
- Add a PR checklist. Make it part of your team’s definition of done.
- Document canonical column names to avoid repeated rename conflicts.
Mini challenge
Two branches changed the same model: one adds WHERE channel IN ('web','app'), the other adds WHERE country = 'US'. Compose the final WHERE so both are applied and ensure indexes/partitions still make sense for performance.
Quick Test: what to expect
Short multiple-choice test on conflict markers, merge vs rebase, and safe SQL resolutions. Available to everyone; log in to save your progress.