Shopify Data Warehouse Schema: Orders, Products, Customers and SQL

For founders and analysts putting Shopify data into Postgres, SQL Server, Snowflake or BigQuery. You get the tables Fivetran and Airbyte create, how they join, a small star schema you can build with views, SQL Server table definitions if you load the data yourself, and queries on top.

· AI2SQL

Quick answer

The five core Shopify tables and how they join, as one query: every order line with its order, customer, product and variant.

-- PostgreSQL (Fivetran Shopify schema "shopify")
select
  o.id as order_id,
  o.created_at,
  c.email,
  p.title as product,
  v.title as variant,
  ol.quantity,
  ol.price * ol.quantity - ol.total_discount as line_revenue
from shopify.order_line ol
join shopify."order" o on o.id = ol.order_id
left join shopify.customer c on c.id = o.customer_id
left join shopify.product p on p.id = ol.product_id
left join shopify.product_variant v on v.id = ol.variant_id
where not o.test and o.cancelled_at is null
limit 100;

Schema assumptions

Fivetran’s classic Shopify schema. Fivetran’s newer GraphQL-based schema keeps the table names but renames money columns (for example total_price_set_shop_amount, currency_code).

TableGrainKeyJoins toColumns used here
orderone row per orderidcustomer.id via customer_idcreated_at, cancelled_at, test, currency, total_price, total_discounts, source_name, _fivetran_deleted
order_lineone row per line itemidorder.id via order_id; product.id; product_variant.id via variant_idquantity, price, total_discount, sku, title
customerone row per customeridemail, created_at, state
productone row per productidtitle, product_type, vendor, status, _fivetran_deleted
product_variantone row per sellable variantidproduct.id via product_idtitle, sku, price, inventory_quantity
refundone row per refundidorder.id via order_idcreated_at
order_line_refundone row per refunded lineidrefund.id via refund_id; order_line.id via order_line_idquantity, subtotal
Other sourcesWhat’s different
AirbytePlural names: orders, customers, products, product_variants, order_refunds. orders.line_items and orders.refunds are JSON arrays; orders.customer is a JSON object.
demo_shop (AI2SQL)customers(id, name, country, signup_date), products(id, name, category, price), orders(id, customer_id, order_date, status, total), order_items(id, order_id, product_id, quantity, unit_price).

Copy-ready SQL

1. Fact table: order lines with net revenue (Postgres view)

One row per order line, refunds subtracted, test, cancelled and deleted rows removed. Change: analytics to the schema you build into.

-- PostgreSQL (Fivetran Shopify schema "shopify")
create schema if not exists analytics;

create or replace view analytics.fct_order_lines as
with line_refunds as (
  select order_line_id, sum(quantity) as refunded_qty, sum(subtotal) as refunded_amount
  from shopify.order_line_refund
  group by order_line_id
)
select
  ol.id as order_line_id,
  o.id as order_id,
  o.customer_id,
  ol.product_id,
  ol.variant_id,
  o.created_at as ordered_at,
  o.currency,
  o.source_name as channel,
  ol.quantity,
  coalesce(lr.refunded_qty, 0) as refunded_qty,
  ol.price * ol.quantity - ol.total_discount as gross_revenue,
  ol.price * ol.quantity - ol.total_discount - coalesce(lr.refunded_amount, 0) as net_revenue
from shopify.order_line ol
join shopify."order" o on o.id = ol.order_id
left join line_refunds lr on lr.order_line_id = ol.id
where not o.test
  and o.cancelled_at is null
  and not coalesce(o._fivetran_deleted, false);

2. Customer dimension (Postgres view)

First order date, order count and lifetime net revenue per customer. Change: nothing.

-- PostgreSQL (Fivetran Shopify schema "shopify")
create or replace view analytics.dim_customers as
select
  c.id as customer_id,
  c.email,
  c.created_at as signed_up_at,
  min(f.ordered_at) as first_order_at,
  count(distinct f.order_id) as orders,
  coalesce(sum(f.net_revenue), 0) as lifetime_net_revenue
