SQL is the common language of analytics. Every warehouse - BigQuery, Snowflake, Redshift, Databricks SQL - speaks a dialect of it, dbt compiles to it, Spark and Flink expose it, and BI tools generate it. A data engineer writes more SQL than any other language, and the difference between a query that scans a partition and one that scans a petabyte is usually a few clauses, not a different tool.

Declarative, set-based, and optimizer-driven

You describe the result you want; the engine's optimizer decides how to get it - which join order, which index, whether to broadcast a small table or shuffle both. This is why two queries that return identical rows can differ 100x in cost, and why reading the EXPLAIN plan matters more than micro-optimizing syntax. The mental shift from procedural code is to stop thinking in loops and think in sets: a join is a relationship, not a nested iteration.

Window functions are the analytics workhorse

Window functions compute across a set of rows related to the current row without collapsing them the way GROUP BY does. They are how you write running totals, rankings, period-over-period change, and deduplication - the bulk of real analytics. This example keeps only the latest row per user, a pattern that appears in nearly every pipeline.

SELECT user_id, event_type, created_at
FROM (
    SELECT *,
           ROW_NUMBER() OVER (
               PARTITION BY user_id
               ORDER BY created_at DESC
           ) AS rn
    FROM events
) ranked
WHERE rn = 1;

The clauses that decide your bill

On columnar warehouses, cost is driven by how much data the engine has to read. Partition pruning (a WHERE on the partition key), selecting only the columns you need instead of SELECT *, and filtering before joining all cut scanned bytes. The table below lists the levers that matter most.

LeverEffect
Filter on partition columnEngine skips whole partitions - biggest single win
Select specific columnsColumnar stores read only those columns from disk
Filter before joinSmaller inputs, less shuffle
Clustering / sort keysSkips blocks within a partition
Avoid SELECT DISTINCT as a fixHides a real join-cardinality bug

Dialects diverge at the edges

Core SQL is portable - joins, aggregation, window functions look the same everywhere. The differences show up in date functions, array and struct handling, QUALIFY (Snowflake, BigQuery), and DDL. dbt smooths some of this with macros and adapters, but you still target one warehouse's dialect at a time. Knowing where your engine departs from ANSI SQL prevents the silent surprises, like integer division or implicit time-zone conversion.

Frequently asked questions

Is SQL enough on its own for data engineering? For transformation inside the warehouse, increasingly yes - dbt plus SQL covers a large share of modeling work. You still need a host language (usually Python) for orchestration, ingestion, and anything stateful, but SQL does the heavy analytical lifting.

CTEs or subqueries? Prefer CTEs (WITH) for readability - they name each step and read top to bottom. On most modern engines they are inlined by the optimizer with no performance penalty; on a few older ones a CTE can act as an optimization fence, so check the plan if a query is slow.

Why is my query scanning the whole table? Usually a missing filter on the partition or clustering column, a function wrapped around that column in the WHERE clause (which defeats pruning), or an implicit type cast. Read the EXPLAIN output and check the bytes-scanned estimate.

How do I dedupe rows in SQL? Use ROW_NUMBER() OVER (PARTITION BY key ORDER BY ...) and keep rn = 1, rather than SELECT DISTINCT. The window approach lets you choose which row to keep and surfaces the real cardinality instead of masking it.