← All posts
·7 min read
#postgres#database#tutorial

E-commerce Database Schema for PostgreSQL: The 11 Tables Every Store Needs

The 11-table PostgreSQL schema that powers Shopify and WooCommerce — customers, products, carts, orders, payments, and reviews with relationships.

The short version

Every online store is the same 11 tables with different CSS. customers have addresses and carts; carts hold cart_items that reference products; products belong to categories and have product_images; orders snapshot cart items into order_items and collect payments; reviews link customers to products. This is the PostgreSQL schema behind Shopify, WooCommerce, and most custom storefronts.

The 11 tables at a glance

TablePurposeKey FK
customersWho shops— (core)
addressesWhere to ship/billcustomer_id
categoriesCatalog taxonomy (self-referential)parent_id → categories.id
productsWhat you sellcategory_id
product_imagesProduct photosproduct_id
cartsActive shopping sessioncustomer_id
cart_itemsMutable cart linescart_id, product_id
ordersPlaced order (snapshot)customer_id, shipping/billing address
order_itemsImmutable order linesorder_id, product_id
paymentsPayment attemptorder_id
reviewsPost-purchase feedbackproduct_id, customer_id

The three flows

Browse → cart: Customer browses products (joined to categories and product_images), adds to carts via cart_items. Cart is mutable — quantities change, items are removed.

Cart → order: At checkout, cart items are copied into order_items as an immutable snapshot. Even if the product price later changes, the order preserves what was actually purchased. The orders row records total, addresses, and timestamps (placed_at, paid_at, shipped_at).

Order → payment → review: payments records each charge attempt against the order. After fulfillment, reviews lets the customer rate the product.

-- Order history with items and payment status
select o.id, o.status, o.total_cents, p.status as payment_status,
       oi.quantity, pr.title
from orders o
join order_items oi on oi.order_id = o.id
join products pr on pr.id = oi.product_id
left join payments p on p.order_id = o.id
where o.customer_id = $1
order by o.placed_at desc;

Design decisions that matter

  • Cart vs order: mutable vs immutable. cart_items can change; order_items never should. Copy, don't move, at checkout.
  • Addresses are separate from customers. One customer has many addresses (home, work). Orders reference specific address rows so the address at order time is preserved even if the customer later moves.
  • Categories are self-referential. categories.parent_id → categories.id builds a tree (Electronics → Phones → Cases) without a separate hierarchy table.
  • Prices in cents as integers. Avoid floating-point rounding. price_cents integer with a currency column is the safe default.

Explore it: the e-commerce schema diagram renders all 11 tables with foreign keys you can hover to trace — the same view you'd get introspecting a live store database.

FAQ

What tables does an e-commerce database need?
11 tables: customers, addresses, categories, products, product_images, carts, cart_items, orders, order_items, payments, and reviews.

What is the difference between cart_items and order_items?
Cart items are mutable until checkout. Order items are immutable snapshots copied at order time, preserving price and product as purchased.

Should I store prices as float or integer?
Integer cents. Float rounding corrupts money. Store price_cents integer and divide by 100 only at display time.

How do categories handle subcategories?
Self-referential foreign key: categories.parent_id → categories.id. Top-level categories have parent_id = NULL; children point at their parent.

Visualize your own database

Paste your PostgreSQL connection string and get an interactive ER diagram in under 10 seconds. No signup required.