Module 10

Performance Optimization

10.1 Partitioning in Microsoft Fabric

12 lessonsMicrosoft FabricDP-700 Track
Module 10 · Lesson 10.1

Partitioning

10.1 Partitioning in Microsoft Fabric

Partitioning means physically organizing a large dataset into smaller groups based on the values of one or more columns.

The main reason we use partitioning is:

Instead of scanning the entire dataset, the query can read only the partitions it needs.

This can significantly improve performance for large datasets.

1. Simple example

Imagine a Sales table containing 1 billion rows from 2022–2026.

Without partitioning:

Sales Table
────────────────────

2022 data

2023 data

2024 data

2025 data

2026 data

Total = 1 Billion Rows

You run:

SELECT *
FROM Sales
WHERE SalesYear = 2026;

Without effective data skipping/partitioning, the engine may need to examine much more data than the 2026 portion.

But if the data is partitioned by year:

Sales
│
├── SalesYear=2022
│
├── SalesYear=2023
│
├── SalesYear=2024
│
├── SalesYear=2025
│
└── SalesYear=2026

The engine can target:

SalesYear=2026

Read required files

Return results

This is called partition pruning.

2. Think of partitioning like a filing cabinet

Imagine you have 1 million documents.

Without organization

Big Box
│
├── January documents
├── March documents
├── December documents
├── February documents
├── July documents
└── ...

Finding December documents requires searching a huge collection.

Partitioned

Documents
│
├── January
├── February
├── March
│

...

└── December

Now:

"Give me December documents."

You go directly to:

December

That's the basic idea behind partitioning.

3. Partitioning in a Fabric Lakehouse

In Microsoft Fabric, partitioning is particularly important when working with Lakehouse / Delta tables and Spark.

Suppose:

Lakehouse
   │
   ▼
Tables
   │
   ▼

FactSales

The table contains:

SalesID

CustomerID

ProductID

SalesDate

Country

Amount

You could partition the underlying Delta data by something like:

Year

or:

Year + Month

depending on data volume and query patterns.

4. Physical organization

Suppose you partition by:

Year

Conceptually, the underlying storage might look like:

FactSales/
│
├── Year=2024/
│   ├── part-00001.parquet
│   ├── part-00002.parquet
│   └── part-00003.parquet
│
├── Year=2025/
│   ├── part-00001.parquet
│   ├── part-00002.parquet
│   └── part-00003.parquet
│
└── Year=2026/
    ├── part-00001.parquet
    ├── part-00002.parquet
    └── part-00003.parquet

Notice something important:

Partition ≠ File

A partition can contain multiple Parquet files.

5. Query without partition pruning

Imagine:

2024 → 200 GB

2025 → 250 GB

2026 → 300 GB

Total:

750 GB

Query:

SELECT SUM(Amount)
FROM FactSales
WHERE Year = 2026;

If the engine had to scan everything:

2024  ─┐
2025   ├── Scan 750 GB
2026  ─┘

That's expensive.

6. Query with partition pruning

With useful partitioning:
WHERE Year = 2026
       │
       ▼
Partition Pruning
       │
       ├── Year=2024 ✕
       ├── Year=2025 ✕
       │
       └── Year=2026 ✓

Only the relevant partition needs to be read.

Conceptually:

750 GB total

Query Year=2026

Read ~300 GB

That can reduce:

I/O

Data scanned

Processing

Query time

7. Partitioning by Date

Date/time columns are common partitioning candidates because analytical queries often filter by time.

Suppose your data spans 10 years.

You could partition by:

Year

Result:

FactSales
│
├── Year=2017
├── Year=2018
├── Year=2019
│

...

├── Year=2025
└── Year=2026

Queries like:

WHERE Year = 2026

can benefit.

8. Year + Month partitioning

What if each year contains enormous amounts of data?

For example:

2026 = 10 TB

A single yearly partition may still be too large.

You might use:

Year
 +

Month

Result:

FactSales
│
└── Year=2026
    │
    ├── Month=01
    ├── Month=02
    ├── Month=03
    ├── Month=04
    │

...

└── Month=12

Now a query for July 2026 can target:

Year=2026

Month=07

instead of reading the whole year.

9. Example

Query:

SELECT
    ProductID,
    SUM(Amount)
FROM FactSales
WHERE Year = 2026

AND Month = 7

GROUP BY ProductID;

The engine can potentially skip:

2024 ✕

2025 ✕

2026
 ├── January         ✕
 ├── February        ✕
 ├── March           ✕
 ├── April           ✕
 ├── May             ✕
 ├── June            ✕
 ├── July            ✓
 ├── August          ✕

...

This is partition pruning.

10. Choosing a partition column

This is where partition design becomes important.

A good partition column is often:

Frequently used in query filters.

Reasonably distributed.

Low-to-moderate cardinality.

Able to create partitions large enough to avoid excessive small files.

Typical candidates:

Year

Month

Date

Region

Country

But the correct choice depends on the data and workload.

11. Bad partition column: CustomerID

Imagine:

100 million customers

and you partition by:

CustomerID

You could potentially create an enormous number of partitions:

CustomerID=1

CustomerID=2

CustomerID=3

...

CustomerID=100000000

That's usually a terrible partitioning strategy.

This is called:

High-cardinality partitioning

12. What is cardinality?

Cardinality means the number of distinct values in a column.

Examples:

Gender
------

Distinct values ≈ 2–few

LOW CARDINALITY

Country
-------

Distinct values ≈ hundreds

MODERATE CARDINALITY

CustomerID
----------

Distinct values = millions

HIGH CARDINALITY

Partitioning on extremely high-cardinality columns can create too many tiny partitions/files.

13. Over-partitioning

Suppose you partition by:

Year

Month

Day

Hour

Minute

You might end up with:

Year=2026

 └── Month=08
      └── Day=03
           └── Hour=10
                └── Minute=01

Then:

Minute=01 → 1 tiny file

Minute=02 → 1 tiny file

Minute=03 → 1 tiny file

...

Now you have potentially thousands or millions of tiny files.

This can hurt performance.

14. The Small Files Problem

This is one of the most important topics in Fabric/Spark performance.

Bad:

Dataset = 100 GB

100,000 files

×

~1 MB each

Spark has to manage huge numbers of files/tasks.

There is overhead for:

File discovery

Task scheduling

Metadata

Opening files

Closing files

Processing

So:

More partitions does NOT automatically mean better performance.

15. Partitioning and File Size are connected

Notice your syllabus:

10.1 Partitioning

10.2 File Sizes

These topics are intentionally related.

Good design:

Large Dataset

Useful Partitioning

Reasonably sized files

Efficient Spark processing

Bad design:

Large Dataset

Too many partitions

Tiny files

Too many Spark tasks

Poor performance

16. Spark partitioning

There is another concept you need to distinguish:

Spark execution/dataframe partitions

versus:

Storage/table partitions

They are related but not exactly the same thing.

Table/storage partitioning

FactSales/
├── Year=2025/
└── Year=2026/

This affects physical data organization.

Spark partitions

When Spark processes data:

Dataset

Partition 1 → Executor

Partition 2 → Executor

Partition 3 → Executor

Partition 4 → Executor

These affect parallel processing.

This distinction becomes important in your 10.3 Spark Optimization topic.

17. Creating a partitioned Delta table with Spark

For example, suppose your DataFrame is:

df

You can write:

df.write \
    .format("delta") \
    .mode("overwrite") \
    .partitionBy("Year") \
    .saveAsTable("FactSales")

Conceptually:

DataFrame

partitionBy("Year")

Delta Table
   │
   ├── Year=2024
   ├── Year=2025
   └── Year=2026

18. Partitioning by Year and Month

You could also use:

df.write \
    .format("delta") \
    .mode("overwrite") \
    .partitionBy("Year", "Month") \
    .saveAsTable("FactSales")

Result:

FactSales
│
├── Year=2025
│   ├── Month=01
│   ├── Month=02
│   └── ...
│
└── Year=2026
    ├── Month=01
    ├── Month=02
    └── ...

But don't automatically choose multiple columns. The decision should be driven by data volume and query patterns.

19. repartition() in Spark

Now we move to Spark partitions, not table partitioning.

Suppose:

df.rdd.getNumPartitions()

returns:

4

You can redistribute the DataFrame:

df2 = df.repartition(20)

Now Spark can process the data using approximately:

20 partitions

Conceptually:

Original

[P1] [P2] [P3] [P4]

repartition(20)

[P1] [P2] [P3] ... [P20]

This can increase parallelism, but it involves a shuffle.

20. repartition() by column

You can also write:

df2 = df.repartition("Region")
Spark redistributes rows according to the specified expression/column.

Conceptually:

Data
 │
 ▼
Shuffle
 │
 ├── Partition
 ├── Partition
 ├── Partition
 └── ...

This may be useful before certain joins, aggregations, or writes, but it should be used deliberately because shuffling is expensive.

21. coalesce()

If you have too many Spark partitions, you may want fewer.

For example:

df2 = df.coalesce(10)

Conceptually:

100 partitions

coalesce(10)
      ↓

10 partitions

A useful beginner distinction:

repartition()
     ↓

Redistributes data

Can increase/decrease partitions

Usually causes shuffle

coalesce()
     ↓

Usually reduces partitions

Often avoids a full shuffle

22. Partitioning and parallelism

Spark is a distributed processing engine.

Suppose:

1 TB Dataset

If effectively processed as one giant partition:

1 TB

One Task

Slow

If appropriately divided:

1 TB
 │
 ├── P1
 ├── P2
 ├── P3
 ├── P4
 ├── ...
 └── Pn

Spark can process multiple partitions in parallel across available executors/cores.

P1 → Executor 1

P2 → Executor 2

P3 → Executor 3

P4 → Executor 4

That's one of the fundamental ideas behind Spark performance.

23. But more partitions aren't always better

Suppose you create:

1,000,000 partitions

for a 10 GB dataset.

Now Spark has huge scheduling/metadata overhead.

Think:

Too Few Partitions

Poor Parallelism

Too Many Partitions

Scheduling + File Overhead

Balanced Partitions

Better Performance

Optimization is about finding an appropriate balance.

24. Data Skew

Another partition-related problem is data skew.

Suppose sales distribution is:

India → 800 GB

USA → 100 GB

UK → 50 GB

Germany → 50 GB

If work is distributed poorly by Country, one worker may receive:

India → 800 GB

while others receive much less.

Executor 1 → 800 GB ← overloaded

Executor 2 → 100 GB

Executor 3 → 50 GB

Executor 4 → 50 GB

Most executors finish quickly, but one keeps working.

This is data skew.

25. Balanced partitioning

Ideal distribution looks more like:

Executor 1 → 250 GB

Executor 2 → 250 GB

Executor 3 → 250 GB

Executor 4 → 250 GB

Balanced work generally improves parallel processing.

You'll encounter this again when learning:

Spark Optimization

26. Partition Pruning

This term is very important for interviews.

Partition pruning means:

The query engine skips partitions that cannot contain the requested data.

Example:

WHERE Year = 2026

Storage:

Year=2023 ✕

Year=2024 ✕

Year=2025 ✕

Year=2026 ✓

Only the required partition is targeted.

This reduces:

Data scanned

I/O

CPU work

Query duration

27. Partition elimination vs full scan

Full scan

Query

Scan

2023

2024

2025

2026

Filter

Result

Partition pruning

Query

WHERE Year=2026
       ↓

Partition metadata

Skip 2023

Skip 2024

Skip 2025

Read 2026

Result

That's the performance benefit you're trying to achieve.

28. Real-world example

Imagine an IoT table:

SensorEvents

100 TB

Data:

Timestamp

SensorID

Temperature

Location

Status

Most queries are:

Last 7 days

Last 30 days

Yesterday

Current month

A time-based partition strategy may make sense because queries frequently filter on time.

SensorEvents
│
├── Date=2026-08-01
├── Date=2026-08-02
├── Date=2026-08-03
└── ...

Then:

WHERE Date = '2026-08-03'

can target the relevant date partition.

However, whether daily, monthly, or another strategy is best depends on how much data arrives each day.

29. When should you partition?

Partitioning becomes most useful when:

✓ Dataset is large

✓ Queries frequently filter on the partition column

✓ Partitions remain reasonably large

✓ Data distribution is appropriate

It may be unnecessary or harmful when:

✕ Dataset is small

✕ Partition column has extremely high cardinality

✕ Each partition contains tiny amounts of data

✕ Queries rarely filter on the partition column

30. Partitioning in Medallion Architecture

Suppose your Fabric architecture is:

Source

Bronze

Silver

Gold

You may use different strategies depending on each layer.

For example:

BRONZE

Raw Events

Date-based organization

SILVER

Cleaned Events

Date/Business-oriented strategy

GOLD

Aggregated Data

Often much smaller

May require less partitioning

Don't assume every table at every layer needs identical partitioning.

31. Common mistakes

Avoid blindly doing these:

Partition every table

or:

Partition by every filter column

or:

Partition by CustomerID

with millions of customers

or:

Create millions of tiny files

or:

Use Year/Month/Day/Hour/Minute

without checking data volume

The best strategy depends on:

Data size + data distribution + query pattern + file sizes

32. Interview question — What is Partitioning?

A strong answer:

Partitioning is a technique for organizing a large dataset into smaller logical/physical groups based on one or more columns. In Fabric Lakehouse and Delta scenarios, good partitioning can improve query performance through partition pruning, where the engine reads only relevant partitions instead of scanning unnecessary data.

33. Interview question — What is Partition Pruning?

Partition pruning is an optimization where the query engine skips partitions that don't match the query filter. For example, if a Delta table is partitioned by year and the query filters Year = 2026, the engine can skip partitions for previous years.

34. Interview question — What makes a good partition column?

