Table of Contents
- 1.1 Executing ANSI-compliant queries against Delta tables
- 1.2 Analyzing query execution and performance characteristics
- 1.3 Navigating the query editor and results pane
- 1.4 Organizing and saving queries in workspaces
- 2.1 Retrieving data with SELECT and WHERE conditions
- 2.2 Improving query readability with aliases, sorting, and expressions
- 2.3 Aggregating data with GROUP BY and HAVING
- 2.4 Combining tables with JOIN operations
- 2.5 Transforming data with built-in SQL functions
- 3.1 Browsing catalogs, schemas, and tables in Unity Catalog
- 3.2 Creating and managing views for organized query logic
- 3.3 Simplifying complex queries with CTEs and subqueries
- 3.4 Managing query history and tracking exploration progress
1. Training overview
Context and motivation
This training addresses a concrete problem that every data team encounters: the data lives in the lakehouse, and the team needs answers. Before Databricks SQL, the typical workflow looked like this: export the data to CSV, open it in Excel, build formulas, and get a basic answer 30 minutes later. The next day, someone asks the same question with a different filter — and it starts again.
Databricks SQL offers a straightforward alternative: write standard SQL directly against Delta tables, get results in seconds from the lakehouse, and save these queries for the entire team to reuse.
Educational objectives
This training covers three main skills:
- Write SQL against Delta tables — ANSI-compliant syntax, filters, aggregations, joins, functions
- Get quick results — understand result caching, Serverless SQL warehouses, performance mechanisms
- Save and share work — organization of queries, version history, sharing with the team, permanent views, CTEs
Demo Database
All training uses a sales database main.sales made up of three tables:
| Table | Content |
|---|---|
main.sales.customers | Buyer Information (12 records) |
main.sales.products | Product catalog (9 records) |
main.sales.orders | Orders placed (25 records) |
Note: All demo files (table creation script, all queries) are included in the course materials.
2. Understanding query execution in Databricks SQL
2.1 Executing ANSI-compliant queries against Delta tables
Concepts covered
Databricks SQL is a SQL-native interface for querying data in the lakehouse. If you’ve written SQL before, you already know most of what you need in Databricks SQL: the syntax is ANSI-compliant, meaning it conforms to the SQL standard.
The customers table
The customers table stores basic information about buyers.
-- Voir toutes les colonnes de la table customers
SELECT *
FROM main.sales.customers;
Result:
| customer_id | first_name | last_name | registration_date | |
|---|---|---|---|---|
| 1 | John | Smith | john.smith@example.com | 2024-03-12 |
| 2 | Sarah | Johnson | sarah.j@example.com | 2025-07-21 |
| 3 | MICHAEL | CHEN | m.chen@example.com | 2026-01-15 |
| 4 | Emily | Davis | emily.davis@example.com | 2026-02-03 |
| 5 | david | WILSON | d.wilson@example.com | 2025-11-08 |
| 6 | Olivia | martinez | olivia.m@example.com | 2026-01-28 |
| 7 | JAMES | Anderson | james.a@example.com | 2024-09-17 |
| 8 | Sophie | Brown | sophia.b@example.com | 2026-03-09 |
| 9 | liam | taylor | sarah.j@example.com | 2025-06-04 |
| 10 | Ava | Thomas | NULL | 2026-02-22 |
| 11 | Noah | GARCIA | noah.g@example.com | 2025-04-11 |
| 12 | Isabella | Lee | isabella.lee@example.com | 2026-03-25 |
Observations: real data is often imperfect — inconsistent case (
MICHAEL,sarah,JAMES), NULL value for Ava Thomas email, duplicate email between Sarah Johnson and Liam Taylor.
-- Sélectionner uniquement les colonnes nécessaires
SELECT first_name,
last_name,
email
FROM main.sales.customers;
Key principle: SELECT * is convenient for mining, but it is better to name the columns explicitly to limit the amount of data transferred and to clarify the intent of the query.
The products table
SELECT *
FROM main.sales.products;
Result:
| product_id | product_name | category | price |
|---|---|---|---|
| 1 | Laptop | Electronics | 1299.99 |
| 2 | Office Chair | Furniture | 249.50 |
| 3 | Smartphone | Electronics | 799.00 |
| 4 | Desk Lamp | Furniture | 89.99 |
| 5 | Wireless Mouse | Electronics | 45.00 |
| 6 | Standing Desk | Furniture | 459.00 |
| 7 | Notebook | Stationery | 12.50 |
| 8 | Coffee Mug | NULL | 15.00 |
| 9 | Sketchpad | Stationery | 22.75 |
Note: The Coffee Mug has no assigned category (NULL).
The orders table
SELECT *
FROM main.sales.orders;
-- (25 commandes au total)
-- Sélectionner les colonnes d'intérêt
SELECT order_id,
customer_id,
order_amount,
order_date
FROM main.sales.orders;
Partial result (first 10 out of 25):
| order_id | customer_id | order_amount | order_date |
|---|---|---|---|
| 1001 | 1 | 1299.99 | 2025-08-14 |
| 1002 | 1 | 799.00 | 2025-12-03 |
| 1003 | 1 | 499.00 | 2026-02-10 |
| 1004 | 1 | 135.00 | 2026-04-20 |
| 1005 | 2 | 459.00 | 2025-09-02 |
| 1006 | 2 | 179.98 | 2026-01-18 |
| 1007 | 2 | 799.00 | 2026-03-30 |
| 1008 | 3 | 1299.99 | 2026-02-05 |
| 1009 | 3 | 249.50 | 2026-04-11 |
| 1010 | 3 | 179.98 | 2026-04-28 |
-- Filtrer sur l'année en cours
SELECT order_id,
customer_id,
order_amount,
order_date
FROM main.sales.orders
WHERE order_date >= '2026-01-01';
-- (20 résultats)
-- Filtrer sur l'année en cours ET les montants élevés
SELECT order_id,
customer_id,
order_amount,
order_date
FROM main.sales.orders
WHERE order_date >= '2026-01-01'
AND order_amount > 500;
-- (4 résultats)
2.2 Analyzing query execution and performance characteristics
Result caching — key performance mechanism
Databricks SQL automatically caches query results. This mechanism is one of the main performance advantages.
How it works:
- First run — Databricks reads from Delta tables, calculates the results, returns them to the screen AND automatically caches them.
- Second execution of the same query — Databricks recognizes the identical query and returns the cached results instead of rereading the Delta tables → 6x faster.
- Slightly modified query — even if only one alias changes, Databricks executes it from scratch (cache miss).
Concrete example observed:
-- Première exécution : ~0.3 secondes
SELECT *
FROM main.sales.products;
-- Deuxième exécution (identique) : ~0.05 secondes (cache hit)
SELECT *
FROM main.sales.products;
-- Requête légèrement différente : ~6 secondes (cache miss)
SELECT product_id,
product_name AS name,
price
FROM main.sales.products;
Important rule: Caching works on requests identical up to the character. Any change — even a column alias — causes a cache miss and a complete execution.
The Serverless SQL Warehouse
Behind the queries is a SQL warehouse, the calculation engine that processes the SQL. In Databricks SQL, Serverless SQL warehouse is the recommended type:
- It boots in seconds (not minutes like traditional clusters)
- It automatically adapts to the load
- It supports automatic sleep after a period of inactivity
Execution architecture:
Requête SQL (SQL editor)
↓
SQL Warehouse (moteur de calcul)
↓
Delta Tables (stockage dans le lakehouse)
↓
Results pane (résultats affichés)
↓
Result Cache (stockage automatique des résultats)
Query Profile — performance analysis
After each execution, the Query Profile in the results pane exposes:
- The physical execution plan
- Time spent in each step
- The number of rows read and processed
- Delta files read
- Whether the cache was used or not
2.3 Navigating the query editor and results pane
SQL Editor interface
The Databricks SQL Editor is made up of four main areas:
| Area | Description |
|---|---|
| Query pane (top) | SQL writing area |
| Results pane (bottom) | Showing results after execution |
| Schema browser (left) | Exploration of available catalogs, schemas, tables and columns |
| Query tabs (top) | Managing multiple requests simultaneously |
Editor Features
- Syntax highlighting — SQL keywords (
SELECT,FROM,WHERE) are colored - Autocomplete — when entering
FROM main., the editor suggests the available schemas; an extra dot shows the tables (customers,orders,products) - These features reduce typos and help discover available items on the fly
Results pane interactivity
The results pane isn’t just a display — it’s an interactive tool for understanding the data:
- Click on a column header — sort ascending on first click, sort descending on second
- Example: click on
order_amount→ sort ascending (44.99 on top) → click again → sort descending (1299.99 on top) - No additional SQL required for this initial exploration
Recommended iterative approach
-- Étape 1 : voir la forme des données
SELECT *
FROM main.sales.orders;
-- → 25 commandes, 7 colonnes
-- Étape 2 : affiner les colonnes d'intérêt
SELECT order_id,
customer_id,
order_amount,
order_date
FROM main.sales.orders;
-- Étape 3 : ajouter des filtres
SELECT order_id,
customer_id,
order_amount,
order_date
FROM main.sales.orders
WHERE order_date >= '2026-01-01';
Principle: Start broad, look at the results, then refine. It’s a natural exploration loop, and the SQL editor is designed for that.
2.4 Organizing and saving queries in workspaces
Save a query
A query not saved in a tab disappears when closing the browser. To keep it:
- Click Save in the toolbar
- Give a clear and descriptive name (e.g.
High Value Ordersrather thanUntitled query) - The query is now in the personal workspace and can be reopened, edited, re-run
Example of saved queries:
-- Requête "High Value Orders" (seuil initial : 500)
SELECT order_id,
customer_id,
order_amount,
order_date
FROM main.sales.orders
WHERE order_amount > 500;
-- Requête "New Customers 2026"
SELECT first_name,
last_name,
email,
registration_date
FROM main.sales.customers
WHERE registration_date >= '2026-01-01';
Result of “New Customers 2026”:
| first_name | last_name | registration_date | |
|---|---|---|---|
| MICHAEL | CHEN | m.chen@example.com | 2026-01-15 |
| Emily | Davis | emily.davis@example.com | 2026-02-03 |
| Olivia | martinez | olivia.m@example.com | 2026-01-28 |
| Sophie | Brown | sophia.b@example.com | 2026-03-09 |
| Ava | Thomas | NULL | 2026-02-22 |
| Isabella | Lee | isabella.lee@example.com | 2026-03-25 |
Version History
Each saved change is saved automatically. Example with High Value Orders:
-- Version 1 : seuil à 500 → 6 résultats
SELECT order_id,
customer_id,
order_amount,
order_date
FROM main.sales.orders
WHERE order_amount > 500;
-- Version 2 : seuil monté à 750 → 5 résultats (la commande à 549 disparaît)
SELECT order_id,
customer_id,
order_amount,
order_date
FROM main.sales.orders
WHERE order_amount > 750;
To access the history: click on the name of the query → Version history → restore any previous version in one click.
Advantage: You can experiment freely without fear of losing a version that worked.
Query sharing
Via the Share button, you can add team members by email and grant them view or edit access. Once shared, the query appears in their workspace. No more need for each member to reconstruct what someone else has already written.
3. Writing SQL queries for analysis
3.1 Retrieving data with SELECT and WHERE conditions
Basic structure of an SQL query
SELECT colonnes -- obligatoire : spécifie les colonnes à retourner
FROM table -- obligatoire : spécifie la table source
WHERE conditions; -- optionnel : filtre les lignes
SELECT and FROM are required. WHERE is optional but it is what transforms a raw data dump into a precise response.
Simple comparison operators
The simplest WHERE filters use comparison operators. The pattern is always: column operator value. They work with numbers, dates and text.
| Operator | Meaning |
|---|---|
= | Equal |
> | Greater than |
< | Less than |
>= | Greater than or equal |
<= | Less than or equal |
<> or != | Different from |
-- Égalité sur du texte : trouver le client Smith
SELECT first_name,
last_name,
email
FROM main.sales.customers
WHERE last_name = 'Smith';
| first_name | last_name | |
|---|---|---|
| John | Smith | john.smith@example.com |
-- Supérieur ou égal sur une date : clients de cette année
SELECT first_name,
last_name,
email,
registration_date
FROM main.sales.customers
WHERE registration_date >= '2026-01-01';
Specialized operators
BETWEEN — range of values (including limits)
-- Produits entre 100 et 500 euros
SELECT product_name,
category,
price
FROM main.sales.products
WHERE price BETWEEN 100 AND 500;
| product_name | category | price |
|---|---|---|
| Office Chair | Furniture | 249.50 |
| Standing Desk | Furniture | 459.00 |
IN — match with a list of values
-- Produits dans deux catégories
SELECT product_name,
category,
price
FROM main.sales.products
WHERE category IN ('Electronics','Furniture');
| product_name | category | price |
|---|---|---|
| Laptop | Electronics | 1299.99 |
| Office Chair | Furniture | 249.50 |
| Smartphone | Electronics | 799.00 |
| Desk Lamp | Furniture | 89.99 |
| Wireless Mouse | Electronics | 45.00 |
| Standing Desk | Furniture | 459.00 |
IS NULL — missing values
-- Produits sans catégorie assignée
SELECT product_name,
category,
price
FROM main.sales.products
WHERE category IS NULL;
| product_name | category | price |
|---|---|---|
| Coffee Mug | NULL | 15.00 |
Warning: Never use
= NULL. In SQL, NULL cannot be compared with=. Always useIS NULLorIS NOT NULL.
LIKE — pattern matching
-- Produits dont le nom commence par 'S'
SELECT product_name,
category,
price
FROM main.sales.products
WHERE product_name LIKE 'S%';
| product_name | category | price |
|---|---|---|
| Smartphone | Electronics | 799.00 |
| Standing Desk | Furniture | 459.00 |
| Sketchpad | Stationery | 22.75 |
Wildcards:
%matches any zero or more characters._matches exactly any character.
Combination of conditions: AND, OR, parentheses
OR — inclusive: at least one condition is true
-- Commandes de haute valeur OU en attente
SELECT order_id,
customer_id,
order_amount,
status
FROM main.sales.orders
WHERE order_amount > 500
OR status = 'pending';
-- 9 résultats
AND — restrictive: all conditions must be true
-- Commandes de haute valeur ET en attente
SELECT order_id,
customer_id,
order_amount,
status
FROM main.sales.orders
WHERE order_amount > 500
AND status = 'pending';
| order_id | customer_id | order_amount | status |
|---|---|---|---|
| 1007 | 2 | 799.00 | pending |
| 1021 | 9 | 549.00 | pending |
Parenthesis — evaluation priority control
-- Commandes de haute valeur ET (en attente OU en traitement)
SELECT order_id,
customer_id,
order_amount,
status
FROM main.sales.orders
WHERE order_amount > 500
AND (status = 'pending' OR status = 'processing');
Important rule: Without parentheses,
ANDis evaluated beforeOR. Parentheses make the intention explicit and avoid surprises.
3.2 Improving query readability with aliases, sorting, and expressions
Column aliases with AS
Database column names (order_amount, customer_id) are suitable for developers but confusing for others. The AS alias allows you to assign a display name to any column.
-- Noms de colonnes bruts (peu lisibles)
SELECT order_id,
customer_id,
order_amount,
order_date
FROM main.sales.orders;
-- Avec des aliases lisibles
SELECT order_id AS `Order ID`,
customer_id AS `Customer ID`,
order_amount AS `Amount`,
order_date AS `Date`
FROM main.sales.orders;
Syntax: Aliases with spaces must be enclosed in backticks (
`) or single quotes. Single-word aliases work without quotes, but using quotes consistently is recommended.
Important: Aliases do not change anything in the underlying table. They only rename columns in the query output.
Sorting results with ORDER BY
Without ORDER BY, the order of results is not guaranteed. A change in data or an execution plan could rearrange them differently tomorrow.
-- Tri par date décroissant : les plus récents en premier
SELECT order_id AS `Order ID`,
customer_id AS `Customer ID`,
order_amount AS `Amount`,
order_date AS `Date`
FROM main.sales.orders
ORDER BY order_date DESC;
-- Tri multi-colonnes : statut A→Z, puis montant du plus grand au plus petit
SELECT order_id AS `Order ID`,
status AS `Status`,
order_amount AS `Amount`
FROM main.sales.orders
ORDER BY status ASC, order_amount DESC;
Partial result:
| OrderID | Status | Amount |
|---|---|---|
| 1025 | canceled | 465.00 |
| 1010 | canceled | 179.98 |
| 1014 | canceled | 44.99 |
| 1001 | completed | 1299.99 |
| 1008 | completed | 1299.99 |
| 1002 | completed | 799.00 |
Expressions calculated in SELECT
Expressions allow you to create derived columns on the fly, without modifying the data.
Arithmetic calculations:
-- Calcul de taxe et total avec taxe
SELECT order_id AS `Order ID`,
order_amount AS `Subtotal`,
order_amount * 0.10 AS `Tax`,
order_amount * 1.10 AS `Total with Tax`
FROM main.sales.orders
ORDER BY order_amount DESC;
| OrderID | Subtotal | Tax | Total with Tax |
|---|---|---|---|
| 1001 | 1299.99 | 130.00 | 1429.99 |
| 1008 | 1299.99 | 130.00 | 1429.99 |
| 1002 | 799.00 | 79.90 | 878.90 |
-- Expressions chaînées avec parenthèses
SELECT order_id AS `Order ID`,
order_amount AS `Original Amount`,
order_amount - 50 AS `After Discount`,
(order_amount - 50) * 1.10 AS `After Discount with Tax`
FROM main.sales.orders
ORDER BY order_amount DESC;
| OrderID | Low Price Amount | After Discount | After Discount with Tax |
|---|---|---|---|
| 1001 | 1299.99 | 1249.99 | 1374.99 |
| 1002 | 799.00 | 749.00 | 823.90 |
String concatenation:
-- Nom complet depuis deux colonnes (opérateur || )
SELECT customer_id AS `Customer ID`,
first_name || ' ' || last_name AS `Full Name`,
email AS `Email`
FROM main.sales.customers
ORDER BY last_name ASC, first_name ASC;
| Customer ID | Full Name | |
|---|---|---|
| 7 | JAMES Anderson | james.a@example.com |
| 8 | Sophia Brown | sophia.b@example.com |
| 3 | MICHAEL CHEN | m.chen@example.com |
| 4 | Emily Davis | emily.davis@example.com |
Note: The
||operator is equivalent toCONCAT(). Both syntaxes work in Databricks SQL. Aliases are essential on calculated expressions — without them, the header displays the raw formula.
3.3 Aggregating data with GROUP BY and HAVING
Aggregate functions — the fundamental five
An aggregate function takes multiple rows and returns a single summary value. It answers the questions: how many?, how many total?, what is the average?, what is the smallest/largest?
-- COUNT(*) : effondre toute la table en un seul nombre
SELECT COUNT(*) AS `Total Orders`
FROM main.sales.orders;
-- Résultat : 25
-- Les cinq agrégats côte à côte
SELECT COUNT(*) AS `Total Orders`,
SUM(order_amount) AS `Total Revenue`,
AVG(order_amount) AS `Average Order`,
MIN(order_amount) AS `Smallest Order`,
MAX(order_amount) AS `Largest Order`
FROM main.sales.orders;
| Total Orders | Total Revenue | Average Order | Smallest Order | Largest Order |
|---|---|---|---|---|
| 25 | 10346.41 | 413.86 | 44.99 | 1299.99 |
The three variants of COUNT
-- COUNT(*) vs COUNT(colonne) vs COUNT(DISTINCT colonne)
SELECT COUNT(*) AS `Total Customers`,
COUNT(email) AS `With Email`,
COUNT(DISTINCT email) AS `Unique Emails`
FROM main.sales.customers;
| Total Customers | With Email | Unique Emails |
|---|---|---|
| 12 | 11 | 10 |
Interpretation:
COUNT(*)= 12: counts all linesCOUNT(email)= 11: Ava Thomas has no email (NULL ignored)COUNT(DISTINCT email)= 10: sarah johnson and liam taylor share the same address (sarah.j@example.com)
Rule:
COUNT(*)counts all lines.COUNT(column)ignores NULLs.COUNT(DISTINCT column)ignores NULLs AND duplicates.
GROUP BY — aggregation by category
-- Par statut de commande : une ligne par statut avec métriques
SELECT status AS `Status`,
COUNT(*) AS `Order Count`,
SUM(order_amount) AS `Revenue`
FROM main.sales.orders
GROUP BY status;
| Status | Order Count | Revenue |
|---|---|---|
| canceled | 3 | 689.97 |
| completed | 15 | 7292.45 |
| pending | 5 | 1889.49 |
| processing | 2 | 474.50 |
-- Par client : les plus gros dépensiers en premier
SELECT customer_id AS `Customer`,
COUNT(*) AS `Orders`,
SUM(order_amount) AS `Total Spent`,
AVG(order_amount) AS `Avg Order`
FROM main.sales.orders
GROUP BY customer_id
ORDER BY SUM(order_amount) DESC;
| Customer | Orders | Total Spent | AvgOrder |
|---|---|---|---|
| 1 | 4 | 2732.99 | 683.25 |
| 3 | 3 | 1729.47 | 576.49 |
| 2 | 3 | 1437.98 | 479.33 |
| 6 | 2 | 960.50 | 480.25 |
| 12 | 2 | 924.00 | 462.00 |
| 7 | 3 | 788.99 | 263.00 |
| 4 | 2 | 684.00 | 342.00 |
| 9 | 2 | 638.99 | 319.50 |
| 10 | 2 | 315.00 | 157.50 |
| 5 | 2 | 134.49 | 67.25 |
HAVING — filter groups
WHERE filters individual rows before grouping. HAVING filters groups after aggregation. You cannot use WHERE with an aggregate function.
-- HAVING seul : clients avec total > 1000
SELECT customer_id AS `Customer`,
COUNT(*) AS `Orders`,
SUM(order_amount) AS `Total Spent`
FROM main.sales.orders
GROUP BY customer_id
HAVING SUM(order_amount) > 1000
ORDER BY SUM(order_amount) DESC;
| Customer | Orders | Total Spent |
|---|---|---|
| 1 | 4 | 2732.99 |
| 3 | 3 | 1729.47 |
| 2 | 3 | 1437.98 |
-- WHERE + GROUP BY + HAVING combinés
-- Commandes complétées seulement, clients avec total > 500
SELECT customer_id AS `Customer`,
COUNT(*) AS `Completed Orders`,
SUM(order_amount) AS `Completed Revenue`
FROM main.sales.orders
WHERE status = 'completed'
GROUP BY customer_id
HAVING SUM(order_amount) > 500
ORDER BY SUM(order_amount) DESC;
| Customer | Completed Orders | Completed Revenue |
|---|---|---|
| 1 | 3 | 2597.99 |
| 3 | 1 | 1299.99 |
| 6 | 1 | 799.00 |
| 4 | 2 | 684.00 |
| 2 | 2 | 638.98 |
| 7 | 2 | 544.00 |
Logical execution order of an SQL query:
FROM → 1. Identifier la table source
WHERE → 2. Filtrer les lignes individuelles
GROUP BY → 3. Regrouper les lignes restantes
HAVING → 4. Filtrer les groupes résultants
SELECT → 5. Calculer les expressions et renommer les colonnes
ORDER BY → 6. Trier le résultat final
3.4 Combining tables with JOIN operations
The problem and the solution
Interesting data lives in connections between tables. Customer names are in customers, their orders in orders. A JOIN matches rows from two tables based on a shared column.
INNER JOIN — only matching rows
-- INNER JOIN : version longue avec noms de tables complets
SELECT customers.first_name AS `First Name`,
customers.last_name AS `Last Name`,
orders.order_id AS `Order ID`,
orders.order_amount AS `Amount`,
orders.order_date AS `Date`
FROM main.sales.customers
INNER JOIN main.sales.orders ON customers.customer_id = orders.customer_id
ORDER BY orders.order_date DESC;
-- INNER JOIN : version avec aliases de tables (syntaxe préférée)
SELECT c.first_name AS `First Name`,
c.last_name AS `Last Name`,
o.order_id AS `Order ID`,
o.order_amount AS `Amount`,
o.order_date AS `Date`
FROM main.sales.customers c
INNER JOIN main.sales.orders o ON c.customer_id = o.customer_id
ORDER BY o.order_date DESC;
Partial result:
| First Name | Last Name | OrderID | Amount | Date |
|---|---|---|---|---|
| MICHAEL | CHEN | 1010 | 179.98 | 2026-04-28 |
| JAMES | Anderson | 1019 | 244.99 | 2026-04-25 |
| Isabella | Lee | 1025 | 465.00 | 2026-04-22 |
| John | Smith | 1004 | 135.00 | 2026-04-20 |
Key behavior of the INNER JOIN: 25 rows returned = as many as orders. Customers without orders (Sophia Brown, Noah GARCIA) do not appear. Customers with multiple orders (John Smith, MICHAEL CHEN) appear multiple times. Only rows that have a match in both tables are returned.
LEFT JOIN — keep all left records
-- LEFT JOIN : garde les clients sans commandes (NULLs côté droit)
SELECT c.first_name AS `First Name`,
c.last_name AS `Last Name`,
o.order_id AS `Order ID`,
o.order_amount AS `Amount`
FROM main.sales.customers c
LEFT JOIN main.sales.orders o ON c.customer_id = o.customer_id
ORDER BY o.order_amount DESC;
Anti-join pattern — customers who never ordered:
-- Clients qui n'ont jamais passé de commande
SELECT c.first_name AS `First Name`,
c.last_name AS `Last Name`
FROM main.sales.customers c
LEFT JOIN main.sales.orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;
| First Name | Last Name |
|---|---|
| Sophie | Brown |
| Noah | GARCIA |
Anti-join pattern:
LEFT JOIN+WHERE right_column IS NULL= find rows from the left table that have no matches in the right table.
FULL OUTER JOIN — preserve both sides
-- FULL OUTER JOIN : préserve les deux côtés sans correspondance
SELECT c.first_name AS `First Name`,
c.last_name AS `Last Name`,
o.order_id AS `Order ID`,
o.order_amount AS `Amount`
FROM main.sales.customers c
FULL OUTER JOIN main.sales.orders o ON c.customer_id = o.customer_id
ORDER BY c.last_name;
Partial result:
| First Name | Last Name | OrderID | Amount |
|---|---|---|---|
| JAMES | Anderson | 1017 | 499.00 |
| Sophie | Brown | NULL | NULL |
| MICHAEL | CHEN | 1008 | 1299.99 |
| Noah | GARCIA | NULL | NULL |
JOIN on three tables
-- Three-table join : customers + orders + products
SELECT c.first_name || ' ' || c.last_name AS `Customer`,
p.product_name AS `Product`,
o.quantity AS `Quantity`,
o.order_amount AS `Amount`,
o.order_date AS `Date`
FROM main.sales.customers c
INNER JOIN main.sales.orders o ON c.customer_id = o.customer_id
INNER JOIN main.sales.products p ON o.product_id = p.product_id
ORDER BY o.order_date DESC;
Partial result:
| Customer | Product | Quantity | Amount | Date |
|---|---|---|---|---|
| MICHAEL CHEN | Desk Lamp | 2 | 179.98 | 2026-04-28 |
| JAMES Anderson | Desk Lamp | 1 | 244.99 | 2026-04-25 |
| Isabella Lee | Laptop | 1 | 465.00 | 2026-04-22 |
| John Smith | Wireless Mouse | 3 | 135.00 | 2026-04-20 |
Summary of JOIN types
| Type | Behavior | Use cases |
|---|---|---|
INNER JOIN | Only rows with matches in both tables | Linked data, no interest in non-matches |
LEFT JOIN | All rows in left table + right matches (NULL if absent) | Keep all master records even without linked data |
RIGHT JOIN | All rows in right table + left matches | Less common, often replaced by a reverse LEFT JOIN |
FULL OUTER JOIN | All rows in both tables, NULLs for no matches | Diagnose data inconsistencies |
CROSS JOIN | Cartesian product of all combinations | Exhaustive combinations, benchmarks |
3.5 Transforming data with built-in SQL functions
Text functions
UPPER() and LOWER() — case normalization
SELECT first_name,
UPPER(first_name) AS `Uppercase`,
LOWER(first_name) AS `Lowercase`
FROM main.sales.customers;
| first_name | Uppercase | Lowercase |
|---|---|---|
| John | JOHN | john |
| Sarah | SARAH | Sarah |
| MICHAEL | MICHAEL | michael |
| Emily | EMILY | emily |
Important use case: without case normalization,
'Smith'and'SMITH'would be treated as different values in comparisons and groupings.
CONCAT() and text extraction
-- CONCAT + SUBSTRING : extraire la partie avant le @ dans un email
SELECT CONCAT(first_name, ' ', last_name) AS `Full Name`,
substr(email, 1, instr(email,'@') - 1) AS `Email Username`
FROM main.sales.customers
WHERE email IS NOT NULL;
| Full Name | Email Username |
|---|---|
| John Smith | john.smith |
| sarah johnson | sarah.j |
| MICHAEL CHEN | m.chen |
| Emily Davis | emily.davis |
| david WILSON | d.wilson |
Explanation:
instr(email, '@')finds the position of the@symbol.substr(email, 1, position-1)extracts everything from the beginning to the character before@.
TRIM() — remove invisible spaces
SELECT TRIM(first_name) AS `Trimmed Name`
FROM main.sales.customers;
Why it’s critical: leading/trailing spaces are not visible but cause silent problems: joins fail, groupings split into unexpected categories, comparisons return false.
Numeric functions
ROUND(), CEIL(), FLOOR() — rounded
-- Trois comportements d'arrondi côte à côte
SELECT order_amount AS `Raw Amount`,
ROUND(order_amount, 0) AS `Rounded`,
CAST((order_amount + 0.999999) AS INTEGER) AS `Rounded Up`,
CAST(order_amount AS INTEGER) AS `Rounded Down`
FROM main.sales.orders
ORDER BY order_amount DESC;
| Raw Amount | Rounded | Rounded Up | Rounded Down |
|---|---|---|---|
| 1299.99 | 1300.00 | 1300 | 1299 |
| 799.00 | 799.00 | 799 | 799 |
| 249.50 | 250.00 | 250 | 249 |
ABS() — absolute value
-- Distance par rapport à 500, triée par proximité
SELECT order_amount AS `Amount`,
order_amount - 500 AS `Difference from 500`,
ABS(order_amount - 500) AS `Distance from 500`
FROM main.sales.orders
ORDER BY ABS(order_amount - 500);
| Amount | Difference from 500 | Distance from 500 |
|---|---|---|
| 499.00 | -1.00 | 1.00 |
| 499.00 | -1.00 | 1.00 |
| 465.00 | -35.00 | 35.00 |
| 459.00 | -41.00 | 41.00 |
Date functions
DATEDIFF() — number of days between two dates
-- Jours écoulés depuis chaque commande
SELECT order_id AS `Order ID`,
order_date AS `Order Date`,
DATE('2026-05-07') AS `Today`,
DATEDIFF('2026-05-07', order_date) AS `Days Since Order`
FROM main.sales.orders
ORDER BY order_date DESC;
| OrderID | Order Date | Today | Days Since Order |
|---|---|---|---|
| 1010 | 2026-04-28 | 2026-05-07 | 9 |
| 1019 | 2026-04-25 | 2026-05-07 | 12 |
| 1025 | 2026-04-22 | 2026-05-07 | 15 |
| 1004 | 2026-04-20 | 2026-05-07 | 17 |
DATE_FORMAT() — format a date
-- Forme lisible et forme de regroupement
SELECT order_id AS `Order ID`,
order_date AS `Raw Date`,
DATE_FORMAT(order_date, 'MMMM d, yyyy') AS `Formatted Date`,
DATE_FORMAT(order_date, 'yyyy-MM') AS `Year-Month`
FROM main.sales.orders
ORDER BY order_date DESC;
| OrderID | Raw Date | Formatted Date | Year-Month |
|---|---|---|---|
| 1010 | 2026-04-28 | 2026 April | 2026-04 |
| 1007 | 2026-03-30 | 2026 March | 2026-03 |
| 1015 | 2026-02-20 | 2026 February | 2026-02 |
4. Organizing data for repeatable workflows
4.1 Browsing catalogs, schemas, and tables in Unity Catalog
The three-level namespace
Each table in Databricks lives at a specific address: catalog.schema.table. You used this three-part path in every query in this course (main.sales.customers, main.sales.orders). Here is what these three levels mean:
catalog ← niveau le plus haut (département ou limite de projet)
└── schema ← organisation par thème ou domaine
├── table_1 ← données réelles
├── table_2
└── view_1
| Level | Role | Example |
|---|---|---|
| Catalog | Groups related datasets. Think in terms of department or project scope | main |
| Diagram | Organizes tables by theme or area | sales, marketing |
| Table / View | Contains actual data | customers, orders, products |
This three-tier structure is part of Unity Catalog, the governance layer that controls who can access what. Writing
main.sales.customersspecifies exactly where this table is located in the hierarchy — no ambiguity.
Explore visually with Data Explorer
- Click on the Data icon in the left sidebar of the SQL editor
- A list of catalogs appears — expand
main - Schemas appear — expand
sales - The tables appear:
customers,orders,products - Click on
customers: the columns and their data type are displayed - Sample Data tab: see the first lines without writing SQL
Explore with DESCRIBE
-- Afficher la structure (colonnes, types, commentaires) d'une table
DESCRIBE main.sales.customers;
| col_name | data_type | how |
|---|---|---|
| customer_id | int | NULL |
| first_name | thong | NULL |
| last_name | thong | NULL |
| thong | NULL | |
| registration_date | date | NULL |
DESCRIBEis particularly useful in environments where the GUI is not available, or when you want to document the structure of a table in a query.
4.2 Creating and managing views for organized query logic
Two types of views in Databricks SQL
A view is a saved query that you reference by name, as a shortcut to a specific slice of your data. Databricks supports two types:
| Characteristic | Temporary View | Permanent View |
|---|---|---|
| Lifespan | Current session only, disappears when disconnected | Persists in catalog until explicit DROP |
| Visibility | Only in your session | Accessible to all users with access to the schema |
| Use cases | Exploration, work sessions, no cleanup required | Reusable logic tomorrow, next week, by the team |
| Syntax | CREATE OR REPLACE TEMPORARY VIEW name AS ... | CREATE OR REPLACE VIEW catalog.schema.name AS ... |
Temporary views
-- Créer une vue temporaire pour les clients à haute valeur
CREATE OR REPLACE TEMPORARY VIEW high_value_customers AS
SELECT c.customer_id,
c.first_name,
c.last_name,
SUM(o.order_amount) AS total_spent
FROM main.sales.customers c
JOIN main.sales.orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.first_name, c.last_name
HAVING SUM(o.order_amount) > 500;
-- (view created — aucune ligne retournée)
-- Interroger la vue : tous les clients au-dessus de 500
SELECT *
FROM high_value_customers
ORDER BY total_spent DESC;
| customer_id | first_name | last_name | total_spent |
|---|---|---|---|
| 1 | John | Smith | 2732.99 |
| 3 | MICHAEL | CHEN | 1729.47 |
| 2 | Sarah | Johnson | 1437.98 |
| 6 | Olivia | martinez | 960.50 |
| 12 | Isabella | Lee | 924.00 |
| 7 | JAMES | Anderson | 788.99 |
| 4 | Emily | Davis | 684.00 |
| 9 | liam | taylor | 638.99 |
-- Réutiliser la vue pour un COUNT
SELECT COUNT(*) AS high_value_count
FROM high_value_customers;
-- Résultat : 8
-- Réutiliser la vue pour une moyenne
SELECT ROUND(AVG(total_spent), 2) AS avg_high_value_spend
FROM high_value_customers;
-- Résultat : 1237.12
Permanent views
-- Créer une vue permanente dans le catalog
CREATE OR REPLACE VIEW main.sales.high_value_customers AS
SELECT c.customer_id,
c.first_name,
c.last_name,
SUM(o.order_amount) AS total_spent
FROM main.sales.customers c
JOIN main.sales.orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.first_name, c.last_name
HAVING SUM(o.order_amount) > 500;
-- Mettre à jour la vue permanente (changer le seuil à 1000)
CREATE OR REPLACE VIEW main.sales.high_value_customers AS
SELECT c.customer_id,
c.first_name,
c.last_name,
SUM(o.order_amount) AS total_spent
FROM main.sales.customers c
JOIN main.sales.orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.first_name, c.last_name
HAVING SUM(o.order_amount) > 1000;
-- Après CREATE OR REPLACE avec seuil 1000 : moins de clients
SELECT *
FROM main.sales.high_value_customers
ORDER BY total_spent DESC;
| customer_id | first_name | last_name | total_spent |
|---|---|---|---|
| 1 | John | Smith | 2732.99 |
| 3 | MICHAEL | CHEN | 1729.47 |
| 2 | Sarah | Johnson | 1437.98 |
-- Supprimer une vue permanente
DROP VIEW IF EXISTS main.sales.high_value_customers;
-- Supprimer une vue temporaire
DROP VIEW IF EXISTS high_value_customers;
Behavior of
CREATE OR REPLACE VIEW: if the view does not exist, it is created. If it already exists, it is replaced without error. No more need forDROPthenCREATE. This is the recommended pattern.
4.3 Simplifying complex queries with CTEs and subqueries
Two tools for organizing logic inside a query
| Tool | Description | Use cases |
|---|---|---|
| Subquery | Query nested inside another (SELECT, FROM, WHERE, HAVING) | Fast online response, logic not reused elsewhere |
| CTE (Common Table Expression) | Named query blocks defined with WITH before the main query | Complex multi-step logic, readability, maintenance |
Difference with views: subqueries and CTEs disappear at the end of query execution. No session persistence, no catalog entry, no cleanup required.
Nested subqueries
-- Clients dont le total dépasse la moyenne des totaux de tous les clients
SELECT c.first_name,
c.last_name,
SUM(o.order_amount) AS total_spent
FROM main.sales.customers c
JOIN main.sales.orders o ON c.customer_id = o.customer_id
GROUP BY c.first_name, c.last_name
HAVING SUM(o.order_amount) > (SELECT AVG(total)
FROM (SELECT SUM(order_amount) AS total
FROM main.sales.orders
GROUP BY customer_id)
)
ORDER BY total_spent DESC;
| first_name | last_name | total_spent |
|---|---|---|
| John | Smith | 2732.99 |
| MICHAEL | CHEN | 1729.47 |
| Sarah | Johnson | 1437.98 |
Logic breakdown:
- The innermost subquery calculates the total per customer:
SUM(order_amount) GROUP BY customer_id - The external subquery calculates the average of these totals:
AVG(total) - The external query filters customers whose total exceeds this average
CTEs — Common Table Expressions
The same logic, written as a multi-step CTE for better readability:
-- CTE en deux étapes : customer lifetime value vs. moyenne
WITH customer_totals AS (
SELECT c.customer_id,
c.first_name,
c.last_name,
SUM(o.order_amount) AS total_spent,
COUNT(o.order_id) AS order_count
FROM main.sales.customers c
JOIN main.sales.orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.first_name, c.last_name
),
avg_spending AS (
SELECT ROUND(AVG(total_spent), 2) AS avg_total
FROM customer_totals
)
SELECT ct.first_name,
ct.last_name,
ct.total_spent,
ct.order_count,
a.avg_total,
ROUND(ct.total_spent - a.avg_total, 2) AS above_average_by
FROM customer_totals ct
CROSS JOIN avg_spending a
WHERE ct.total_spent > a.avg_total
ORDER BY ct.total_spent DESC;
| first_name | last_name | total_spent | order_count | avg_total | above_average_by |
|---|---|---|---|---|---|
| John | Smith | 2732.99 | 4 | 1034.64 | 1698.35 |
| MICHAEL | CHEN | 1729.47 | 3 | 1034.64 | 694.83 |
| Sarah | Johnson | 1437.98 | 3 | 1034.64 | 403.34 |
Structure of a CTE:
WITH nom_cte_1 AS (
-- requête 1
),
nom_cte_2 AS (
-- requête 2 (peut référencer nom_cte_1)
)
SELECT ...
FROM nom_cte_1 ct
JOIN nom_cte_2 a ON ...
Advantages of CTE vs. Subquery:
- Reads from top to bottom like a plan, each step is named
- Each block can be tested independently
- Less nesting = fewer errors
- The same CTE may be referenced multiple times in the same query
4.4 Managing query history and tracking exploration progress
Query History — automatically saves everything
Unlike saved queries (which require explicitly clicking Save), Query History captures everything you run, including throwaway experiments and happy discoveries that you didn’t think to save.
Access: Click on Query History in the left sidebar.
Each entry displays:
- The full SQL text (first lines)
- Execution timestamp
- Execution time
- Status (success or failure)
- The warehouse that processed it
History filtering
| Filter | Utility |
|---|---|
| By user | Useful in a shared workspace to only see your own queries |
| By date range | Find something executed last week |
| By status | Find only failed queries for debugging |
Open a query from history
- Click on an entry in the history (eg: the CTE of the previous clip)
- The full SQL text is displayed, with execution details (duration, rows returned, warehouse)
- Click on Open in Editor → the query loads in a new tab, ready to be relaunched or modified
History value for long crawl sessions
During a long exploration session — where you start with a vague question and end up somewhere unexpected — history becomes particularly valuable:
- You try a filter, adjust a threshold, obtain a surprising result
- You want to return to a previous query that returned something interesting
- Without history, you would have to remember exactly what you typed
- With the history, you find and repropose any stage of the exploration
Practical tip: Combine Query History (find past work) with Saved Queries (share validated work with the team) and Views (encapsulate reusable logic forever).
5. Demo Database Schema
main.sales
├── customers
│ ├── customer_id INT (PK)
│ ├── first_name STRING
│ ├── last_name STRING
│ ├── email STRING (peut être NULL, peut être dupliqué)
│ └── registration_date DATE
│
├── products
│ ├── product_id INT (PK)
│ ├── product_name STRING
│ ├── category STRING (peut être NULL)
│ └── price DECIMAL
│
└── orders
├── order_id INT (PK)
├── customer_id INT (FK → customers)
├── order_date DATE
├── product_id INT (FK → products)
├── quantity INT
├── order_amount DECIMAL
└── status STRING ('completed', 'pending', 'processing', 'cancelled')
Relationships:
orders.customer_id→customers.customer_idorders.product_id→products.product_id
Product categories: Electronics, Furniture, Stationery (+ NULL) Order statuses: completed (15), pending (5), processing (2), canceled (3)
6. Complete demo query reference
Module 1 — Fundamental queries
-- CLIP 1.1 — Première exploration de la table customers
SELECT *
FROM main.sales.customers;
-- CLIP 1.1 — Sélection de colonnes précises
SELECT first_name,
last_name,
email
FROM main.sales.customers;
-- CLIP 1.2 — Table products (démonstration du caching)
SELECT *
FROM main.sales.products;
-- CLIP 1.2 — Requête légèrement différente (cache miss)
SELECT product_id,
product_name AS name,
price
FROM main.sales.products;
-- CLIP 1.3 — Table orders complète
SELECT *
FROM main.sales.orders;
-- CLIP 1.3 — Colonnes sélectionnées
SELECT order_id,
customer_id,
order_amount,
order_date
FROM main.sales.orders;
-- CLIP 1.3 — Filtre sur l'année en cours
SELECT order_id,
customer_id,
order_amount,
order_date
FROM main.sales.orders
WHERE order_date >= '2026-01-01';
-- CLIP 1.3 — Double filtre : cette année ET montant élevé
SELECT order_id,
customer_id,
order_amount,
order_date
FROM main.sales.orders
WHERE order_date >= '2026-01-01'
AND order_amount > 500;
-- CLIP 1.4 — Requête "New Customers 2026"
SELECT first_name,
last_name,
email,
registration_date
FROM main.sales.customers
WHERE registration_date >= '2026-01-01';
-- CLIP 1.4 — "High Value Orders" seuil 500
SELECT order_id,
customer_id,
order_amount,
order_date
FROM main.sales.orders
WHERE order_amount > 500;
-- CLIP 1.4 — "High Value Orders" seuil 750 (version mise à jour)
SELECT order_id,
customer_id,
order_amount,
order_date
FROM main.sales.orders
WHERE order_amount > 750;
Module 2 — Analysis queries
-- CLIP 2.1 — Opérateurs de filtre : égalité sur texte
SELECT first_name,
last_name,
email
FROM main.sales.customers
WHERE last_name = 'Smith';
-- CLIP 2.1 — Filtre sur date
SELECT first_name,
last_name,
email,
registration_date
FROM main.sales.customers
WHERE registration_date >= '2026-01-01';
-- CLIP 2.1 — BETWEEN : produits entre 100 et 500
SELECT product_name,
category,
price
FROM main.sales.products
WHERE price BETWEEN 100 AND 500;
-- CLIP 2.1 — IN : deux catégories
SELECT product_name,
category,
price
FROM main.sales.products
WHERE category IN ('Electronics','Furniture');
-- CLIP 2.1 — IS NULL : produits sans catégorie
SELECT product_name,
category,
price
FROM main.sales.products
WHERE category IS NULL;
-- CLIP 2.1 — LIKE : noms commençant par 'S'
SELECT product_name,
category,
price
FROM main.sales.products
WHERE product_name LIKE 'S%';
-- CLIP 2.1 — OR : haute valeur OU en attente
SELECT order_id,
customer_id,
order_amount,
status
FROM main.sales.orders
WHERE order_amount > 500
OR status = 'pending';
-- CLIP 2.1 — AND : haute valeur ET en attente
SELECT order_id,
customer_id,
order_amount,
status
FROM main.sales.orders
WHERE order_amount > 500
AND status = 'pending';
-- CLIP 2.1 — AND/OR avec parenthèses
SELECT order_id,
customer_id,
order_amount,
status
FROM main.sales.orders
WHERE order_amount > 500
AND (status = 'pending' OR status = 'processing');
-- CLIP 2.2 — Aliases de colonnes
SELECT order_id AS `Order ID`,
customer_id AS `Customer ID`,
order_amount AS `Amount`,
order_date AS `Date`
FROM main.sales.orders;
-- CLIP 2.2 — ORDER BY date décroissante
SELECT order_id AS `Order ID`,
customer_id AS `Customer ID`,
order_amount AS `Amount`,
order_date AS `Date`
FROM main.sales.orders
ORDER BY order_date DESC;
-- CLIP 2.2 — ORDER BY multi-colonnes
SELECT order_id AS `Order ID`,
status AS `Status`,
order_amount AS `Amount`
FROM main.sales.orders
ORDER BY status ASC, order_amount DESC;
-- CLIP 2.2 — Calcul de taxe
SELECT order_id AS `Order ID`,
order_amount AS `Subtotal`,
order_amount * 0.10 AS `Tax`,
order_amount * 1.10 AS `Total with Tax`
FROM main.sales.orders
ORDER BY order_amount DESC;
-- CLIP 2.2 — Expression chaînée avec parenthèses
SELECT order_id AS `Order ID`,
order_amount AS `Original Amount`,
order_amount - 50 AS `After Discount`,
(order_amount - 50) * 1.10 AS `After Discount with Tax`
FROM main.sales.orders
ORDER BY order_amount DESC;
-- CLIP 2.2 — Concaténation de chaînes
SELECT customer_id AS `Customer ID`,
first_name || ' ' || last_name AS `Full Name`,
email AS `Email`
FROM main.sales.customers
ORDER BY last_name ASC, first_name ASC;
-- CLIP 2.3 — COUNT(*)
SELECT COUNT(*) AS `Total Orders`
FROM main.sales.orders;
-- CLIP 2.3 — Cinq agrégats
SELECT COUNT(*) AS `Total Orders`,
SUM(order_amount) AS `Total Revenue`,
AVG(order_amount) AS `Average Order`,
MIN(order_amount) AS `Smallest Order`,
MAX(order_amount) AS `Largest Order`
FROM main.sales.orders;
-- CLIP 2.3 — Trois variantes de COUNT
SELECT COUNT(*) AS `Total Customers`,
COUNT(email) AS `With Email`,
COUNT(DISTINCT email) AS `Unique Emails`
FROM main.sales.customers;
-- CLIP 2.3 — GROUP BY statut
SELECT status AS `Status`,
COUNT(*) AS `Order Count`,
SUM(order_amount) AS `Revenue`
FROM main.sales.orders
GROUP BY status;
-- CLIP 2.3 — GROUP BY client, trié par total décroissant
SELECT customer_id AS `Customer`,
COUNT(*) AS `Orders`,
SUM(order_amount) AS `Total Spent`,
AVG(order_amount) AS `Avg Order`
FROM main.sales.orders
GROUP BY customer_id
ORDER BY SUM(order_amount) DESC;
-- CLIP 2.3 — HAVING : clients avec total > 1000
SELECT customer_id AS `Customer`,
COUNT(*) AS `Orders`,
SUM(order_amount) AS `Total Spent`
FROM main.sales.orders
GROUP BY customer_id
HAVING SUM(order_amount) > 1000
ORDER BY SUM(order_amount) DESC;
-- CLIP 2.3 — WHERE + GROUP BY + HAVING combinés
SELECT customer_id AS `Customer`,
COUNT(*) AS `Completed Orders`,
SUM(order_amount) AS `Completed Revenue`
FROM main.sales.orders
WHERE status = 'completed'
GROUP BY customer_id
HAVING SUM(order_amount) > 500
ORDER BY SUM(order_amount) DESC;
-- CLIP 2.4 — INNER JOIN avec noms de tables complets
SELECT customers.first_name AS `First Name`,
customers.last_name AS `Last Name`,
orders.order_id AS `Order ID`,
orders.order_amount AS `Amount`,
orders.order_date AS `Date`
FROM main.sales.customers
INNER JOIN main.sales.orders ON customers.customer_id = orders.customer_id
ORDER BY orders.order_date DESC;
-- CLIP 2.4 — INNER JOIN avec aliases de tables
SELECT c.first_name AS `First Name`,
c.last_name AS `Last Name`,
o.order_id AS `Order ID`,
o.order_amount AS `Amount`,
o.order_date AS `Date`
FROM main.sales.customers c
INNER JOIN main.sales.orders o ON c.customer_id = o.customer_id
ORDER BY o.order_date DESC;
-- CLIP 2.4 — LEFT JOIN : garder tous les clients
SELECT c.first_name AS `First Name`,
c.last_name AS `Last Name`,
o.order_id AS `Order ID`,
o.order_amount AS `Amount`
FROM main.sales.customers c
LEFT JOIN main.sales.orders o ON c.customer_id = o.customer_id
ORDER BY o.order_amount DESC;
-- CLIP 2.4 — Pattern anti-join : clients sans commandes
SELECT c.first_name AS `First Name`,
c.last_name AS `Last Name`
FROM main.sales.customers c
LEFT JOIN main.sales.orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;
-- CLIP 2.4 — FULL OUTER JOIN
SELECT c.first_name AS `First Name`,
c.last_name AS `Last Name`,
o.order_id AS `Order ID`,
o.order_amount AS `Amount`
FROM main.sales.customers c
FULL OUTER JOIN main.sales.orders o ON c.customer_id = o.customer_id
ORDER BY c.last_name;
-- CLIP 2.4 — JOIN sur trois tables
SELECT c.first_name || ' ' || c.last_name AS `Customer`,
p.product_name AS `Product`,
o.quantity AS `Quantity`,
o.order_amount AS `Amount`,
o.order_date AS `Date`
FROM main.sales.customers c
INNER JOIN main.sales.orders o ON c.customer_id = o.customer_id
INNER JOIN main.sales.products p ON o.product_id = p.product_id
ORDER BY o.order_date DESC;
-- CLIP 2.5 — UPPER et LOWER
SELECT first_name,
UPPER(first_name) AS `Uppercase`,
LOWER(first_name) AS `Lowercase`
FROM main.sales.customers;
-- CLIP 2.5 — CONCAT + SUBSTRING extraction avant @
SELECT CONCAT(first_name, ' ', last_name) AS `Full Name`,
substr(email, 1, instr(email,'@') - 1) AS `Email Username`
FROM main.sales.customers
WHERE email IS NOT NULL;
-- CLIP 2.5 — TRIM
SELECT TRIM(first_name) AS `Trimmed Name`
FROM main.sales.customers;
-- CLIP 2.5 — ROUND, CEIL simulé, FLOOR simulé
SELECT order_amount AS `Raw Amount`,
ROUND(order_amount, 0) AS `Rounded`,
CAST((order_amount + 0.999999) AS INTEGER) AS `Rounded Up`,
CAST(order_amount AS INTEGER) AS `Rounded Down`
FROM main.sales.orders
ORDER BY order_amount DESC;
-- CLIP 2.5 — ABS : distance par rapport à 500
SELECT order_amount AS `Amount`,
order_amount - 500 AS `Difference from 500`,
ABS(order_amount - 500) AS `Distance from 500`
FROM main.sales.orders
ORDER BY ABS(order_amount - 500);
-- CLIP 2.5 — DATEDIFF
SELECT order_id AS `Order ID`,
order_date AS `Order Date`,
DATE('2026-05-07') AS `Today`,
DATEDIFF('2026-05-07', order_date) AS `Days Since Order`
FROM main.sales.orders
ORDER BY order_date DESC;
-- CLIP 2.5 — DATE_FORMAT
SELECT order_id AS `Order ID`,
order_date AS `Raw Date`,
DATE_FORMAT(order_date, 'MMMM d, yyyy') AS `Formatted Date`,
DATE_FORMAT(order_date, 'yyyy-MM') AS `Year-Month`
FROM main.sales.orders
ORDER BY order_date DESC;
Module 3 — Organization Queries
-- CLIP 3.1 — DESCRIBE : structure d'une table
DESCRIBE main.sales.customers;
-- CLIP 3.2 — Créer une vue temporaire
CREATE OR REPLACE TEMPORARY VIEW high_value_customers AS
SELECT c.customer_id,
c.first_name,
c.last_name,
SUM(o.order_amount) AS total_spent
FROM main.sales.customers c
JOIN main.sales.orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.first_name, c.last_name
HAVING SUM(o.order_amount) > 500;
-- CLIP 3.2 — Interroger la vue temporaire
SELECT *
FROM high_value_customers
ORDER BY total_spent DESC;
-- CLIP 3.2 — Réutilisation de la vue : COUNT
SELECT COUNT(*) AS high_value_count
FROM high_value_customers;
-- CLIP 3.2 — Réutilisation de la vue : AVG
SELECT ROUND(AVG(total_spent), 2) AS avg_high_value_spend
FROM high_value_customers;
-- CLIP 3.2 — Créer une vue permanente (seuil 500)
CREATE OR REPLACE VIEW main.sales.high_value_customers AS
SELECT c.customer_id,
c.first_name,
c.last_name,
SUM(o.order_amount) AS total_spent
FROM main.sales.customers c
JOIN main.sales.orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.first_name, c.last_name
HAVING SUM(o.order_amount) > 500;
-- CLIP 3.2 — Mettre à jour la vue permanente (seuil 1000)
CREATE OR REPLACE VIEW main.sales.high_value_customers AS
SELECT c.customer_id,
c.first_name,
c.last_name,
SUM(o.order_amount) AS total_spent
FROM main.sales.customers c
JOIN main.sales.orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.first_name, c.last_name
HAVING SUM(o.order_amount) > 1000;
-- CLIP 3.2 — Supprimer la vue permanente
DROP VIEW IF EXISTS main.sales.high_value_customers;
-- CLIP 3.2 — Supprimer la vue temporaire
DROP VIEW IF EXISTS high_value_customers;
-- CLIP 3.3 — Subquery imbriquée : clients au-dessus de la moyenne
SELECT c.first_name,
c.last_name,
SUM(o.order_amount) AS total_spent
FROM main.sales.customers c
JOIN main.sales.orders o ON c.customer_id = o.customer_id
GROUP BY c.first_name, c.last_name
HAVING SUM(o.order_amount) > (SELECT AVG(total)
FROM (SELECT SUM(order_amount) AS total
FROM main.sales.orders
GROUP BY customer_id)
)
ORDER BY total_spent DESC;
-- CLIP 3.3 — CTE multi-étapes : customer lifetime value
WITH customer_totals AS (
SELECT c.customer_id,
c.first_name,
c.last_name,
SUM(o.order_amount) AS total_spent,
COUNT(o.order_id) AS order_count
FROM main.sales.customers c
JOIN main.sales.orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.first_name, c.last_name
),
avg_spending AS (
SELECT ROUND(AVG(total_spent), 2) AS avg_total
FROM customer_totals
)
SELECT ct.first_name,
ct.last_name,
ct.total_spent,
ct.order_count,
a.avg_total,
ROUND(ct.total_spent - a.avg_total, 2) AS above_average_by
FROM customer_totals ct
CROSS JOIN avg_spending a
WHERE ct.total_spent > a.avg_total
ORDER BY ct.total_spent DESC;
7. Summary of key concepts
Architecture and performance
| Concept | Description |
|---|---|
| Delta Lake | Open table format, basis for all data in the Databricks lakehouse |
| SQL Warehouse | Calculation engine that executes SQL; Serverless type starts in seconds |
| Result Cache | Automatic cache of identical query results; 6x faster on second run |
| Cache Hit / Cache Miss | Hit = identical query, results from cache. Miss = modified query, complete execution |
| Unity Catalog | Governance layer; namespace catalog.schema.table |
SQL Fundamentals
| Clause | Role | Mandatory |
|---|---|---|
SELECT | Specify columns to return | Yes |
FROM | Specify source table | Yes |
WHERE | Filter rows before aggregation | No |
GROUP BY | Group rows for aggregation | No |
HAVING | Filter groups after aggregation | No |
ORDER BY | Sort final result | No |
Filter operators
| Operator | Usage |
|---|---|
=, <>, >, <, >=, <= | Simple comparisons |
BETWEEN a AND b | Inclusive beach |
IN (val1, val2, ...) | List of values |
IS NULL / IS NOT NULL | Missing values |
LIKE 'pattern%' | Match by Pattern |
AND | All conditions true |
OR | At least one condition true |
NOT | Negation |
Aggregation functions
| Function | Description |
|---|---|
COUNT(*) | All lines |
COUNT(column) | Non-NULL rows |
COUNT(DISTINCT column) | Non-NULL unique values |
SUM(column) | Sum |
AVG(column) | Average |
MIN(column) | Minimum value |
MAX(column) | Maximum value |
Built-in functions
| Category | Functions |
|---|---|
| Text | UPPER(), LOWER(), TRIM(), CONCAT(), substr(), instr(), LENGTH() |
| Digital | ROUND(), ABS(), CAST() |
| Date | DATE(), DATEDIFF(), DATE_FORMAT(), YEAR(), MONTH(), DAY() |
JOIN Types
| Type | Returned rows |
|---|---|
INNER JOIN | Only matches in both tables |
LEFT JOIN | All lines on the left + matches on the right (NULL if absent) |
RIGHT JOIN | All lines on the right + matches on the left (NULL if absent) |
FULL OUTER JOIN | All lines on both sides |
CROSS JOIN | Cartesian product |
Query organization tools
| Tool | Scope | Persistence | Use cases |
|---|---|---|---|
| Saved Query | Personal workspace | Permanent, shareable | Team Reference Queries |
| History version | By saved query | Automatic | Experiment without risk |
| Query History | By user | Session / history | Find past experiences |
| Temporary View | Current session | Session only | Exploration, intra-session reuse |
| Permanent View | Catalog.Schema | Permanent until DROP | Logic shared with the team |
| Subquery | Inside a query | Execution only | Online intermediate calculation |
| CTE (WITH) | Inside a query | Execution only | Complex logic in named steps |
Search Terms
query · explore · data · databricks · sql · azure · spark · engineering · analytics · functions · join · queries · history · performance · tables · views · analysis · editor · expressions · operators · organizing · types · aggregation · aliases