from shopify.customer c
left join analytics.fct_order_lines f on f.customer_id = c.id
group by c.id, c.email, c.created_at;

3. Product dimension at variant level (Postgres view)

One row per sellable variant with its product attributes. Change: nothing.

-- PostgreSQL (Fivetran Shopify schema "shopify")
create or replace view analytics.dim_products as
select
  v.id as variant_id,
  p.id as product_id,
  p.title as product,
  v.title as variant,
  v.sku,
  coalesce(nullif(p.product_type, ''), '(none)') as product_type,
  p.vendor,
  v.price as list_price,
  v.inventory_quantity
from shopify.product_variant v
join shopify.product p on p.id = v.product_id
where not coalesce(p._fivetran_deleted, false);

4. Net revenue by product type per month, on the star schema

Every report gets simpler once the views exist. Change: nothing.

-- PostgreSQL (Fivetran Shopify schema "shopify")
select
  date_trunc('month', f.ordered_at)::date as month,
  d.product_type,
  sum(f.quantity - f.refunded_qty) as net_units,
  sum(f.net_revenue) as net_revenue
from analytics.fct_order_lines f
join analytics.dim_products d on d.variant_id = f.variant_id
group by 1, 2
order by 1 desc, net_revenue desc;
monthproduct_typenet_unitsnet_revenue
2026-09-01Bags120428890.00
2026-09-01Shirts86021310.00

5. SQL Server tables for Shopify orders, products and customers

If you load Shopify’s API into SQL Server yourself. Keys are bigint, money decimal(18,2), timestamps datetimeoffset. Change: column lists to the fields you pull.

-- SQL Server (T-SQL)
create table dbo.customers (
  id bigint not null primary key,
  email nvarchar(320) null,
  first_name nvarchar(255) null,
  last_name nvarchar(255) null,
  created_at datetimeoffset not null
);

create table dbo.products (
  id bigint not null primary key,
  title nvarchar(255) not null,
  product_type nvarchar(255) null,
  vendor nvarchar(255) null,
  status nvarchar(20) not null
);

create table dbo.product_variants (
  id bigint not null primary key,
  product_id bigint not null references dbo.products (id),
  title nvarchar(255) null,
  sku nvarchar(255) null,
  price decimal(18, 2) not null,
  inventory_quantity int null
);

create table dbo.orders (
  id bigint not null primary key,
  customer_id bigint null references dbo.customers (id),
  created_at datetimeoffset not null,
  cancelled_at datetimeoffset null,
  currency char(3) not null,
  total_price decimal(18, 2) not null,
  total_discounts decimal(18, 2) not null default 0,
  financial_status nvarchar(30) null,
  source_name nvarchar(100) null,
  test bit not null default 0
);

create table dbo.order_lines (
  id bigint not null primary key,
  order_id bigint not null references dbo.orders (id),
  product_id bigint null references dbo.products (id),
  variant_id bigint null references dbo.product_variants (id),
  quantity int not null,
  price decimal(18, 2) not null,
  total_discount decimal(18, 2) not null default 0
);

create index ix_orders_customer_created on dbo.orders (customer_id, created_at);
create index ix_order_lines_order on dbo.order_lines (order_id);

6. Revenue by product type per month on those SQL Server tables

The same report on the tables above. Change: nothing.

-- SQL Server (T-SQL)
select
  datefromparts(year(o.created_at), month(o.created_at), 1) as month,
  coalesce(nullif(p.product_type, ''), '(none)') as product_type,
  sum(ol.quantity) as units,
  sum(ol.price * ol.quantity - ol.total_discount) as revenue
from dbo.order_lines ol
join dbo.orders o on o.id = ol.order_id
left join dbo.products p on p.id = ol.product_id
where o.test = 0 and o.cancelled_at is null
group by datefromparts(year(o.created_at), month(o.created_at), 1), coalesce(nullif(p.product_type, ''), '(none)')
order by month desc, revenue desc;