A strong answer:

A good partition column is frequently used in query filters, has suitable cardinality and distribution, and produces partitions large enough to avoid the small-files problem. Date-related columns such as year or month are common candidates for large time-based datasets, but the design should depend on the workload.

35. Interview question — Why not partition by CustomerID?

CustomerID usually has very high cardinality. Partitioning by millions of customer IDs could create millions of small partitions and files, increasing metadata and task-management overhead and potentially reducing performance instead of improving it.

36. Interview question — repartition() vs coalesce()

For Spark:

repartition() redistributes data across partitions and typically causes a shuffle; it can increase or decrease the partition count. coalesce() is commonly used to reduce the number of partitions and can often do so without a full shuffle.

37. Quick revision

PARTITIONING
================================

Purpose:

Split large datasets

into manageable groups

Example:

FactSales
│
├── Year=2024
├── Year=2025
└── Year=2026

Query:

WHERE Year = 2026

Partition Pruning:

2024 ✕

2025 ✕

2026 ✓

Benefits:
--------

Less data scanned

Less I/O

Faster queries

Better manageability

Risks:
------

Too many partitions

Small files

High-cardinality columns

Data skew

One diagram to remember

              10 TB FACT_SALES
                     │
                     ▼
                 PARTITION
                     │
        ┌────────────┼────────────┐
        ▼            ▼            ▼

Year=2024 Year=2025 Year=2026

      3 TB          3 TB          4 TB
        │            │            │

✕ ✕ ✓

                         WHERE Year=2026
                                  │
                                  ▼
                         Read required data
                                  │
                                  ▼
                           Faster Query

The key takeaway

Remember this chain:

Large Table → Partition → Query Filter → Partition Pruning → Less Data Scanned → Better Performance

And one important warning:

Partitioning is not "the more partitions, the better."

Good partitioning means choosing a partition strategy that matches your data volume and query patterns.

↑ Back to top
Module 10 · Lesson 10.2

Module 10 · Lesson 10.2

File Sizes

10.2 File Sizes in Microsoft Fabric

File size optimization in Microsoft Fabric means organizing your Lakehouse/Delta data into appropriately sized Parquet files so that Spark and other Fabric engines can read and process the data efficiently.

This topic is directly connected to the Partitioning topic you just studied.

The easiest concept to remember is:

Too many tiny files = overhead. Too few huge files = poor parallelism. Well-sized files = better performance.

1. First understand how a Delta table stores data

Suppose you create:

FactSales

with:

1 Billion Rows

Logically, you see one table:

FactSales

But physically in OneLake, the Delta table is backed by files and a transaction log:

FactSales/
│
├── _delta_log/
│
├── part-00001.parquet
├── part-00002.parquet
├── part-00003.parquet
├── part-00004.parquet
└── ...

So remember:

Delta Table = Parquet data files + Delta transaction log

2. What is Parquet?

Apache Parquet is a columnar file format widely used by Fabric Lakehouse, Delta Lake, and Spark workloads.

Suppose your table contains:

CustomerID

CustomerName

Country

ProductID

Quantity

Amount

SalesDate

A row-oriented format conceptually focuses on records:

Row 1 → 101,Ravi,India,501,2,50000

Row 2 → 102,John,USA,502,1,30000

Row 3 → 103,Priya,India,503,3,60000

Parquet is columnar:

CustomerID

101

102

103

Country

India

USA

India

Amount

50000

30000

60000

This is useful for analytics because a query such as:

SELECT SUM(Amount)
FROM FactSales;

primarily needs the Amount column rather than every column.

3. Why does file size matter?

Imagine you have:

1 TB dataset

There are many ways that 1 TB could be stored.

Scenario A — Tiny files

1,000,000 files

×

~1 MB

Scenario B — Larger files

~2,000 files

×

~500 MB

Both represent roughly the same amount of data.

But they can behave very differently during processing.

4. The Small Files Problem

This is the most important concept in this lesson.

Suppose your Delta table contains:

500,000 Parquet files

Each file contains only a tiny amount of data.

Spark has overhead for operations such as:

Discover files

Read metadata

Plan tasks

Open files

Process files

Close files

Doing this a few thousand times may be manageable.

Doing it hundreds of thousands or millions of times can become expensive.

This is called:

The Small Files Problem

5. Example

Imagine processing:

100 GB

Bad layout

100,000 files

×

~1 MB

Spark may need to coordinate work across a huge number of files.

Conceptually:

File 1

File 2

File 3

File 4

...

File 100000

Metadata + scheduling overhead

Slower processing

Better layout

Suppose the same dataset is stored in a much smaller number of appropriately sized files.

100 GB

Hundreds of files

Less file-management overhead

This can be much more efficient.

6. Why not create one giant file?

You might now think:

"If small files are bad, I'll put everything into one huge file."

That's also not a good general strategy.

Suppose:

1 TB dataset

1 file

Distributed engines such as Spark work best when they can divide work across multiple tasks.

Conceptually:

1 Huge File

Limited parallelism

Some resources underutilized

Compare that with:

Dataset
 │
 ├── File 1 → Task
 ├── File 2 → Task
 ├── File 3 → Task
 ├── File 4 → Task
 └── ...

Multiple appropriately sized files can provide better parallelism.

7. The balance

Think:

VERY SMALL FILES

Too much overhead

VERY LARGE / TOO FEW FILES

Can limit parallelism

APPROPRIATELY SIZED FILES

Better balance

Better performance

There is no single perfect file size for every Fabric workload.

It depends on:

Dataset size

Compute resources

Query patterns

Write patterns

Partitioning

Compression

Concurrency

8. What file size should I target?

For learning purposes, you'll often see guidance around hundreds of MB per file, and Delta optimization commonly aims to create larger, more efficient files rather than tiny files.

But don't memorize:

"Every file must be exactly 256 MB."

or:

"Every file must be exactly 1 GB."

That's not how real-world optimization works.

A better interview answer is:

Avoid very small files and aim for reasonably large, balanced files appropriate to the workload. Validate the result using actual Fabric/Spark performance measurements.

9. How are small files created?

A common reason is frequent incremental loading.

Imagine a pipeline runs every 5 minutes.

12 loads/hour

×

24 hours

=

288 loads/day

Each load writes:

5 files

Now:

288 × 5

=

1,440 files/day

After 365 days:

~525,600 files

Your table can gradually develop a small-files problem.

10. Streaming can create small files

This is particularly important with streaming.

Suppose:

Eventstream

Lakehouse

Data continuously arrives in small batches.

Batch 1 → Small file

Batch 2 → Small file

Batch 3 → Small file

Batch 4 → Small file

...

Over time:

Thousands / millions

of small files

can accumulate if the write/maintenance strategy isn't appropriate.

11. Partitioning can also cause small files

This connects directly to your previous lesson.

Suppose you partition by:

Year

Month

Day

Hour

Minute

Storage becomes:

Year=2026/

└── Month=08/
    └── Day=03/
        └── Hour=10/
            ├── Minute=01/
            ├── Minute=02/
            ├── Minute=03/
            └── ...

If each minute contains very little data, each partition may contain tiny files.

Now you have:

Over-partitioning

Tiny partitions

Tiny files

Too many files

Poor performance

That's why partitioning and file size must be designed together.

12. Partition ≠ File

This distinction is very important.

Suppose:

FactSales/
└── Year=2026/

Year=2026 is a partition.

Inside it:

Year=2026/

│
├── part-00001.parquet
├── part-00002.parquet
├── part-00003.parquet
└── part-00004.parquet

These are files.

Therefore:

One partition can contain many files.

Don't confuse:

Partition

with:

Parquet file

13. Spark partitions vs files

Another important distinction:

Storage Partition

Spark Partition

Parquet File

are related concepts, but they're not identical.

Storage/table partition

Year=2026/

Used to physically organize table data.

File

part-00001.parquet

Physical data stored in OneLake.

Spark partition

P1

P2

P3

P4

A logical chunk of data Spark uses for distributed processing.

Think:

Delta Table

Storage Partitions

Parquet Files

Spark reads data

Spark Tasks / Partitions

14. Spark write example

Suppose:

df.write \
    .format("delta") \
    .mode("append") \
    .saveAsTable("FactSales")
Spark determines how work is distributed and writes output files accordingly.

If the DataFrame has an unnecessarily large number of small Spark partitions, the write may produce many small output files.

Conceptually:

Spark Partition 1 → File 1

Spark Partition 2 → File 2

Spark Partition 3 → File 3

...

It's not always strictly one-to-one in every operation, but this is a useful beginner mental model.

15. repartition() before writing

Suppose your DataFrame has:

2,000 partitions

but the dataset isn't large enough to justify that.

Writing it could produce excessive small files.

You might redistribute the data before writing:

df2 = df.repartition(100)

Then write:

df2.write \

   .format("delta") \

.mode("overwrite") \

.saveAsTable("FactSales")

Conceptually:

2,000 tiny partitions

repartition(100)
       ↓

100 larger partitions

Fewer output files

However, repartition() causes a shuffle, so it shouldn't be used blindly.

16. coalesce() before writing

If you're simply reducing the number of Spark partitions, you may sometimes use:

df2 = df.coalesce(100)

Conceptually:

500 Spark partitions

coalesce(100)
       ↓

100 partitions

Potentially fewer files

Remember from the previous lesson:

repartition()
    ↓

Full redistribution

Usually shuffle

coalesce()
    ↓

Typically reduces partitions

Often less expensive

Which one is appropriate depends on the workload and data distribution.

17. Delta OPTIMIZE

One of the most important Delta Lake concepts for file management is:

OPTIMIZE

Conceptually, OPTIMIZE compacts smaller files into larger files.

Before:

FactSales

1 MB

2 MB

4 MB

3 MB

5 MB

2 MB

1 MB

...

After compaction:

FactSales

Large File 1

Large File 2

Large File 3

...

Conceptually:

Many Small Files

OPTIMIZE

Fewer Larger Files

This can improve read efficiency.

18. Simple OPTIMIZE example

For supported Delta scenarios, the idea is:

OPTIMIZE FactSales;

Conceptually:

Before

[f][f][f][f][f][f][f][f][f][f]

OPTIMIZE

After

[ FILE ]

[ FILE ]

[ FILE ]

Always check the current Fabric runtime/engine support for the exact command and behavior in the environment you're using.

19. What does OPTIMIZE actually help?

It can reduce:

Number of files

Metadata overhead

File-open operations

Task-planning overhead

Potentially improving:

Spark reads

Delta queries

Lakehouse query performance

Downstream analytics

20. OPTIMIZE does not mean compressing everything into one file

This is important.

Bad understanding:

OPTIMIZE

=

Turn 10,000 files

into 1 file

No.

The goal is more like:

10,000 tiny files

OPTIMIZE

Fewer appropriately sized files

while retaining useful parallelism and Delta semantics.

21. Compaction

The general operation of combining many small files into fewer larger files is called:

File Compaction

Small File

Small File

Small File

Small File

Small File

COMPACTION

Larger File

Delta OPTIMIZE is a mechanism associated with this idea.

22. File sizes and query performance

Suppose a query needs:

50 GB

Layout A

50,000 tiny files

The engine spends significant effort on:

File metadata

Opening files

Task planning

Scheduling

Layout B

A manageable number

of larger files

Now a greater proportion of work can go toward:

Actual data processing

That's why file layout can matter even though the total data volume is identical.

23. File size and parallelism

Suppose you have:

8 Spark cores

and your dataset has several suitable files/chunks.

Spark can distribute work:

Core 1 → Data chunk

Core 2 → Data chunk

Core 3 → Data chunk

...

Core 8 → Data chunk

That's good parallelism.

But if your physical/logical layout results in insufficient parallel work:

Core 1 → Huge work unit

Core 2 → Waiting

Core 3 → Waiting

...

resources may not be utilized efficiently.

Again:

Balance matters.

24. File sizes and partition pruning

Now combine the previous two lessons.

Suppose:

FactSales/
│
├── Year=2025/
│   ├── file1.parquet
│   ├── file2.parquet
│   └── file3.parquet
│
└── Year=2026/
    ├── file1.parquet
    ├── file2.parquet
    └── file3.parquet

Query:

SELECT SUM(Amount)
FROM FactSales
WHERE Year = 2026;

First:

Partition Pruning

Skip Year=2025

Then the engine reads only the relevant files under:

Year=2026

So good performance comes from combining:

Good Partitioning
       +

Good File Layout

=

Efficient Reads

25. Example: 10 TB Sales table

Imagine:

FactSales

=

10 TB

Partitioned:

Year

Month

Conceptually:

FactSales/
│
├── Year=2025/
│
└── Year=2026/
    │
    ├── Month=01/
    ├── Month=02/
    ├── Month=03/
    └── ...

Within each month, maintain a reasonable number of reasonably sized files.

Then a query:

WHERE Year = 2026

AND Month = 7

can benefit from:

Partition pruning

Read July partition

Efficient file reads

Better performance

26. Example: Daily pipeline

Suppose your Fabric pipeline runs:

Every day

and loads:

10 GB/day

You could end up with a healthy pattern such as:

Daily Data

Spark processing

Reasonable number of output files

Delta Table

But if the pipeline creates:

10,000 tiny files/day

after one year:

3,650,000 files

That's a serious file-management problem.

27. Bronze, Silver and Gold

File-size considerations apply throughout a Medallion architecture.

SOURCE

BRONZE

SILVER

GOLD

Bronze

Raw ingestion can naturally produce many files.

Source

Frequent ingestion

Bronze

Silver

Cleaning/transformation is an opportunity to improve organization.

Bronze

Transform

Compact / organize

Silver

Gold

Gold datasets are optimized for consumption and analytical queries.

Silver

Business transformation

