Temp Table to CTE or Lakeflow Declarative Pipeline

Estimated read: 3 min

A session-scoped temp table exists in legacy procedures for one reason: PL/SQL has no first-class way to hold an intermediate, tabular result in memory between two steps of a procedure, so a CREATE GLOBAL TEMPORARY TABLE fakes it using real table storage instead. Databricks has two better answers, and picking the right one depends entirely on one question: does the intermediate result get used once, immediately, or does it need to persist and be shared across multiple steps?

Used once, immediately: a CTE

If the temp table exists only to stage one intermediate result that the very next statement consumes and discards, it doesn’t need storage at all – a common table expression (CTE) or a chained PySpark DataFrame does the same job without ever materializing anything:

-- Legacy: CREATE GLOBAL TEMPORARY TABLE stg_high_value AS SELECT ... ;
--         (next statement reads FROM stg_high_value)
WITH stg_high_value AS (
  SELECT order_id, customer_id, total
  FROM orders
  WHERE total > 1000
)
UPDATE orders SET priority = 'HIGH'
WHERE order_id IN (SELECT order_id FROM stg_high_value);
# Equivalent as a DataFrame chain -- no intermediate table, no CTE needed
stg_high_value = orders_df.filter("total > 1000")
orders_df = orders_df.join(stg_high_value.select("order_id"), "order_id", "left_anti") \
                      .unionByName(stg_high_value.withColumn("priority", lit("HIGH")))

Both versions compute the intermediate result inline, as part of query planning, rather than writing it out and reading it back – which is strictly less work than the legacy pattern ever did, since the temp table’s write was never necessary in the first place.

Persisted and shared across steps: a Declarative Pipeline flow

If the intermediate result needs to survive across multiple procedure steps, get inspected for debugging, or feed more than one downstream consumer, collapsing it into a single CTE loses something real – visibility into that intermediate state. That’s when the intermediate result should become its own table: a streaming table or materialized view inside a Lakeflow Declarative Pipeline, with its own name, its own lineage, and its own query-ability.

import dlt

@dlt.table(name="stg_high_value_orders")
def stg_high_value_orders():
    return spark.read.table("orders").filter("total > 1000")

@dlt.table(name="orders_flagged")
def orders_flagged():
    stg = dlt.read("stg_high_value_orders")
    return stg.withColumn("priority", lit("HIGH"))

Because the pipeline’s DAG shows stg_high_value_orders as a named node feeding orders_flagged, anyone debugging the pipeline can query the intermediate table directly – something a session-scoped temp table never allowed, since it vanished with the session that created it.

The decision in one line

Signal Choose
Used once, by the very next statement, never inspected independently CTE or a chained DataFrame
Reused by multiple later steps, or valuable to inspect on its own A named table in a Declarative Pipeline

Don’t default to materializing every temp table as its own persisted table just because that’s what the legacy code did – most temp tables in real procedures are single-use staging that a CTE handles for free. Reserve the Declarative Pipeline table for the cases where the intermediate result genuinely earns its own name and lineage.

The next lecture covers the pattern nearly every one of these procedures eventually needs regardless of which path this decision takes: the upsert, and its direct translation from Oracle MERGE to Delta’s MERGE INTO.


| ← Previous: Trigger to CDC, DBMS_SCHEDULER to Workflows | Next: Oracle MERGE to Delta Lake MERGE INTO → | |:—|—:|


Back to top

Independent, self-authored Databricks companion notes.

This site uses Just the Docs, a documentation theme for Jekyll.