7. Order lines from Airbyte’s nested JSON (Postgres)

Airbyte keeps line items inside each order. Unnest them to get the same order-line grain. Change: the airbyte_shopify schema name.

-- PostgreSQL (Airbyte Shopify schema "airbyte_shopify")
select
  o.id as order_id,
  o.created_at,
  (li ->> 'product_id')::bigint as product_id,
  (li ->> 'variant_id')::bigint as variant_id,
  li ->> 'title' as title,
  (li ->> 'quantity')::int as quantity,
  (li ->> 'price')::numeric as price
from airbyte_shopify.orders o
cross join lateral jsonb_array_elements(o.line_items) as li
where not o.test and o.cancelled_at is null
limit 100;
order_idcreated_atproduct_idvariant_idtitlequantityprice
58102352938452026-10-11 16:20812345678901244123456789012Canvas Tote224.00

8. Revenue by product type per month in BigQuery

Fivetran’s schema in BigQuery; order needs backticks. Change: your_project.shopify to your dataset.

-- BigQuery (Fivetran Shopify dataset "shopify")
select
  date_trunc(date(o.created_at), month) as month,
  coalesce(nullif(p.product_type, ''), '(none)') as product_type,
  sum(ol.price * ol.quantity - ol.total_discount) as revenue
from `your_project.shopify.order_line` ol
join `your_project.shopify.order` o on o.id = ol.order_id
left join `your_project.shopify.product` p on p.id = ol.product_id
where not o.test and o.cancelled_at is null
group by month, product_type
order by month desc, revenue desc

Run it on demo data

Every AI2SQL account comes with demo_shop, a read-only Postgres store with customers, products, orders and order_items. This version of the main query runs on it as-is.

demo_shop has the same shape as a Shopify sync: orders, order lines (order_items), products and customers. category stands in for Shopify's product_type.

select
  date_trunc('month', o.order_date)::date as month,
  p.category,
  sum(oi.quantity) as units,
  sum(oi.quantity * oi.unit_price) as revenue
from order_items oi
join orders o on o.id = oi.order_id
join products p on p.id = oi.product_id
where o.status in ('completed', 'shipped')
group by 1, 2
order by 1 desc, revenue desc
Run on demo_shop free →

Run it on your own database with AI2SQL

If Fivetran or Airbyte syncs your Shopify store into Postgres, MySQL or SQL Server, connect that database read-only and ask in plain English. For Snowflake or BigQuery, AI2SQL writes the query in that dialect and you run it there.

Free to try. See pricing for what each plan includes.

TRY IT — FREE, NO SIGNUP
Try one:

Common mistakes

Frequently asked questions

What tables does a Shopify data warehouse need? +

For sales analysis: order (one row per order), order_line (one per line item), customer, product, product_variant, refund and order_line_refund. Fivetran and Airbyte both create these, with different names and nesting, plus many more tables (fulfillments, transactions, inventory, discounts).

How do Shopify orders, products and customers join? +

order.customer_id → customer.id; order_line.order_id → order.id; order_line.product_id → product.id; order_line.variant_id → product_variant.id; product_variant.product_id → product.id; refund.order_id → order.id; order_line_refund.refund_id → refund.id and order_line_refund.order_line_id → order_line.id.

Should I build a star schema for Shopify data? +

For anything beyond a few ad-hoc queries, yes. A fact table at order-line grain with net revenue, plus customer and product dimensions, keeps every report consistent. The views below are a starting point; dbt packages such as Fivetran's dbt_shopify go further.

How do I store Shopify data in SQL Server? +

Use a sync tool that supports SQL Server as a destination, or load the Shopify API yourself into tables like the DDL below: bigint keys, decimal(18,2) money, datetimeoffset timestamps and foreign keys from lines to orders and variants.

What changes with Airbyte instead of Fivetran? +

Airbyte keeps line items as a JSON array inside orders and uses plural table names. Unnest line_items to get an order-line table; the Postgres example below shows how.

Related templates

All templates: SQL templates for Stripe, Shopify and Supabase