Gold

BI / Analytics

You should design file and partition strategy according to each layer's workload rather than applying the same rule everywhere.

28. File sizes and compression

Parquet supports efficient compression.

Suppose raw data is:

100 GB

The stored Parquet representation may be significantly smaller depending on:

Data types

Repeated values

Cardinality

Compression algorithm

Column structure

Therefore when discussing file sizes, remember:

Logical/uncompressed data size and physical Parquet file size are not necessarily the same.

29. Small file symptoms

How do you recognize a potential small-files problem?

You might observe:

Huge number of files

Very small average file size

Slow Spark job startup/planning

Large numbers of tiny Spark tasks

Slow metadata operations

Poor read performance

This doesn't automatically prove small files are the only issue, but it's a strong signal to investigate.

30. How to fix small files

Common approaches include:

1. Avoid unnecessary over-partitioning

2. Control Spark partitions before writes

3. Use appropriate batch sizes

4. Compact Delta files

5. Use supported Delta optimization features

6. Monitor file counts and sizes

7. Avoid unnecessarily frequent tiny writes

31. Bad design example

Suppose:

FactTransactions

=

5 TB

Partitioned by:

CustomerID

with:

10 million customers

Now you could create:

Millions of partitions

Tiny files

Metadata overhead

Poor performance

Changing only file size won't fully solve this.

The underlying partition strategy is wrong.

32. Better design thinking

Instead, examine query patterns.

Suppose 90% of queries are:

WHERE TransactionDate >= ...

A date-oriented strategy may make more sense.

Transaction Date

Appropriate time partition

Reasonable files

Partition pruning

Efficient query

The lesson is:

Partitioning and file-size optimization should be designed together based on actual query patterns.

33. Interview question — Why do file sizes matter?

A strong answer:

File sizes affect distributed query and Spark performance because very small files create metadata, file-open, and task-scheduling overhead, while too few very large work units can limit parallelism. For Fabric Lakehouse and Delta workloads, we generally want a manageable number of appropriately sized Parquet files based on the workload.

34. Interview question — What is the Small Files Problem?

The small-files problem occurs when a large dataset is stored as a very large number of tiny files. Even if the total data size is reasonable, Spark and other engines must discover, open, schedule, and process many files, which increases metadata and task-management overhead and can reduce performance.

35. Interview question — How do you solve the Small Files Problem?

A strong answer:

I would first identify why small files are being created, such as over-partitioning or frequent small writes. Then I would optimize the partition strategy, control Spark partition counts where appropriate, increase batch efficiency, and use supported Delta compaction/optimization capabilities such as OPTIMIZE.

36. Interview question — Partition vs File

A partition is a logical/physical grouping of table data based on partition-column values, while a Parquet file is an actual physical data file stored inside that table or partition. One partition can contain many Parquet files.

For example:

Year=2026/ ← Partition

│
├── part01.parquet       ← File
├── part02.parquet       ← File
└── part03.parquet       ← File

37. Interview question — Is a bigger file always better?

No.

Very small files can cause excessive metadata and scheduling overhead, but simply creating one enormous file is not optimal either because distributed processing benefits from parallel work. The goal is a balanced file layout appropriate to the dataset, compute, and query patterns.

38. Partitioning vs File Sizes

Keep these two syllabus topics separate:

PartitioningFile Sizes
Organizes data into groupsControls physical file layout
Example: Year=2026Example: Parquet files
Helps partition pruningHelps efficient reads
Too many → over-partitioningToo many tiny files → overhead
Based on columnsBased on write/compaction behavior

39. Quick revision

FILE SIZE OPTIMIZATION
===================================
Delta Table
    │
    ├── _delta_log
    │
    └── Parquet Files
BAD:
-----------------------------------

1 TB

1,000,000 tiny files

Metadata overhead

File-open overhead

Task overhead

Slow processing

ALSO BAD:
-----------------------------------

1 TB

One giant work unit

Limited parallelism

BETTER:
-----------------------------------

1 TB

Manageable number

of appropriately sized files

Parallel processing

Lower overhead

Better performance

40. One diagram to remember

                  LARGE DATASET
                       │
                       ▼
                  DELTA TABLE
                       │
          ┌────────────┴────────────┐
          ▼                         ▼
     SMALL FILES               HUGE/TOO FEW
          │                         │
          ▼                         ▼

Too much metadata Poor parallelism

  Too many tasks             Resource imbalance
          │                         │
          └────────────┬────────────┘
                       ▼
                  OPTIMIZATION
                       │
                       ▼

Appropriately sized

                Parquet files
                       │
                       ▼
                Better Parallelism
                       +
                 Lower Overhead
                       │
                       ▼

BETTER PERFORMANCE

The one line to memorize

Partitioning decides where data is organized; file-size optimization decides how efficiently that data is physically stored and processed.

And remember this performance chain:

Good Partitioning
        +
Good File Sizes
        +
Partition Pruning
        +

Delta Optimization

Less unnecessary I/O

Better Spark / Lakehouse Performance

The next topic, 10.3 Spark Optimization, brings these concepts together: partitions, files, shuffles, joins, caching, skew, executors, repartition(), coalesce(), and Adaptive Query Execution.

↑ Back to top
Module 10 · Lesson 10.4

Module 10 · Lesson 10.3

Spark Optimization

10.3 Spark Optimization in Microsoft Fabric

Spark Optimization means improving the way Apache Spark reads, distributes, transforms, and writes data so that jobs run faster, use fewer resources, and cost less capacity.

In Microsoft Fabric, Spark is commonly used through Notebooks and Lakehouse workloads.

The easiest formula to remember is:

Spark Optimization = Read less + Shuffle less + Balance partitions + Use memory wisely + Write efficiently

1. First understand how Spark works

Suppose your Fabric Lakehouse contains:

FactSales = 1 TB

You run a PySpark notebook:

df = spark.read.table("FactSales")

result = (

    df.groupBy("Country")
      .sum("SalesAmount")

)

result.show()

Behind the scenes:

                    SPARK APPLICATION
                           │
                           ▼
                        DRIVER
                           │
                   Creates execution plan
                           │
              ┌────────────┼────────────┐
              ▼            ▼            ▼
          EXECUTOR 1   EXECUTOR 2   EXECUTOR 3
              │            │            │
             Task         Task         Task
              │            │            │
              ▼            ▼            ▼

Partition Partition Partition

Understanding these components is the foundation of Spark optimization.

2. Driver

The Driver coordinates the Spark application.

It handles things such as:

Your PySpark Code

Driver

Create execution plan

Schedule jobs/stages/tasks

Coordinate executors

Return results

Think:

Driver = Manager

The Driver generally coordinates work rather than doing all distributed processing itself.

3. Executors

Executors perform distributed processing.

Conceptually:

                DRIVER
                   │
        ┌──────────┼──────────┐
        ▼          ▼          ▼
    Executor 1 Executor 2 Executor 3
        │          │          │

Tasks Tasks Tasks

Executors use:

CPU

Memory

to process Spark tasks.

Think:

Executors = Workers

4. Tasks

A Task is a unit of Spark execution applied to a partition.

For example:

Dataset
   │
   ├── Partition 1 → Task 1
   ├── Partition 2 → Task 2
   ├── Partition 3 → Task 3
   └── Partition 4 → Task 4

This is how Spark achieves parallel processing.

5. Spark Partitions

You learned about partitioning earlier.

Here we're specifically talking about Spark execution/DataFrame partitions.

Suppose:

1 TB Dataset

Spark divides processing into partitions:

1 TB
 │
 ├── P1
 ├── P2
 ├── P3
 ├── P4
 ├── P5
 ├── ...
 └── Pn

Executors can process multiple partitions concurrently.

6. Why partition count matters

Suppose you have:

800 GB

but only:

4 Spark partitions

Then:

P1 → 200 GB

P2 → 200 GB

P3 → 200 GB

P4 → 200 GB

If your Spark environment can run much more parallel work, you're not using it efficiently.

Too few partitions

Large Dataset

Few Partitions

Few Tasks

Low Parallelism

Slow Processing

7. Too many partitions are also bad

Now suppose:

10 GB Dataset

is divided into:

100,000 partitions

Spark must manage:

100,000 tasks

That creates overhead:

Task scheduling

Metadata

Task startup

Network communication

File operations

Therefore:

Too Few Partitions

Poor parallelism

Too Many Partitions

Too much overhead

Balanced Partitions

Better performance

8. repartition()

You learned this during File Sizes.

repartition() redistributes data into a specified number of Spark partitions.

Example:

df2 = df.repartition(100)

Conceptually:

Original

[P1] [P2] [P3] [P4]

repartition(100)

[P1][P2][P3]...[P100]

However, there is an important cost:

repartition() normally causes a shuffle.

9. coalesce()

coalesce() is commonly used to reduce partitions.

Example:

df2 = df.coalesce(20)

Conceptually:

100 Partitions

coalesce(20)
      ↓

20 Partitions

Easy comparison:

repartition()coalesce()
Increase or decrease partitionsMainly decrease
Redistributes dataCombines partitions
Usually full shuffleOften avoids full shuffle
Better redistributionOften cheaper when reducing

Remember:

Repartition = Redistribute

Coalesce = Reduce

10. What is a Shuffle?

This is one of the most important Spark performance concepts.

A shuffle occurs when Spark needs to redistribute data between executors/partitions.

For example:

Executor 1
   │
   ├───────┐
   │       │
   ▼       ▼

Executor 2 Executor 3

Data moves across the cluster.

Shuffles involve:

Network I/O

Disk I/O

Serialization

CPU

Memory

Therefore:

Shuffles are expensive.

A major Spark optimization goal is:

Avoid unnecessary shuffles.

11. What operations cause shuffles?

Common examples include:

groupBy()

join()

distinct()

orderBy()

repartition()

some aggregations

For example:

df.groupBy("Country").sum("SalesAmount")
Spark needs all India rows together, all USA rows together, etc.

Before:

P1 → India, USA, UK

P2 → USA, India

P3 → UK, India, USA

After shuffle:

India → Partition A

USA → Partition B

UK → Partition C

Data had to move.

12. Reduce data before a shuffle

Suppose:

FactSales = 1 TB

You only need:

Year = 2026

Bad approach conceptually:

Read 1 TB

Join

Group

Filter 2026

Better:

Read

Filter 2026

Select required columns

Join

Aggregate

The principle is:

Reduce the amount of data as early as possible.

13. Filter early

Instead of processing everything:

df = spark.read.table("FactSales")

then performing many operations before filtering, apply selective filters early when possible:

df = (
    spark.read.table("FactSales")
         .filter("SalesYear = 2026")

)

Conceptually:

1 TB

Filter

200 GB

Process 200 GB

instead of:

1 TB

Process

Process

Filter

14. Select only required columns

Suppose FactSales contains 100 columns.

You only need:

ProductID

Country

SalesAmount

Avoid carrying unnecessary columns through the pipeline.

Instead of:

df = spark.read.table("FactSales")

use:

df = (
    spark.read.table("FactSales")
         .select(
             "ProductID",
             "Country",
             "SalesAmount"
         )

)

Conceptually:

100 columns

Select 3

Less data processed

Less memory / I/O

Remember:

Filter rows early + select columns early.

15. Predicate Pushdown

Suppose you write:

df = (
    spark.read.table("FactSales")
         .filter("SalesYear = 2026")

)

With supported data sources/formats, Spark can push filtering closer to the data read.

Conceptually:

Spark Query

Filter Year=2026

Storage/Data Source

Read relevant data

instead of:

Read everything

Send to Spark

Filter

This is called:

Predicate Pushdown

16. Column Pruning

Similarly:

df.select("ProductID", "SalesAmount")
Spark can often avoid reading unnecessary columns from columnar formats such as Parquet.

This is:

Column Pruning

Conceptually:

Parquet

CustomerID ✕

CustomerName ✕

Address ✕

ProductID ✓

SalesAmount ✓

Description ✕

Only required columns are processed.

17. Partition Pruning

From your Partitioning lesson:
df.filter("Year = 2026")

If the Delta table is appropriately partitioned by Year, Spark may skip:

Year=2023 ✕

Year=2024 ✕

Year=2025 ✕

Year=2026 ✓

So three important pruning concepts are:

Partition Pruning

Skip partitions

Column Pruning

Skip columns

Predicate Pushdown

Push filters toward source

All aim to:

Read less data.

18. Join Optimization

Joins are often one of the biggest performance problems in Spark.

Imagine:

FactSales

=

1 TB

and:

DimCountry

=

5 MB

Query:

fact.join(dim, "CountryID")

A normal join may require significant data movement.

But because DimCountry is tiny, Spark can potentially use a:

Broadcast Join

19. Broadcast Join

Instead of shuffling the huge FactSales table around, Spark sends a copy of the small table to executors.

DimCountry

                    5 MB
                      │
                 BROADCAST
        ┌─────────────┼─────────────┐
        ▼             ▼             ▼
    Executor 1    Executor 2    Executor 3
        │             │             │
     Fact P1        Fact P2        Fact P3
        │             │             │
        ▼             ▼             ▼

JOIN JOIN JOIN

This can avoid a large shuffle.

20. Broadcast Join example

PySpark:

from pyspark.sql.functions import broadcast

result = fact.join(

broadcast(dim),

"CountryID"

)

Think:

Huge Fact
   +

Tiny Dimension

Broadcast small dimension

Reduce large-table shuffle

Potentially faster join

Do not broadcast a genuinely large table—the table must fit appropriately in executor memory.

21. Join strategy in a Star Schema

This is particularly relevant to your earlier Star Schema topic.

                     DimDate
                        │
                        ▼
DimCustomer ──────► FactSales ◄────── DimProduct
                        ▲
                        │

