Pipeline to Dashboardnihalsulaiman.com

Week 4 · Load to the warehouse › Day 23

Day 23. Azure SQL Database, hands on

⏱ 4h · learn 60m · lab 145m · ship 20m · review 15m🏷 marketing agencyBefore: Day 22

By the end of today you can

  • Create an Azure SQL database on the free offer (or a local SQL Server container) safely
  • Connect from Python with ODBC Driver 18, SQLAlchemy and Microsoft Entra tokens
  • Bulk-load fast with fast_executemany, bcp or BULK INSERT
  • Organise a database into raw, stg and mart schemas with least-privilege users

1. Why this matters

This is #TODO item 1 in your own plan: real pipelines writing to Azure SQL. Today you build the connection layer every later load uses, and you do it the way a security team would approve.

2. Create the database

Option A: Azure. In the Azure portal create a resource group rg-p2d, then Azure SQL Database. Microsoft offers a free Azure SQL Database offer (a General Purpose serverless database with a monthly allowance of vCore-seconds and storage; check the current terms in the portal when you create it). Choose:

  • Authentication: Microsoft Entra only or Entra + SQL. Set yourself as Entra admin.
  • Networking: public endpoint, allow your client IP only. Don't tick "allow all Azure services" unless you need it.
  • Compute: serverless with auto-pause so it costs nothing while idle. Set a cost alert in Cost Management on day one.

Option B: local, free, offline. The same engine in Docker:

docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=Str0ng!Passw0rd" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2022-latest

Everything in this week works on both. Use B for heavy experiments; use A for the portfolio screenshot and Entra auth.

3. Connect from Python

Install the Microsoft ODBC Driver 18 for SQL Server (Windows installer, or apt/brew per Microsoft docs), then:

import struct, urllib.parse
import sqlalchemy as sa
from azure.identity import DefaultAzureCredential

SERVER, DB = "p2d-sql.database.windows.net", "p2d"
odbc = (f"Driver={{ODBC Driver 18 for SQL Server}};Server=tcp:{SERVER},1433;Database={DB};"
        "Encrypt=yes;TrustServerCertificate=no;Connection Timeout=30")

def entra_token_connect_args():
    tok = DefaultAzureCredential().get_token("https://database.windows.net/.default").token.encode("utf-16-le")
    return {"attrs_before": {1256: struct.pack(f"<I{len(tok)}s", len(tok), tok)}}  # SQL_COPT_SS_ACCESS_TOKEN

engine = sa.create_engine(f"mssql+pyodbc:///?odbc_connect={urllib.parse.quote_plus(odbc)}",
                          connect_args=entra_token_connect_args(), fast_executemany=True, pool_pre_ping=True)

DefaultAzureCredential uses your az login locally and a managed identity in Azure (Day 27), so there's no password anywhere. For the local container, use a SQL login in the connection string from .env, with TrustServerCertificate=yes (local only).

Microsoft also ships a newer native Python driver (mssql-python); check its status before choosing it for production.

4. Schemas and users

create schema raw;   create schema stg;   create schema mart;   create schema ops;   -- ops = audit, watermarks

-- a loader identity that can write raw/stg/ops but not drop the database
create user [p2d-loader] from external provider;          -- Entra app or managed identity
-- (local container: create login p2d_loader with password = '...'; create user p2d_loader for login p2d_loader;)
grant select, insert, update, delete, alter on schema::raw to [p2d-loader];
grant select, insert, update, delete, alter on schema::stg to [p2d-loader];
grant select, insert, update on schema::ops to [p2d-loader];

-- a reader for Power BI
create role report_reader;
grant select on schema::mart to report_reader;

5. Loading fast

Method Speed When
df.to_sql(..., method=None) with fast_executemany=True Good to ~1M rows Default for daily loads
executemany without fast mode Very slow (row by row round trips) Never for bulk
bcp CLI Fast Big one-off loads from CSV
BULK INSERT / OPENROWSET from Azure Blob Fastest in Azure Files already in storage
dlt mssql destination Good, plus schema evolution When using dlt
Maths and principle

Principle: network round trips dominate small-batch loading. Batching turns \(N\) trips into \(N/b\).

Equation: \(T \approx \frac{N}{b} \cdot \text{RTT} + \frac{N \cdot s}{\text{bandwidth}}\). Row-by-row (\(b=1\)) with 30 ms RTT for 100,000 rows: 3,000 s. Batches of 10,000: 0.3 s of latency plus transfer.

Analogy: moving house in one van trip vs a trip per box. Where it breaks: huge batches hit memory and transaction-log limits; tune \(b\) (10k–100k rows).

Try it: 500,000 rows, RTT 20 ms, \(b\) = 5,000. Latency part? (100 trips × 20 ms = 2 s.)

The 5-year lens

Security teams say yes to: Entra-only auth, managed identities, IP-restricted or private endpoints, least-privilege users per job, auditing on. They say no to: a shared sa-style admin login in a config file. Build it the "yes" way in your portfolio and say so in the README; a hiring manager in a Microsoft shop will notice immediately.

Lab

  1. Create the database (A, B or both). Set a cost alert if A.
  2. Build warehouse/azure_sql.py: engine factory with Entra token (A) or SQL login (B), chosen by settings.
  3. Create the schemas, a loader user and a reader role.
  4. Load raw.ads_daily from your staging Parquet with to_sql + fast_executemany. Time it at 10k, 100k, 1M rows (repeat rows to scale). Then time without fast_executemany at 10k. Record the table.
  5. Write ops.load_audit(load_id, asset, partition_key, rows, started_at, finished_at, status) and insert a row per load.

Ship

feat(warehouse): Azure SQL connection with Entra tokens, schemas, roles, timed bulk load.

Check yourself

1. Which connection method avoids storing any password for a pipeline running in Azure?

2. Loading 200,000 rows with pyodbc executemany without fast_executemany is slow because...

3. What permission should the Power BI reader have?

My notes