1. What will you learn in this Snowflake SQL tutorial?
This Snowflake SQL tutorial teaches you to create a small practice dataset, filter orders, handle NULLs, summarize sales, join customer data and select the latest order per customer. Run the commands in sequence, compare the expected results and explain each business rule. You need an authorized Snowflake environment and basic familiarity with tables.
The deliverable is a reusable SQL worksheet and a checked customer-sales report, not a screenshot of an unexplained query. The examples suit freshers, Excel users and learners moving from another SQL database. Python, external cloud storage and a production dataset are unnecessary. If basic table terminology is unfamiliar, use the Snowflake prerequisites guide first.
This page concentrates on writing queries. It does not replace the broader Snowflake learning roadmap or repeat a file-ingestion project. Match your current starting level to the Snowflake course syllabus if you need structured instruction. All records below are synthetic, and expected outputs are calculated from them; no connected-account Snowflake execution is claimed.
2. Understand the business question and row grain
A small shop wants paid sales by customer, a list of orders without known customers, and the latest order for each identified customer. One order is one row. The amount is the order total, not an item price; therefore multiplying it by an invented quantity would be wrong. A separate customer table contains one row per customer ID.
| Object | Grain | Purpose |
|---|---|---|
| SQL_CUSTOMERS | One row per customer_id | Attach customer name and region |
| SQL_ORDERS | One row per order_id | Store date, customer, amount and status |
| Paid-sales report | One row per customer | Count and sum only paid orders |
The reporting rule includes paid orders only. Pending and refunded rows remain in the source but do not contribute to this particular report. That is an exercise policy; a net-revenue report could use a different refund rule. State the question before changing SQL. “The total changed” is not an explanation unless you can identify which rows should change.
Conceptual workflow: synthetic tables → source checks → paid-order filter → customer join → aggregate → reconciliation. The sequence matters because a join can duplicate facts and an early filter can remove evidence. Keep the raw rows available while you inspect a summary.
3. Prepare context and create the complete dataset
Use an assigned small warehouse and a dedicated practice database/schema. Replace every YOUR_ identifier with names provided by your account owner. The role needs USAGE on the context objects and appropriate table-creation access. These are temporary tables: run the walkthrough in the same session and save the setup script before reconnecting.
USE WAREHOUSE YOUR_PRACTICE_WAREHOUSE;
USE DATABASE YOUR_PRACTICE_DATABASE;
USE SCHEMA YOUR_PRACTICE_SCHEMA;
SELECT CURRENT_ROLE(),CURRENT_WAREHOUSE(),CURRENT_DATABASE(),CURRENT_SCHEMA();
CREATE TEMP TABLE SQL_CUSTOMERS
(customer_id NUMBER,customer_name VARCHAR,region VARCHAR);
INSERT INTO SQL_CUSTOMERS VALUES
(1,'Asha','South'),(2,'Ravi','North'),(3,'Meena','South');
CREATE TEMP TABLE SQL_ORDERS
(order_id NUMBER,customer_id NUMBER,order_date DATE,
amount NUMBER(10,2),status VARCHAR);
INSERT INTO SQL_ORDERS VALUES
(101,1,'2026-10-01',100,'paid'),
(102,1,'2026-10-02',150,'paid'),
(103,2,'2026-10-02',200,'paid'),
(104,2,'2026-10-03',50,'refunded'),
(105,3,'2026-10-03',75,'pending'),
(106,NULL,'2026-10-04',25,'paid');CREATE intentionally fails if these names already exist. Inspect earlier work or choose new names rather than using CREATE OR REPLACE to overwrite an object. Confirm three customers and six orders. If your interface opens a new execution session for each worksheet, use one worksheet/session or ask for an isolated persistent lab; temporary objects do not survive every reconnect.
Keep warehouse auto-suspend enabled and check trial/training account limits. Small inputs do not eliminate compute costs. Nothing here needs ACCOUNTADMIN, AI functions or a new AWS account.
4. Read rows with SELECT, WHERE and ORDER BY
SELECT order_id,customer_id,order_date,amount,status
FROM SQL_ORDERS ORDER BY order_id;
SELECT order_id,amount FROM SQL_ORDERS
WHERE status='paid' AND amount>=100
ORDER BY amount DESC,order_id;| order_id | amount |
|---|---|
| 103 | 200.00 |
| 102 | 150.00 |
| 101 | 100.00 |
SELECT chooses the output columns; WHERE decides which source rows qualify. ORDER BY controls presentation. The second condition excludes paid order 106 because its amount is only 25, and it excludes refunded/pending rows regardless of amount. SQL does not promise the same display order without ORDER BY, even when a tiny table appears stable.
Compare the three returned IDs with the seed before trusting a count. Use a deterministic tie-breaker such as order_id when business values can tie. LIMIT is useful for inspection, but a limited preview is not a complete validation of the input. Keep semicolons between statements and run only the intended selection in your editor.
Reference: Snowflake SELECT syntax.
5. Handle NULLs and separate missing values from zero
SELECT order_id FROM SQL_ORDERS WHERE customer_id IS NULL;
SELECT COUNT(*) AS paid_rows,COUNT(customer_id) AS identified_paid_rows,
SUM(amount) AS paid_total
FROM SQL_ORDERS WHERE status='paid';The missing-customer result is order 106. Paid rows = 4, identified paid rows = 3, paid total = 475.00. COUNT(*) counts rows; COUNT(customer_id) excludes NULL customer IDs. A missing identifier is not customer 0, and a missing amount is not automatically a genuine zero sale. Do not make data appear complete by filling every blank with zero.
SELECT order_id,
CASE WHEN customer_id IS NULL THEN 'Needs review'
ELSE 'Identified' END AS customer_check
FROM SQL_ORDERS ORDER BY order_id;CASE creates a result label without updating the table. IS NULL is the predicate for this check; customer_id = NULL does not work like a normal equality test. Preserve exceptions in a separate list so a report consumer can see what is unresolved. COALESCE can be useful for a display label, but its default must not hide a quality issue.
Reference: COUNT behavior.
6. Build a paid-sales report with GROUP BY and HAVING
SELECT customer_id,COUNT(*) AS paid_orders,SUM(amount) AS paid_sales
FROM SQL_ORDERS
WHERE status='paid' AND customer_id IS NOT NULL
GROUP BY customer_id
ORDER BY customer_id;| customer_id | paid_orders | paid_sales |
|---|---|---|
| 1 | 2 | 250.00 |
| 2 | 1 | 200.00 |
The identified-customer subtotal is 450.00 across three orders. The 25.00 exception remains outside this report and explains the gap to all paid sales of 475.00. GROUP BY changes the result grain to one row per customer. Every selected expression must make sense at that grain; an arbitrary order_date would not describe the whole customer group.
SELECT customer_id,SUM(amount) AS paid_sales
FROM SQL_ORDERS WHERE status='paid' AND customer_id IS NOT NULL
GROUP BY customer_id HAVING SUM(amount)>=225
ORDER BY customer_id;Only customer 1 survives HAVING, with 250.00. WHERE filters individual input rows; HAVING filters the aggregate groups. Moving the 225 condition into WHERE amount >= 225 would remove every order and answer a different question. Predict this difference before testing it.
7. Join customer names without losing or multiplying orders
SELECT o.order_id,o.customer_id,c.customer_name,c.region,o.amount
FROM SQL_ORDERS o
LEFT JOIN SQL_CUSTOMERS c ON o.customer_id=c.customer_id
WHERE o.status='paid' ORDER BY o.order_id;
SELECT customer_id,COUNT(*) AS copies FROM SQL_CUSTOMERS
GROUP BY customer_id HAVING COUNT(*)>1;The LEFT JOIN keeps four paid orders, including order 106 with NULL customer details. An INNER JOIN would return three and remove that exception. Neither join is universally correct: choose according to whether unmatched facts must remain visible. The duplicate-customer check should return no rows for this seed. If a customer key repeats, the join can multiply that customer’s orders.
| order_id | customer_name | region | amount |
|---|---|---|---|
| 101 | Asha | South | 100.00 |
| 102 | Asha | South | 150.00 |
| 103 | Ravi | North | 200.00 |
| 106 | NULL | NULL | 25.00 |
Reconcile the paid-order count and amount before and after the join. Adding DISTINCT to suppress extra rows can conceal the key problem and discard legitimate facts. Also note that filtering c.region in WHERE after a LEFT JOIN removes unmatched rows; a condition on the optional side changes which records remain.
Reference: Working with joins.
8. Use a CTE to make the reporting rule readable
WITH paid_orders AS (
SELECT order_id,customer_id,amount FROM SQL_ORDERS WHERE status='paid'
)
SELECT COALESCE(c.customer_name,'Unknown customer') AS customer_name,
COUNT(*) AS paid_orders,SUM(p.amount) AS paid_sales
FROM paid_orders p LEFT JOIN SQL_CUSTOMERS c
ON p.customer_id=c.customer_id
GROUP BY c.customer_name ORDER BY customer_name;| customer_name | paid_orders | paid_sales |
|---|---|---|
| Asha | 2 | 250.00 |
| Ravi | 1 | 200.00 |
| Unknown customer | 1 | 25.00 |
A CTE names a query block for the statement that follows; it does not automatically create a stored reporting table. Separating paid_orders makes the inclusion rule easy to review. This example groups by names only because the tiny seed has unique names. In a real report, group by the stable customer key as well: two people can share a name.
COALESCE makes the unknown group readable while the underlying exception query remains available. Preserve order_id in the CTE so you can investigate a specific amount. Readability is a reason to use a CTE; it is not a guarantee of faster execution or materialization. Compare correct results before discussing tuning.
Reference: Common table expressions.
9. Find the latest order with ROW_NUMBER and QUALIFY
SELECT customer_id,order_id,order_date,status,
ROW_NUMBER() OVER (
PARTITION BY customer_id ORDER BY order_date DESC,order_id DESC
) AS row_num
FROM SQL_ORDERS WHERE customer_id IS NOT NULL
QUALIFY row_num=1 ORDER BY customer_id;| customer_id | latest order_id | status |
|---|---|---|
| 1 | 102 | paid |
| 2 | 104 | refunded |
| 3 | 105 | pending |
PARTITION BY starts a separate ranking for each customer. ORDER BY chooses the most recent date, then the greatest order ID as an explicit tie-breaker. QUALIFY filters after the window function is computed. Here latest means latest of all statuses. Customer 2’s latest order is refunded, which is useful information rather than an error.
To find the latest paid order, add status = paid to the source WHERE condition. Then customer 2 returns 103 and customer 3 has no result. This is a different business question, not a cosmetic rearrangement of clauses. QUALIFY is Snowflake-specific SQL syntax and is not part of ANSI SQL; other engines may need a subquery around the ranking.
Reference: QUALIFY reference.
Reference: ROW_NUMBER reference.
10. Validate results with a controlled changed-input test
Run these checks before showing the report. A correct total alone can hide duplicate and missing records that offset each other. Inspect keys, counts, exceptions and business rules together. Save actual output from your own authorized account; the values shown here are calculated expectations, not execution screenshots.
| Check | Expected baseline |
|---|---|
| Customer rows / order rows | 3 / 6 |
| Paid orders / paid amount | 4 / 475.00 |
| Paid known-customer orders / amount | 3 / 450.00 |
| Missing-customer paid amount | 25.00 |
| Latest known-customer rows | 3 |
-- Only in this disposable lab, after confirming order 105 is unique.
SELECT * FROM SQL_ORDERS WHERE order_id=105;
UPDATE SQL_ORDERS SET status='paid' WHERE order_id=105 AND status='pending';
SELECT COUNT(*),SUM(amount) FROM SQL_ORDERS WHERE status='paid';
-- Expected: 5 paid rows and 550.00. Save evidence, then restore.
UPDATE SQL_ORDERS SET status='pending' WHERE order_id=105 AND status='paid';During the test, Meena contributes 75.00; the known-customer subtotal becomes 525.00. After restoration, totals return to 475.00 and 450.00. The latest-order IDs do not change because status changed, not date or ID. This separates a reporting-policy change from a ranking change. Verify restoration rather than assuming the final UPDATE ran.
11. Troubleshoot common beginner SQL mistakes
| Symptom | Likely check | Correction |
|---|---|---|
| Object missing | Active schema, role and session | Restore approved context; recreate temporary seed if disconnected |
| Invalid identifier | Spelling and quoted case | Use the exact object/column name |
| Too many joined rows | Duplicate lookup key | Fix key contract; do not mask with DISTINCT |
| Unknown customer disappears | INNER JOIN or right-side WHERE filter | Keep LEFT JOIN when exceptions must remain |
| Unexpected count | COUNT(column) versus COUNT(*) | Explain NULL handling |
| Aggregate in WHERE | Row versus group filter | Use HAVING for group totals |
| Latest row changes on ties | Missing tie-breaker | Add a deterministic business-approved ordering |
Run one block at a time and retain the error text plus query context. Unquoted identifiers are resolved differently from deliberately quoted mixed-case names. Use simple unquoted names for this starter and avoid introducing case-sensitive object names accidentally. A semicolon splits statements, while a missing comma changes the syntax within a statement.
If a query runs but the answer is wrong, compare the included IDs with the seed before changing warehouse size. Faster compute cannot repair a wrong join or status rule. Continue to the query optimization guide only after equivalent correct results are established.
12. Practise independently and package your work
- Without copying, list paid orders in increasing date order and explain the four IDs.
- Produce customer paid totals while retaining the unknown group; reconcile 475.00.
- Change the group threshold and predict which customer remains before executing.
- Explain the difference between latest order and latest paid order.
- Write a README with setup, seed, run order, expected results, actual evidence and one solved error.
Keep sql/01_setup.sql, sql/02_queries.sql, sql/03_checks.sql and evidence/actual-results.csv. A truthful portfolio description is “Built a synthetic order-analysis SQL lab using joins, aggregation, NULL checks and deterministic latest-row selection.” Do not claim production usage, performance gains or a working GitHub repository unless you actually created and verified that evidence.
-- Save evidence, then remove only these temporary lab tables.
DROP TABLE SQL_ORDERS;
DROP TABLE SQL_CUSTOMERS;Temporary data is session-scoped, but scripts and notes should be saved deliberately. Do not suspend a shared warehouse. Select a larger exercise from Snowflake Projects after you can explain this dataset independently. Compare remaining gaps with the Snowflake course syllabus and use the interview guide to practise explaining choices.
13. Snowflake SQL tutorial FAQs
Is Snowflake SQL different from SQL?
It supports standard SQL and additional platform syntax. SELECT, joins and aggregates transfer across systems; verify extensions such as QUALIFY in the target engine.
Do I need Python for this tutorial?
No. These examples use SQL in an approved Snowflake environment. Python belongs to a separate connector or Snowpark learning path.
Can I practise without uploading a CSV?
Yes. The INSERT statements provide the complete synthetic input. File loading is a different skill and is covered in the learning roadmap.
Why did my temporary tables disappear?
They belong to a session. Reconnecting or changing execution sessions can remove access to them; rerun your saved seed in the intended session.
What is the difference between WHERE and HAVING?
WHERE filters source rows before aggregation. HAVING filters grouped results, such as customers whose total exceeds a threshold.
Why does COUNT(customer_id) return fewer rows?
It excludes NULL values. COUNT(*) counts all selected rows, including a paid order whose customer identifier is missing.
When should I use LEFT JOIN?
Use it when every left-side fact must remain, including unmatched records. Validate lookup-key uniqueness to avoid multiplying facts.
Does a CTE store the data?
A CTE names a query expression within a statement. Do not assume it creates a persistent table or guarantees a performance improvement.
Why use QUALIFY?
It filters a window-function result directly, such as ROW_NUMBER equal to one. Use explicit ordering and tie-breakers for deterministic selection.
Should refunded orders be negative?
That depends on the report policy. This paid-only report excludes refunds; a separate net-revenue calculation could treat them differently.
Were these queries executed on a Snowflake account?
No connected Snowflake execution was performed. Compare your actual outputs with the documented expectations and record any differences.
What should I learn after these queries?
Practise loading, validation, permissions and recovery with an isolated project. Use the Projects hub and course syllabus to choose the next task.
Related Snowflake Articles
Snowflake Projects
Choose a portfolio exercise with SQL, expected results and validation steps.
Snowflake Interview Questions
Practise explaining SQL, validation and project decisions.
Snowflake Certification
Explore the separate certification learning path and exam preparation.
Related Courses
Snowflake Training
Explore the syllabus, practical learning topics and available Snowflake training formats.
Azure Data Factory Training
Explore data integration, orchestration and cloud pipeline training.