DimRegion

Typically:

FactSales

=

Huge

while dimensions may be much smaller.

Spark may be able to use efficient join strategies, including broadcast joins for sufficiently small dimensions.

This is one reason star schemas work well for analytics.

22. Data Skew

Suppose you're grouping by Country:

India → 800 GB

USA → 100 GB

UK → 50 GB

Germany → 50 GB

After shuffle:

Partition 1 → India 800 GB

Partition 2 → USA 100 GB

Partition 3 → UK 50 GB

Partition 4 → Germany 50 GB

Now:

Executor 1 → Working...

Executor 2 → Finished

Executor 3 → Finished

Executor 4 → Finished

Everyone waits for Executor 1.

This is:

Data Skew

23. How do you identify skew?

Common symptoms:

Most tasks finish quickly

One/few tasks run much longer

Job waits

Poor performance

For example:

Task 1 → 30 sec

Task 2 → 28 sec

Task 3 → 31 sec

Task 4 → 18 minutes ← suspicious

That often indicates skew or another partition-specific issue.

24. Handling skew

Possible strategies include:

Better partitioning

Filtering unnecessary data

Adaptive Query Execution

Broadcasting suitable small tables

Salting highly skewed keys

Reworking joins/aggregations

The right solution depends on the cause.

25. Salting

Suppose:

Country = India

contains most of your dataset.

Instead of one hot key:

India

you can sometimes create multiple logical keys:

India_1

India_2

India_3

India_4

Conceptually:

India = 800 GB

↓ SALTING

India_1 → ~200 GB

India_2 → ~200 GB

India_3 → ~200 GB

India_4 → ~200 GB

This can distribute processing more evenly.

Salting is an advanced technique and adds complexity, so use it when skew actually warrants it.

26. Caching

Suppose your notebook uses the same transformed DataFrame repeatedly:

sales2026 = df.filter("Year = 2026")

Then you run:

Query 1 → sales2026

Query 2 → sales2026

Query 3 → sales2026

Without caching, Spark may recompute parts of the transformation repeatedly.

You can cache:

sales2026.cache()

Conceptually:

Compute DataFrame

Cache

Memory / storage

Reuse

This can improve performance for reused data.

27. Don't cache everything

Caching consumes resources.

Bad:

Cache Table A

Cache Table B

Cache Table C

Cache Table D

Cache Everything

Eventually:

Memory pressure

Eviction / spill

Potentially worse performance

Cache when:

The same expensive intermediate data is reused multiple times.

28. Unpersist

When cached data is no longer needed:

sales2026.unpersist()

This releases cached resources.

Conceptually:

Cache

Use

Use

Finished

Unpersist

Good resource management matters.

29. Lazy Evaluation

Spark uses lazy evaluation.

Suppose:

df2 = df.filter("Year = 2026")

df3 = df2.select("ProductID", "Amount")
Spark doesn't necessarily execute everything immediately.

It builds an execution plan.

Execution starts when you call an action, such as:

df3.show()

or:

df3.count()

or write the result.

Conceptually:

Transformation

Transformation

Transformation

Execution Plan

ACTION

Execute

30. Transformations vs Actions

Transformations

Examples:

select()

filter()

join()

groupBy()

withColumn()

They build the computation plan.

Actions

Examples include:

show()

count()

collect()

write

Actions trigger execution.

Easy memory:

Transformation = Define work

Action = Execute work

31. Avoid unnecessary actions

Bad notebook pattern:

df.count()
df.show()
df.count()
df.collect()

Each action can trigger Spark jobs.

If results aren't cached/reused appropriately, Spark may repeat expensive work.

Therefore:

Avoid actions you don't actually need.

32. Be careful with collect()

This is one of the most important Spark rules.

df.collect()

brings the result to the Driver.

Imagine:

500 GB Dataset

collect()
      ↓

DRIVER

That can overwhelm driver memory.

Bad:

huge_df.collect()

Use collect() only when the resulting data is small enough for the driver.

33. Avoid converting huge Spark DataFrames to Pandas

Similarly:

df.toPandas()

collects data into driver-side Python/Pandas memory.

For huge data:

Spark Cluster

500 GB

Driver Memory

💥

Use Pandas for appropriately sized data, not enormous distributed datasets.

34. Built-in functions vs Python UDFs

Suppose you need to transform a column.

Whenever possible, prefer Spark's built-in functions.

For example:

from pyspark.sql.functions import upper

df2 = df.withColumn(
    "CountryUpper",
    upper("Country")

)

rather than writing a Python UDF for something Spark already supports.

Why?

Spark built-in functions are generally easier for Spark's optimizer to understand and optimize.

Think:

Spark Built-in Functions

Optimizer understands

Efficient execution

Custom Python UDFs can introduce serialization and optimization limitations.

35. Catalyst Optimizer

Spark SQL uses the Catalyst Optimizer.

Conceptually:

Your Query

Logical Plan

Catalyst Optimizer

Optimized Plan

Physical Plan

Execution

For example, Spark can optimize:

Filters

Column selection

Join strategies

Expression evaluation

This is why writing transformations using Spark SQL/DataFrame APIs is often preferable to opaque custom logic.

36. Adaptive Query Execution (AQE)

Adaptive Query Execution allows Spark to adjust parts of the execution strategy using runtime information.

Think:

Initial Plan

Start Processing

Observe Runtime Data

Adjust Strategy

Continue Execution

AQE can help with areas such as:

Shuffle partitions

Join strategies

Skew handling

depending on the Spark/runtime configuration.

37. Explain the execution plan

One of the most useful optimization tools is:

df.explain()

or:

df.explain("formatted")

This helps you understand the query plan.

You can inspect for things such as:

Scan

Filter

Exchange

Join

Sort

Aggregate

An Exchange commonly indicates a data redistribution/shuffle boundary.

38. Example optimization

Suppose your original code is:

sales = spark.read.table("FactSales")
customers = spark.read.table("DimCustomer")

result = (

sales

    .join(customers, "CustomerID")
    .groupBy("Country")
    .sum("Amount")

)

Suppose FactSales contains 10 years of data, but you only need 2026.

A better approach:

sales = (

    spark.read.table("FactSales")
         .filter("Year = 2026")
         .select(
             "CustomerID",
             "Amount"
         )

)

customers = (

    spark.read.table("DimCustomer")
         .select(
             "CustomerID",
             "Country"
         )

)

result = (

sales

    .join(customers, "CustomerID")
    .groupBy("Country")
    .sum("Amount")

)

Now you've reduced unnecessary rows and columns before the expensive operations.

39. Potential broadcast optimization

If DimCustomer is sufficiently small:

from pyspark.sql.functions import broadcast

result = (

sales

    .join(
        broadcast(customers),
        "CustomerID"
    )
    .groupBy("Country")
    .sum("Amount")

)

Conceptually:

Before

Large Sales

Shuffle

Customer

Join

Potentially optimized

Small Customer

Broadcast

Executors

Join locally with

FactSales partitions

40. Optimize writes

Spark performance isn't only about reading.

Writes matter too.

Bad:

DataFrame

Thousands of tiny partitions

Write

Thousands of tiny Parquet files

Better:

DataFrame

Appropriate partition strategy

Write Delta

Reasonably sized files

This connects directly to your previous File Sizes lesson.

41. Delta optimization

For Lakehouse workloads:

Spark

Delta Table

Many small files

Compaction / OPTIMIZE

Better file layout

Therefore Spark optimization and Delta optimization go together.

42. Partitioning + File Size + Spark Optimization

Your Module 10 sequence now makes sense:

10.1 Partitioning

How data is organized

10.2 File Sizes

How data is physically stored

10.3 Spark Optimization

How data is processed

Together:

Good Partitioning
       +
Good File Layout
       +

Good Spark Execution

Better Performance

43. Common Spark performance problems

When a Spark job is slow, investigate:

1. Reading too much data

2. Too many small files

3. Too many partitions

4. Too few partitions

5. Large shuffles

6. Data skew

7. Poor join strategy

8. Unnecessary transformations

9. Excessive caching

10. Driver-heavy operations

11. Python UDFs

12. Repeated actions

Don't immediately assume:

"Spark needs more CPU."

Often the real problem is the data layout or execution plan.

44. Performance troubleshooting flow

Use this approach:

Spark Job Slow
      │
      ▼
Check Spark UI / Metrics
      │
      ▼

Which stage is slow?

      │
      ├── Scan?
      │      ↓
      │   Check files,
      │   pruning, columns
      │
      ├── Join?
      │      ↓
      │   Check shuffle,
      │   broadcast, skew
      │
      ├── Aggregate?
      │      ↓
      │   Check shuffle
      │   and skew
      │
      └── Write?

Check partitions

and file sizes

This is much better than randomly changing configurations.

45. Spark UI

Spark provides monitoring information for jobs, stages, and tasks.

You can investigate:

Jobs

Stages

Tasks

Executors

Duration

Input

Output

Shuffle Read

Shuffle Write

Memory

Disk Spill

For example:

Stage 1 → 20 seconds

Stage 2 → 25 seconds

Stage 3 → 15 minutes ← investigate

Then check Stage 3.

Maybe:

Shuffle Read = Huge

or:

One Task = 14 minutes

Other Tasks = 20 seconds

That could indicate a skew problem.

46. What does "Spill" mean?

Suppose Spark needs more memory than is available for an operation.

It may write intermediate data to disk.

Data Processing

Memory insufficient

Disk Spill

Disk I/O

Slower

Large spill values can indicate:

Large partitions

Heavy aggregation

Large joins

Memory pressure

Data skew

47. Optimize before increasing compute

Suppose your notebook is slow.

Bad first reaction:

Slow Job

Increase Spark Capacity

Better:

Slow Job

Inspect execution

Check scans

Check partitions

Check shuffle

Check joins

Check skew

Check file sizes

Optimize

Then evaluate compute needs

More compute cannot fully compensate for badly designed processing.

48. Real-world optimization example

Imagine:

FactTransactions = 5 TB

DimCountry = 5 MB

Requirement:

Calculate Indian transactions for 2026.

Bad approach:

Read 5 TB

Join

Aggregate

Filter India

Filter 2026

Better:

FactTransactions

Filter Year=2026

Select required columns

Broadcast DimCountry

Join

Filter India

Aggregate

If the table is partitioned appropriately:

Partition Pruning
      +
Column Pruning
      +
Predicate Pushdown
      +

Broadcast Join

Much less work

49. Interview question — What is Spark Optimization?

A strong answer:

Spark optimization is the process of improving distributed data processing by reducing unnecessary reads and shuffles, using appropriate partitioning and join strategies, handling data skew, managing caching correctly, and optimizing file layout. In Microsoft Fabric, I would also use Spark execution metrics and query plans to identify bottlenecks before changing compute resources.

50. Interview question — What is a Shuffle?

A shuffle is the redistribution of data between Spark partitions or executors, commonly caused by operations such as joins, groupBy, distinct, orderBy, and repartition. Shuffles are expensive because they involve network, disk, serialization, and memory overhead, so reducing unnecessary shuffles is an important Spark optimization technique.

51. Interview question — What is a Broadcast Join?

A broadcast join is a join optimization where Spark sends a sufficiently small table to the executors so each executor can join it locally with partitions of a large table. This can avoid shuffling the large table and significantly improve performance when the broadcast side is small enough.

52. Interview question — What is Data Skew?

Data skew occurs when data is distributed unevenly across partitions, causing some Spark tasks to process much more data than others. This results in a few slow tasks holding up the entire stage. It can be addressed through techniques such as better partitioning, broadcast joins, AQE, or salting depending on the scenario.

53. Interview question — repartition() vs coalesce()

repartition() redistributes data and can increase or decrease the partition count, but normally causes a shuffle. coalesce() is primarily used to reduce partitions and can often avoid a full shuffle, making it less expensive when redistribution isn't required.

54. Interview question — Why avoid collect()?

collect() brings all result rows from the distributed Spark executors to the driver. If the dataset is large, this can exhaust driver memory and cause the application to fail. It should only be used when the result is known to be small.

55. Quick revision

SPARK OPTIMIZATION
====================================
1. READ LESS
   │
   ├── Filter early
   ├── Select required columns
   ├── Partition pruning
   ├── Predicate pushdown
   └── Column pruning
2. SHUFFLE LESS
   │
   ├── Avoid unnecessary repartition
   ├── Optimize joins
   └── Broadcast small tables
3. BALANCE DATA
   │
   ├── Correct partition count
   ├── Handle skew
   └── AQE
4. USE MEMORY WISELY
   │
   ├── Cache reused data
   ├── Unpersist
   ├── Avoid huge collect()
   └── Watch disk spill
5. WRITE EFFICIENTLY
   │
   ├── Avoid tiny files
   ├── Appropriate partitions
   └── Delta optimization
6. MONITOR
   │
   ├── Spark UI
   ├── Jobs
   ├── Stages
   ├── Tasks
   ├── Shuffle
   └── Spill

One diagram to memorize

                     LARGE DATASET
                          │
                          ▼
                  PARTITION PRUNING
                          │
                          ▼
                     FILTER EARLY
                          │
                          ▼
                  SELECT COLUMNS
                          │
                          ▼
                    OPTIMIZE JOIN
                          │
                   ┌──────┴──────┐
                   ▼             ▼

Broadcast Normal Join

if suitable

                   │
                   ▼
              REDUCE SHUFFLE
                   │
                   ▼
              BALANCE PARTITIONS
                   │
                   ▼
                HANDLE SKEW
                   │
                   ▼
              CACHE IF REUSED
                   │
                   ▼
              EFFICIENT WRITE
                   │
                   ▼
           GOOD DELTA FILE SIZES
                   │
                   ▼

FASTER SPARK JOB

The five rules to memorize

