When I see “Advanced proficiency in SQL, DBT, and Python” on a resume, I am probing for three distinct archetypes in one person: The Data Analyst (SQL), The Software Engineer (Python), and The Analytics Engineer (DBT).
Below is my comprehensive interview playbook. I have organized the questions by Skill, Complexity (Junior vs. Senior), and Behavioral/System-Design crossovers. For each, I provide the Answer Framework and a Model Answer.
Part 1: Advanced SQL (The Foundation)
My goal here is to see if you treat SQL as a scripting language or an engineering language. I care about set-based thinking, not loops.
Q1: (Intermediate) “Explain the exact execution order of a SQL query. Why does this matter for performance?”
The Framework: Don’t just list SELECT first. Start with FROM/JOIN, then WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT. Explain that WHERE filters rows before aggregation, while HAVING filters after aggregation.
The Model Answer:
“The logical execution order is FROM (including JOINs) -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY -> LIMIT/OFFSET. This matters critically for performance because if I put a filter on a non-aggregated column in HAVING instead of WHERE, the database is forced to scan and aggregate millions of rows before discarding them. Conversely, putting a filter in WHERE reduces the dataset before the expensive GROUP BY operation, which is exponentially faster.”
Q2: (Advanced) “We have a 10-billion-row fact table. Write a query to find the top 5 customers by sales in the last 30 days. Show me three different ways to write this and tell me which performs best.”
The Framework: They need to show ROW_NUMBER(), RANK(), and a LIMIT with GROUP BY. The best performer is usually GROUP BY + ORDER BY + LIMIT because it allows the query planner to use a top-N sort algorithm (which doesn’t sort the whole dataset).
The Model Answer:
“Option 1 (Window Function): SELECT customer_id, SUM(sales), ROW_NUMBER() OVER (ORDER BY SUM(sales) DESC) as rn FROM fact WHERE date > CURRENT_DATE - 30 GROUP BY 1 QUALIFY rn <= 5;
Option 2 (Subquery with LIMIT): SELECT customer_id, SUM(sales) FROM fact [...] GROUP BY 1 ORDER BY 2 DESC LIMIT 5;
I would choose Option 2 for a 10-billion-row table. Most modern MPP engines (Snowflake, Redshift, BigQuery) have a ‘Top-N’ optimization. When they see ORDER BY ... LIMIT, they push a LIMIT down into the sort operation, using a heap sort that only keeps 5 rows in memory. Window functions, conversely, have to generate a rank for every single customer before filtering, which is vastly more memory-intensive.”
Q3: (Expert) “Explain the ‘Halloween Problem’ in SQL and how you would avoid it when writing incremental DBT models.”
The Framework: This is a trick question for DBAs. The Halloween Problem is when an UPDATE or MERGE reads a row, updates it, and then reads it again in the same operation, causing infinite loops or double-counting.
The Model Answer:
“The Halloween Problem occurs when a DML operation changes a row’s physical location or attributes in a way that causes it to be revisited during the same scan. In DBT, this usually rears its head during incremental MERGE statements. If my unique_key is a timestamp and I update it, the merge might try to re-insert the same row. I avoid this by: 1) Using a static, immutable unique_key (like a UUID or hash of the natural key). 2) Explicitly materializing the new records into a temporary staging table before the merge, so the source dataset is completely static during the MERGE operation. I never join directly to the target table during the on_conflict clause.”
Part 2: DBT (The Analytics Engineering Core)
I don’t care if you know dbt run. I care if you understand data lineage, incremental strategies, and testing.
Q4: (Intermediate) “Compare and contrast ‘Views’, ‘Tables’, and ‘Incremental’ materializations. Give me a specific business case for each.”
The Framework: Views = light transformation, low cost, high latency. Tables = heavy transformation, high cost, low latency. Incremental = massive datasets where full refreshes are impossible.
The Model Answer:
“- Views: I use these for raw staging models where I am just renaming columns or casting types. Zero storage cost, and they always reflect the source truth. However, they are slow for complex joins.
- Tables: I use these for complex joins or window functions that hit the optimizer hard. I materialize as table for BI tools like Tableau, so queries are sub-second. I schedule a full refresh overnight.
- Incremental: I use these exclusively for event-streaming tables (e.g., 1 billion clicks per day). I run
dbt runhourly. Theis_incremental()macro filters the source to only the last 2 hours (to catch late-arriving data), and I merge into the target. This saves 99.9% of compute cost compared to a table rebuild.”
Q5: (Advanced) “Walk me through your DBT project structure. How do you handle environment-specific configurations (dev vs. prod) and sensitive secrets?”
The Framework: They should mention targets in profiles.yml, vars in dbt_project.yml, and the importance of sources and freshness tests.
The Model Answer:
“I follow the Staging -> Intermediate -> Marts structure.
- Staging: One-to-one with source tables. Just clean names and data types.
- Intermediate: Joins and heavy business logic.
- Marts: Aggregated, exposed to the business.
For environments, I rely ontarget.namein myprofiles.yml. In mydbt_project.yml, I usevarslike{'schema_prefix': 'dev_'}. When I rundbt run --target dev, all models materialize in my personal development schema.
For secrets (like API tokens for external sources), I never hardcode them. I use environment variables (e.g.,{{ env_var('DBT_SNOWFLAKE_PASSWORD') }}) and pass them via the CLI or our CI/CD pipeline (GitHub Actions). I also leveragedbt source freshnesswith awarn_afterof 1 hour to send Slack alerts if our event streams stop.”
Q6: (Expert) “Your DBT incremental model is producing duplicates. You cannot change the source query. How do you diagnose and fix it?”
The Framework: They must differentiate between intra-run duplicates and cross-run duplicates. They should mention the unique_key and merge_exclude_columns.
The Model Answer:
“Duplicates usually come from three areas:
- The
unique_keyis wrong: If I’m merging onuser_idbut the source sends multiple rows per user per day, the merge will only keep one. I would change theunique_keytouser_id || '_' || date_dayand adjust theorder_byin the incremental strategy to ensure the latest record wins. - The
is_incrementalfilter is overlapping: If my filter isWHERE date > (SELECT MAX(date) FROM {{ this }}), and I have records inserted at exactly the same millisecond on the boundary, I need to add a buffer. I useWHERE date > DATEADD(day, -1, (SELECT MAX(date) FROM {{ this }}))to capture overlaps, and then userow_number()within the model to deduplicate before the merge. - Backfills: When we backfill historical data, I temporarily set
+full_refresh: trueto rebuild the entire table, because incremental logic breaks when inserting dates that are older than the current max.”
The Framework: Do not use pd.read_csv() without chunks. Use pandas.read_csv(chunksize=...) or, ideally, Dask or Polars. They must mention lazy evaluation and garbage collection.
The Model Answer:
“Reading a 50GB file into a single DataFrame will cause a MemoryError. I use chunking.
python
import pandas as pd
chunk_iter = pd.read_csv('large_file.csv', chunksize=100000)
for chunk in chunk_iter:
# Process transformation (e.g., filter columns, rename)
processed = chunk[['col1', 'col2']].dropna()
# Append to a partitioned Parquet file using 'mode='a''
processed.to_parquet('output_folder/', engine='pyarrow', partition_cols=['date'], append=True)
del chunk # Explicitly clear memoryHowever, in a production environment, I would advocate for Polars because it uses Apache Arrow and parallelizes across all CPU cores, or I’d convert this to a PySpark job if the cluster supports it. The key is to never store the entire dataset in a Python list or DataFrame.”
Q8: (Advanced) “What is the Global Interpreter Lock (GIL) and how does it affect your data pipelines? How do you bypass it?”
The Framework: The GIL prevents multiple threads from executing Python bytecode simultaneously. For CPU-bound data tasks, threading is useless; you need multiprocessing or vectorized libraries that release the GIL.
The Model Answer:
“The GIL is a mutex that protects access to Python objects, preventing deadlocks but also preventing true parallel execution of threads. In data pipelines, this is deadly because if I spin up 10 threads to process 10 JSON files, only one runs at a time.
To bypass it:
- I/O Bound: I use
asyncioorthreadingfor API calls, because the GIL is released while waiting for network responses. - CPU Bound: I use the
multiprocessingmodule (Pool.map) to spawn separate Python processes. - Best Practice: I avoid pure Python loops entirely. I use
NumpyorPandas, which are written in C and release the GIL. For example,df['col'].apply(lambda x: x**2)is slow and GIL-locked;df['col']**2uses vectorized C operations and is 100x faster.”
Q9: (Expert) “Write a Python decorator that retries a data pipeline function if it fails due to a transient Snowflake error, but with exponential backoff.”
The Framework: This tests their grasp of closures, functools.wraps, and time.sleep. I want to see try/except and a variable wait time.
The Model Answer:
python
import time
from functools import wraps
import logging
def retry_snowflake(max_retries=3, base_delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
retries = 0
while retries < max_retries:
try:
return func(*args, **kwargs)
except Exception as e:
# Check for transient errors (e.g., 000604, 000606)
if "000604" in str(e) or "connection" in str(e).lower():
retries += 1
wait_time = base_delay * (2 ** retries) # Exponential: 2s, 4s, 8s
logging.warning(f"Transient error. Retry {retries} in {wait_time}s")
time.sleep(wait_time)
else:
raise e # Critical error, don't retry
raise Exception(f"Function failed after {max_retries} retries.")
return wrapper
return decoratortruncates.”
The Framework: They must propose a Staging Layer (Python for heavy logic) -> Parsing Layer (SQL/DBT for joins). They must handle the “Schrodinger’s data” problem (the source is empty during the load).
The Model Answer:
“We will use a Medallion Architecture:
- Bronze (Python + S3): A Python script runs via Airflow. It extracts the source via API, processes the heavy business logic using Pandas (handling nested JSONs), and writes the output to S3 as Parquet files, partitioned by
ingestion_date. Crucially, we write to a staging prefix (s3://bucket/staging/) first. - Silver (DBT + External Tables): In DBT, I create a Source that points to the staging prefix. I run an intermediate DBT model that uses a
COPY INTOorCREATE OR REPLACE EXTERNAL TABLEto point to this new data. - The ‘Zero Downtime’ Swap (DBT): I do not truncate the production table. Instead, I materialize the final marts as Tables, but I use DBT’s
contractfeature to ensure the schema matches. I run the model with+full_refresh: truein a separate schema (e.g.,analytics_prod_new). Once the DBT run succeeds, I run a singleALTER TABLE analytics_prod SWAP WITH analytics_prod_new;command. The dashboard querying the old table never sees an empty dataset; the swap is atomic and instantaneous.”
Part 5: The “Gotcha” Questions (For Senior/Staff Level)
These are behavioral, but they reveal technical philosophy.
Q11: “Explain to me when you would deliberately write inefficient SQL, Python, or DBT code.”
The Framework: This tests if they understand Developer Efficiency vs. Runtime Efficiency.
The Model Answer:
“I deliberately write inefficient code during the Discovery Phase. If I am exploring a dataset with complex joins, I will write a messy, nested subquery with a SELECT * just to get the data into a CSV for analysis quickly.
I also deliberately choose ‘inefficient’ SQL in DBT when I use materialized='view'. A view is computationally expensive every time it’s queried, but it saves storage costs and allows me to iterate on the logic rapidly during development. I make it efficient (switch to table) only once the business signs off on the logic.”
Q12: “How do you manage schema changes (columns being added/dropped) in your source data across a DBT pipeline that uses Python for preprocessing?”
The Framework: They must mention Defensive Programming and Schema Contracts.
The Model Answer:
“I treat schema changes as breaking changes unless explicitly handled.
- In Python: I never use
df[column]directly without a guard. I usedf.get(column, default_value)or write a schema validation function usingPydanticorGreat Expectationsat the raw ingestion layer. If a column is missing, the pipeline fails gracefully and alerts rather than passing silent NULLs downstream. - In DBT: I use the
columnsconfiguration in myschema.ymlto enforce a Contract. If the source adds a column I don’t know about, DBT will ignore it. If it drops a column I have defined asnot null, DBT will throw a compilation error before it even runs. This prevents the ‘garbage in, garbage out’ problem. We enforce semantic versioning on our data contracts.”
Advanced proficiency in SQL, dbt, and Python is a core expectation for senior data engineering, analytics engineering, and data platform roles. Below is a structured collection of high-signal interview questions with detailed, interview-ready answers. Focus is on advanced concepts, real-world trade-offs, performance, testing, collaboration, and production patterns.
1. Advanced SQL
Q1: Explain the difference between ROW_NUMBER(), RANK(), and DENSE_RANK(). When would you use each?
Answer: All three are window functions that assign rankings within a partition ordered by a specified column.
- ROW_NUMBER() assigns a unique sequential number starting at 1, even for ties.
- RANK() assigns the same rank to ties and leaves gaps (e.g., 1, 2, 2, 4).
- DENSE_RANK() assigns the same rank to ties with no gaps (e.g., 1, 2, 2, 3).
Use ROW_NUMBER() when you need a deterministic unique identifier (e.g., “latest record per key” via QUALIFY ROW_NUMBER() OVER (…) = 1). Use RANK() when gaps matter for competition-style ranking. Use DENSE_RANK() for continuous ranking without gaps. Always combine with a deterministic ORDER BY (include a unique tie-breaker) to avoid non-deterministic results.
Q2: How do you optimize a slow query that joins multiple large tables and uses aggregations?
Answer:
- Examine the execution plan (EXPLAIN / EXPLAIN ANALYZE).
- Ensure proper indexing (covering indexes on join and filter columns, composite indexes matching predicate order).
- Filter early (push predicates as far down as possible; use CTEs or subqueries carefully—modern optimizers handle them well, but materialization can hurt).
- Prefer hash joins for large equi-joins when memory allows; watch for nested-loop joins on large sets.
- Use approximate algorithms (APPROX_COUNT_DISTINCT, HyperLogLog) when exactness is not required.
- Partition large tables by high-cardinality date or key columns and prune partitions.
- Rewrite correlated subqueries as joins or window functions.
- Consider materialized views or incremental tables (especially with dbt) for repeated heavy aggregations.
- Check statistics and vacuum/analyze status.
- Profile with actual runtime metrics and rewrite the most expensive operators first.
Q3: Explain window functions for running totals, moving averages, and cumulative distributions. Give production examples.
Answer:
- Running total: SUM(amount) OVER (PARTITION BY customer_id ORDER BY order_date ROWS UNBOUNDED PRECEDING).
- Moving average (e.g., 7-day): AVG(metric) OVER (PARTITION BY … ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW).
- Cumulative distribution: CUME_DIST() OVER (ORDER BY score) or PERCENT_RANK().
Production examples: customer lifetime value calculation, sessionization (gap detection with LAG), cohort retention curves, anomaly detection baselines, and financial period-to-date metrics. Always specify the frame (ROWS vs RANGE) explicitly; RANGE can produce unexpected results with ties.
Q4: How do you handle slowly changing dimensions (SCD) Type 2 in pure SQL?
Answer: Maintain valid_from, valid_to, and is_current columns. On change:
- Close the previous version (UPDATE … SET valid_to = CURRENT_TIMESTAMP, is_current = FALSE WHERE key = ? AND is_current).
- Insert the new version with valid_from = CURRENT_TIMESTAMP, valid_to = ‘9999-12-31’, is_current = TRUE.
Use a merge/upsert pattern or staging + window functions to detect changes (LAG on hash of attributes). In warehouses that support it, use temporal tables or built-in SCD helpers. Always keep a surrogate key for the versioned row.
Q5: What is the difference between EXISTS, IN, and JOIN for semi-joins? Performance implications?
Answer:
- EXISTS short-circuits on the first match and is usually the most efficient for existence checks.
- IN can be rewritten by the optimizer to a semi-join; with large lists or NULLs it can behave differently.
- Explicit JOIN can produce duplicates if not carefully written (use DISTINCT or GROUP BY).
Prefer EXISTS for correlated existence checks. Modern optimizers often turn all three into the same plan, but test with real data volumes.
Q6: Explain anti-joins and how you implement them safely.
Answer: Anti-join returns rows from the left table with no match on the right. Preferred patterns:
- LEFT JOIN … WHERE right.key IS NULL
- WHERE NOT EXISTS (SELECT 1 FROM right WHERE …)
Avoid NOT IN with possible NULLs (it returns empty). Always consider NULL-handling and indexes on the join keys.
Q7: How do you write efficient recursive CTEs and when should you avoid them?
Answer: Recursive CTEs are useful for hierarchies, graphs, and sequential generation (bill-of-materials, org charts, date series). Structure: anchor member + recursive member with a termination condition.
Avoid deep recursion on large graphs (risk of stack depth or performance explosion). Prefer iterative approaches, adjacency-list + closure tables, or graph engines for complex graphs. Always set a maximum recursion depth guard.
Q8: Discuss query performance differences between columnar and row-oriented stores, and how that affects SQL writing.
Answer: Columnar stores (Snowflake, BigQuery, Redshift, DuckDB) excel at aggregations and selective column reads; they benefit from projection push-down and late materialization. Row stores favor OLTP and point lookups.
In columnar environments: avoid SELECT *, prefer predicate push-down, use clustering/partitioning keys that match common filters, and leverage automatic clustering or search optimization services. Write queries that minimize data scanned.
2. Advanced dbt
Q1: Explain the dbt project structure and the purpose of each layer (staging, intermediate, marts).
Answer:
- Staging (stg_): 1:1 with source tables, light cleaning, renaming, type casting, basic tests. Sources are declared in sources.yml.
- Intermediate (int_): Business logic that is reusable but not yet final. Often ephemeral or views.
- Marts (fct_, dim_): Business-facing tables (facts and dimensions) consumed by BI tools or downstream systems.
This layered approach improves readability, testing granularity, and incremental rebuild control. Document materializations and ownership in schema.yml.
Q2: How do you implement incremental models correctly? Discuss strategies and pitfalls.
Answer:
SQL
{{ config(materialized='incremental', unique_key='id', incremental_strategy='merge') }}
SELECT ... FROM source
{% if is_incremental() %}
WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
{% endif %}Strategies: append, merge (or delete+insert), insert_overwrite (partition-based).
Pitfalls:
- Late-arriving data requires look-back windows or full-refresh capability.
- Changing unique_key or schema requires careful migration.
- Source data quality issues can silently create duplicates.
- Always test incremental logic with both full-refresh and incremental runs. Use dbt run –full-refresh in CI for critical models.
Q3: Explain dbt testing strategy (generic, singular, custom, unit tests).
Answer:
- Generic tests: unique, not_null, accepted_values, relationships declared in YAML.
- Singular tests: custom SQL files that should return zero rows.
- Custom generic tests: macros that accept arguments.
- Unit tests (dbt 1.8+): isolate model logic with given inputs/expected outputs.
Best practice: apply not_null + unique on primary keys, relationships on foreign keys, and business-rule singular tests. Run tests in CI on every PR; fail the pipeline on critical test failures. Use store_failures for debugging.
Q4: How do you manage environments, sources, and secrets in dbt?
Answer: Use profiles.yml (or environment variables / dbt Cloud credentials) for connection details. Target different schemas or databases per environment (dev, ci, prod).
Sources are declared once and referenced with {{ source(‘src’, ‘table’) }}. Use source freshness tests. Secrets never live in the repo; inject via environment variables or secret managers. Use dbt_project.yml variables and vars for environment-specific configuration.
Q5: Discuss macros, packages, and code reuse patterns.
Answer: Macros encapsulate repeated SQL patterns (surrogate keys, date spine, SCD logic, pivot). Packages (dbt-utils, dbt-expectations, audit-helper, etc.) provide battle-tested macros.
Write macros that are pure where possible, document arguments, and keep side-effects minimal. Prefer composition over deep nesting. Version packages carefully and pin versions in packages.yml.
Q6: How do you handle model documentation, lineage, and collaboration at scale?
Answer: Rich schema.yml descriptions, column descriptions, and meta properties. Generate documentation site with dbt docs generate. Use exposures for downstream BI dashboards.
Lineage is automatic via ref() and source(). For large teams: enforce naming conventions, code owners, PR reviews that include model contracts (dbt contracts), and a clear ownership model (domain-oriented data products).
Q7: Explain snapshots and when to use them vs SCD Type 2 models.
Answer: Snapshots capture point-in-time state of a mutable source using timestamp or check strategy. They are ideal for slowly changing source data you do not control.
Custom SCD Type 2 models give more control (custom valid_from/to logic, additional columns). Prefer snapshots for simplicity when the source is external; build explicit SCD models when you own the transformation logic and need complex business rules.
Q8: How do you debug and optimize dbt runs in production?
Answer:
- Use dbt run –select and state-based selection (state:modified+).
- Analyze compiled SQL and warehouse query history.
- Monitor model runtime and bytes scanned.
- Use incremental models, clustering, and materialization choices appropriately.
- Enable query tagging / comments for warehouse cost attribution.
- Fail fast on tests; use Slim CI (only modified + downstream models).
3. Advanced Python (for Data / Analytics Engineering)
Q1: How do you structure a production-grade Python data pipeline?
Answer:
- Clear package structure with src/, tests, configs.
- Dependency management with poetry or uv + lock files.
- Configuration via environment variables or typed config (Pydantic).
- Logging with structured logs (JSON).
- Idempotent, retryable tasks.
- Orchestration via Airflow, Dagster, Prefect, or dbt Cloud + external schedulers.
- Containerization and CI/CD that runs unit + integration tests.
- Observability (metrics, traces, alerts on data freshness/quality).
Q2: Explain efficient data processing with pandas vs Polars vs PySpark. When do you choose each?
Answer:
- pandas: excellent for small-to-medium data, rich ecosystem, but single-threaded and memory-hungry.
- Polars: lazy evaluation, multi-threaded, much lower memory footprint, excellent for medium data on a single machine.
- PySpark / Dask: distributed processing when data exceeds single-node memory.
Prefer Polars for most modern single-node workloads. Use Spark when you already have a cluster or data volume demands it. Always profile memory and CPU.
Q3: How do you write robust, testable data transformation code?
Answer:
- Pure functions where possible.
- Explicit schemas (Pandera, Pydantic, Great Expectations, or Spark schemas).
- Unit tests with small fixtures; property-based testing for edge cases.
- Integration tests against a real (or Testcontainers) database.
- Type hints + mypy/pyright.
- Avoid hidden global state; inject dependencies.
Q4: Discuss concurrency and parallelism in Python data jobs.
Answer:
- concurrent.futures (ThreadPoolExecutor for I/O, ProcessPoolExecutor for CPU).
- asyncio for high-concurrency I/O (API calls, many small DB queries).
- Avoid the GIL for CPU-bound work by using processes, Polars, or native extensions.
- For Spark, rely on the distributed engine.
- Always handle timeouts, retries (tenacity), and graceful cancellation.
Q5: How do you interact with databases and warehouses from Python efficiently?
Answer:
- Use SQLAlchemy (Core) or native connectors with connection pooling.
- Prefer parameterized queries; never string-format SQL.
- For bulk loads: COPY, write_pandas (Snowflake), to_sql with method=’multi’, or cloud storage staging + external tables.
- Use Arrow-based transfer (pandas → PyArrow → warehouse) where supported.
- Manage transactions carefully; prefer idempotent writes.
Q6: Explain error handling, retries, and observability patterns.
Answer:
- Structured exceptions with context.
- Retry transient failures with exponential backoff + jitter.
- Circuit breakers for external systems.
- Emit metrics (rows processed, latency, data quality scores) and traces.
- Dead-letter queues or quarantine tables for bad records.
- Alert on SLA breaches (freshness, volume anomalies).
Q7: How do you manage Python dependencies and environments in a data team?
Answer:
- Lock files, reproducible builds.
- Separate environments for orchestration vs transformation vs BI.
- Use Docker or virtual environments per project.
- Prefer dbt for SQL-heavy transformations; keep Python for complex logic, ML feature pipelines, or custom extractors.
- Version everything; pin transitive dependencies when necessary for stability.
4. Combined / System Design & Behavioral Questions
Q1: Design a reliable, incremental ELT pipeline from multiple SaaS sources into a warehouse, transformed with dbt, orchestrated with Python.
Answer outline: Extract with Python (or Fivetran/Airbyte) → load raw into landing zone → dbt staging → intermediate business logic → marts. Use incremental models + source freshness. Orchestrate with Dagster/Airflow/Prefect. Add data quality tests, alerting, and a backfill strategy. Document contracts and SLAs.
Q2: How do you ensure data quality across SQL, dbt, and Python layers?
Answer: Tests at every layer: source freshness + schema tests, dbt generic/singular tests, Python schema validation (Pandera), and end-to-end reconciliation queries. Monitor with anomaly detection on volume/distribution. Treat data quality as a product with clear ownership.
Q3: A critical dbt model is taking 4 hours and costing too much. Walk through your debugging process.
Answer:
- Check warehouse query profile / bytes scanned.
- Examine compiled SQL and joins.
- Verify incremental logic is actually incremental.
- Look for data skew, exploding joins, or unnecessary columns.
- Consider clustering, partitioning, or rewriting with approximate methods.
- Split into smaller models or materialize intermediate results.
- Validate with a reduced data set.
Q4: How do you collaborate with analytics engineers, data scientists, and software engineers?
Answer: Clear contracts (dbt model contracts, OpenAPI for services), shared documentation, PR review culture, domain-oriented ownership, and regular alignment on SLAs and data products. Prefer SQL + dbt for transformations that analysts need to understand; use Python when the logic is complex or requires external libraries.
Q5: Describe a time you improved performance or reliability of a data platform (STAR format expected in real interviews).
Prepare a concrete story covering situation, metrics before/after, technical decisions (SQL rewrite, incremental strategy, Python optimization, testing), and business impact.
Preparation Tips for Senior / Managerial Level
- Be ready to whiteboard SQL and dbt model design.
- Discuss trade-offs (cost vs latency vs freshness vs complexity).
- Show ownership of production incidents and post-mortems.
- Demonstrate mentoring: how you raise the SQL/dbt/Python bar for the team.
- Know the latest features (dbt unit tests, contracts, semantic layer; modern Python packaging; warehouse-specific optimizations).
This set covers the majority of advanced technical and system-design questions you will encounter. Practice writing the SQL and dbt code live, and prepare 2–3 detailed war stories that demonstrate depth. Good luck.
For a Senior Technical Manager / Senior Data Engineer / Analytics Engineering interview, “advanced proficiency in SQL, dbt, and Python” usually means much more than syntax. Interviewers will test whether you can design reliable data transformations, optimize workloads, enforce data quality, build reusable frameworks, troubleshoot production failures, and explain trade-offs to engineering/business stakeholders.
Below is a comprehensive interview bank organized from fundamentals through senior/architect-level scenarios.
Advanced SQL + dbt + Python Interview Guide
1. What interviewers are really looking for
At senior level, expect questions around:
| Area | What they expect |
|---|---|
| SQL | Complex queries, window functions, CTEs, optimization, transactions, indexing, dimensional modeling |
| Advanced SQL | Query plans, partitioning, SARGability, concurrency, isolation, recursive CTEs |
| dbt | Models, materializations, incremental models, snapshots, tests, macros, Jinja, packages |
| dbt Architecture | Layering, DAG design, sources, staging/intermediate/marts, CI/CD |
| Data Quality | Generic/singular tests, contracts, freshness, reconciliation |
| Python | Data structures, OOP, generators, decorators, exceptions, typing |
| Python Data Engineering | Pandas, APIs, JSON, files, SQL integration, parallelism |
| Production | Logging, retries, idempotency, monitoring, alerting |
| System Design | End-to-end ELT pipelines and scalable transformation architecture |
| Leadership | Code reviews, standards, mentoring, incident management, technical decisions |
PART I — ADVANCED SQL
2. What is the difference between WHERE and HAVING?
Answer:
WHERE filters rows before aggregation, while HAVING filters groups after aggregation.
SELECT
department_id,
COUNT(*) AS employee_count
FROM employees
WHERE status = 'ACTIVE'
GROUP BY department_id
HAVING COUNT(*) > 10;Execution conceptually:
FROM
↓
WHERE
↓
GROUP BY
↓
HAVING
↓
SELECT
↓
ORDER BYA common mistake is trying to use an aggregate in WHERE:
WHERE COUNT(*) > 10This is invalid because aggregation has not happened yet.
3. Explain INNER JOIN, LEFT JOIN, RIGHT JOIN, and FULL OUTER JOIN.
INNER JOIN
Returns only matching records.
SELECT *
FROM customers c
JOIN orders o
ON c.customer_id = o.customer_id;LEFT JOIN
Returns every record from the left table and matching records from the right.
SELECT *
FROM customers c
LEFT JOIN orders o
ON c.customer_id = o.customer_id;Useful for identifying customers who have never ordered.
SELECT c.customer_id
FROM customers c
LEFT JOIN orders o
ON c.customer_id = o.customer_id
WHERE o.customer_id IS NULL;FULL OUTER JOIN
Returns matched and unmatched records from both sides.
Useful for reconciliation.
4. UNION vs UNION ALL
UNION removes duplicates.
SELECT customer_id FROM customers_2025
UNION
SELECT customer_id FROM customers_2026;UNION ALL preserves duplicates and normally performs better.
SELECT customer_id FROM customers_2025
UNION ALL
SELECT customer_id FROM customers_2026;Senior-level answer:
I prefer
UNION ALLunless deduplication is explicitly required because duplicate elimination introduces additional processing, typically involving sorting or hashing.
5. Explain window functions.
Window functions perform calculations across related rows without collapsing the result set.
Example:
SELECT
employee_id,
department_id,
salary,
AVG(salary) OVER (
PARTITION BY department_id
) AS department_avg_salary
FROM employees;Unlike:
GROUP BY department_idthe window function retains the individual employee rows.
6. Difference between ROW_NUMBER, RANK, and DENSE_RANK
Suppose salaries are:
100000
100000
90000
80000ROW_NUMBER
1
2
3
4RANK
1
1
3
4DENSE_RANK
1
1
2
3Use:
ROW_NUMBER()when you need exactly one deterministic record.
For example, latest customer record:
SELECT *
FROM (
SELECT
t.*,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY updated_at DESC
) AS rn
FROM customer_history t
) x
WHERE rn = 1;7. How would you find the second-highest salary?
One approach:
SELECT MAX(salary)
FROM employees
WHERE salary < (
SELECT MAX(salary)
FROM employees
);Better for more complex ranking:
SELECT salary
FROM (
SELECT
salary,
DENSE_RANK() OVER (
ORDER BY salary DESC
) AS rnk
FROM employees
) x
WHERE rnk = 2;The second solution handles duplicate salaries correctly.
8. Find duplicate records.
SELECT
customer_id,
COUNT(*) AS cnt
FROM customers
GROUP BY customer_id
HAVING COUNT(*) > 1;To retrieve the actual duplicate rows:
SELECT *
FROM (
SELECT
c.*,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY created_at
) AS rn
FROM customers c
) x
WHERE rn > 1;9. How do you remove duplicates while retaining the latest record?
DELETE FROM customer_table
WHERE customer_id IN (
SELECT customer_id
FROM (
SELECT
customer_id,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY updated_at DESC
) AS rn
FROM customer_table
) x
WHERE rn > 1
);In production, I would first validate the candidate rows with a SELECT, run the operation inside an appropriate transaction where supported, and ensure backups/recovery procedures exist.
10. Explain CTEs.
CTE = Common Table Expression.
WITH active_customers AS (
SELECT *
FROM customers
WHERE status = 'ACTIVE'
)
SELECT *
FROM active_customers;Advantages:
- readability
- modular SQL
- easier debugging
- recursive queries
- reusable logical steps
But a CTE is not automatically a performance optimization. Depending on the database engine, it may be inlined, materialized, or optimized differently.
11. CTE vs temporary table
CTE
Best for:
- one query
- readability
- logical decomposition
Temporary table
Useful when:
- intermediate results are reused
- you need indexes/statistics
- processing is large
- you need multiple statements against the intermediate dataset
Senior answer:
I choose based on optimizer behavior, data volume, reuse requirements, and whether indexing or materialization provides a measurable benefit.
12. What is a correlated subquery?
A correlated subquery references a column from the outer query.
SELECT
e.employee_id,
e.salary
FROM employees e
WHERE e.salary > (
SELECT AVG(e2.salary)
FROM employees e2
WHERE e2.department_id = e.department_id
);This finds employees earning above their department average.
For large datasets, I would often rewrite this using a window function:
SELECT *
FROM (
SELECT
e.*,
AVG(salary) OVER (
PARTITION BY department_id
) AS avg_salary
FROM employees e
) x
WHERE salary > avg_salary;13. EXISTS vs IN
Example:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
);EXISTS is often useful when you only care whether a matching row exists.
However, don’t claim that EXISTS is always faster than IN. Modern optimizers may transform both into similar execution strategies.
Senior answer:
I inspect the execution plan and data characteristics rather than relying on blanket rules.
14. What is SARGability?
SARG = Search ARGument.
A predicate is SARGable when the database can efficiently use an index/access path.
Bad:
WHERE YEAR(order_date) = 2026Potentially better:
WHERE order_date >= '2026-01-01'
AND order_date < '2027-01-01'Why?
The first applies a function to the column. The second allows the optimizer to search a range directly.
15. How do you optimize a slow SQL query?
I use a systematic process:
Step 1 — Establish baseline
Measure:
- execution time
- CPU
- logical reads
- physical reads
- rows processed
- concurrency
Step 2 — Inspect execution plan
Look for:
- full table scans
- expensive joins
- incorrect cardinality estimates
- large sorts
- spills
- repeated scans
- poor join strategies
Step 3 — Check predicates
Look for:
- non-SARGable expressions
- unnecessary functions
- implicit conversions
- excessive
OR - unnecessary columns
Step 4 — Check indexes/partitioning
Evaluate:
- join keys
- filter columns
- composite indexes
- clustering/partitioning strategy
Step 5 — Reduce data early
Instead of:
SELECT *select only required columns and filter as early as appropriate.
Step 6 — Re-test
Never assume the optimization worked.
16. What is an execution plan?
An execution plan describes how the database intends to execute a query.
It may show:
- scans
- seeks
- joins
- sorts
- aggregations
- filters
- estimated rows
- actual rows
- memory grants
- parallelism
A senior engineer should compare estimated vs actual cardinality because major differences can indicate statistics or data-distribution problems.
17. Explain indexes.
An index provides an additional access structure that can make data retrieval faster.
Common types/concepts:
- B-tree
- hash
- clustered
- nonclustered
- composite
- covering
- unique
- filtered/partial indexes depending on database
But indexes have costs:
- storage
- INSERT overhead
- UPDATE overhead
- DELETE overhead
- maintenance
Therefore:
More indexes do not necessarily mean better performance.
18. What is a composite index?
Example:
CREATE INDEX idx_customer_status_date
ON orders(customer_id, status, order_date);Column order matters.
For queries frequently filtering by:
customer_idand:
statusthis may be useful.
But index design depends heavily on the database engine, query patterns, selectivity, and workload.
19. Explain normalization vs denormalization.
Normalization
Reduces redundancy.
Typical forms:
- 1NF
- 2NF
- 3NF
- BCNF
Good for:
- OLTP
- data integrity
- transactional workloads
Denormalization
Introduces redundancy for performance or analytical usability.
Good for:
- analytics
- reporting
- dimensional models
- read-heavy systems
Senior answer:
I don’t treat normalization and denormalization as ideological choices. I select the model based on workload, integrity requirements, query patterns, latency, and storage/compute economics.
20. Explain ACID.
Atomicity
Transaction succeeds completely or fails completely.
Consistency
Database moves from one valid state to another.
Isolation
Concurrent transactions should not improperly interfere.
Durability
Committed data survives failures according to the system’s durability guarantees.
21. Explain transaction isolation levels.
Common levels:
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLESome databases also support:
SNAPSHOT
READ COMMITTED SNAPSHOTHigher isolation generally provides stronger consistency but can increase locking/contention or resource usage depending on implementation.
Potential anomalies include:
- dirty reads
- non-repeatable reads
- phantom reads
22. What is a deadlock?
A deadlock occurs when transactions wait on each other indefinitely.
Example:
Transaction A locks Row 1
Transaction B locks Row 2
A waits for Row 2
B waits for Row 1Mitigation:
- consistent lock ordering
- shorter transactions
- appropriate indexes
- smaller batches
- appropriate isolation
- retry logic where appropriate
23. What is a recursive CTE?
Example organizational hierarchy:
WITH employee_hierarchy AS (
SELECT
employee_id,
manager_id,
employee_name,
0 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT
e.employee_id,
e.manager_id,
e.employee_name,
h.level + 1
FROM employees e
JOIN employee_hierarchy h
ON e.manager_id = h.employee_id
)
SELECT *
FROM employee_hierarchy;Useful for:
- organizational trees
- category hierarchies
- bill of materials
- graph-like relationships
24. How do you calculate a running total?
SELECT
customer_id,
order_date,
amount,
SUM(amount) OVER (
PARTITION BY customer_id
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW
) AS running_total
FROM orders;Notice the explicit ROWS frame. Understanding window frames is important for senior-level interviews.
25. Find gaps in dates.
A common technique uses LAG():
SELECT
customer_id,
order_date,
LAG(order_date) OVER (
PARTITION BY customer_id
ORDER BY order_date
) AS previous_date
FROM orders;Then calculate the difference between dates and identify gaps exceeding the expected interval.
26. What is a Slowly Changing Dimension?
SCD manages historical changes in dimensional attributes.
Type 1
Overwrite.
Old address → New addressNo history.
Type 2
Maintain history:
Customer | Address | Start Date | End Date | CurrentExample:
101 | NY | 2024-01-01 | 2025-05-10 | N
101 | CA | 2025-05-10 | NULL | YType 3
Store limited previous values in additional columns.
27. How would you implement SCD Type 2 in SQL?
Typical approach:
- Detect changed records.
- Expire current records.
- Insert new versions.
- Maintain effective dates.
- Maintain current indicator.
Conceptually:
UPDATE dim_customer
SET
end_date = CURRENT_DATE,
is_current = 0
WHERE customer_id = :customer_id
AND is_current = 1;Then:
INSERT INTO dim_customer (
customer_id,
address,
start_date,
end_date,
is_current
)
VALUES (
:customer_id,
:new_address,
CURRENT_DATE,
NULL,
1
);In production, the implementation should account for concurrency, duplicate events, late-arriving data, and idempotency.
PART II — DBT
28. What is dbt?
dbt is a transformation framework primarily used to transform data inside the data warehouse/lakehouse using SQL and Jinja.
Typical architecture:
Sources
↓
Staging
↓
Intermediate
↓
Marts
↓
BI / Analytics / MLdbt focuses primarily on the T in ELT.
29. Explain dbt’s DAG.
DAG = Directed Acyclic Graph.
Example:
raw_customers
↓
stg_customers
↓
int_customer_orders
↓
dim_customers
↓
customer_metrics
↓
BI Dashboarddbt builds dependencies through references such as:
{{ ref('stg_customers') }}This enables:
- dependency management
- execution ordering
- lineage
- documentation
- selective execution
30. What is ref()?
Example:
SELECT *
FROM {{ ref('stg_orders') }}ref():
- creates dependency metadata
- resolves the physical relation
- enables environment-aware deployment
- contributes to DAG lineage
Instead of hardcoding:
FROM analytics.stg_ordersyou use:
FROM {{ ref('stg_orders') }}31. What is source()?
Example:
SELECT *
FROM {{ source('sales', 'orders') }}Sources represent raw/external data entering the dbt project.
They provide:
- lineage
- documentation
- freshness checks
- centralized source definitions
32. What are dbt materializations?
Common materializations:
View
materialized: viewTable
materialized: tableIncremental
materialized: incrementalEphemeral
materialized: ephemeralDynamic/custom approaches
Depending on platform and dbt version, additional materialization patterns may exist.
33. View vs table vs incremental
View
Good for:
- lightweight transformations
- frequently changing logic
- relatively small datasets
Table
Good for:
- expensive transformations
- frequently queried datasets
- stable models
Incremental
Good for:
- very large datasets
- append/change-based processing
- reducing repeated computation
Senior answer:
I select materialization based on data volume, transformation cost, query frequency, freshness requirements, warehouse cost, and update semantics.
34. Explain an incremental dbt model.
Example:
{{ config(
materialized='incremental',
unique_key='order_id'
) }}
SELECT
order_id,
customer_id,
amount,
updated_at
FROM {{ source('sales', 'orders') }}
{% if is_incremental() %}
WHERE updated_at >
(
SELECT COALESCE(MAX(updated_at), '1900-01-01')
FROM {{ this }}
)
{% endif %}The key consideration is how you identify changed records.
Possible strategies:
- timestamp watermark
- CDC
- ingestion sequence
- batch ID
- hash comparison
35. What is is_incremental()?
It allows dbt SQL to behave differently when the model is being incrementally built.
{% if is_incremental() %}
WHERE updated_at > (
SELECT MAX(updated_at)
FROM {{ this }}
)
{% endif %}When doing a full refresh, the incremental condition isn’t applied.
36. What is unique_key in an incremental model?
It tells dbt what identifies a logical record.
Example:
unique_key: order_idDepending on the adapter and strategy, this can support update/merge behavior rather than simply appending records.
A strong candidate should mention that exact behavior depends on the warehouse/adapter and incremental strategy.
37. What is an incremental strategy?
Common strategies include concepts such as:
- append
- merge
- delete+insert
- insert overwrite
The appropriate strategy depends on:
- source change patterns
- warehouse
- partitioning
- update frequency
- late-arriving data
- delete requirements
38. How would you handle late-arriving data in dbt?
Suppose:
Today's data arrives
but yesterday's records can still change.A naïve:
WHERE updated_at > MAX(updated_at)may miss certain corrections depending on the watermark logic.
A common approach is a lookback window:
WHERE updated_at >=
DATEADD(day, -2, CURRENT_DATE)Then use a MERGE/appropriate incremental strategy to overwrite affected records.
For high-integrity pipelines, I would also consider CDC or a reliable source-side change sequence.
39. How do you handle deletes in dbt?
Options include:
CDC
Consume delete events.
Soft delete
Maintain:
is_deleted = trueMerge logic
Synchronize target state with source state.
Snapshot
Track historical changes where appropriate.
The correct choice depends on whether the business needs:
- current state
- historical state
- deletion history
- auditability
40. What are dbt snapshots?
Snapshots capture changes to records over time.
They are commonly used for SCD Type 2-style historical tracking.
Conceptually:
Customer
↓
Address changes
↓
Snapshot
↓
Historical versionsA snapshot can preserve previous versions instead of simply overwriting them.
41. Snapshot vs incremental model
Incremental
Optimizes processing.
Snapshot
Captures historical changes.
They solve different problems.
An incremental model asks:
How can I process only the data that needs processing?
A snapshot asks:
How can I preserve historical versions of records as they change?
42. What are dbt tests?
Tests validate assumptions about data.
Common built-in generic tests include:
not_null
unique
accepted_values
relationshipsExample:
columns:
- name: customer_id
tests:
- not_null
- unique43. What is a singular test?
A singular test is a custom SQL query designed to return records that violate a business rule.
Example:
SELECT *
FROM {{ ref('orders') }}
WHERE amount < 0;If this returns rows, the test fails.
44. Generic test vs singular test
Generic
Reusable.
Example:
- uniqueSingular
Specific business rule.
Example:
Order amount cannot be negative.Senior engineers often create reusable generic tests for common patterns and singular tests for domain-specific rules.
45. How would you design dbt project structure?
A common structure:
models/
│
├── staging/
│ ├── customers/
│ ├── orders/
│
├── intermediate/
│ ├── customer_orders/
│
└── marts/
├── finance/
├── sales/
└── customer/Principle:
Raw
↓
Staging
↓
Intermediate
↓
Business MartsAvoid putting massive business logic directly into staging models.
46. What should staging models contain?
Typically:
- source renaming
- type casting
- basic cleansing
- standardization
- simple derived fields
Avoid excessive business logic.
Example:
SELECT
CAST(order_id AS BIGINT) AS order_id,
CAST(customer_id AS BIGINT) AS customer_id,
CAST(order_date AS DATE) AS order_date,
amount
FROM {{ source('sales', 'orders') }}47. What belongs in marts?
Marts should represent business concepts.
Examples:
dim_customer
dim_product
fct_orders
fct_claims
customer_360
monthly_revenueThey should be understandable to downstream consumers.
48. What are dbt macros?
Macros are reusable Jinja code.
Example:
{% macro cents_to_dollars(column_name) %}
{{ column_name }} / 100.0
{% endmacro %}Then:
SELECT
{{ cents_to_dollars('amount_cents') }} AS amount
FROM ordersMacros reduce repetitive SQL.
49. When should you NOT use a macro?
Don’t create macros simply because you can.
Avoid macros when:
- logic is only used once
- abstraction makes SQL harder to understand
- business logic becomes hidden
- debugging becomes difficult
Senior principle:
Abstract repeated technical patterns, but keep business logic explicit where readability matters.
50. Explain Jinja in dbt.
Jinja allows dynamic SQL generation.
Example:
{% if target.name == 'prod' %}
SELECT *
FROM production.orders
{% else %}
SELECT *
FROM development.orders
{% endif %}Common constructs:
{% if %}
{% for %}
{% set %}
{{ variable }}
{{ ref() }}
{{ source() }}51. What is dbt run?
Runs models.
Example:
dbt runYou can selectively execute:
dbt run --select model_nameOr dependencies:
dbt run --select +model_nameOr downstream:
dbt run --select model_name+52. What is dbt build?
dbt build is broader than dbt run.
It can execute relevant resources such as:
- models
- tests
- snapshots
- seeds
and respects the DAG/dependencies.
This makes it particularly useful in CI/CD pipelines where you want transformation and validation together.
53. dbt run vs dbt build
Senior answer:
dbt runfocuses on executing models, whiledbt buildorchestrates multiple dbt resource types according to their dependencies, including tests. For CI/CD validation,dbt buildis often more appropriate because it can validate the generated data as part of the workflow.
54. How do you optimize a slow dbt project?
I would investigate:
1. Model materialization
Could an expensive view become a table?
2. Incremental processing
Can a huge table be processed incrementally?
3. DAG design
Are models unnecessarily rebuilding?
4. SQL
Inspect warehouse query plans.
5. Warehouse configuration
Evaluate:
- compute size
- concurrency
- clustering/partitioning
- caching
6. Dependency design
Avoid unnecessary fan-out.
7. Incremental predicates
Process only relevant partitions/records.
55. How do you prevent dbt DAG complexity?
I establish conventions:
sources
↓
staging
↓
intermediate
↓
martsThen enforce:
- naming standards
- ownership
- model documentation
- code review
- dependency review
- test coverage
- domain boundaries
I also avoid circular dependencies and excessive model chaining that provides little business or technical value.
56. How would you implement CI/CD for dbt?
Typical workflow:
Developer
↓
Git branch
↓
Pull Request
↓
Lint
↓
Compile
↓
dbt build
↓
Tests
↓
Review
↓
Merge
↓
Production deploymentProduction should include:
- environment-specific credentials
- secrets management
- automated testing
- deployment approvals where required
- artifact/log retention
- rollback strategy
PART III — ADVANCED PYTHON
57. What Python data structures do you use most?
List
Ordered, mutable collection.
items = [1, 2, 3]Tuple
Ordered, immutable.
items = (1, 2, 3)Set
Unique elements.
items = {1, 2, 3}Dictionary
Key-value mapping.
data = {
"customer_id": 101
}For data engineering, dictionaries and sets are particularly useful for:
- lookups
- deduplication
- joins in memory
- configuration
- indexing
58. List vs tuple
List:
[1, 2, 3]Tuple:
(1, 2, 3)Lists are mutable.
Tuples are immutable.
Tuples can also be used as dictionary keys when their contents are hashable.
59. What is a dictionary comprehension?
squares = {
x: x * x
for x in range(10)
}Equivalent conceptually to:
squares = {}
for x in range(10):
squares[x] = x * xUse comprehensions when they improve readability; avoid excessively complicated one-liners.
60. What is a generator?
A generator produces values lazily.
def read_numbers():
for i in range(1000000):
yield iInstead of loading one million values into memory, values are generated as needed.
Useful for:
- large files
- streaming data
- ETL
- memory-efficient processing
61. List vs generator
List:
data = [process(x) for x in records]Generator:
data = (process(x) for x in records)The generator is lazy.
For very large datasets, this can substantially reduce memory consumption.
62. What is a decorator?
A decorator modifies or wraps function behavior.
def logger(func):
def wrapper(*args, **kwargs):
print("Calling function")
result = func(*args, **kwargs)
print("Completed")
return result
return wrapperUsage:
@logger
def process_data():
passCommon uses:
- logging
- authorization
- retry
- caching
- timing
- instrumentation
63. What is *args and **kwargs?
def function(*args, **kwargs):
print(args)
print(kwargs)*args captures positional arguments.
**kwargs captures keyword arguments.
Useful for wrappers/decorators and flexible APIs.
64. Explain Python exception handling.
try:
result = process()
except ValueError as e:
logger.error("Invalid value: %s", e)
except Exception as e:
logger.exception("Unexpected failure")
finally:
cleanup()Best practices:
- catch specific exceptions
- don’t silently swallow errors
- preserve context
- log appropriately
- use retries only for retryable failures
Avoid:
except:
pass65. What is a context manager?
Example:
with open("data.txt") as f:
data = f.read()The context manager ensures cleanup.
Useful for:
- files
- database connections
- locks
- transactions
- resource management
66. What is the GIL?
The Global Interpreter Lock in CPython allows only one thread at a time to execute Python bytecode within a process.
This means:
- threading is useful for many I/O-bound workloads
- CPU-bound Python workloads may not scale through threads in the same way
For CPU-heavy work, options include:
- multiprocessing
- native extensions
- distributed processing
- vectorized libraries
The exact behavior depends on Python implementation/version and workload.
67. Threading vs multiprocessing vs asyncio
Threading
Good for:
I/O-bound tasksExample:
API calls
database calls
file operationsMultiprocessing
Useful for:
CPU-bound workloadsAsyncio
Good for large numbers of asynchronous I/O operations.
Example:
1000 API requests68. How would you process a 100 GB CSV using Python?
I would not simply do:
pandas.read_csv("100gb.csv")because memory requirements could be excessive.
Options:
Chunking
for chunk in pd.read_csv(
"100gb.csv",
chunksize=100000
):
process(chunk)Other options:
- PyArrow
- Polars
- Spark
- distributed processing
- database bulk loading
Senior answer:
First I would determine whether Python is actually the appropriate processing engine. For a 100 GB transformation workload, I would generally prefer pushing computation into a warehouse/lakehouse or using distributed processing rather than forcing the entire workload through a single Python process.
69. How do you optimize Python performance?
I follow:
Measure
↓
Profile
↓
Identify bottleneck
↓
Optimize
↓
BenchmarkTechniques:
- avoid unnecessary loops
- use appropriate data structures
- vectorize operations
- generators
- batching
- caching
- parallelism
- efficient I/O
- database-side processing
Do not optimize blindly.
70. Why are sets useful?
Set membership:
if customer_id in customer_ids:
...Average-case membership lookup is typically O(1).
Compared with list membership:
if customer_id in customer_list:which is typically O(n).
This can matter significantly for large in-memory lookup operations.
71. What is the difference between shallow copy and deep copy?
import copy
a = [[1, 2], [3, 4]]
b = copy.copy(a)
c = copy.deepcopy(a)Shallow copy copies the outer structure but nested objects may remain shared.
Deep copy recursively copies nested objects.
72. Explain Python memory management.
CPython primarily uses:
- reference counting
- cyclic garbage collection
Objects are allocated on the heap.
When reference counts reach zero, objects can generally be reclaimed immediately; cyclic references require garbage collection mechanisms.
Senior candidates should understand that memory leaks can still occur through:
- retained references
- caches
- global structures
- unbounded collections
- external resources
PART IV — PYTHON + SQL
73. How do you connect Python to a database?
Depending on the platform:
SQLAlchemy
psycopg
pyodbc
database-specific connectorsExample conceptually:
from sqlalchemy import create_engine
engine = create_engine(connection_string)
df = pd.read_sql(
"SELECT * FROM customers",
engine
)In production:
- credentials should come from a secrets manager
- connection pooling should be considered
- parameterized queries should be used
- transactions should be managed explicitly
74. How do you prevent SQL injection in Python?
Never concatenate user input:
query = f"""
SELECT *
FROM users
WHERE id = {user_id}
"""Instead use parameterized queries:
cursor.execute(
"SELECT * FROM users WHERE id = ?",
(user_id,)
)Exact placeholder syntax depends on the database driver.
75. How would you load a Pandas DataFrame into a database?
Possible approaches:
df.to_sql(...)or database-native bulk loading.
For large datasets, I would generally prefer:
DataFrame
↓
Object storage / staged file
↓
Bulk-load mechanism
↓
Databaserather than millions of individual INSERT statements.
76. How would you process a large API response?
I would consider:
- pagination
- streaming
- batching
- retries
- exponential backoff
- rate limiting
- checkpointing
- schema validation
- incremental writes
Architecture:
API
↓
Pagination
↓
Batch
↓
Validate
↓
Transform
↓
Persist
↓
Checkpoint77. How do you implement retry logic?
Conceptually:
for attempt in range(max_retries):
try:
response = call_api()
break
except RetryableError:
sleep(backoff(attempt))A production implementation should distinguish:
Retryable
- timeout
- transient network error
- HTTP 429
- selected 5xx errors
Non-retryable
- authentication failure
- malformed request
- validation failure
Use exponential backoff and jitter where appropriate.
PART V — SENIOR SCENARIO QUESTIONS
78. A dbt pipeline suddenly takes 3 hours instead of 20 minutes. What do you do?
I would not immediately increase warehouse size.
First:
1. Identify what changed
Code?
Data volume?
Warehouse?
Source schema?
Execution concurrency?2. Inspect dbt artifacts/logs
Identify the slowest models.
3. Inspect query plans
Look for:
- full scans
- changed join strategy
- skew
- spills
- poor statistics
- increased cardinality
4. Check incremental behavior
A model may accidentally be doing a full refresh.
5. Check upstream data growth
Maybe data increased from:
100M → 1B rows6. Fix root cause
Then benchmark again.
79. Your incremental dbt model is producing duplicates. How do you troubleshoot?
Check:
- Is
unique_keyactually unique? - Is the incremental filter correct?
- Are late-arriving records being reprocessed?
- Are source records duplicated?
- Is the incremental strategy appropriate?
- Are multiple source records matching the same target?
- Is there concurrent execution?
- Did someone perform a partial/full refresh?
Then create a test:
SELECT
order_id,
COUNT(*)
FROM {{ ref('fct_orders') }}
GROUP BY order_id
HAVING COUNT(*) > 1;80. A business user says yesterday’s revenue changed. How do you investigate?
I would establish lineage:
Dashboard
↓
Semantic model
↓
dbt mart
↓
Intermediate model
↓
Staging
↓
SourceThen reconcile counts and amounts at each layer.
Check:
- source corrections
- late-arriving records
- duplicate events
- timezone issues
- joins
- currency conversions
- incremental processing
- snapshot logic
This is much stronger than simply rerunning the pipeline.
81. How would you design a production data pipeline using SQL, dbt, and Python?
Example:
External APIs / Applications
↓
Python
↓
Raw Object Storage
↓
Warehouse
↓
dbt
↓
Staging Models
↓
Intermediate Models
↓
Data Marts
↓
BI / Analytics / MLPython handles:
- ingestion
- API interaction
- file processing
- orchestration integrations
SQL/dbt handles:
- transformation
- modeling
- business logic
- data quality
Warehouse handles:
- scalable computation
- storage
- aggregation
82. Where should business logic live: Python or SQL?
My default preference for analytical transformation is:
SQL/dbt → warehouse transformation
Python → orchestration, ingestion, specialized processingIf a transformation can efficiently run in SQL inside the warehouse, I generally avoid extracting large datasets into Python unnecessarily.
Python becomes appropriate when:
- external API interaction is required
- specialized algorithms are needed
- complex non-SQL processing is required
- orchestration/integration is involved
83. How would you ensure idempotency?
Idempotency means running the same operation multiple times produces the same final state.
Techniques:
- deterministic business keys
MERGE- upserts
- checkpoints
- batch IDs
- deduplication
- source event IDs
- transaction boundaries
Example:
Event ID = 12345
First execution:
INSERT
Second execution:
Recognize 12345 already processed
→ no duplicateThis is a critical production-data-engineering concept.
84. How would you design data quality?
I would implement multiple layers:
Schema validation
Expected columns
Expected data typesdbt tests
not_null
unique
relationships
accepted_valuesBusiness rules
Revenue >= 0
Order date <= current dateReconciliation
Source count ≈ Target count
Source amount ≈ Target amountFreshness
Latest source timestampOperational monitoring
Pipeline duration
Failure rate
Row counts
Anomalies85. How do you handle schema changes?
For example:
Source adds customer_segmentI would determine:
- Is it backward compatible?
- Does downstream logic depend on the schema?
- Do contracts/tests need updating?
- Does the dbt model need modification?
- Is historical backfill required?
For breaking changes, I prefer controlled versioning rather than allowing production pipelines to fail unexpectedly.
86. How do you handle a production dbt failure?
My incident approach:
Detect
↓
Assess impact
↓
Identify failing model
↓
Determine root cause
↓
Mitigate
↓
Recover
↓
Validate
↓
Communicate
↓
Prevent recurrenceAfterward:
- root cause analysis
- permanent fix
- regression test
- monitoring improvement
- documentation
PART VI — ARCHITECT / SENIOR MANAGER QUESTIONS
87. How do you establish SQL coding standards across a team?
I establish standards for:
- naming
- indentation
- CTE conventions
- aliases
- capitalization
- joins
- null handling
- date handling
- performance
- documentation
Then automate where possible through:
Linting
CI/CD
Code review
Pre-commit hooks
Automated testsThe goal is to make the correct approach the easiest approach.
88. How do you review a junior engineer’s SQL?
I evaluate:
Correctness
Does it produce the right result?
Performance
Does it scale?
Maintainability
Can another engineer understand it?
Data quality
Are edge cases handled?
Security
Could it expose sensitive data?
Testability
Can assumptions be validated?
I avoid rewriting everything myself. I explain the reasoning and coach the engineer.
89. How do you decide between dbt and stored procedures?
I generally prefer dbt for:
- modular transformations
- version-controlled SQL
- DAG management
- testing
- documentation
- lineage
- CI/CD
Stored procedures may still be appropriate for:
- procedural database operations
- complex transactional workflows
- database-native administrative operations
- legacy integrations
The decision should be based on architecture and workload rather than ideology.
90. How would you migrate legacy SQL pipelines to dbt?
I would use:
Inventory
↓
Dependency mapping
↓
Business logic identification
↓
Model classification
↓
Staging
↓
Intermediate
↓
Marts
↓
Tests
↓
Parallel validation
↓
CutoverCritical requirement:
Do not migrate syntax blindly.
First understand what the legacy pipeline actually does.
91. How do you validate a dbt migration?
Run source-vs-target reconciliation:
Row count
Null count
Distinct keys
Aggregates
Min/max dates
Financial totals
Record-level samplesExample:
SELECT
COUNT(*) AS row_count,
SUM(amount) AS total_amount,
COUNT(DISTINCT customer_id) AS customers
FROM legacy_table;Compare against:
SELECT
COUNT(*) AS row_count,
SUM(amount) AS total_amount,
COUNT(DISTINCT customer_id) AS customers
FROM {{ ref('new_model') }};92. How would you explain technical debt in a dbt project?
Examples:
1000-line SQL models
No tests
No documentation
Hardcoded schemas
Duplicate business logic
Poor incremental strategy
Circular/complex dependencies
No CI/CDI prioritize technical debt based on:
Business impact × Probability × CostRather than attempting to clean everything simultaneously.
93. How do you mentor engineers in SQL/dbt/Python?
I use:
Code reviews
Focus on reasoning, not just syntax.
Pair programming
Work through difficult transformations together.
Standards
Provide templates and examples.
Design reviews
Discuss architecture before implementation.
Production learning
Use incidents as learning opportunities.
Progressive ownership
Give engineers increasingly complex components.
PART VII — VERY ADVANCED QUESTIONS
94. What happens when a dbt incremental model’s watermark is corrupted?
Suppose:
MAX(updated_at)incorrectly moves forward.
The pipeline may permanently skip records.
Solutions include:
- source CDC sequence
- durable ingestion watermark
- lookback windows
- audit tables
- checkpoint validation
- periodic reconciliation
- controlled backfills
Senior insight:
A watermark is an operational state, not merely a query predicate. It needs observability and recovery mechanisms.
95. How would you handle a billion-row fact table?
I would consider:
Storage
- partitioning
- clustering
- compression
Transformation
- incremental models
- partition pruning
- predicate pushdown
- early filtering
Query design
- avoid unnecessary scans
- pre-aggregate where justified
- avoid Cartesian joins
Data model
Fact
↓
Dimensions
↓
Aggregates / martsOperational
- workload isolation
- concurrency management
- monitoring
- cost controls
96. How do you handle data skew?
Suppose:
Customer A → 500 million records
Customer B → 10 recordsThis can cause uneven processing.
Potential solutions:
- identify skewed keys
- repartition appropriately
- isolate extreme keys
- pre-aggregate
- change join strategy
- use warehouse-specific clustering/partitioning techniques
97. How would you troubleshoot a SQL query that works in development but fails in production?
Check:
Data volume
Schema differences
Permissions
Statistics
Indexes
Warehouse size
Concurrency
Parameter distribution
NULL patterns
Data skew
Execution planA query can behave completely differently at production scale.
98. How do you balance performance vs maintainability?
I use this hierarchy:
Correctness
↓
Maintainability
↓
Performance
↓
Cost optimizationBut in production, all four matter.
I avoid premature optimization.
When optimization is necessary, I document:
Problem
Baseline
Change
Result
Trade-off99. What makes someone genuinely “advanced” in SQL?
A senior candidate shouldn’t simply know:
JOIN
GROUP BY
CTE
WINDOW FUNCTIONSThey should understand:
Query optimizer
Execution plans
Cardinality
Indexes
Partition pruning
Transactions
Concurrency
Data modeling
Performance
Cost
Data correctnessAnd most importantly:
They should know why one implementation is better than another.
100. What makes someone genuinely advanced in dbt?
An advanced dbt engineer understands:
DAG design
Incremental processing
Snapshots
Materializations
Jinja
Macros
Testing
Contracts
Sources
Freshness
Lineage
CI/CD
Environment management
Warehouse optimization
Data governanceBut the strongest candidates understand that dbt is not the data warehouse.
dbt orchestrates and manages transformations; the underlying warehouse/lakehouse executes the SQL.
101. What makes someone genuinely advanced in Python for data engineering?
They should understand:
Data structures
Algorithms
OOP
Functional concepts
Generators
Decorators
Context managers
Exceptions
Typing
Testing
Logging
Concurrency
Async I/O
Memory management
Performance
APIs
Database connectivity
PackagingBut they should also know when not to use Python.
For example:
If I need to aggregate 5 TB of warehouse data, I would generally push that computation to the warehouse instead of downloading the data into Python.
That answer demonstrates architectural maturity.
PART VIII — TOP 20 QUESTIONS YOU SHOULD ABSOLUTELY MASTER
If your interview is for a Senior Technical Manager / Senior Data Engineer position, prioritize these:
- How do you optimize a complex SQL query?
- How do you read an execution plan?
- Explain window functions and window frames.
- How do you implement SCD Type 2?
- How do you handle duplicate and late-arriving data?
- Explain transaction isolation and deadlocks.
- Explain indexing and composite indexes.
- What is SARGability?
- Explain dbt DAG architecture.
ref()vssource().- Incremental models and
unique_key. - How do you handle late-arriving data in dbt?
- Snapshot vs incremental model.
- How do you design dbt tests?
- How do you optimize a slow dbt project?
- How do you implement dbt CI/CD?
- How do you process huge datasets with Python?
- Threading vs multiprocessing vs asyncio.
- How do you build idempotent pipelines?
- Design an end-to-end production SQL + dbt + Python data platform.
PART IX — THE “DESIGN THIS” INTERVIEW QUESTION
A very likely senior-level question is:
“Design a production-grade data platform using SQL, dbt, and Python.”
A strong answer would look like:
┌───────────────────┐
│ APIs / Applications│
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Python │
│ Ingestion Layer │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Raw Storage │
│ Immutable Landing │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Warehouse/Lakehouse│
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ dbt │
│ Staging │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Intermediate │
│ Transformations│
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Business Marts │
│ Facts + Dimensions│
└─────────┬─────────┘
│
┌─────────────┼──────────────┐
▼ ▼ ▼
Power BI Analytics MLThen discuss:
Reliability
- idempotency
- retries
- checkpoints
- data quality
- reconciliation
Security
- secrets management
- RBAC
- least privilege
- encryption
- PII controls
Performance
- incremental models
- partitioning
- clustering
- predicate pushdown
- query optimization
Governance
- lineage
- ownership
- documentation
- contracts
- auditability
DevOps
Git
↓
Pull Request
↓
CI
↓
dbt build
↓
Tests
↓
Approval
↓
ProductionObservability
Monitor:
Pipeline success/failure
Runtime
Freshness
Row counts
Duplicates
Null rates
Data drift
Warehouse costPART X — HOW I WOULD ANSWER AS A SENIOR TECHNICAL MANAGER
For scenario questions, don’t answer only with syntax.
Use this framework:
1. Clarify the requirement
What is the business requirement and SLA?
2. Understand the data
What is the volume, velocity, source behavior and update pattern?
3. Design
I would separate ingestion, transformation and consumption responsibilities.
4. Choose technology
Python for ingestion/integration, SQL/dbt for warehouse transformations.
5. Address scalability
Incremental processing, partition pruning, appropriate materialization and warehouse optimization.
6. Address reliability
Idempotency, retries, checkpoints and reconciliation.
7. Address security
Least privilege, secrets management and controlled access.
8. Address observability
Logging, metrics, freshness and data-quality monitoring.
9. Explain trade-offs
Explain why your design is better than the alternatives.
10. Explain operational ownership
How will the team deploy, monitor, troubleshoot and maintain it?
That last part is what often separates a senior engineer from a senior technical manager/architect.
Recommended preparation sequence
Given the breadth of this requirement, I would prepare in this order:
Phase 1 — SQL
JOINs
→ CTEs
→ Window Functions
→ Subqueries
→ Query Optimization
→ Execution Plans
→ Indexes
→ Transactions
→ SCD
→ Advanced Data ModelingPhase 2 — dbt
Models
→ Sources
→ ref/source
→ DAG
→ Materializations
→ Incremental Models
→ Snapshots
→ Tests
→ Macros/Jinja
→ CI/CD
→ Performance
→ Production TroubleshootingPhase 3 — Python
Data Structures
→ OOP
→ Functions
→ Generators
→ Decorators
→ Exceptions
→ Pandas
→ APIs
→ SQL Integration
→ Concurrency
→ Testing
→ PerformancePhase 4 — Senior/Manager
Architecture
→ System Design
→ Data Quality
→ Security
→ Cost
→ Observability
→ Incident Management
→ Code Reviews
→ Mentoring
→ Technical LeadershipFor an actual senior interview, I would not stop at memorizing these answers. The highest-value next step is to practice hands-on SQL coding problems + dbt case studies + Python coding exercises + 10–15 production architecture scenarios, because that is where interviewers usually differentiate candidates.

