Contoso Airlines' management has been asking the same question for months and nobody can answer it with data: which routes are genuinely profitable by season. It is not an idle query; the winter schedule, the renegotiation of charges with two airports and the decision to keep or close the Palma–Munich route all depend on it. Answering it means crossing three years of sales from db-reservas, the real occupancy of the flights, the fares applied that live in Cosmos DB, the per-rotation costs from the crew system and the operational delays: some 400 million rows.
The first instinct — firing that query at db-reservas — is exactly what must not be done. This lesson builds the alternative: an analytics platform that answers big questions about historical data without touching the operational databases even once. And with it, the module closes.
Important cost warning: analytics is where bills spiral fastest. A Synapse dedicated SQL pool costs more than €1,000 a month if you leave it running, and you have to pause it by hand. Serverless SQL pools bill per TB read, so a badly written query over unpartitioned data can cost more than a whole day of virtual machines. To learn, use serverless SQL with small files, and delete the resource group when you finish.
Contents
- Why operational databases are no good for analytics
- The analytics architecture and its five pieces
- Azure Data Lake Storage Gen2
- The bronce, plata and oro layers
- File formats: CSV versus Parquet and Delta
- Azure Data Factory: the nightly pipeline
- Copy activity versus mapping data flows
- Azure Synapse Analytics
- Microsoft Fabric, honestly
- Power BI and the profitability dashboard
- Data governance with Microsoft Purview
- The costs of analytics and how they spiral
- Common Mistakes and Tips
- Exercises
- Conclusion
- Why operational databases are no good for analytics
A transactional database (OLTP) and an analytical one (OLAP) are optimized for opposite things:
OLTP (db-reservas) |
OLAP (analytics platform) | |
|---|---|---|
| Typical question | "Give me booking X7K2QP" | "Revenue by route and month over three years" |
| Rows per query | One or a few | Hundreds of millions |
| Columns per query | Nearly all of one row | 3 or 4 out of a great many rows |
| Storage | By rows | By columns |
| Writes | Constant, small and concurrent | Periodic bulk loads |
| Model | Normalized | Denormalized (star schema) |
| Priority | Latency per operation and integrity | Volume processed per query |
| Concurrency | Thousands of users | Dozens of analysts |
The practical consequence: an analytical query against the operational database walks over millions of rows, fills the engine's memory, invalidates its caches and locks resources. Even if it eventually works, it degrades ticket sales for every customer for as long as it runs. That is why the two worlds are kept apart, and the separation has a business advantage too: the analytical history keeps data that the operational database deletes or archives.
- The analytics architecture and its five pieces
Every modern analytics platform, whatever it is called, has the same five pieces:
flowchart LR
subgraph origenes[Sources]
SQL[(Azure SQL<br/>db-reservas)]
COS[(Cosmos DB<br/>fares)]
PG[(PostgreSQL<br/>crews)]
EXT[External files<br/>fuel, airport charges]
end
ADF[Azure Data Factory<br/>INGESTION]
subgraph lago[Data Lake Gen2 - STORAGE]
BRO[bronce<br/>raw]
PLA[plata<br/>clean]
ORO[oro<br/>aggregated]
end
SYN[Synapse<br/>TRANSFORM AND SERVE]
PBI[Power BI<br/>CONSUMPTION]
SQL --> ADF
COS --> ADF
PG --> ADF
EXT --> ADF
ADF --> BRO --> PLA --> ORO
SYN -.queries and transforms.-> lago
ORO --> PBI
Ingestion (bringing the data in), storage (keeping it cheaply and raw), transformation (cleaning and aggregating it), serving (exposing it for querying) and consumption (visualizing it). Azure has a service for each piece, and what matters is understanding each one's role before its menus.
- Azure Data Lake Storage Gen2
Data Lake Storage Gen2 is not a separate service: it is an Azure Storage account, the same one from lesson 02-04, with one option turned on at creation time: the hierarchical namespace. That changes two fundamental things:
- Real directories. In Blob Storage,
2026/07/ventas.parquetis a flat name that simulates folders; renaming "a folder" holding a million files means copying and deleting them one by one. With a hierarchical namespace, the directory genuinely exists and renaming it is an atomic operation that takes milliseconds. For an analytics engine that writes and reorganizes thousands of files, the difference is enormous. - POSIX ACLs. In addition to RBAC access control, you can assign read, write and execute permissions at directory and file level to Microsoft Entra ID users and groups. Contoso uses this so that Nuria Peña's finance team can read the
orolayer but has no access to the personal data in thebroncelayer.
RG="rg-contoso-reservas-pro"
LAKE="stlagocontosopro"
az storage account create --name $LAKE --resource-group $RG --location westeurope \
--sku Standard_ZRS --kind StorageV2 \
--enable-hierarchical-namespace true \ # this is what makes it Data Lake Gen2
--tags entorno=produccion proyecto=contoso-reservas centro-coste=CC-1042 [email protected]
az storage fs create -n lago --account-name $LAKE --auth-mode login
az storage fs directory create -n bronce -f lago --account-name $LAKE --auth-mode login
az storage fs directory create -n plata -f lago --account-name $LAKE --auth-mode login
az storage fs directory create -n oro -f lago --account-name $LAKE --auth-mode loginEverything learned in 02-04 still applies: Hot, Cool and Archive access tiers with lifecycle policies, redundancy, encryption and access with an Entra ID identity instead of keys.
- The bronce, plata and oro layers
Organizing the lake into three layers — also known as the medallion architecture — avoids the usual fate of data lakes: turning into a swamp where nobody knows which file is trustworthy.
| Layer | What it holds | Format | At Contoso |
|---|---|---|---|
| bronce (raw) | A raw copy of the source, untransformed, with the ingestion date | Parquet or the original format | bronce/reservas/anio=2026/mes=08/dia=11/ |
| plata (clean) | Clean, typed, deduplicated data with personal data pseudonymized | Parquet or Delta | plata/reservas/ with PasajeroId replaced by an irreversible identifier |
| oro (aggregated) | Aggregates ready to consume, organized around business questions | Parquet or Delta | oro/rentabilidad_ruta_mes/ |
The three rules that make it work: the bronce layer is never modified (if a transformation fails, it is redone from there without bothering the source again); transformations only move forwards; and only the oro layer is connected to dashboards, which stops every analyst building their own version of the truth.
Look at the path in bronce: the data is stored partitioned by date in anio=/mes=/dia= directories. That is not cosmetic. When a query filters on August 2026, the engine reads only that directory instead of three years of files. It is the optimization that saves the most money in this entire lesson.
- File formats: CSV versus Parquet and Delta
| CSV | Parquet | Delta Lake | |
|---|---|---|---|
| Organization | By rows, plain text | By columns, binary | Parquet plus a transaction log |
| Compression | None | High (5–10 times smaller) | High |
| Schema and types | Not stored | Included in the file | Included, with evolution control |
| Reading 3 of 40 columns | Reads the whole file | Reads only those 3 | Reads only those 3 |
Transactions and UPDATE/DELETE |
No | No | Yes, with versioning and time travel |
| Use | Exchange with external systems | The analytics standard | When updates or ACID are needed |
The reason the columnar format matters so much is directly financial. The profitability query uses 4 columns out of a 40-column table. In CSV you have to read all 400 GB; in Parquet you read around 12 GB, and since Synapse serverless bills per TB read, the same query costs about 30 times less. Practical rule: data comes in however it comes, but in the lake it is stored in Parquet (or Delta if rows have to be updated), always partitioned by date.
- Azure Data Factory: the nightly pipeline
Data Factory is the integration and orchestration service. Its five concepts, which are the same in Synapse Pipelines and in Fabric:
| Concept | What it is |
|---|---|
| Linked service | The connection string to a source or destination (db-reservas, the lake, Cosmos DB) |
| Dataset | The specific shape of the data within that service: a table, a folder, a file |
| Activity | A step: copy, run a notebook, call a procedure, branch on a condition |
| Pipeline | A sequence of activities with its dependency and retry logic |
| Trigger | What sets it off: a schedule, a tumbling window or an event |
| Integration runtime | Where it actually runs: Azure (managed), self-hosted (for on-premises sources) or SSIS |
That last concept is the one that causes the most confusion and the one Contoso needs to understand properly: to read db-reservas, which is only reachable through a private endpoint, you need a runtime with access to the virtual network; and to read the legacy billing system in the Barcelona basement you would need a self-hosted runtime, an agent installed on an on-premises server that establishes the outbound connection.
Contoso's nightly pipeline copies the previous day's bookings into the bronce layer:
{
"name": "pl-reservas-diario",
"properties": {
"activities": [{
"name": "CopiarReservasDelDia",
"type": "Copy",
"typeProperties": {
"source": {
"type": "AzureSqlSource",
"sqlReaderQuery": "SELECT * FROM dbo.Reservas WHERE CreadaUtc >= '@{formatDateTime(pipeline().parameters.dia,'yyyy-MM-dd')}' AND CreadaUtc < '@{formatDateTime(addDays(pipeline().parameters.dia,1),'yyyy-MM-dd')}'"
},
"sink": {
"type": "ParquetSink",
"storeSettings": { "type": "AzureBlobFSWriteSettings" }
}
},
"policy": { "timeout": "01:00:00", "retry": 2, "retryIntervalInSeconds": 300 }
}],
"parameters": { "dia": { "type": "String" } }
}
}Three design decisions are written into that JSON. The query brings back only the previous day instead of the whole table: that is called an incremental load and it is the difference between a two-minute copy and a two-hour one that also punishes the operational database. The destination is Parquet in the lake. And the retry policy stops a transient network failure forcing somebody to relaunch the pipeline by hand in the morning.
The trigger is a tumbling window at 03:00, not a simple schedule, because it guarantees non-overlapping time windows and it lets you reprocess a specific day if an error is found: you relaunch the window for 11 August and only that directory is corrected.
- Copy activity versus mapping data flows
| Copy activity | Mapping data flow | |
|---|---|---|
| What it does | Moves data from A to B | Transforms: joins, aggregates, derives columns, deduplicates |
| How it is defined | A source and a sink | A visual design with no code to write |
| Where it runs | The integration runtime | A managed Spark cluster that Azure spins up |
| Cost | Low, by integration units and time | High: you pay for the cluster, with a start-up of several minutes |
| When to use it | Ingestion into the bronce layer |
Transformations from bronce to plata without writing Spark |
The criterion is straightforward: use the copy activity to move data and data flows only when the transformation justifies them. If your team can write SQL or Python, transforming with a Spark notebook or with serverless Synapse views is usually cheaper and easier to version in Git than a visual flow.
- Azure Synapse Analytics
Synapse is a workspace that brings several engines together. What matters is knowing which one to use:
| Engine | How it bills | What it is for |
|---|---|---|
| Serverless SQL pool | Per TB read; nothing is running | Exploring and querying the lake directly, creating views for Power BI |
| Dedicated SQL pool | Per DWU hour while it is active | A classic data warehouse with concurrent workloads and constant high performance |
| Spark pool | Per node hour, with auto-pause | Complex transformations, Python, machine learning |
| Synapse Pipelines | The same as Data Factory | Orchestration inside the same workspace |
The serverless pool is the natural way in, because it lets you query the lake without loading anything anywhere:
-- Query Parquet directly from the lake, with no ingestion at all
SELECT r.origen_destino,
YEAR(r.creada_utc) AS booking_year,
MONTH(r.creada_utc) AS booking_month,
COUNT_BIG(*) AS bookings,
SUM(r.importe_eur) AS revenue_eur
FROM OPENROWSET(
BULK 'https://stlagocontosopro.dfs.core.windows.net/lago/plata/reservas/**',
FORMAT = 'PARQUET'
) AS r
WHERE r.creada_utc >= '2024-01-01'
GROUP BY r.origen_destino, YEAR(r.creada_utc), MONTH(r.creada_utc)
ORDER BY revenue_eur DESC;OPENROWSET reads the files where they are; the ** walks the partition subdirectories. There are no tables to create and no data to load, and if more files land on that path tomorrow, the same query picks them up. When a query like this is used daily, it is saved as a view in a serverless database and Power BI connects to it.
The dedicated pool, on the other hand, is a full data warehouse with its own columnar storage and table distribution. It brings constant performance and high concurrency, and it costs what it costs: you pay per hour it is running, used or not, so it is paused when it is not needed. Contoso, with 400 million rows and a dozen analysts, starts with serverless and will only consider a dedicated pool if concurrency demands it.
# If a dedicated pool ever gets used, pausing it when you finish is MANDATORY
az synapse sql pool pause --name sqlpool-contoso --workspace-name syn-contoso-analitica-pro -g $RG
- Microsoft Fabric, honestly
Microsoft Fabric is the evolution towards which Microsoft is converging all of this: Data Factory, Synapse, Power BI and the lake in a single SaaS platform, with common storage called OneLake, Delta format by default and billing by capacity instead of by service.
The honest position is three statements. First: this lesson's concepts — lake layers, Parquet, pipelines, SQL and Spark engines — are the same in Fabric, so nothing you have learned is lost. Second: Fabric is where new development is pointing, and for a project starting today it deserves serious evaluation. Third: Synapse and Data Factory are still fully supported and they are what is deployed in most organizations, including this course's Contoso, which already has its platform built and is not going to rebuild it for a new arrival.
- Power BI and the profitability dashboard
Power BI is the consumption layer: it connects to the oro layer or to the serverless views and produces the dashboard that answers management's question. Contoso's has one page per question: revenue and margin by route and season, average occupancy against break-even, the trend over the last 36 months, and per-route detail.
Two technical decisions worth knowing: Import mode copies the data into the Power BI model and gives instant answers, at the price of the data being as fresh as the last refresh; DirectQuery mode queries the source on every interaction, always up to date but slower and with a cost per query — important here, because every interaction with serverless reads TB and bills for them. For a dashboard refreshed every night, Import is the right and the cheapest choice.
- Data governance with Microsoft Purview
Once you have a lake with three layers, dozens of files and several teams consuming from it, new questions appear: where does this number come from? Who can see this column? Where is there personal data?
Microsoft Purview answers that with a catalog that automatically scans the sources and builds an inventory, classifies sensitive data (detecting that a column contains email addresses or identity document numbers) and shows the lineage: which source feeds which file and which report. For Contoso it matters especially because of GDPR: knowing exactly which lake layers hold passengers' personal data is a requirement, not a convenience. It is only introduced here; governance is picked up again with Azure Policy in lesson 04-06.
- The costs of analytics and how they spiral
The four causes of unpleasant bills, with their remedies:
| Cause | What happens | Remedy |
|---|---|---|
| A dedicated pool left running unused | Thousands of euros a month for an idle warehouse | Always pause it; automate it with Automation (07-04) |
| Serverless queries over unpartitioned CSV | Whole TB are read for a 4-column query | Parquet and partitioning by date; filter by partition |
| Copying whole tables every night | The same data is transferred and stored over and over | Incremental load by date, as in the nightly pipeline |
| Data flows for trivial transformations | You pay for a Spark cluster to rename columns | A copy activity, or serverless SQL |
And one hygiene measure that stops the slow drip: apply a lifecycle policy to the lake, like the one from lesson 02-04, so that bronce data more than a year old moves to the Cool tier and anything more than three years old to Archive. The oro layer, which is the one being queried, stays in Hot.
Common Mistakes and Tips
- Querying the operational database to produce reports. It degrades ticket sales for as long as it runs. It is the mistake this whole lesson exists because of.
- Forgetting to pause the dedicated SQL pool. It is Azure's most common surprise bill and the easiest to avoid.
- Storing the lake in CSV. It multiplies the cost of every serverless query by tens and loses the data types.
- Not partitioning by date. Without partitions, every query reads the whole history even if it only asks for one month.
- Modifying the
broncelayer. It is the faithful copy of the source and the safety net for redoing any transformation. Touch it and you lose that. - Connecting dashboards to the
platalayer "because it is more complete". Every analyst ends up with their own version of the truth and two reports give different figures in the same meeting. - Copying personal data into the
orolayer with no need. GDPR applies in the lake just the same; pseudonymize on the way intoplata. - Tip: always start with serverless SQL. Only when you have measured that concurrency or performance are not enough should you consider a dedicated pool.
- Tip: set a budget alert specifically for the analytics resource group. It is the one that drifts fastest.
Exercises
Exercise 1: designing the ingestion
Three sources have to be brought into the lake: db-reservas (growing by 40,000 rows a day), the Cosmos DB fare catalog (changing twice a day) and a monthly CSV of fuel costs sent by an external supplier.
- For each source, state the load strategy (full or incremental), the frequency and the type of trigger.
- What type of integration runtime does each one need, and why?
- In what format and with what directory structure would you store them in
bronce?
Exercise 2: choosing the engine and estimating the cost
The profitability query walks over three years of bookings: 400 GB in CSV or the equivalent 40 GB in Parquet partitioned by year and month, and it uses 4 columns out of 40. It will run once a day to refresh the dashboard and, occasionally, interactively.
- Which Synapse engine would you choose, and why do you rule out the others?
- Estimate the order of magnitude of the data read in CSV versus partitioned Parquet, if the query filters on a single year.
- Which Power BI connection mode would you use, and what impact does it have on cost?
Exercise 3: auditing a platform that has drifted
Nuria Peña spots that the analytics resource group has gone from €300 to €2,400 a month. You find: a dedicated SQL pool active for six weeks with no queries in the last four, unpartitioned CSV files throughout the bronce layer, a pipeline copying dbo.Reservas in full every night, and three data flows that only rename columns.
- Rank the four problems by potential saving.
- Propose a concrete fix for each one.
- What preventive measure would you put in place so that it does not happen again?
Solutions
Solution 1:
- Strategies:
db-reservas, incremental by date (only the previous day's bookings), daily in the small hours, with a tumbling window trigger so that specific days can be reprocessed. Cosmos DB, incremental via the change feed (lesson 03-03) or a full daily load, given that the catalog is small. The fuel CSV, a full monthly load with an event-based trigger that fires when the file appears in storage. db-reservasand Cosmos DB: an Azure runtime with access to the virtual network, because both sit behind a private endpoint and are not reachable from the internet. The CSV: a managed Azure runtime if the supplier drops it in a storage account, or self-hosted if it had to be collected from an on-premises server.- In Parquet, partitioned by date:
bronce/reservas/anio=2026/mes=08/dia=11/,bronce/tarifas/anio=2026/mes=08/dia=11/andbronce/combustible/anio=2026/mes=08/. The original CSV is kept exactly as it arrived alongside its Parquet version, because thebroncelayer must be able to reproduce the source.
Solution 2:
- The serverless SQL pool. The dedicated pool is ruled out because one daily query and a few interactive ones do not justify paying for a warehouse by the hour, and you would have to remember to pause it; Spark is ruled out because the transformation is a simple SQL aggregation and spinning up a cluster adds start-up minutes and cost without contributing anything.
- In unpartitioned CSV you read all 400 GB, since the row-based format forces you to walk every column and there are no partitions to discard. In partitioned Parquet, filtering on one year, you read roughly a third of the data and only 4 columns out of 40: on the order of 1–2 GB. The difference is two orders of magnitude, and since serverless bills per TB read, that is also the difference on the bill.
- Import, with a nightly refresh after the pipeline. With DirectQuery, every interaction by every user with the dashboard would fire a query against serverless and bill for the data read; with Import you pay for one read a day and the answers are instant.
Solution 3:
- Ranked by saving: (a) the idle dedicated pool, which on its own explains most of the overspend; (b) the unpartitioned CSV, which makes every serverless query more expensive; (c) the nightly full copy, which pays for transfer, storage and load on the operational database; (d) the trivial data flows, which spin up a Spark cluster to rename columns.
- Fixes: pause the dedicated pool immediately and, if there have been no queries in four weeks, delete it and work with serverless; convert the
broncelayer to Parquet partitioned by date and rewrite the query paths; change the pipeline to an incremental load with a day parameter and a tumbling window trigger; replace the three flows with a copy activity with column mapping, or with a serverless view. - Prevention: a budget alert specifically for the analytics resource group (lesson 01-03 and module 8), an Automation runbook that pauses the dedicated pool outside working hours, and a monthly Azure Advisor review. In module 4, Azure Policy is added to directly prevent certain resources from being created without tags or outside the permitted regions.
Conclusion
This lesson closes module 3 and Contoso Airlines' data map is complete. You know why a transactional database is no good for analytics — row storage versus columnar, normalization versus star schema, latency per operation versus volume processed — and why firing the profitability report at db-reservas would have degraded ticket sales. You know the five pieces of an analytics platform and which service fills each one. You have created Data Lake Storage Gen2 understanding what it adds over Blob Storage — a hierarchical namespace with real directories, and POSIX ACLs — and you have organized it into the bronce, plata and oro layers with their three rules: bronce is never touched, transformations only move forwards, and only oro feeds the dashboards. You know why Parquet partitioned by date is the decision that saves the most money, and when Delta is needed.
You have built the ingestion with Azure Data Factory, with its linked services, datasets, activities, pipelines, triggers and integration runtimes, and with a nightly incremental pipeline that copies the previous day's bookings into the lake with retries and reprocessable windows; and you can tell when a copy activity is enough and when paying for a mapping data flow is justified. In Synapse you have compared the four engines and queried the lake with serverless SQL through OPENROWSET, without loading anything anywhere, knowing that the dedicated pool is paid for by the hour it runs and has to be paused. You can place Microsoft Fabric as the convergence Microsoft is heading towards without what you have learned losing its value, you have taken the oro layer into Power BI choosing Import over DirectQuery on performance and cost grounds, and you know Microsoft Purview for cataloging, classifying and tracing data lineage.
Recapping the whole module: you started with the criteria for choosing a data service and came away with a reasoned map of five stores. You deployed db-reservas on Azure SQL Database with its schema, its indexes, its private endpoint and its point-in-time restore. You put the fare catalog in Cosmos DB with the partition key /origenDestino, its request units and its consistency level chosen piece of data by piece of data. You migrated the legacy portal to Azure Database for MySQL without rewriting a line of WordPress, and the crew system to Azure Database for PostgreSQL with PostGIS, PgBouncer and autovacuum under control. And today you have built the analytics platform that finally answers which routes are profitable by season.
That entire platform — compute, storage, networking and now data — has been built with minimal security settings: administrator passwords typed in by hand, connection strings with secrets inside them, broad permissions because that was the quick way, a WAF that does not exist yet, and not a single policy stopping anyone from creating an untagged resource in the wrong region. It was a conscious decision so that we could move forward, and now it is time to close it properly. In module 4, Security in Azure, you will start with Microsoft Entra ID and identity management, move on to RBAC and managed identities so that applications authenticate without a single password, centralize secrets in Azure Key Vault, protect the perimeter with DDoS protection and a web application firewall, assess your whole posture with Microsoft Defender for Cloud and finish by enforcing the rules of the game with Azure Policy. See you there.
Azure Course
Module 1: Introduction to Azure
- What Is Azure?
- Service Models, Regions and Availability Zones
- Creating and Setting Up Your Azure Account
- A Tour of the Azure Portal
- Azure Resource Manager: Subscriptions, Resource Groups and Tags
- Azure CLI, PowerShell and Cloud Shell
Module 2: Core Azure Services
- Azure Virtual Machines
- Compute Scaling and High Availability
- Azure App Service
- Azure Storage: Blobs, Files, Queues and Tables
- Azure Networking: Virtual Networks, Subnets and NSGs
- Hybrid Connectivity and Global Delivery
Module 3: Azure Databases
- Choosing the Right Data Service
- Azure SQL Database
- Azure Cosmos DB
- Azure Database for MySQL
- Azure Database for PostgreSQL
- Data Analytics: Data Lake, Data Factory and Synapse
Module 4: Security in Azure
- Microsoft Entra ID and Identity Management
- RBAC and Managed Identities
- Azure Key Vault
- DDoS Protection and Web Application Firewall
- Microsoft Defender for Cloud
- Governance and Compliance with Azure Policy
Module 5: Azure DevOps
- Introduction to Azure DevOps
- Azure Repos
- Azure Pipelines: Continuous Integration
- Continuous Deployment with Environments and Approvals
- Azure Artifacts
- Infrastructure as Code with Bicep
Module 6: Advanced Azure Services
- Containers in Azure: Container Registry and Container Apps
- Azure Kubernetes Service (AKS)
- Azure Functions
- Azure Logic Apps
- Messaging and Events: Service Bus, Event Grid and Event Hubs
- Azure AI Services
Module 7: Monitoring and Management
- Azure Monitor: Metrics, Alerts and Dashboards
- Log Analytics and KQL Queries
- Application Insights
- Azure Automation and Runbooks
- Backup and Disaster Recovery
Module 8: Cost Management and Optimization
- Pricing Calculator and Cost Estimation
- Azure Cost Management: Analysis, Budgets and Alerts
- Reservations, Savings Plans and Azure Hybrid Benefit
- Azure Advisor
- Optimization Strategies and FinOps Culture