If an interviewer asks "How do you optimize a Spark job?", start with these:

1. Read only required data. 2. Reduce expensive shuffles. 3. Use appropriate partitioning. 4. Optimize joins and handle skew. 5. Monitor the execution plan/Spark metrics before adding more compute.

And connect the first three lessons of your module:

Partitioning = How the data is organized File Sizes = How the data is physically stored Spark Optimization = How the data is processed

↑ Back to top
Module 10 · Lesson 10.6

10.4 Caching 10.4 Caching in Microsoft Fabric

Caching means temporarily storing frequently reused data in memory and/or disk so Spark doesn't have to repeatedly read or recompute the same data.

The easiest definition:

Caching = Compute once → Store temporarily → Reuse many times

Caching is especially useful in Microsoft Fabric Spark notebooks when the same DataFrame is used repeatedly.

1. Simple example

Suppose your Fabric Lakehouse contains:

FactSales = 2 TB

You create:

sales2026 = (

    spark.read.table("FactSales")
         .filter("Year = 2026")

)

Then you perform several analyses:

sales2026.groupBy("Country").sum("Amount").show()

sales2026.groupBy("ProductID").sum("Amount").show()

sales2026.groupBy("CustomerID").sum("Amount").show()

Without caching, Spark may need to repeatedly evaluate the transformations required to produce sales2026.

Conceptually:

Query 1

Read FactSales

Filter 2026

Calculate

Query 2

Read FactSales

Filter 2026

Calculate

Query 3

Read FactSales

Filter 2026

Calculate

That's repeated work.

2. With caching

We can cache the reusable DataFrame:

sales2026 = (

    spark.read.table("FactSales")
         .filter("Year = 2026")

)

sales2026.cache()

Conceptually:

FactSales

Filter Year=2026

sales2026

CACHE

Memory / storage
    │
    ├── Query 1
    ├── Query 2
    └── Query 3

Now Spark can reuse cached data instead of recomputing the same upstream work every time.

3. Real-world analogy

Imagine you're cooking.

You need chopped onions for three dishes.

Without caching

Dish 1

Take onion

Chop onion

Dish 2

Take onion

Chop onion

Dish 3

Take onion

Chop onion

You're repeating the same preparation.

With caching

Chop onions once

Store prepared onions

 ┌────┼────┐
 ▼    ▼    ▼

Dish1 Dish2 Dish3

That's essentially caching.

4. Why caching improves Spark performance

Suppose an expensive transformation takes:

5 minutes

and you use it five times.

Without caching, conceptually:

5 min
+
5 min
+
5 min
+
5 min
+

5 min

≈ 25 minutes

With caching:

Compute once

Cache

Reuse

The first operation still has to perform the computation, but subsequent operations may be much faster.

5. Spark uses Lazy Evaluation

This is extremely important.

When you write:

sales2026.cache()

you should understand Spark's lazy evaluation behavior.

Spark transformations aren't necessarily executed immediately.

For example:

sales2026 = df.filter("Year = 2026")
Spark builds an execution plan.

Then:

sales2026.cache()

marks the DataFrame for caching.

An action such as:

sales2026.count()

causes Spark to actually execute the plan and populate the cache.

Conceptually:

filter()
    ↓

Define transformation

cache()
    ↓

Mark for caching

count()
    ↓

ACTION

Execute

Populate Cache

6. Practical caching pattern

A common pattern is:

sales2026 = (

    spark.read.table("FactSales")
         .filter("Year = 2026")
         .select(
             "CustomerID",
             "ProductID",
             "Country",
             "Amount"
         )

)

sales2026.cache()

sales2026.count()

Then reuse it:

sales2026.groupBy("Country").sum("Amount").show()

and:

sales2026.groupBy("ProductID").sum("Amount").show()

and:

sales2026.groupBy("CustomerID").sum("Amount").show()

7. Why filter before caching?

Suppose:

FactSales = 5 TB

but you only need:

Year = 2026

Don't unnecessarily think:

5 TB

CACHE EVERYTHING

A better pattern is:

5 TB

Partition Pruning

Filter

Select required columns

Smaller reusable dataset

CACHE

For example:

sales2026 = (

    spark.read.table("FactSales")
         .filter("Year = 2026")
         .select(
             "CustomerID",
             "ProductID",
             "Amount"
         )

)

sales2026.cache()

This follows the Spark optimization rule you learned:

Read less → process less → cache only what is useful.

8. cache() vs persist()

Spark provides two closely related concepts:

cache()

persist()
cache()

Simple:

df.cache()

You let Spark use its default caching/storage behavior for that DataFrame.

persist()

Provides more explicit control over the storage level.

Conceptually:

df.persist(...)

allows you to choose how cached data should be stored.

9. Storage levels

Depending on Spark/runtime APIs, persistence can involve storage strategies such as:

Memory

Memory + Disk

Disk

The important beginner concept is:

Memory

Cached Data

RAM

Very fast, but RAM is limited.

Memory + Disk

Cached Data
     │
     ├── RAM
     │
     └── Disk if needed

More resilient when the full cached dataset doesn't fit in memory.

Disk

Cached Data

Disk

Slower than memory but can avoid expensive recomputation.

10. Memory vs Disk

Think:

                 CACHE STORAGE
                       │
          ┌────────────┴────────────┐
          ▼                         ▼
        MEMORY                     DISK
          │                         │
        Fast                      Slower
          │                         │

Limited RAM More capacity

Choosing a storage strategy is a trade-off.

11. Example using persist()

PySpark conceptually:

from pyspark import StorageLevel

df.persist(StorageLevel.MEMORY_AND_DISK)

Then trigger computation:

df.count()

Now Spark can reuse persisted partitions.

When you're finished:

df.unpersist()

12. What is unpersist()?

Caching uses resources.

When you no longer need the cached DataFrame:

sales2026.unpersist()

This tells Spark:

"I don't need this cached dataset anymore."

Conceptually:

DataFrame

CACHE

Memory occupied

Queries finished

unpersist()

Resources released

This is an important habit.

13. Why not cache everything?

Suppose your Spark environment has:

Available executor memory

and you cache:

Table A

Table B

Table C

Table D

Table E

Table F

...

Eventually:

Memory

████████████████████

100%

Spark may need to:

Evict cached blocks

Spill data

Use disk

Recompute data

Performance can actually become worse.

Therefore:

Caching is an optimization—not a default requirement.

14. When should I cache?

Caching is useful when a dataset is:

Expensive to compute + reused multiple times + reasonable enough to cache.

For example:

Huge Raw Table

Complex Filter

Join

Transformation

Reusable DataFrame

CACHE
      │
      ├── Analysis 1
      ├── Analysis 2
      ├── Analysis 3
      └── Analysis 4

This is a good caching candidate.

15. When should I NOT cache?

Suppose:

df = spark.read.table("FactSales")

result = df.groupBy("Country").sum("Amount")

result.write.saveAsTable("SalesSummary")

and result is used only once.

Caching:

result.cache()

may provide little or no benefit.

You are:

Compute

Cache

Use once

Discard

Caching itself has a cost.

So:

If a DataFrame is used only once, caching usually isn't useful.

16. Good caching example

Suppose:

FactTransactions

=

5 TB

You need:

2026 transactions
+
Active customers
+

Valid products

You create:

result = (

transactions

    .filter("Year = 2026")
    .join(customers, "CustomerID")
    .join(products, "ProductID")

)

This is expensive.

Now you need:

Revenue by Country

Revenue by Product

Revenue by Customer

Revenue by Month

Revenue by Channel

This is a good scenario to consider:

result.cache()

result.count()

Then reuse:

Cached Result
    │
    ├── Country analysis
    ├── Product analysis
    ├── Customer analysis
    ├── Monthly analysis
    └── Channel analysis

17. Bad caching example

Suppose:

FactSales

=

5 TB

You write:

df = spark.read.table("FactSales")

df.cache()

df.count()

but later only need:

India
+
2026
+

Amount

You cached far more data than necessary.

Better:

df = (
    spark.read.table("FactSales")
         .filter(
             "Year = 2026 AND Country = 'India'"
         )
         .select("ProductID", "Amount")

)

df.cache()

Now the cached dataset is smaller and more relevant.

18. Cache after expensive transformations

Imagine:

Raw Data

Filter

Join

Clean

Calculate columns

Aggregate

Result

If Result is reused repeatedly, caching it can prevent Spark from repeatedly executing the entire lineage.

Without cache:

Query 1

Raw → Filter → Join → Clean → Calculate

Query 2

Raw → Filter → Join → Clean → Calculate

Query 3

Raw → Filter → Join → Clean → Calculate

With cache:

Raw

Filter

Join

Clean

Calculate

CACHE
 ├── Query 1
 ├── Query 2
 └── Query 3

19. Cache and Spark lineage

Spark remembers how a DataFrame was created.

For example:

FactSales

Filter

Join Customer

Join Product

Calculate Profit

sales_enriched

This chain is called the DataFrame's lineage.

Without caching, Spark can recompute the lineage when necessary.

Caching allows reusable intermediate results to be retained.

Long Lineage

CACHE useful result

Reuse

20. Caching and joins

Caching can be useful when the same prepared dataset participates in multiple operations.

For example:

active_customers = (

customers

    .filter("Status = 'Active'")
    .select("CustomerID", "Country", "Segment")

)

active_customers.cache()

active_customers.count()

Then:

Sales

Join Active Customers

Orders

Join Active Customers

Returns

Join Active Customers

Spark can reuse the cached customer dataset.

However, for a very small lookup table, a broadcast join may sometimes be a more important optimization than caching alone.

21. Caching vs Broadcast

These solve different problems.

Caching

Repeated computation

Store reusable data

Avoid recomputation

Broadcast

Large Table
     +

Small Table

Send small table to executors

Avoid expensive large-table shuffle

Therefore:

Cache = Avoid recomputation

Broadcast = Optimize joins/data movement

22. Caching vs Partitioning

Don't confuse them.

Partitioning

Controls how data is organized/distributed.

FactSales
│
├── Year=2025
└── Year=2026

Caching

Temporarily keeps reusable data closer to computation.

Year=2026 result

CACHE

Reuse

Think:

Partitioning = Organize data

Caching = Reuse data

23. Caching vs Delta files

Another important distinction:

Delta / Parquet Files

Persistent Storage

OneLake

Caching:

Spark Cache

Temporary

Memory / Disk

A Delta table remains available after your Spark session.

A Spark cache is temporary computational state.

So:

Cache is not permanent storage.

24. Caching vs writing a table

Suppose you've calculated:

Customer360

and need it repeatedly over many future pipelines/notebooks.

Caching may not be the correct solution because the cache is temporary.

Instead:

Expensive Transformation

Write Delta Table

Customer360

Persistent in OneLake

Think:

Need within current Spark workload/session?

CACHE

Need across future jobs/sessions?

MATERIALIZE / WRITE TABLE

That's an important architecture decision.

25. SQL caching

Spark SQL can also work with cached tables/views.

Conceptually:

CACHE TABLE Sales2026;

Then repeated queries against the cached object can benefit from caching where supported/configured.

Later:

UNCACHE TABLE Sales2026;

The exact behavior and supported syntax should be checked against your Fabric Spark runtime.

26. Cache lifecycle

Think of cache lifecycle as:

CREATE DATAFRAME

TRANSFORM

CACHE / PERSIST

ACTION

CACHE POPULATED

REUSE

REUSE

REUSE

UNPERSIST

This is the complete caching flow.

27. How caching fits into Spark optimization

From your previous lesson:

Spark Optimization
│
├── Read less
├── Shuffle less
├── Balance partitions
├── Optimize joins
├── Handle skew
├── Cache reused data
└── Write efficiently

Caching solves specifically:

Repeated computation/read problems

It doesn't automatically solve:

Bad partitioning

Data skew

Too many small files

Large shuffles

Poor joins

Bad query logic

28. Example without caching

Imagine this transformation:

FactSales 2 TB

Filter 2026

Join Customer

Join Product

Calculate Margin

sales_enriched

Now:

Query 1

Revenue by Country

Query 2

Revenue by Product

Query 3

Margin by Customer

Without cache:

Query 1

Read → Filter → Join → Join → Calculate

Query 2

Read → Filter → Join → Join → Calculate

Query 3

Read → Filter → Join → Join → Calculate

Potentially expensive.

29. Same example with caching

FactSales

Filter

Join

Join

Calculate

sales_enriched

  CACHE
    │
 ┌──┼────────┐
 ▼  ▼        ▼

Q1 Q2 Q3

The expensive shared work is performed once and reused where possible.

30. Memory pressure

Caching isn't free.

Suppose:

Executor Memory

=

100 GB

and you attempt to cache:

Dataset

=

500 GB

Spark cannot simply keep everything in memory.

Depending on the storage level and runtime behavior, data may be:

Memory

Disk

or

Evicted

Recomputed

That's why selecting an appropriate caching strategy matters.

31. Monitor caching

When optimizing a Spark workload, don't just write:

df.cache()

and assume performance improved.

Monitor things like:

Spark UI

Storage

Memory usage

Executor memory

Job duration

Stages

Tasks

Disk spill

Cache hit/reuse behavior

Then compare:

Before caching

vs

After caching

Optimization should be measured.

32. Common caching mistakes

Avoid these:

❌ Cache every DataFrame

❌ Cache huge raw tables unnecessarily

❌ Cache data used only once

❌ Forget to unpersist

❌ Assume cache is permanent

❌ Cache before filtering unnecessary rows

❌ Cache unnecessary columns

❌ Ignore memory pressure

❌ Assume caching fixes bad joins

❌ Assume caching fixes bad partitioning

33. Better approach

Instead:

Raw Table

Partition Pruning

Filter required rows

