Week 4 · Load to the warehouse › Day 23
Day 23. Azure SQL Database, hands on
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
- Create the database (A, B or both). Set a cost alert if A.
- Build
warehouse/azure_sql.py: engine factory with Entra token (A) or SQL login (B), chosen by settings. - Create the schemas, a loader user and a reader role.
- Load
raw.ads_dailyfrom your staging Parquet withto_sql+fast_executemany. Time it at 10k, 100k, 1M rows (repeat rows to scale). Then time withoutfast_executemanyat 10k. Record the table. - 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?
Managed identity + Entra means no secret to leak or rotate.
2. Loading 200,000 rows with pyodbc executemany without fast_executemany is slow because...
Round trips dominate. Batch them.
3. What permission should the Power BI reader have?
Readers read the modelled layer, nothing else.