Anti-Pattern Gallery and Pattern Cheat Sheet

Estimated read: 3 min

Every pattern in this section has a literal, line-by-line translation that compiles, runs, and quietly performs badly or produces subtly wrong results. This gallery catalogs the five translations experienced migration teams learn to recognize and avoid, followed by a one-page cheat sheet for the whole section.

1. The collect() loop. A cursor translated as for row in df.collect(): ... instead of a set-based operation. It runs, but pulls the entire DataFrame to the driver first, discarding distributed execution and risking an out-of-memory failure on any dataset larger than the driver’s memory.

# Anti-pattern
for row in df.collect():
    if row.total > 1000:
        update_priority(row.order_id, "HIGH")

# Correct
df_high = df.filter("total > 1000")
target_df.join(df_high.select("order_id"), "order_id") \
         .withColumn("priority", lit("HIGH"))

2. The polling job pretending to be a trigger. Instead of consuming Change Data Feed, a Lakeflow Job runs every five minutes and does a full-table comparison to detect what changed – reinventing CDC badly, at far more compute cost than reading table_changes() directly.

3. The unconditional temp-table materialization. Every intermediate result gets written out as a persisted Declarative Pipeline table “to be safe,” even single-use staging that a CTE would compute for free. This inflates both storage and pipeline DAG complexity with nodes nobody ever queries.

4. The MERGE with an ambiguous join key. A MERGE INTO whose ON condition doesn’t uniquely identify target rows, ported directly from a legacy MERGE that happened to tolerate duplicate matches. Delta’s stricter behavior surfaces this as a runtime error – treat that error as a data-quality finding to fix, not an obstacle to suppress.

5. The hand-rolled SCD Type 2 that assumes ordered arrival. A two-pass MERGE implementation of SCD Type 2 that silently assumes updates arrive in timestamp order, breaking the moment a late or out-of-order record shows up – exactly the case create_auto_cdc_flow’s sequence_by parameter exists to handle correctly.

The common thread across all five: each one is a literal translation that preserves the legacy code’s shape instead of its intent. Every anti-pattern in this gallery still “works” in a demo with a small sample dataset – they fail at production data volume or under real-world arrival order, which is exactly why they survive code review and surface later as performance or correctness incidents.

The pattern cheat sheet

Legacy construct Symptom if translated literally Databricks-native pattern
Cursor loop Row-at-a-time processing defeats distributed execution Set-based DataFrame/SQL op, window function, or UDF only for genuinely row-scoped logic
Row-level trigger No direct equivalent; hidden, unqueryable execution Delta Change Data Feed (table_changes(), readChangeFeed)
DBMS_SCHEDULER job Scheduling/retry logic buried in scheduler catalog Lakeflow Jobs task with declared schedule, retries, dependencies
Session temp table Materializing single-use staging as a real table CTE or chained DataFrame (single-use); Declarative Pipeline table (reused/shared)
Oracle MERGE Ambiguous join keys silently tolerated Delta MERGE INTO, which rejects ambiguous matches by default
SCD Type 2 via cursor + two DML statements Breaks silently on out-of-order arrivals create_auto_cdc_flow with stored_as_scd_type=2

Keep this table next to the decomposition worksheet from the previous section – together they cover every “target pattern” entry a real procedure autopsy is likely to produce. The section’s quiz next checks recall of both the individual patterns and the reasoning behind each anti-pattern.


| ← Previous: SCD Type 2 the Lakehouse Way | Next: Check Your Knowledge → | |:—|—:|


Back to top

Independent, self-authored Databricks companion notes.

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