Learn the Databricks interface by using it
Fourteen modules that walk through the screens you will actually touch — the workspace, compute, notebooks, the catalog, the SQL editor and pipelines — each one anchored to a real task: land a file, clean it, join it, prove it matches, write a table. The simulations on this page are clickable practice runs; every code block is copy-ready for the real thing.
Before you start
You need a Databricks workspace URL and a login from your admin, plus permission to attach to a cluster or a SQL warehouse. If you only have one of the two, start at Module 2 — it explains which one your task needs. Nothing on this page connects to a real workspace, so click freely.
Orientation — the workspace and its sidebar
15 minEverything in Databricks hangs off one left sidebar. It never changes, whatever screen you are on, and it is the fastest way to build a mental model: each entry is a different kind of object, not a different app. Click through the rail below — the panel on the right explains what each entry gives you and what to try first.
{{ sel.label }}
{{ sel.what }}
The three things that trip people up on day one
Code needs compute attached. An idle notebook is just a text file until you pick a cluster.
You will mostly work with governed tables in Catalog, not paths on a disk. Files land in Volumes, then become tables.
Notebooks run on clusters; the SQL editor runs on warehouses. Same data, different motor.
Compute — the thing you start before anything works
20 minIn SAS the engine was already running when you opened the client. Here you rent it by the minute, so you start it, and it stops itself when you walk away. There are two kinds and picking the wrong one is the most common first-day mistake.
For notebooks
Runs Python, SQL, Scala and R. This is what you attach a notebook to. Takes 3–6 minutes to start from cold.
- Set Terminate after to 30 min — it is your bill
- Note the runtime version; libraries depend on it
- Shared vs single-user access mode changes what you may read
For the SQL editor & dashboards
SQL only, tuned for queries and BI. Serverless ones wake in seconds, which is why dashboards use them.
- Sized S / M / L rather than by node count
- Shared across a team by default
- Cannot run Python — that is the giveaway
Open Compute. Find a cluster you are allowed to use, read its state chip (Terminated / Starting / Running), start it, and while it boots open its Libraries tab. Anything listed there is available to your code — this is where a reader for SAS files gets installed in Module 5.
Notebooks — cells, languages, results
30 minA notebook is a stack of cells. Each cell holds code or text, runs on its own, and prints its result directly underneath. Unlike a SAS program, order is your responsibility: a cell remembers whatever ran before it, so re-running things out of order is how people confuse themselves. Run the simulation below top to bottom.
{{ c.code }}
What the toolbar and cell chrome actually do
| Control | What it does | Watch out for |
|---|---|---|
| Run cell (►) | Executes just that cell against the attached compute | Shift+Enter runs and moves down |
| Language magics | %sql, %python, %md, %sh switch one cell's language | A %md cell is documentation — write them as you go |
display(df) | Renders a scrollable, sortable grid instead of raw text | Plain df alone prints nothing useful |
| + chart tab under a result | Turns the same result into a bar/line chart, no code | Charts here are for looking, not for dashboards |
| Clear state & run all | Wipes variables, then runs top to bottom | The only honest way to prove a notebook works |
Catalog — finding data before you write a line
25 minUnity Catalog names everything in three parts: catalog.schema.table. If you know SAS, a schema is your libref and a table is your dataset — the extra level on the front is the environment or domain boundary. Browse the tree, then read the columns panel; this is exactly the screen you use to check a table before querying it.
{{ fullName }}
{{ selTable.format }}
{{ selTable.rows }}
| Column | Type | Note |
|---|---|---|
{{ col.name }} | {{ col.type }} | {{ col.note }} |
Those schema names are a convention, not a feature: bronze is raw landed data, silver is cleaned and typed, gold is aggregated for reporting. Everything in Modules 5–9 walks a file from bronze to gold. Also note hive_metastore — the legacy namespace. If a colleague's old code has two-part names, it lives there.
Ingestion — CSV, Excel, SAS datasets, Snowflake
35 minFiles live in a Volume — a governed folder inside a schema, reachable at /Volumes/catalog/schema/volume/. Upload one from Catalog › your schema › Create › Volume, or drag it into an existing volume. Then read it into a DataFrame, which is the Spark equivalent of a work dataset in memory.
CSV
df = (spark.read.format("csv")
.option("header", True)
.option("inferSchema", True)
.option("sep", ",")
.load("/Volumes/main/raw_bronze/landing/claims_2024.csv"))
display(df)
inferSchema (it reads the file twice) and pass an explicit schema — same discipline as declaring INFORMATs in a SAS INFILE step.Excel
import pandas as pd
pdf = pd.read_excel("/Volumes/main/raw_bronze/landing/rates.xlsx",
sheet_name="2024", header=0)
df = spark.createDataFrame(pdf)
com.crealytics:spark-excel library on the cluster and read it as a Spark format instead.SAS datasets (.sas7bdat)
import pandas as pd
pdf = pd.read_sas("/Volumes/main/raw_bronze/landing/claims.sas7bdat",
format="sas7bdat", encoding="latin-1")
df = spark.createDataFrame(pdf)
# For files too big for one machine, install the spark-sas7bdat library
# on the cluster and read it in parallel instead:
df = spark.read.format("com.github.saurfang.sas.spark").load(path)
Snowflake
options = {
"sfUrl": "acme.eu-west-1.snowflakecomputing.com",
"sfUser": dbutils.secrets.get("acme", "sf_user"),
"sfPassword": dbutils.secrets.get("acme", "sf_pwd"),
"sfDatabase": "ANALYTICS",
"sfSchema": "PUBLIC",
"sfWarehouse": "LOAD_WH",
}
df = (spark.read.format("snowflake").options(**options)
.option("dbtable", "DIM_CUSTOMER").load())
dbutils.secrets.get reads from a secret scope your admin created, and the value prints as [REDACTED] if you try to display it.Upload any CSV you have to a volume, read it, then run df.printSchema() and df.count(). Those two commands are your PROC CONTENTS.
Manipulating data — your DATA step, translated
45 minOne habit to unlearn: there is no row-by-row loop. You describe the whole column at once and Spark works out the loop. Everything a DATA step does with IF, WHERE, retained variables and formats becomes a column expression. Switch the target language below — SAS on the left stays put so you can read across.
{{ r.sas }}
{{ r.target }}
The one rule that explains the rest
Transformations are lazy. filter, select, join build a plan and return instantly; nothing reads data until an action — display, count, write — forces it. So a cell that finishes in 0.2 s has not done your work yet, and the cell that takes two minutes is paying for all of them.
By-group logic — FIRST., LAST., RETAIN, TRANSPOSE
45 minThis is the module where SAS habits hurt most. Everything you did with BY processing — flagging the first row of a group, retaining a running total, lagging a value, ranking, transposing — has one answer here: a window. You declare which rows belong together (partitionBy) and in what order (orderBy), and the calculation walks them for you. No sort step, and it works across the whole cluster.
from pyspark.sql import Window as W, functions as F
{{ r.sas }}
{{ r.target }}
Using samples.nyctaxi.trips, produce one row per pickup zip with the most expensive trip of the day and its rank. You will need partitionBy, orderBy and either QUALIFY or a row_number filter — nothing else.
Joins, unions and MERGE — Delta Lake basics
40 minEvery table you create is a Delta table by default. Practically, that buys you three things a plain file never had: updates and deletes, transactions that either finish or leave nothing behind, and a full history you can query. That is what makes MERGE INTO — the update/insert pattern you would have written as a MODIFY or a two-step PROC SQL — safe here.
| SAS | Databricks | Difference that matters |
|---|---|---|
MERGE a b; BY id; in a DATA step | a.join(b, "id", "left") | No BY sort needed, and no silent overwrite of same-named columns — rename first |
PROC SQL ... LEFT JOIN | Same SQL, unchanged | Your PROC SQL mostly runs as-is in a %sql cell |
SET a b; | a.unionByName(b) | union matches by position; unionByName matches by name — use the latter |
PROC APPEND | .mode("append").saveAsTable() | Transactional — a failed append leaves the table untouched |
MODIFY / update-in-place | MERGE INTO | One statement handles matched updates and unmatched inserts |
The upsert you will use constantly
MERGE INTO main.claims_silver.claims AS tgt USING updates_today AS src ON tgt.claim_id = src.claim_id WHEN MATCHED AND src.status <> tgt.status THEN UPDATE SET tgt.status = src.status, tgt.updated_at = current_timestamp() WHEN NOT MATCHED THEN INSERT *
Writing a table, and looking backwards
(clean_df.write
.format("delta")
.mode("overwrite")
.saveAsTable("main.claims_silver.claims"))
# what happened to this table, and undoing it
DESCRIBE HISTORY main.claims_silver.claims;
SELECT * FROM main.claims_silver.claims VERSION AS OF 12;
RESTORE TABLE main.claims_silver.claims TO VERSION AS OF 12;
SQL editor & dashboards
30 minIf your day is queries and charts, this is the half of the product you will live in — no Python required. The SQL editor is a query, a warehouse selector and a result grid; a dashboard is a canvas of those queries plus filters.
SELECT product_line,
count(*) AS claims,
round(sum(amount)) AS total_eur
FROM main.analytics_gold.claims_enriched
WHERE claim_date >= '2024-01-01'
GROUP BY product_line
ORDER BY total_eur DESC
| product_line | claims | total_eur |
|---|---|---|
| Motor | 18,204 | 7,412,880 |
| Household | 11,067 | 3,980,145 |
| Travel | 6,533 | 1,204,700 |
| Marine | 812 | 905,330 |
Editor habits worth forming
- Save the query with a real name — that is how a dashboard finds it
- Highlight a fragment and run it to test one CTE at a time
- Use parameters (
{{ paramSyntax }}) instead of editing dates by hand - Schedule a refresh from the query itself, not from a job
Building the dashboard
- Data tab: define the datasets (your queries)
- Canvas tab: drop visualisations and drag to size
- Add a filter widget once, bind it to several datasets
- Publish, then share — an unpublished dashboard is invisible to others
Pipelines — when the notebook should run itself
25 minA notebook you re-run by hand every Monday is a job waiting to happen. Declarative pipelines (formerly Delta Live Tables) let you describe the tables instead of the steps: you write one function per table, and the pipeline works out the order, the dependencies and the retries. The graph it draws is the interface — each box is a table, each arrow a dependency.
import dlt
from pyspark.sql import functions as F
@dlt.table(name="claims_raw")
def claims_raw():
return (spark.readStream.format("cloudFiles")
.option("cloudFiles.format", "csv")
.load("/Volumes/main/raw_bronze/landing/"))
@dlt.table(name="claims_clean")
@dlt.expect_or_drop("valid_amount", "amount > 0")
def claims_clean():
return (dlt.read("claims_raw")
.withColumn("claim_date", F.to_date("claim_date", "yyyy-MM-dd"))
.filter(F.col("status") != "TEST"))
expect_or_drop line is a data-quality rule. Failed rows are counted and shown on the pipeline graph — the check that used to be a PROC FREQ you ran and forgot.For anything that is not a table-building pipeline — running a notebook nightly, chaining three of them, emailing on failure — use Jobs on the same page: add tasks, draw dependencies, set a schedule, open a run to read its logs.
Making it fast — and not expensive
30 minCompute is metered, so slow code has a price tag. You do not need to understand the engine to avoid the expensive mistakes — five habits cover almost everything an analyst hits.
Cut rows and columns in the first step, not the last. Every later operation then moves less data. This one change beats all the tuning below.
collect() and toPandas() move everything onto one machine. Fine for 10,000 rows, fatal for ten million.
display(df.limit(1000)) while you build. Run the full thing once, when the logic is settled.
Every cell prints its duration. If a step jumped from seconds to minutes, the change you just made caused it — look there first.
Auto-termination at 30 minutes costs you three minutes of restart and saves hours of idle billing. Never disable it on a cluster you use interactively.
Symptom to lever
| What you see | Usual cause | What to do |
|---|---|---|
| A join takes minutes on small inputs | Both sides are being shuffled across the cluster | Nothing — Spark broadcasts small tables automatically; check the table really is small |
| Queries on one big table are always slow | Thousands of tiny files, or no clustering on the filter column | OPTIMIZE the table; cluster it by the column you filter on |
| The same query is slow every time | Notebook compute, not a warehouse | Run reporting SQL on a SQL warehouse — Photon is on there |
| A step reruns work you already did | Lazy evaluation recomputing the chain | Write the intermediate result to a table, or .cache() it if you will reuse it several times in one session |
| The bill is high but nothing is running | A cluster left on, or auto-termination disabled | Compute page — sort by state and check what is Running |
-- compact small files and co-locate rows you filter on OPTIMIZE main.claims_silver.claims; ALTER TABLE main.claims_silver.claims CLUSTER BY (claim_date, product_line); -- what is this query actually doing? (Query profile in the SQL editor shows the same) EXPLAIN SELECT * FROM main.claims_silver.claims WHERE claim_date > '2024-06-01';
Proving the new output matches the old
35 minNobody switches off a SAS job because the Databricks version looks right. You run both for a while and prove they agree — and when they disagree, you need to know within minutes whether it is a real defect or a rounding artefact. Work the checks in this order; each one is cheap and rules out a whole class of difference.
sas = spark.table("main.validation.claims_from_sas") # exported and loaded once
new = spark.table("main.claims_silver.claims_enriched")
print(sas.count(), new.count())
print(sas.select("claim_id").distinct().count(),
new.select("claim_id").distinct().count())
# 3 — column-by-column profile, side by side
def profile(df, label):
return df.agg(
F.sum("amount").alias("total"),
F.min("claim_date").alias("first_date"),
F.max("claim_date").alias("last_date"),
F.sum(F.col("policy_id").isNull().cast("int")).alias("null_policy"),
).withColumn("source", F.lit(label))
display(profile(sas, "sas").unionByName(profile(new, "databricks")))
# 4 — the rows that actually differ, tolerant of floating point
key = ["claim_id"]
cmp_cols = ["policy_id", "product_line", "band"]
a = sas.select(*key, *cmp_cols, F.round("amount", 2).alias("amount"))
b = new.select(*key, *cmp_cols, F.round("amount", 2).alias("amount"))
display(a.exceptAll(b)) # in SAS, not in Databricks
display(b.exceptAll(a)) # in Databricks, not in SAS
Row order (Spark has none unless you sort). Trailing spaces (SAS pads fixed-width character columns; trim both sides). Missing-value handling in sums — SAS sum() ignores missings, Spark's + propagates nulls. And floating point at the cent: cast money to decimal(18,2) on both sides and the noise disappears.
Sharing, permissions and not losing work
25 minTwo people can type in the same notebook at the same time, which is either useful or alarming depending on what you expected. Three interface habits keep collaborative work sane.
Version history
Every notebook keeps its own revision list in the right-hand panel. Restore any point, no Git required. Name a revision before a big change and you can always get back.
Git folders
For anything that ships. The folder shows a branch selector and a commit dialog; you pull, work on a branch, commit and push without leaving the workspace.
Comments
Select code, leave a comment, tag a colleague. Reviews happen in the notebook rather than in a thread nobody can find later.
Who can do what
| On an object | Levels | The one that surprises people |
|---|---|---|
| Notebook / query | Can View · Can Run · Can Edit · Can Manage | Can Run executes it against your compute — and their permissions, not yours |
| Dashboard | Can View · Can Edit · Can Manage | Sharing is not enough; an unpublished dashboard stays invisible |
| Cluster | Can Attach · Can Restart · Can Manage | Without Can Attach, a shared notebook simply will not run for them |
| Catalog / schema / table | USE · SELECT · MODIFY · ALL PRIVILEGES | USE on the parents is required before SELECT on the table works |
GRANT USE CATALOG ON CATALOG main TO `analysts`; GRANT USE SCHEMA ON SCHEMA main.claims_silver TO `analysts`; GRANT SELECT ON TABLE main.claims_silver.claims TO `analysts`; SHOW GRANTS ON TABLE main.claims_silver.claims;
SHOW GRANTS is faster than clicking through three levels.A notebook with hard-coded dates is a notebook only you can run. Widgets put input boxes at the top — the macro variables of this world, and what a scheduled job fills in.
dbutils.widgets.text("start_date", "2024-01-01")
start = dbutils.widgets.get("start_date")
df = spark.table("main.claims_silver.claims").filter(F.col("claim_date") >= start)
Capstone — a SAS dataset, end to end
60 minDo this one in a real workspace, in a notebook you create yourself. It uses every screen from the modules above: a volume, a cluster, a notebook, the catalog, and finally the SQL editor to check your own work.
The whole thing, if you want to read it first
import pandas as pd
from pyspark.sql import functions as F
# 1 — read the SAS dataset from a volume
pdf = pd.read_sas("/Volumes/main/raw_bronze/landing/claims.sas7bdat",
encoding="latin-1")
claims = spark.createDataFrame(pdf)
# 2 — clean: trim, retype, convert SAS dates, band the amounts
claims = (claims
.withColumn("policy_id", F.trim(F.col("policy_id")))
.withColumn("amount", F.col("amount").cast("double"))
.withColumn("claim_date", F.expr("date_add(to_date('1960-01-01'), cast(claim_date as int))"))
.withColumn("band", F.when(F.col("amount") < 500, "small")
.when(F.col("amount") < 5000, "medium")
.otherwise("large"))
.filter(F.col("status") != "TEST"))
# 3 — join the policy dimension from Snowflake or the catalog
policies = spark.table("main.raw_bronze.policies").select(
"policy_id", "product_line", "region")
enriched = claims.join(policies, on="policy_id", how="left")
# 4 — write a governed Delta table
(enriched.write.format("delta").mode("overwrite")
.saveAsTable("main.claims_silver.claims_enriched"))
# 5 — check it, then go look at it in Catalog
display(spark.sql("""
SELECT product_line, band, count(*) AS n, round(sum(amount)) AS total
FROM main.claims_silver.claims_enriched
GROUP BY product_line, band ORDER BY total DESC"""))
Keyboard shortcuts
{{ k.keys }}
Shown for macOS; use Ctrl in place of ⌘ on Windows. Press Shift+? in any notebook for the live list.
Glossary
| Term | What it means here | Nearest SAS idea |
|---|---|---|
| {{ g.term }} | {{ g.def }} | {{ g.sas }} |
Function lookup: SAS to Spark
Spark SQL names are given; in PySpark the same function lives on pyspark.sql.functions — F.substring, F.instr, and so on.
| SAS | Spark SQL | Watch out |
|---|---|---|
{{ f.sas }} |
{{ f.spark }} |
{{ f.note }} |