Select required columns

Perform expensive transformation

Is result reused?

      │
   ┌──┴───┐
   │      │
  YES     NO
   │      │
   ▼      ▼

CACHE Don't Cache

   │
   ▼
Reuse
   │
   ▼

Unpersist

34. Interview question — What is caching in Spark?

A strong answer:

Caching in Spark stores reusable DataFrame or table data in memory and/or disk so Spark can avoid repeatedly reading or recomputing the same transformations. It is most useful when an expensive intermediate dataset is reused multiple times.

35. Interview question — cache() vs persist()

cache() is a convenient way to persist a DataFrame using Spark's default storage behavior, while persist() allows you to explicitly choose a storage level such as memory-and-disk depending on the runtime and requirements.

36. Interview question — When would you use caching?

I would consider caching when the same expensive intermediate DataFrame is reused multiple times within a Spark workload—for example, after filtering and joining a large sales dataset that is then used for several aggregations.

37. Interview question — When would you avoid caching?

I would avoid caching data that is used only once, extremely large raw datasets that provide little reuse benefit, or datasets that create excessive memory pressure. Caching has a storage and management cost, so it should be used selectively.

38. Interview question — Why use unpersist()?

unpersist() removes cached data when it is no longer required, freeing executor memory and storage resources for other Spark operations.

39. Interview question — Cache vs Delta table

A Spark cache is temporary and primarily used to avoid repeated computation within a Spark workload, while a Delta table is persistent data stored in OneLake and can be reused across sessions and workloads.

40. Interview question — Does cache() immediately load the data?

A strong answer:

Not necessarily. Spark uses lazy evaluation. Calling cache() marks the DataFrame to be cached, and the cache is populated when an action such as count(), show(), or another operation triggers computation.

This is a very useful interview point.

41. Complete example

# Read only required data

sales2026 = (

    spark.read.table("FactSales")
         .filter("Year = 2026")
         .select(
             "CustomerID",
             "ProductID",
             "Country",
             "Amount"
         )

)

# Mark for caching

sales2026.cache()

# Trigger execution / populate cache

sales2026.count()

# Reuse cached data

sales2026.groupBy("Country") \

.sum("Amount") \

.show()

sales2026.groupBy("ProductID") \

.sum("Amount") \

.show()

sales2026.groupBy("CustomerID") \

.sum("Amount") \

.show()

# Release resources

sales2026.unpersist()

Conceptually:

Lakehouse

FactSales

Filter 2026

Select Columns

CACHE
   │
   ├── Country Analysis
   ├── Product Analysis
   └── Customer Analysis

UNPERSIST

42. Your Module 10 so far

The four topics you've covered connect nicely:

10.1 PARTITIONING
       │
       ▼
Organize large data
       │
       ▼

Partition Pruning

10.2 FILE SIZES
       │
       ▼
Avoid tiny files
       │
       ▼

Efficient reads

10.3 SPARK OPTIMIZATION
       │
       ▼

Reduce shuffle

Optimize joins

Balance processing

10.4 CACHING
       │
       ▼
Avoid repeated computation
       │
       ▼

Reuse intermediate results

Together:

           LARGE DATASET
                 │
                 ▼
         GOOD PARTITIONING
                 │
                 ▼
          GOOD FILE SIZES
                 │
                 ▼
         READ REQUIRED DATA
                 │
                 ▼
          OPTIMIZE SPARK
                 │
                 ▼
        CACHE REUSED RESULTS
                 │
                 ▼

FASTER PROCESSING

Quick revision

ConceptPurpose
PartitioningOrganize large data
File optimizationAvoid inefficient small files
Spark partitionsEnable parallel processing
BroadcastReduce join shuffle
CachingAvoid recomputation
PersistCache with storage-level control
UnpersistRelease cached resources
Delta tablePersistent storage

One line to memorize

Caching = Compute once, temporarily store the result, reuse it multiple times, and unpersist when finished.

And the key rule:

Don't ask "Can I cache this?" Ask "Will I reuse this expensive result enough times for caching to be worthwhile?"

↑ Back to top
Module 10 · Lesson 10.5

Query Performance

10.5 Query Performance in Microsoft Fabric

Query Performance means making queries execute faster and more efficiently while using fewer compute and I/O resources.

In Microsoft Fabric, query performance can apply across:

Fabric
│
├── Warehouse → T-SQL
├── Lakehouse → Spark / SQL analytics endpoint
├── Delta Tables
├── Semantic Models
└── Power BI

The easiest formula to remember is:

Query Performance = Read less data + Process less data + Move less data + Use the right data model

1. Simple example

Suppose you have:

FactSales

=

5 Billion Rows

=

5 TB

You need sales for India in 2026.

Poor query:

SELECT *

FROM FactSales;

This asks for:

All rows
+

All columns

A better query is:

SELECT
    ProductID,
    SalesDate,
    SalesAmount
FROM FactSales
WHERE Country = 'India'

AND SalesYear = 2026;

Now you're requesting only:

Required rows
+

Required columns

That's the foundation of query optimization.

2. Rule #1 — Avoid SELECT *

This is one of the easiest performance rules.

Bad:

SELECT *

FROM FactSales;

Suppose FactSales contains:

100 columns

but your report needs only:

ProductID

SalesAmount

Use:

SELECT

ProductID,

SalesAmount

FROM FactSales;

Conceptually:

SELECT *

100 columns

More data processed/transferred

SELECT required columns

2 columns

Less data

Especially with columnar storage, selecting only required columns can significantly reduce unnecessary work.

3. Rule #2 — Filter early

Suppose:

FactSales

=

5 TB

but:

Year = 2026

=

500 GB

Filter as early as possible:

SELECT
    ProductID,
    SalesAmount
FROM FactSales
WHERE SalesYear = 2026;

Think:

5 TB

Filter

500 GB

Process

rather than carrying unnecessary data through expensive joins and aggregations.

4. Partition pruning

This connects directly to 10.1 Partitioning.

Suppose your Delta table is organized by:

Year

Storage:

FactSales/
│
├── Year=2023
├── Year=2024
├── Year=2025
└── Year=2026

Query:

SELECT SUM(SalesAmount)
FROM FactSales
WHERE Year = 2026;

The engine can potentially skip:

2023 ✕

2024 ✕

2025 ✕

2026 ✓

This is:

Partition Pruning

Less data scanned generally means better performance.

5. Column pruning

Suppose the table contains:

CustomerID

CustomerName

Address

Phone

Email

ProductID

Description

Quantity

SalesAmount

SalesDate

...

Query:

SELECT

ProductID,

SalesAmount

FROM FactSales;

With columnar formats such as Parquet, the engine can focus on the required columns.

Conceptually:

CustomerID ✕

CustomerName ✕

Address ✕

Phone ✕

Email ✕

ProductID ✓

Description ✕

Quantity ✕

SalesAmount ✓

SalesDate ✕

This is column pruning.

6. Predicate pushdown

Suppose you request:

WHERE SalesYear = 2026
Where supported, the query engine can push that filter toward the data source/storage scan so unnecessary data isn't brought into later processing.

Query

WHERE Year=2026

Storage Scan

Read relevant data

This is called:

Predicate Pushdown

7. Partition pruning vs predicate pushdown

These sound similar but are different.

Partition pruning

Skips entire partitions.

Year=2024 ✕

Year=2025 ✕

Year=2026 ✓

Predicate pushdown

Pushes filters closer to the underlying data read so irrelevant records/data groups can be avoided where supported.

Both follow the same principle:

Don't read data you don't need.

8. Filter before joining

Suppose:

FactSales

=

5 TB

and:

DimCustomer

=

2 GB

You only need 2026.

Poor logical pattern:

FactSales 5 TB

Join Customer

Large intermediate result

Filter 2026

Better:

FactSales

Filter 2026

Smaller dataset

Join Customer

Example:

SELECT
    c.Country,
    SUM(f.SalesAmount)
FROM FactSales f
JOIN DimCustomer c
    ON f.CustomerID = c.CustomerID
WHERE f.SalesYear = 2026
GROUP BY c.Country;

Modern optimizers can often rearrange predicates, but writing clear, selective logic helps you reason about performance.

9. Optimize joins

Joins are often one of the biggest sources of query cost.

Suppose:

FactSales = 5 TB

DimProduct = 100 MB

DimCustomer = 2 GB

DimDate = 10 MB

Architecture:

                 DimDate
                    │
                    ▼
DimCustomer ───► FactSales ◄─── DimProduct

A good star schema makes analytical joins predictable and efficient.

10. Avoid unnecessary joins

Suppose your query needs only:

ProductID

SalesAmount

but you write:

SELECT
    f.ProductID,
    f.SalesAmount
FROM FactSales f
JOIN DimCustomer c
    ON f.CustomerID = c.CustomerID
JOIN DimProduct p
    ON f.ProductID = p.ProductID
JOIN DimDate d
    ON f.DateKey = d.DateKey;

If none of those dimension columns or filters are required, those joins may be unnecessary.

Simpler:

SELECT

ProductID,

SalesAmount

FROM FactSales;

Rule:

Don't join a table unless the query actually needs it.

11. Aggregate early when appropriate

Suppose you have:

1 Billion Sales Rows

but ultimately need:

Sales by Month

Conceptually, reducing data through aggregation can make later processing cheaper:

1 Billion rows

GROUP BY Month

12 / limited result groups

Example:

SELECT
    SalesYear,
    SalesMonth,
    SUM(SalesAmount) AS TotalSales
FROM FactSales
GROUP BY
    SalesYear,
    SalesMonth;

For repeatedly requested aggregates, you might also consider maintaining curated Gold-layer summary tables.

12. Gold-layer aggregate tables

Suppose Power BI runs this repeatedly:

SELECT
    Country,
    ProductCategory,
    SUM(SalesAmount)
FROM FactSales
GROUP BY
    Country,
    ProductCategory;

against billions of rows.

Instead, your Gold layer might contain:

Gold_SalesSummary

with already summarized data:

Country

ProductCategory

Year

Month

TotalSales

TotalQuantity

Then:

Power BI

Gold Summary

Millions / thousands of rows

instead of:

Power BI

Raw Fact

Billions of rows

This can be a major architectural optimization.

13. Data type matters

Use appropriate data types.

For example, don't store:

SalesAmount = "12500.50"

as text if it is genuinely numeric.

Prefer an appropriate numeric type.

Similarly:

SalesDate

should generally be represented using an appropriate date/time type rather than arbitrary text when the business requirement allows it.

Correct data types improve:

Storage

Comparisons

Aggregations

Query semantics

14. Avoid unnecessary transformations in filters

Suppose you repeatedly need 2026 data.

You might write:

WHERE YEAR(SalesDate) = 2026

In many database/storage systems, applying functions to a filtering column can make some optimizations harder than a range predicate.

A clearer alternative is often:

WHERE SalesDate >= '2026-01-01'

AND SalesDate < '2027-01-01';

The exact performance difference depends on the Fabric engine and storage path, but the general rule is:

Prefer predicates that allow the engine to eliminate data efficiently.

15. Avoid unnecessary DISTINCT

Developers sometimes use:

SELECT DISTINCT

CustomerID,

ProductID

FROM FactSales;

without checking why duplicates exist.

DISTINCT requires deduplication work and may involve expensive data movement/sorting/hashing.

Don't use it as:

"My joins created duplicates, so I'll add DISTINCT."

Instead investigate:

Why are duplicates appearing?

Is the join correct?

Is the relationship one-to-many?

Is the grain correct?

Use DISTINCT when the business requirement genuinely requires distinct values.

16. GROUP BY can be expensive

Example:

SELECT
    CustomerID,
    SUM(SalesAmount)
FROM FactSales
GROUP BY CustomerID;

If:

CustomerID

=

100 million distinct customers

that's a large aggregation.

The engine may need significant:

Memory

CPU

Data movement

Intermediate storage

So reduce unnecessary data before large aggregations.

17. Sorting can be expensive

Example:

SELECT *

FROM FactSales

ORDER BY SalesAmount DESC;

Sorting billions of rows is expensive.

Conceptually:

5 Billion Rows

Global Sort

Large processing requirement

If you only need the highest values, structure the query accordingly using supported top/limit patterns rather than returning and sorting unnecessary results.

Example in T-SQL:

SELECT TOP (100)
    ProductID,
    SalesAmount
FROM FactSales
ORDER BY SalesAmount DESC;

Now the business requirement is clearer.

18. COUNT(*) on huge datasets

This looks simple:

SELECT COUNT(*)
FROM FactSales;

but if:

FactSales = billions of rows

the query can still require substantial work depending on engine metadata and execution strategy.

Don't repeatedly run large counts just to:

"Check whether the table exists."

Use appropriate metadata/monitoring techniques when that's the actual requirement.

19. File sizes affect query performance

This connects to 10.2 File Sizes.

Suppose:

FactSales = 1 TB

stored as:

1,000,000 tiny files

Even a well-written query may suffer because the engine has to handle excessive:

File metadata

File opens

Task planning

Better file layout:

1 TB

Reasonably sized files

Lower overhead

Better reads

Therefore:

SQL optimization cannot completely fix bad physical data layout.

20. Delta optimization

For Lakehouse/Delta workloads, maintaining healthy file layout is important.

Conceptually:

Many Small Files

Compaction / Optimization

Fewer Efficient Files

Faster Reads

So query optimization includes not just the query itself but also:

Table design

Partitioning

File layout

Maintenance

21. Caching

From 10.4 Caching:

Suppose the same expensive intermediate dataset is queried repeatedly.

Expensive Result

Cache
      │
      ├── Query 1
      ├── Query 2
      └── Query 3

Caching may reduce repeated computation.

But:

Caching is not a replacement for good query design.

