Hands-on tutorial  /  ~7 hours, at your own pace

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.

Analyst track SAS → Spark No prior Spark needed

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.

01

Orientation — the workspace and its sidebar

15 min

Everything 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.

Sidebar
{{ sel.kicker }}

{{ sel.label }}

{{ sel.what }}

You open it when
{{ sel.when }}
Coming from SAS
{{ sel.sas }}
Simulated rail — the real sidebar collapses to icons; pin it open from the arrow at its top edge.

The three things that trip people up on day one

Nothing runs by itself

Code needs compute attached. An idle notebook is just a text file until you pick a cluster.

Files are not the point

You will mostly work with governed tables in Catalog, not paths on a disk. Files land in Volumes, then become tables.

Two engines, one screen

Notebooks run on clusters; the SQL editor runs on warehouses. Same data, different motor.

02

Compute — the thing you start before anything works

20 min

In 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.

All-purpose cluster

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
SQL warehouse

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
Practice

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.

03

Notebooks — cells, languages, results

30 min

A 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.

claims_ingest Python {{ clusterLabel }}
{{ c.numLabel }}
{{ c.code }}
{{ c.output }}
Simulated notebook — results here are canned, timings are typical of a warm cluster.

What the toolbar and cell chrome actually do

ControlWhat it doesWatch out for
Run cell (►)Executes just that cell against the attached computeShift+Enter runs and moves down
Language magics%sql, %python, %md, %sh switch one cell's languageA %md cell is documentation — write them as you go
display(df)Renders a scrollable, sortable grid instead of raw textPlain df alone prints nothing useful
+ chart tab under a resultTurns the same result into a bar/line chart, no codeCharts here are for looking, not for dashboards
Clear state & run allWipes variables, then runs top to bottomThe only honest way to prove a notebook works
04

Catalog — finding data before you write a line

25 min

Unity 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.

Catalog
Schema
Table
{{ fullName }} {{ selTable.format }} {{ selTable.rows }}
ColumnTypeNote
{{ col.name }}{{ col.type }}{{ col.note }}
Simulated catalog browser. The real one adds Sample data, Permissions, History and Lineage tabs on this same panel.
Bronze, silver, gold

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.

05

Ingestion — CSV, Excel, SAS datasets, Snowflake

35 min

Files 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)
On a wide or messy file, skip 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)
Fine for a workbook that fits in memory, which is nearly every real spreadsheet. For very large or many-sheet files install the 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)
Two things to check straight away: encoding (Latin-1 is the usual culprit behind mangled accents) and SAS dates, which arrive as numbers offset from 1960-01-01 — see the conversion in Module 6.

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())
Never paste a password into a cell. dbutils.secrets.get reads from a secret scope your admin created, and the value prints as [REDACTED] if you try to display it.
Practice

Upload any CSV you have to a volume, read it, then run df.printSchema() and df.count(). Those two commands are your PROC CONTENTS.

06

Manipulating data — your DATA step, translated

45 min

One 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.

Show equivalent in
{{ r.task }}
SAS
{{ r.sas }}
{{ langLabel }}
{{ r.target }}
{{ r.note }}

The one rule that explains the rest

Transformations are lazy. filter, select, join build a plan and return instantly; nothing reads data until an actiondisplay, 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.

07

By-group logic — FIRST., LAST., RETAIN, TRANSPOSE

45 min

This 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
Put this at the top of any notebook doing the work below — every example in this module assumes it. The language switch in Module 6 controls these panels too.
{{ r.task }}
SAS
{{ r.sas }}
{{ langLabel }}
{{ r.target }}
{{ r.note }}
Practice

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.

08

Joins, unions and MERGE — Delta Lake basics

40 min

Every 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.

SASDatabricksDifference that matters
MERGE a b; BY id; in a DATA stepa.join(b, "id", "left")No BY sort needed, and no silent overwrite of same-named columns — rename first
PROC SQL ... LEFT JOINSame SQL, unchangedYour 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-placeMERGE INTOOne 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 *
Run it twice on the same source and the second run changes nothing. That property — idempotence — is why this replaces the delete-then-reload habit.

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;
Time travel is the safety net that lets you work directly on real tables. Check History in the Catalog UI for the same information without writing SQL.
09

SQL editor & dashboards

30 min

If 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.

