7.3 Snowflake SQL basics on ShopFlow¶
You already know SQL from the lab. This lesson covers what is different in Snowflake: how objects are organised, how tables are rebuilt and dropped, where the metadata lives, the timestamp types and casting, on ShopFlow data. At the end you'll have built a small Silver and Gold layer.
Every SQL file in this part starts with:
Concept¶
Database → schema → objects¶
Snowflake names everything with three parts: DATABASE.SCHEMA.OBJECT — e.g.
SHOPFLOW.BRONZE.ORDERS. That's the same three levels as the lab's catalog.namespace.table
(iceberg.bronze.orders). Objects are tables, views, stages, streams, tasks, functions…
SHOPFLOW
├── BRONZE raw copy of the source
├── SILVER cleaned / typed / joined
└── GOLD business-ready marts
Rebuilding tables and views¶
create or replace table … as select(CTAS) builds a new table from a query and swaps it in atomically — readers see the old table until the new one is complete.create or replace viewonly replaces the definition. A view stores no data.- The replaced table isn't gone at once — it's kept in Time Travel for a while (Cloning, Time Travel & table types).
Dropping objects¶
drop tablefor tables,drop viewfor views — Snowflake errors if the type doesn't match.if existsavoids an error when the object is already gone.undrop table/undrop schema/undrop databasebring back a dropped object within its Time Travel period. The lab's Iceberg tables have noUNDROP.
Timestamps¶
| Type | Stores | Shows |
|---|---|---|
TIMESTAMP_NTZ |
wall-clock time, no time zone | as stored |
TIMESTAMP_LTZ |
an instant in UTC | in your session time zone |
TIMESTAMP_TZ |
the time and its offset | with the offset |
ShopFlow uses TIMESTAMP_NTZ (like Postgres TIMESTAMP). There is no automatic "last
modified" column and no ON UPDATE trigger: default current_timestamp() fills a value on
insert only, so an updated_at column must be set by whoever updates the row.
Lab¶
1 · Look at the metadata: INFORMATION_SCHEMA¶
Every database has an INFORMATION_SCHEMA schema with views about its objects:
select table_name, table_type, row_count, bytes, created, last_altered
from SHOPFLOW.INFORMATION_SCHEMA.TABLES
where table_schema = 'BRONZE'
order by row_count desc;
table_type—BASE TABLEorVIEW.row_count,bytes— kept by Snowflake as metadata; reading them scans no data.last_altered— the last change of any kind to the table. dbt's source freshness uses it when a table has no timestamp column.- Names in
INFORMATION_SCHEMAare stored in upper case, so filter with'BRONZE', not'bronze'.
Example output (first rows):
| TABLE_NAME | TABLE_TYPE | ROW_COUNT | … |
|---|---|---|---|
| ORDER_ITEMS | BASE TABLE | 100000 | … |
| ORDERS | BASE TABLE | 40000 | … |
| PAYMENTS | BASE TABLE | 35954 | … |
The same information as a command: show tables in schema SHOPFLOW.BRONZE;
2 · Silver: a clean orders table¶
Silver joins and types the data. One row per order line with the product and customer attributes, cancelled orders removed:
create or replace table SHOPFLOW.SILVER.ORDER_LINES as
select o.order_id,
o.order_ts,
o.order_ts::date as order_date,
o.channel,
o.status,
o.customer_id,
c.country,
c.loyalty_tier,
oi.product_id,
p.category,
oi.quantity,
oi.unit_price,
oi.quantity * oi.unit_price as line_revenue,
oi.quantity * (oi.unit_price - p.cost) as line_margin
from ORDERS o
join ORDER_ITEMS oi on oi.order_id = o.order_id
join CUSTOMERS c on c.customer_id = o.customer_id
join PRODUCTS p on p.product_id = oi.product_id
where o.status <> 'cancelled';
select count(*) from SHOPFLOW.SILVER.ORDER_LINES;
::date— Snowflake's short cast, the same ascast(o.order_ts as date).
Example: 90347 rows (100,000 lines minus the ~9,650 lines of cancelled orders).
3 · Gold: daily sales and a view on top¶
create or replace table SHOPFLOW.GOLD.DAILY_SALES as
select order_date,
count(distinct order_id) as orders,
sum(line_revenue) as revenue,
sum(line_margin) as margin
from SHOPFLOW.SILVER.ORDER_LINES
group by order_date;
create or replace view SHOPFLOW.GOLD.MONTHLY_SALES as
select date_trunc('month', order_date) as month,
sum(orders) as orders,
sum(revenue) as revenue
from SHOPFLOW.GOLD.DAILY_SALES
group by month;
select * from SHOPFLOW.GOLD.MONTHLY_SALES order by month limit 3;
date_trunc('month', …)— first day of the month; the same function as in Trino.group by month— Snowflake lets you group by a column alias from theselect.
Example output:
| MONTH | ORDERS | REVENUE |
|---|---|---|
| 2023-06-01 | 1451 | 2905593.39 |
| 2023-07-01 | 1490 | 2904795.29 |
| 2023-08-01 | 1534 | 3143068.60 |
Same Medallion, same SQL as the lab — you built a Gold mart in Spark the same way.
4 · Casting: :: and Snowflake's automatic conversion¶
The usual join mistakes — fan-out, count(*) after a join, WHERE on a left join, NOT IN
with NULL — are the same in every engine; they're in
Join pitfalls. What is different in Snowflake is casting.
Snowflake has a short form for cast(x as type): x::type. And where Trino or Postgres
refuse to compare text with a number, Snowflake converts the text automatically:
-- works: Snowflake turns '42' into a number for the comparison
select count(*) from ORDERS where customer_id = '42';
-- fails as soon as one value isn't a number
select count(*) from ORDERS where customer_id = 'abc';
- The first returns customer 42's orders; Trino would refuse it.
- The second fails with Numeric value 'abc' is not recognized — the automatic conversion breaks at runtime, on the first bad value, maybe months after the query was written.
So cast explicitly, and for messy data use the try_ functions, which return NULL instead
of failing:
select '42'::int as with_colons, -- 42
cast('42' as int) as with_cast, -- 42, the same
try_to_number('abc') as bad_value, -- NULL, no error
'2025-01-31'::date as a_date; -- 2025-01-31
::int,::date,::varchar— Snowflake's cast shorthand (you used::datein step 2).try_to_number(text)— like::number, butNULLfor values that aren't numbers.try_to_date,try_to_timestampandtry_cast(x as type)work the same way.
In a join, cast the text side:
select count(*)
from ORDERS o
join (select '42' as customer_id) x -- a text key, like from a CSV
on o.customer_id = x.customer_id::int;
5 · Drop and undrop¶
drop table SHOPFLOW.GOLD.MONTHLY_SALES; -- error: it's a view, not a table
drop view if exists SHOPFLOW.GOLD.NO_SUCH_VIEW; -- no error, thanks to IF EXISTS
undrop table SHOPFLOW.GOLD.DAILY_SALES; -- error: the table isn't dropped
drop table SHOPFLOW.GOLD.DAILY_SALES;
select count(*) from SHOPFLOW.GOLD.DAILY_SALES; -- error: does not exist
undrop table SHOPFLOW.GOLD.DAILY_SALES; -- back again, with all its rows
select count(*) from SHOPFLOW.GOLD.DAILY_SALES;
Run them one at a time and read each error — they're the ones you'll meet in real projects.
Check yourself¶
where customer_id = '42'works butwhere customer_id = 'abc'fails. Why? (Snowflake converts the text to a number automatically — fine for'42', an error for'abc'. Cast explicitly, or usetry_to_number.)- Which column in
INFORMATION_SCHEMA.TABLEStells you a table changed recently, even without anupdated_atcolumn? (LAST_ALTERED.)