Don't cache a badly designed 5-TB query and assume the problem is solved.

22. Query performance in Fabric Warehouse

For Fabric Warehouse, query optimization is largely handled by the SQL engine.

Your responsibility includes designing:

Good SQL
    +
Good Data Model
    +
Appropriate Tables
    +
Appropriate Data Types
    +
Efficient Joins
    +

Reasonable Aggregations

Fabric handles much of the underlying distributed execution for you.

23. Query execution plan

When a query is slow, you need to understand what the engine is doing.

Conceptually:

SQL Query

Parser

Optimizer

Execution Plan

Execution

Result

The optimizer decides strategies for operations such as:

Scans

Filters

Joins

Aggregations

Sorting

Data movement

24. Spark execution plan

For Spark:

df.explain("formatted")

can help you inspect the plan.

Look for operations such as:

Scan

Filter

Exchange

Join

Aggregate

Sort

In Spark, an:

Exchange

often indicates data redistribution/shuffling.

If you see multiple large exchanges:

Scan

Exchange

Join

Exchange

Aggregate

Exchange

you may have significant shuffle costs.

25. Query performance troubleshooting

When someone tells you:

"This Fabric query is slow."

Don't immediately increase capacity.

Use a systematic approach:

Slow Query

Check data volume

Check rows/columns scanned

Check filters

Check partition pruning

Check joins

Check aggregation/sorting

Check file layout

Check execution plan

Check resource utilization

Then consider capacity

26. Example: Bad query

Suppose:

FactSales = 10 TB

Bad:

SELECT DISTINCT *
FROM FactSales f
JOIN DimCustomer c
    ON f.CustomerID = c.CustomerID
JOIN DimProduct p
    ON f.ProductID = p.ProductID
WHERE YEAR(f.SalesDate) = 2026
ORDER BY f.SalesAmount;

Potential issues:

SELECT * → unnecessary columns

DISTINCT → expensive deduplication

Large joins → potentially expensive

Function in filter → may limit elimination opportunities

ORDER BY → expensive sorting

27. Better query thinking

If the actual requirement is:

Total 2026 sales by product category for India customers.

Write the query specifically for that requirement:

SELECT
    p.ProductCategory,
    SUM(f.SalesAmount) AS TotalSales
FROM FactSales AS f
JOIN DimCustomer AS c
    ON f.CustomerID = c.CustomerID
JOIN DimProduct AS p
    ON f.ProductID = p.ProductID
WHERE f.SalesDate >= '2026-01-01'

AND f.SalesDate < '2027-01-01'

AND c.Country = 'India'

GROUP BY

p.ProductCategory;

Now:

Required columns only
        +
Selective filters
        +
Required joins only
        +

Required aggregation

The query expresses the business requirement cleanly.

28. Star schema and query performance

From your earlier Fabric Warehouse module:

                  DimDate
                     │
                     ▼
DimCustomer ───► FactSales ◄─── DimProduct
                     ▲
                     │

DimRegion

A star schema helps analytical workloads because:

Fact

Measures + Keys

Dimensions

Descriptive attributes

This provides a simple and predictable structure for analytics and Power BI.

29. Query performance and Power BI

Suppose:

Power BI Report

Semantic Model

Fabric Warehouse

A poorly designed report might generate:

Many expensive queries

Large scans

Complex calculations

Poor user experience

So end-to-end performance involves:

Report Design
     +
Semantic Model
     +
SQL Query
     +
Warehouse/Lakehouse
     +

Physical Data Layout

Performance isn't only a database problem.

30. Direct Lake

In Fabric, Power BI can use Direct Lake in supported scenarios.

Conceptually:

Power BI

Semantic Model

Direct Lake

OneLake / Delta data

This architecture is designed to provide high-performance analytical access without requiring the traditional import-copy workflow for every scenario.

But good:

Data modeling

Table design

File organization

Data volume management

still matter.

31. Medallion architecture and performance

Suppose:

Bronze

Raw Data

Billions of rows

You generally don't want every business dashboard performing complex cleanup directly over raw Bronze data.

Instead:

BRONZE

Raw

SILVER

Clean / standardized

GOLD

Business-ready

Semantic Model

Power BI

Gold can contain:

Star schemas

Business aggregates

Curated dimensions

Optimized fact tables

which improves usability and can improve performance.

32. Query performance layers

Think of optimization as layers:

                QUERY PERFORMANCE
                       │
        ┌──────────────┼──────────────┐
        ▼              ▼              ▼

QUERY DATA STORAGE

      DESIGN          MODEL          DESIGN
        │              │              │

Filters Star Schema Partitioning

Columns Aggregates File Sizes

Joins Correct Grain Delta Layout

     Grouping
        │              │              │
        └──────────────┼──────────────┘
                       ▼
                    COMPUTE
                       │
                  Spark / SQL
                       │
                       ▼

MONITORING

33. Common causes of slow queries

A useful checklist:

Slow Query
│
├── Reading too many rows
├── Reading unnecessary columns
├── Missing useful filters
├── Poor partition strategy
├── Small files
├── Unnecessary joins
├── Large joins
├── Data skew
├── Expensive GROUP BY
├── Unnecessary DISTINCT
├── Large ORDER BY
├── Poor data types
├── Poor data model
├── Repeated computation
└── Insufficient compute/resources

34. Performance optimization order

A useful order is:

1. Understand requirement

2. Reduce rows

3. Reduce columns

4. Check joins

5. Check aggregation/sorting

6. Check partitioning

7. Check file sizes

8. Check execution plan

9. Check skew/shuffle

10. Check caching

11. Check capacity/resources

This prevents the common mistake:

Slow Query

Buy more compute

without investigating the underlying problem.

35. Interview question — How do you optimize query performance?

A strong answer:

I start by reducing the amount of data processed: select only required columns, apply selective filters early, and make sure partition pruning and predicate pushdown can occur where applicable. Then I review joins, aggregations, sorting, table design, partitioning and file sizes. For Spark workloads I also inspect shuffles, skew and execution plans, and I increase compute only after identifying the actual bottleneck.

36. Interview question — What is predicate pushdown?

Predicate pushdown is an optimization where filter conditions are pushed closer to the underlying data source or storage scan so unnecessary data can be eliminated before later processing.

37. Interview question — What is column pruning?

Column pruning means reading or processing only the columns required by the query rather than all columns. It is especially valuable with columnar formats such as Parquet.

38. Interview question — How does partitioning improve queries?

Partitioning can improve performance through partition pruning. If a table is partitioned by a column such as year and the query filters for 2026, the engine can skip unrelated partitions and reduce the amount of data scanned.

39. Interview question — Why is SELECT * discouraged?

SELECT * requests every column even when many are unnecessary. This can increase data scanning, processing, memory use and transfer. Selecting only required columns allows the engine to take better advantage of columnar storage and column pruning.

40. Interview question — How would you troubleshoot a slow Fabric query?

A strong answer:

First I would identify where the time is being spent using the available query or Spark monitoring information. Then I'd check data scanned, filters, partition pruning, joins, aggregations, sorting, file sizes, shuffles and skew. I would optimize the query and physical design before considering additional capacity.

41. Your Module 10 connection

You have now covered:

10.1 PARTITIONING

Skip unnecessary partitions

10.2 FILE SIZES

Efficient physical reads

10.3 SPARK OPTIMIZATION

Reduce shuffle

Optimize joins

Balance partitions

10.4 CACHING

Avoid repeated computation

10.5 QUERY PERFORMANCE

Bring everything together

So query performance isn't an isolated topic.

It is the result of all the previous optimizations working together.

42. One complete example

Suppose:

FactSales

=

10 TB

Requirement:

Find total 2026 sales by product category for India.

Optimized flow:

                 10 TB FactSales
                       │
                       ▼
              Partition Pruning
                       │

Year = 2026

                       │
                       ▼
                  Filter India
                       │
                       ▼
             Select required columns
                       │
                       ▼
               Join DimProduct
                       │
                       ▼
             GROUP BY Category
                       │
                       ▼
                SUM(SalesAmount)
                       │
                       ▼

RESULT

Instead of:

10 TB

Read Everything

Join Everything

Sort Everything

Distinct Everything

Finally Filter

Quick Revision

TechniquePurpose
Filter earlyReduce rows
Select columnsReduce data processed
Partition pruningSkip partitions
Predicate pushdownFilter closer to storage
Column pruningSkip unnecessary columns
Good joinsReduce processing/data movement
Avoid unnecessary DISTINCTReduce expensive deduplication
Avoid unnecessary sortingReduce compute
Star schemaEfficient analytical model
Good file sizesEfficient physical reads
CachingAvoid repeated computation
Execution planIdentify bottlenecks
MonitoringMeasure actual performance

The five rules to memorize

1. Read fewer rows. 2. Read fewer columns. 3. Avoid unnecessary joins, sorting and shuffling. 4. Use good partitioning, file layout and data modeling. 5. Measure the execution plan before adding more compute.

And the simplest formula for 10.5 Query Performance:

Less Data Scanned + Less Data Moved + Less Work Performed = Faster Queries.

↑ Back to top
Module 10 · Lesson 10.8

Module 10 · Lesson 10.9

Module 10 · Lesson 10.6

Monitoring

10.6 Monitoring in Microsoft Fabric

Monitoring in Microsoft Fabric means continuously observing the status, performance, resource consumption, failures, and behavior of Fabric workloads.

Monitoring helps answer questions like:

Did my pipeline succeed or fail?

Why is my Spark notebook slow?

Which query is consuming resources?

Is my Fabric capacity overloaded?

How long did a Dataflow Gen2 refresh take?

Which activity failed?

Is performance getting worse over time?

The easiest definition to remember is:

Monitoring = Observe → Detect → Diagnose → Optimize

1. Why do we need monitoring?

Imagine this Fabric architecture:

Sources
   │
   ▼
Data Factory
   │
   ▼
Lakehouse
   │
   ▼
Spark Notebook
   │
   ▼
Warehouse
   │
   ▼
Semantic Model
   │
   ▼

Power BI

Many things can go wrong:

Pipeline failed

Notebook slow

Data missing

SQL query slow

Capacity overloaded

Refresh failed

Streaming stopped

Without monitoring:

Problem

User reports it

Team investigates

With monitoring:

Problem

Monitoring detects it

Investigate

Identify root cause

Fix

2. What can we monitor in Fabric?

Fabric contains multiple workloads.

Microsoft Fabric
│
├── Data Factory
│   ├── Pipelines
│   └── Dataflows Gen2
│
├── Data Engineering
│   ├── Spark Notebooks
│   ├── Spark Jobs
│   └── Lakehouse
│
├── Data Warehouse
│   └── SQL queries
│
├── Real-Time Intelligence
│   ├── Eventstream
│   └── Eventhouse
│
├── Power BI
│   ├── Semantic Models
│   └── Refreshes
│
└── Fabric Capacity
    └── Resource utilization

Different Fabric monitoring experiences cover different parts of this architecture.

3. Monitoring Hub

One of the important Fabric monitoring experiences is the Monitoring hub.

Think of it as:

A centralized place for viewing Fabric activity and job execution.

Conceptually:

               Monitoring Hub
                     │
       ┌─────────────┼─────────────┐
       ▼             ▼             ▼
    Pipelines     Notebooks     Dataflows
       │             │             │

Success Success Success

Failed Failed Failed

Running Running Running

Instead of opening every individual Fabric item, Monitoring hub helps you inspect activities centrally.

4. What information do we monitor?

Typically you care about information such as:

Item / Activity

Status

Start Time

End Time

Duration

Item Type

Workspace

Failure Information

For example:

PipelineStatusDuration
LoadCustomersSucceeded3 min
LoadOrdersSucceeded7 min
LoadProductsFailed2 min
LoadSalesRunning15 min

Immediately you can identify:

LoadProducts

FAILED

and investigate further.

5. Pipeline monitoring

Suppose your Fabric pipeline is:

Sales_Ingestion

Architecture:

SQL Server

Fabric Pipeline

Lakehouse

You want to monitor:

Pipeline status

Activity status

Start time

End time

Duration

Failure

Error details

For example:

Sales_Ingestion
      │
      ├── Copy Customers ✓
      ├── Copy Products  ✓
      ├── Copy Orders    ✕
      └── Notebook       Not Run

Now you know exactly where execution stopped.

6. Pipeline activity monitoring

Suppose:

Pipeline
   │
   ├── Copy Data
   │
   ├── Notebook
   │
   └── Stored Procedure

The pipeline fails.

Don't just ask:

"Why did the pipeline fail?"

Find the failed activity:

Pipeline
   │
   ├── Copy Data          ✓
   │
   ├── Notebook           ✕
   │
   └── Stored Procedure   -

Then investigate the notebook error.

This gives you:

Pipeline Failure

Activity Failure

Error Details

Root Cause

7. Monitoring duration

Failure isn't the only problem.

Imagine:

Pipeline usually

=

10 minutes

Today:

Pipeline

=

55 minutes

Technically:

Status = Success

but operationally something may be wrong.

This is why monitoring should include:

Performance degradation, not just failures.

8. Establish a baseline

Suppose a pipeline normally runs:

Average duration

=

10 minutes

You can establish:

Normal Range

8–12 minutes

Now:

Monday 9 min

Tuesday 10 min

Wednesday 11 min

Thursday 42 min ← investigate

This is trend monitoring.

9. Spark monitoring

From your 10.3 Spark Optimization lesson, Spark has:

Driver

Executors

Jobs

Stages

Tasks

Conceptually:

Spark Application
      │
      ▼
     Job
      │
      ▼
    Stages
      │
      ▼

Tasks

When a notebook is slow, you want to determine:

Which job?

Which stage?

Which task?

What operation?

10. Spark performance example

Suppose:

