Designing the Analytical Database#
Database Schema Selection#
Let’s create an analytical database wwi_analytics for the dashboard.
To do this, we will transform the OLTP (current structure) to OLAP.
We will use a star schema as it is optimally suited for analytical databases.
Let’s switch to the analytical database.
con('dst')
Connected to dst
Let’s create a new schema where we will create the tables.
%%sql
CREATE SCHEMA analytics;
Install the dblink extension for communication between databases.
%%sql
CREATE EXTENSION IF NOT EXISTS dblink;
Form the connection string for dblink.
dblink_conn_str = f"""
host={src_db_config['host']}
dbname={src_db_config['db']}
user={src_db_config['user']}
password={src_db_config['pwd']}
port={src_db_config['port']}
"""
Dimension Tables#
Customers#
Create a dimension table for customers.
with wwi_analytics_engine.connect() as conn:
stmt = text(f"""
CREATE TABLE analytics.dim_customers AS
SELECT * FROM dblink(
'{dblink_conn_str}'
, 'SELECT
c.customer_id
, c.customer_name
, cc.customer_category_name
, ci.city_name
, sp.state_province_name
, CURRENT_DATE AS valid_from
, NULL::DATE AS valid_to
, TRUE AS current_record
FROM
sales.customers c
LEFT JOIN sales.customer_categories cc ON c.customer_category_id = cc.customer_category_id
LEFT JOIN application.cities ci ON c.delivery_city_id = ci.city_id
LEFT JOIN application.state_provinces sp ON ci.state_province_id = sp.state_province_id'
) AS t(
customer_id INT
, customer_name TEXT
, customer_category_name TEXT
, city_name TEXT
, state_province_name TEXT
, valid_from DATE
, valid_to DATE
, current_record BOOLEAN
);
ALTER TABLE analytics.dim_customers ADD PRIMARY KEY (customer_id);
""")
conn.execute(stmt)
conn.commit()
Products#
Create a dimension table for products.
with wwi_analytics_engine.connect() as conn:
stmt = text(f"""
CREATE TABLE analytics.dim_products AS
SELECT * FROM dblink(
'{dblink_conn_str}'
, 'WITH stock_groups_agg AS (
SELECT
sisg.stock_item_id
, STRING_AGG(sg.stock_group_name, '', '') AS stock_group_names
FROM
warehouse.stock_item_stock_groups sisg
LEFT JOIN warehouse.stock_groups sg ON sisg.stock_group_id = sg.stock_group_id
GROUP BY
sisg.stock_item_id
)
SELECT
si.stock_item_id
, si.stock_item_name
, sg.stock_group_names
, c.color_name
, ptu.package_type_name AS unit_package_type_name
, pto.package_type_name AS outer_package_type_name
, si.brand
, si.size
, CURRENT_DATE AS valid_from
, NULL::DATE AS valid_to
, TRUE AS current_record
FROM
warehouse.stock_items si
LEFT JOIN stock_groups_agg sg ON si.stock_item_id = sg.stock_item_id
LEFT JOIN warehouse.package_types ptu ON si.unit_package_id = ptu.package_type_id
LEFT JOIN warehouse.package_types pto ON si.outer_package_id = pto.package_type_id
LEFT JOIN warehouse.colors c ON c.color_id = si.color_id'
) AS t(
stock_item_id INT
, stock_item_name TEXT
, stock_group_names TEXT
, color_name TEXT
, unit_package_type_name TEXT
, outer_package_type_name TEXT
, brand TEXT
, size TEXT
, valid_from DATE
, valid_to DATE
, current_record BOOLEAN
);
ALTER TABLE analytics.dim_products ADD PRIMARY KEY (stock_item_id)
""")
conn.execute(stmt)
conn.commit()
Dates#
Create a dimension table for dates.
Select a range from 1 year before the minimum date in the database to 1 year after the maximum date in the database.
with wwi_analytics_engine.connect() as conn:
stmt = text(f"""
CREATE TABLE analytics.dim_dates AS
SELECT * FROM dblink(
'{dblink_conn_str}'
, 'WITH date_range AS (
SELECT
MIN(order_date) AS min_date
, MAX(confirmed_delivery_time::DATE) AS max_date
FROM
sales.orders o
JOIN
sales.invoices i ON o.order_id = i.order_id
)
SELECT
date_series::DATE AS date_id
, EXTRACT(DAY FROM date_series)::INT AS day
, EXTRACT(MONTH FROM date_series)::INT AS month
, EXTRACT(YEAR FROM date_series)::INT AS year
, EXTRACT(QUARTER FROM date_series)::INT AS quarter
, EXTRACT(DOW FROM date_series)::INT + 1 AS day_of_week
, TO_CHAR(date_series, ''Day'') AS day_name
, EXTRACT(DOW FROM date_series) IN (0, 6) AS is_weekend
, TO_CHAR(date_series, ''Month'') AS month_name
FROM
GENERATE_SERIES(
(SELECT min_date - INTERVAL ''1 year'' FROM date_range)
, (SELECT max_date + INTERVAL ''1 year'' FROM date_range)
, ''1 day''
) AS date_series;'
) AS t(
date_id DATE
, day INT
, month INT
, year INT
, quarter INT
, day_of_week INT
, day_name TEXT
, is_weekend BOOLEAN
, month_name TEXT
);
ALTER TABLE analytics.dim_dates ADD PRIMARY KEY (date_id)
""")
conn.execute(stmt)
conn.commit()
Delivery Methods#
Create a dimension table for delivery methods.
with wwi_analytics_engine.connect() as conn:
stmt = text(f"""
CREATE TABLE analytics.dim_delivery_methods AS
SELECT * FROM dblink(
'{dblink_conn_str}'
, 'SELECT
delivery_method_id
, delivery_method_name
FROM
application.delivery_methods'
) AS t(
delivery_method_id INT
, delivery_method_name TEXT
);
ALTER TABLE analytics.dim_delivery_methods ADD PRIMARY KEY (delivery_method_id)
""")
conn.execute(stmt)
conn.commit()
Fact Tables#
Orders#
Create a fact table for orders.
with wwi_analytics_engine.connect() as conn:
stmt = text(f"""
CREATE TABLE analytics.fact_orders AS
SELECT * FROM dblink(
'{dblink_conn_str}'
, 'SELECT
order_id
, customer_id
, order_date
, expected_delivery_date
, picking_completed_when
FROM
sales.orders'
) AS t(
order_id INT
, customer_id INT
, order_date DATE
, expected_delivery_date DATE
, picking_completed_when TIMESTAMP
);
ALTER TABLE analytics.fact_orders ADD PRIMARY KEY (order_id);
ALTER TABLE analytics.fact_orders ADD CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES analytics.dim_customers(customer_id);
ALTER TABLE analytics.fact_orders ADD CONSTRAINT fk_orders_date
FOREIGN KEY (order_date) REFERENCES analytics.dim_dates(date_id);
CREATE INDEX idx_fact_orders_customer ON analytics.fact_orders(customer_id);
CREATE INDEX idx_fact_orders_date ON analytics.fact_orders(order_date);
""")
conn.execute(stmt)
conn.commit()
Invoices#
Create a fact table for invoices.
with wwi_analytics_engine.connect() as conn:
stmt = text(f"""
CREATE TABLE analytics.fact_invoices AS
SELECT * FROM dblink(
'{dblink_conn_str}'
, 'WITH invoice_totals AS (
SELECT
i.invoice_id
, SUM(il.line_profit) AS invoice_profit
, SUM(il.extended_price) AS invoice_amount
, COUNT(il.invoice_line_id) AS invoice_lines_count
FROM
sales.invoices i
LEFT JOIN sales.invoice_lines il ON i.invoice_id = il.invoice_id
GROUP BY
i.invoice_id
),
payment_totals AS (
SELECT
invoice_id
, SUM(transaction_amount) AS paid_amount
FROM
sales.customer_transactions
where
is_finalized = TRUE
GROUP BY
invoice_id
)
SELECT
i.invoice_id
, i.order_id
, pt.paid_amount
, i.invoice_date
, i.delivery_method_id
, i.confirmed_delivery_time
, i.returned_delivery_data
, CASE WHEN i.confirmed_delivery_time IS NOT NULL THEN TRUE ELSE FALSE END AS is_delivered
, it.invoice_profit
, it.invoice_amount
, it.invoice_lines_count
FROM
sales.invoices i
LEFT JOIN invoice_totals it ON i.invoice_id = it.invoice_id
LEFT JOIN payment_totals pt ON it.invoice_id = pt.invoice_id;'
) AS t(
invoice_id INT
, order_id INT
, paid_amount NUMERIC
, invoice_date DATE
, delivery_method_id INT
, confirmed_delivery_time TIMESTAMP
, returned_delivery_data TEXT
, is_delivered BOOLEAN
, invoice_profit NUMERIC
, invoice_amount NUMERIC
, invoice_lines_count INT
);
ALTER TABLE analytics.fact_invoices ADD PRIMARY KEY (invoice_id);
ALTER TABLE analytics.fact_invoices ADD CONSTRAINT fk_invoices_order
FOREIGN KEY (order_id) REFERENCES analytics.fact_orders(order_id);
ALTER TABLE analytics.fact_invoices ADD CONSTRAINT fk_invoice_delivery
FOREIGN KEY (delivery_method_id) REFERENCES analytics.dim_delivery_methods(delivery_method_id);
ALTER TABLE analytics.fact_invoices ADD CONSTRAINT fk_invoices_date
FOREIGN KEY (invoice_date) REFERENCES analytics.dim_dates(date_id);
CREATE INDEX idx_fact_invoices_order ON analytics.fact_invoices(order_id);
CREATE INDEX idx_fact_invoices_date ON analytics.fact_invoices(invoice_date);
CREATE INDEX idx_fact_invoices_delivery_status ON analytics.fact_invoices(is_delivered);
CREATE INDEX idx_fact_invoices_delivery_method ON analytics.fact_invoices(delivery_method_id);
""")
conn.execute(stmt)
conn.commit()
Order Lines#
Create a fact table for order lines.
with wwi_analytics_engine.connect() as conn:
stmt = text(f"""
CREATE TABLE analytics.fact_order_lines AS
SELECT * FROM dblink(
'{dblink_conn_str}'
, 'SELECT
ol.order_line_id
, o.order_id
, ol.stock_item_id
, ol.quantity
, ol.unit_price
, il.quantity AS il_quantity
, il.line_profit AS il_line_profit
, il.extended_price AS il_extended_price
FROM
sales.orders o
LEFT JOIN sales.order_lines ol ON o.order_id = ol.order_id
LEFT JOIN sales.invoices i ON i.order_id = o.order_id
LEFT JOIN sales.invoice_lines il ON i.invoice_id = il.invoice_id AND ol.stock_item_id = il.stock_item_id'
) AS t(
order_line_id INT
, order_id INT
, stock_item_id INT
, quantity INT
, unit_price NUMERIC
, il_quantity INT
, il_line_profit NUMERIC
, il_extended_price NUMERIC
);
ALTER TABLE analytics.fact_order_lines ADD PRIMARY KEY (order_line_id);
ALTER TABLE analytics.fact_order_lines ADD CONSTRAINT fk_orderlines_order
FOREIGN KEY (order_id) REFERENCES analytics.fact_orders(order_id);
ALTER TABLE analytics.fact_order_lines ADD CONSTRAINT fk_orderlines_product
FOREIGN KEY (stock_item_id) REFERENCES analytics.dim_products(stock_item_id);
CREATE INDEX idx_fact_order_lines_product ON analytics.fact_order_lines(stock_item_id);
CREATE INDEX idx_fact_order_lines_order ON analytics.fact_order_lines(order_id);
CREATE INDEX idx_fact_order_lines_profit ON analytics.fact_order_lines(il_line_profit);
CREATE INDEX idx_fact_order_extended_price ON analytics.fact_order_lines(il_extended_price);
""")
conn.execute(stmt)
conn.commit()
Update database statistics to ensure optimal query performance.
%%sql
ANALYZE analytics.dim_customers;
ANALYZE analytics.dim_products;
ANALYZE analytics.dim_dates;
ANALYZE analytics.dim_delivery_methods;
ANALYZE analytics.fact_orders;
ANALYZE analytics.fact_invoices;
ANALYZE analytics.fact_order_lines;
As a result, we obtained the following schema.