revenue_by_line Warehouse: analytics_M Run (⌘⏎)
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_lineclaimstotal_eur
Motor18,2047,412,880
Household11,0673,980,145
Travel6,5331,204,700
Marine812905,330
4 rows · 1.8 s · results cached — re-running is free until the table changes

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
10

Pipelines — when the notebook should run itself

25 min

A 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.

Bronze
claims_raw
Reads new files from the volume as they land. Nothing is changed.
Silver
claims_clean
Types fixed, dates converted, rows failing expectations dropped.
Gold
claims_by_line
Aggregated for the dashboard in Module 8.
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"))
The 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.

11

Making it fast — and not expensive

30 min

Compute 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.

Habit 1
Filter and select early

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.

Habit 2
Never pull it all local

collect() and toPandas() move everything onto one machine. Fine for 10,000 rows, fatal for ten million.

Habit 3
Explore on a sample

display(df.limit(1000)) while you build. Run the full thing once, when the logic is settled.

Habit 4
Watch the clock on the cell

Every cell prints its duration. If a step jumped from seconds to minutes, the change you just made caused it — look there first.

Habit 5
Let the cluster stop

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 seeUsual causeWhat to do
A join takes minutes on small inputsBoth sides are being shuffled across the clusterNothing — Spark broadcasts small tables automatically; check the table really is small
Queries on one big table are always slowThousands of tiny files, or no clustering on the filter columnOPTIMIZE the table; cluster it by the column you filter on
The same query is slow every timeNotebook compute, not a warehouseRun reporting SQL on a SQL warehouse — Photon is on there
A step reruns work you already didLazy evaluation recomputing the chainWrite 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 runningA cluster left on, or auto-termination disabledCompute 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';
In the SQL editor, every run has a Query profile link under the result. It shows where the time went — usually one node with a huge row count. That node is your problem.
12

Proving the new output matches the old

35 min

Nobody 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.

Check
If it fails
1
Row count on both sides
A filter differs, or a join is multiplying rows. Stop here — nothing downstream will match.
2
Distinct key count and duplicates
The grain changed. Usually a lookup table with duplicate keys.
3
Column totals — sum, min, max, null count per column
Points straight at the column that broke, without inspecting a single row.
4
Row-level diff with EXCEPT, both directions
Gives you the actual offending rows. Round money columns first or you will drown in noise.
5
Re-run the whole thing from clean state
Out-of-order cell execution is the most common false alarm in a notebook.
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
Both directions matter: one tells you what you lost, the other what you invented. An empty result from both is the only clean pass.
Differences that are expected, not bugs

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.

13

Sharing, permissions and not losing work

25 min

Two 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 objectLevelsThe one that surprises people
Notebook / queryCan View · Can Run · Can Edit · Can ManageCan Run executes it against your compute — and their permissions, not yours
DashboardCan View · Can Edit · Can ManageSharing is not enough; an unpublished dashboard stays invisible
ClusterCan Attach · Can Restart · Can ManageWithout Can Attach, a shared notebook simply will not run for them
Catalog / schema / tableUSE · SELECT · MODIFY · ALL PRIVILEGESUSE 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;
The Permissions tab in Catalog does exactly this with buttons. Use the UI to grant, the SQL to check — SHOW GRANTS is faster than clicking through three levels.
Parameters, so others can run your work

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)
14

Capstone — a SAS dataset, end to end

60 min

Do 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.

{{ s.n }}
{{ s.title }}
{{ s.body }}

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"""))
Finished? Open the table in Catalog, read its History tab, then rebuild the last query in the SQL editor and pin it to a dashboard. That round trip is the whole platform in one sitting.
A

Keyboard shortcuts

{{ k.what }} {{ k.keys }}

Shown for macOS; use Ctrl in place of ⌘ on Windows. Press Shift+? in any notebook for the live list.

B

Glossary

TermWhat it means hereNearest SAS idea
{{ g.term }} {{ g.def }} {{ g.sas }}
C

Function lookup: SAS to Spark

Spark SQL names are given; in PySpark the same function lives on pyspark.sql.functionsF.substring, F.instr, and so on.

SASSpark SQLWatch out
{{ f.sas }} {{ f.spark }} {{ f.note }}
D

When it goes wrong

Why: {{ f.why }}
Fix: {{ f.fix }}
Course complete when all fourteen modules are ticked. Interface details move — when a screen does not match, trust the screen and tell whoever maintains this page.
Back to the top