Stage 1 → 25 seconds

Stage 2 → 32 seconds

Stage 3 → 18 minutes

Stage 4 → 40 seconds

Immediately:

Stage 3

Investigate

Then perhaps you discover:

Huge Shuffle Read

or:

Task 1 → 30 sec

Task 2 → 32 sec

Task 3 → 31 sec

Task 4 → 17 min

That could indicate data skew.

11. What should you monitor in Spark?

Important Spark metrics include:

Job duration

Stage duration

Task duration

Input size

Output size

Shuffle Read

Shuffle Write

Memory

Disk spill

Executor utilization

Failed tasks

These metrics help diagnose performance problems.

12. Shuffle monitoring

Remember:

Shuffle = Data moving between Spark partitions/executors.

Suppose monitoring shows:

Shuffle Read

=

800 GB

Shuffle Write

=

750 GB

That is significant.

You should investigate:

Joins

groupBy

distinct

orderBy

repartition

because those commonly trigger data redistribution.

13. Disk spill monitoring

Suppose Spark needs:

100 GB memory

but sufficient memory isn't available for an operation.

Spark may spill intermediate data:

Memory

Insufficient

Disk Spill

Disk I/O

Slower Job

Large spill values may indicate:

Large partitions

Large joins

Heavy aggregation

Memory pressure

Data skew

14. Monitor data skew

Suppose:

100 tasks

and:

99 tasks → 30 seconds

1 task → 20 minutes

That's suspicious.

Conceptually:

Executor 1 ✓

Executor 2 ✓

Executor 3 ✓

Executor 4 ███████████████

Still working

Possible cause:

Data skew

Monitoring lets you identify these patterns rather than guessing.

15. Warehouse monitoring

For Fabric Warehouse, you may need to investigate:

Running queries

Query duration

Resource consumption

Query patterns

Failures

Concurrency

Performance trends

Suppose:

Query A → 3 sec

Query B → 7 sec

Query C → 14 min ← investigate

Query D → 5 sec

Then inspect Query C.

16. Slow query investigation

Suppose:

SELECT DISTINCT *

FROM FactSales

ORDER BY SalesAmount;

Monitoring shows:

Duration = 18 minutes

You then investigate the query.

Potential problems:

SELECT *

DISTINCT

Large table

Large sort

Monitoring identifies where the problem is.

Query optimization determines how to fix it.

That's why:

Monitoring
    +

Query Optimization

=

Performance Management

17. Capacity monitoring

This is one of the most important Fabric concepts.

Fabric workloads consume capacity resources.

Think:

Fabric Capacity
      │
      ├── Warehouse
      ├── Spark
      ├── Pipelines
      ├── Power BI
      └── Other workloads

If multiple workloads consume heavy resources simultaneously:

Spark Job ████████

Warehouse ███████

Power BI ████████

Pipeline ████

the capacity may experience pressure.

18. Fabric Capacity Metrics app

The Microsoft Fabric Capacity Metrics app helps administrators understand Fabric capacity consumption and performance.

Conceptually:

Fabric Capacity

Capacity Metrics

Utilization

Workloads / Items

Identify heavy consumers

This helps answer:

"Why is everything slow?"

Maybe the problem isn't your individual SQL query.

It could be:

Capacity contention

from multiple workloads.

19. Example capacity problem

Imagine at 9:00 AM:

Spark ETL
     +
Warehouse Load
     +
Power BI Refresh
     +
Dataflow
     +

User Queries

all run together.

Conceptually:

9:00 AM

Spark ███████

Warehouse ██████

Power BI ███████

Dataflow █████

CAPACITY

PRESSURE

Users may experience degraded performance.

20. Possible solution

Monitoring may show that heavy jobs overlap.

Instead of immediately increasing capacity, you might change scheduling.

Before:

09:00 → Spark

09:00 → Warehouse

09:00 → Semantic Model Refresh

09:00 → Dataflow

Potential improvement:

08:00 → Dataflow

08:30 → Spark

09:15 → Warehouse

10:00 → Semantic Model Refresh

Now resource-intensive workloads don't all compete at exactly the same time.

This is an example of:

Workload management through monitoring.

21. Monitoring Dataflows Gen2

For Dataflows Gen2, you care about:

Refresh status

Start time

End time

Duration

Failure information

Example:

Customer_Dataflow

Yesterday → Success → 6 min

Today → Failed → 3 min

You investigate the refresh failure.

22. Monitoring Power BI refreshes

For semantic models, important operational information includes:

Refresh success/failure

Refresh duration

Refresh history

Data freshness

Example:

Sales Semantic Model

Scheduled Refresh

Failed

Report still shows old data

Monitoring helps detect the problem before users rely on stale information.

23. Data freshness monitoring

This is extremely important in real projects.

Suppose:

Pipeline = Success

but no new records arrived.

Technically:

Execution Status = SUCCESS

but:

Business Status = FAILURE

because the data is stale.

So advanced monitoring should consider:

Pipeline Health
       +

Data Health

24. Example data-quality monitoring

Suppose yesterday:

FactSales

=

100 million rows

Today:

FactSales

=

100 million rows

Maybe that's normal.

Or maybe today's load never arrived.

You could monitor:

Record count

MAX(LoadDate)

MAX(BusinessDate)

NULL counts

Duplicate counts

Business totals

For example:

SELECT
    COUNT(*) AS RecordCount,
    MAX(LoadDate) AS LatestLoad
FROM FactSales;

This checks the data itself rather than only the pipeline status.

25. Technical vs business monitoring

This distinction is very useful.

Technical monitoring

Pipeline succeeded?

Notebook failed?

Query slow?

Capacity overloaded?

Business/data monitoring

Did today's sales arrive?

Are record counts reasonable?

Is revenue unexpectedly zero?

Is latest business date today?

Are duplicate records present?

A mature monitoring solution uses both.

26. Monitoring architecture

A production Fabric monitoring approach could conceptually look like:

             MICROSOFT FABRIC
                    │
       ┌────────────┼─────────────┐
       ▼            ▼             ▼
   Pipelines      Spark        Warehouse
       │            │             │
       └────────────┼─────────────┘
                    ▼
              Monitoring
                    │
       ┌────────────┼────────────┐
       ▼            ▼            ▼
    Status       Performance    Capacity
       │            │            │
       └────────────┼────────────┘
                    ▼
                Analysis
                    │
                    ▼

Alert / Action

27. Monitoring KPIs

For a production project, useful KPIs include:

KPIExample
Pipeline Success Rate98.7%
Failed Pipelines3
Average Pipeline Duration14 min
Longest Pipeline48 min
Notebook Failures1
Dataflow Failures2
Slow Queries5
Latest Data Load05:30 AM

These can form a monitoring dashboard.

28. Pipeline success rate

Suppose:

Total Runs

=

100

Successful

=

97

Failed

=

3

Success rate:

97 / 100 × 100

=

97%

This gives management a quick view of platform health.

29. SLA monitoring

Suppose business says:

Sales data must be available by 7:00 AM every day.

Pipeline completes:

Monday 06:15 ✓

Tuesday 06:30 ✓

Wednesday 07:45 ✕

Wednesday technically succeeded.

But it violated the business SLA.

Therefore:

Technical Status

=

Success

SLA Status

=

Failure

This distinction is very important in enterprise monitoring.

30. Alerts

Monitoring becomes more valuable when important conditions trigger alerts.

For example:

Pipeline Failed

Alert

Support Team

or:

Pipeline Duration > 60 minutes

Alert

or:

LatestLoadDate != Today

Alert

or:

Record Count Variance > Threshold

Alert

The exact alerting mechanism depends on your Fabric environment and monitoring architecture.

31. Good alert vs bad alert

Bad:

Alert on everything

100 alerts/day

Team ignores alerts

This is alert fatigue.

Better:

Critical

Immediate alert

Warning

Investigate

Information

Dashboard/log only

Alerts should be actionable.

32. Monitoring vs logging

These are related but different.

Logging

Records events.

10:01 Pipeline Started

10:04 Copy Completed

10:07 Notebook Started

10:12 Notebook Failed

Monitoring

Uses information to understand system health.

Failure Rate

Duration Trend

Capacity Usage

Data Freshness

SLA Compliance

Think:

Logging records what happened. Monitoring tells you what it means operationally.

33. Monitoring vs alerting

Another important distinction:

MONITORING

Observe system

ALERTING

Notify when condition occurs

Example:

Monitoring:

Pipeline duration = 72 minutes

Alert:

Duration > 60 minutes

Notify support

34. Monitor trends, not just individual failures

Suppose:

Week 1 → Average 10 min

Week 2 → Average 12 min

Week 3 → Average 18 min

Week 4 → Average 27 min

Nothing has failed.

But performance is degrading.

Trend:

10

\

12

\

18

\

27

Monitoring should detect this before it becomes:

Pipeline timeout

35. Baseline → Threshold → Alert

A strong monitoring design uses:

BASELINE

What is normal?

THRESHOLD

What is abnormal?

ALERT

Who needs to know?

ACTION

What should they do?

Example:

Baseline:

Pipeline = 15 min

Threshold:

> 30 min

Alert:

Support Team

Action:

Check source,

Spark stages,

capacity

36. Monitoring workflow

Use this model:

OBSERVE

DETECT

DIAGNOSE

RESOLVE

VERIFY

OPTIMIZE

Example:

Observe

Notebook taking 45 min

Detect

Normal = 10 min

Diagnose

Huge shuffle in Stage 3

Resolve

Optimize join

Verify

Runtime = 9 min

Optimize

Add performance monitoring

37. Interview question — What is Monitoring in Microsoft Fabric?

A strong answer:

Monitoring in Microsoft Fabric is the process of observing workload execution, performance, failures and capacity consumption across Fabric workloads such as pipelines, notebooks, Dataflows, Warehouse and Power BI. It helps identify failures and performance bottlenecks and supports troubleshooting, SLA management and optimization.

38. Interview question — What is Monitoring Hub?

Monitoring hub provides a centralized experience for viewing and investigating Fabric activities and job runs across supported workloads, helping users identify running, successful and failed activities without opening each item individually.

39. Interview question — How would you monitor a Fabric pipeline?

I would monitor pipeline and activity status, start and end times, duration and failure details. In production I would also monitor trends, SLA completion and data freshness, because a pipeline can technically succeed while still loading stale or incomplete data.

That's a strong real-world answer.

40. Interview question — How do you monitor Spark performance?

I inspect Spark jobs, stages and tasks and review metrics such as task duration, shuffle read/write, memory usage and disk spill. I look for patterns such as one task taking much longer than others, which may indicate data skew, or unusually large shuffles caused by joins or aggregations.

41. Interview question — How do you monitor Fabric capacity?

I use the Fabric Capacity Metrics app and related Fabric monitoring experiences to analyze capacity consumption and identify which workloads or items are driving resource usage. I also look for overlapping resource-intensive jobs before deciding whether additional capacity is required.

42. Interview question — Pipeline succeeded, but data didn't arrive. What do you do?

This is an excellent scenario question.

Answer:

I wouldn't rely only on pipeline execution status. I would validate data freshness using checks such as maximum load date, expected business date, record counts and key business metrics. A technically successful pipeline can still represent a business failure if the expected data wasn't loaded.

43. Interview question — What would you monitor in production?

A strong answer:

I would monitor workload success/failure, execution duration, SLA compliance, data freshness, record counts, query and Spark performance, capacity utilization and performance trends. Alerts should focus on actionable conditions such as failures, SLA breaches, stale data or significant performance degradation.

44. Your Module 10 so far

Everything now connects:

10.1 PARTITIONING

Organize data

Partition pruning

10.2 FILE SIZES

Efficient storage

10.3 SPARK OPTIMIZATION

Efficient processing

10.4 CACHING

Avoid recomputation

10.5 QUERY PERFORMANCE

Efficient queries

10.6 MONITORING

Measure everything above

Monitoring answers:

"Did our optimizations actually work?"

45. Complete performance lifecycle

This is the most important diagram for this topic:

                 FABRIC WORKLOAD
                       │
                       ▼
                    MONITOR
                       │
                       ▼
               Identify Bottleneck
                       │
          ┌────────────┼────────────┐
          ▼            ▼            ▼
     Partitioning   File Sizes    Query
          │            │            │
          ▼            ▼            ▼
        Spark       Caching      Capacity
          │            │            │
          └────────────┼────────────┘
                       ▼
                    OPTIMIZE
                       │
                       ▼
                    EXECUTE
                       │
                       ▼
                  MONITOR AGAIN
                       │
                       ▼

Compare Performance

This is a continuous cycle:

Monitor → Identify → Optimize → Measure → Repeat

Quick Revision

Monitoring AreaWhat to Check
PipelineSuccess, failure, duration
Pipeline ActivityFailed activity, errors
Dataflow Gen2Refresh status and duration
SparkJobs, stages, tasks, shuffle, spill
WarehouseQuery performance
Power BIRefresh and data freshness
CapacityResource consumption
DataCount, latest date, quality
SLACompletion before deadline
TrendPerformance degradation

Five points to memorize

1. Monitoring Hub → Monitor Fabric jobs and activities centrally. 2. Spark monitoring → Jobs, stages, tasks, shuffle and spill. 3. Capacity Metrics → Understand Fabric capacity consumption. 4. Data monitoring → Check freshness, counts and quality—not just pipeline success. 5. Monitor → Detect → Diagnose → Optimize → Verify.

One-line interview answer

Microsoft Fabric monitoring helps us understand whether workloads are running successfully, whether data is arriving correctly, where performance bottlenecks exist, and how efficiently Fabric capacity is being consumed.

↑ Back to top
Module 10 · Lesson 10.11

Module 10 · Lesson 10.7

Optimize Large Dataset