DP-800 · Case study practice 2 · Caldera Energy

Second Full-Length Case Study

A full-length practice case study. Nine questions on regex validation, temporal clause selection, randomized encryption, window functions and grouping sets.

Target 22 minutes · answers hidden · do all nine before revealing
⚠ Before you start — pacing, since you did the last one in 12 minutes

Twelve minutes for nine case-study questions is too fast, and one of your two mistakes was "did not see the correct one of two options". That's the signature of rushing.

Your overall exam pace is proven — 45 minutes for 50 questions. That means you have time to spend, and the case study is precisely where to spend it, because you cannot go back to it. Every other question on the exam is revisitable; these nine are not.

Target 20–22 minutes this time, deliberately. If you finish early, re-read the two options you were torn between rather than moving on. Speed is a resource you've already banked — the case study is where you cash it in.

Case study — Caldera Energy 00:00

General overview

Caldera Energy is a United Kingdom domestic energy supplier with 2.6 million customers. It operates a smart-meter network, a field engineering workforce of 1,400 staff organised into eight regions, and a customer call centre.

Caldera Energy runs its data platform on Azure SQL Database. It is modernising meter-data retention, opening a partner API for price-comparison websites, and deploying an AI assistant for field engineers.

Planned changes

Caldera Energy plans to replace its nightly meter-data purge, enforce meter serial validation at the database, formalise its deployment pipeline, launch the partner API, and deploy the engineer assistant.

Databases

MeteringDB contains Meter.Reading (3.8 billion rows, growing by approximately 220 million rows per month), Meter.Device, and dbo.Customer.
OpsDB contains dbo.Tariff, dbo.EngineerJob, and dbo.MaintenanceReport (400,000 reports averaging 2,500 words each).

Current issues

A nightly job deletes meter readings older than 84 months in batches. It runs for seven hours, blocks the ingestion process, and generates very large volumes of transaction log.

Meter serial numbers are currently validated only in the mobile application used by engineers. Approximately 0.4% of rows in Meter.Device contain malformed serials that entered through a legacy bulk import.

A developer recently added a nonclustered index to Meter.Reading to support a new report. It was created with ON [PRIMARY].

Applications

The billing engine is a .NET service in Azure Container Apps.
The partner API will be consumed by third-party comparison websites. No server-side application code will be written for it.
The engineer assistant has not yet been built.

Data sensitivity

dbo.Customer contains BankAccountNumber and ContactPhone. The bank account number is used only for direct-debit file generation. It is never used in a WHERE clause, a JOIN, or a GROUP BY. The contact telephone number is not considered sensitive under Caldera's data classification and is not encrypted.

AI resources

An Azure AI Foundry resource named foundry-caldera hosts text-embedding-3-small and gpt-4.1-mini deployments.

Business requirements

  • B1Caldera Energy must be able to retire the oldest month of meter reading data in under one minute, with minimal transaction log usage.
  • B2Field engineers must see only jobs in their own region, and must not be able to create a job for another region or reassign an existing job to another region.
  • B3BankAccountNumber must never be readable in plaintext by database administrators.
  • B4Call centre agents must see only the last four digits of ContactPhone on screen. This is a usability measure and is not a compliance control.
  • B5No API keys may be stored in application code, configuration files, or a secret store.
  • B6Field engineers must be able to ask questions in natural language against the maintenance reports, with results returned in under one second.

Technical requirements

  • T1Reporting queries that filter on ReadingDate must read only the relevant months of data.
  • T2Meter serial numbers must match the format two uppercase letters, hyphen, seven digits, hyphen, one uppercase letter (for example GB-4471902-K). Invalid values must be rejected at insert.
  • T3Compliance must be able to (a) reconstruct a customer's tariff record as it stood on any past date, and (b) list only the tariff versions that both began and were replaced within a given quarter.
  • T4Comparison partners must retrieve a tariff, its region, and the current unit price in a single request.
  • T5Anomaly detection must compare each meter reading to the immediately preceding reading for the same meter.
  • T6A billing report must return totals by region, by region and tariff, and a grand total, in a single query.
  • T7Schema changes must be validated by a build before being merged into main, and the same artifact deployed to test and to production.
  • T8All partitions must reside on the PRIMARY filegroup.

Exhibit — Meter.Reading

CREATE TABLE Meter.Reading (
    ReadingID    BIGINT IDENTITY(1,1) NOT NULL,
    MeterID      INT           NOT NULL,
    ReadingDate  DATETIME2(0)  NOT NULL,
    ReadingValue DECIMAL(12,3) NOT NULL,
    CONSTRAINT PK_Reading PRIMARY KEY CLUSTERED (ReadingID, ReadingDate)
);

-- added last week by a developer
CREATE NONCLUSTERED INDEX IX_Reading_Meter
    ON Meter.Reading (MeterID) INCLUDE (ReadingValue)
    ON [PRIMARY];

Exhibit — Meter.Device

CREATE TABLE Meter.Device (
    MeterID     INT PRIMARY KEY,
    SerialCode  VARCHAR(20) NOT NULL,   -- e.g. 'GB-4471902-K'
    RegionID    INT NOT NULL,
    InstalledOn DATE NOT NULL
);

The nine questions Answer all nine on paper before opening a single reveal.

1
How should you configure partitioning for Meter.Reading?
  • A. Partition function RANGE LEFT on ReadingDate with end-of-month boundaries; scheme mapping each partition to its own filegroup.
  • B. Partition function RANGE RIGHT on ReadingDate with first-of-month boundaries; scheme mapping all partitions to [PRIMARY].
  • C. Partition function RANGE RIGHT on ReadingID; scheme mapping all partitions to [PRIMARY].
  • D. Keep the batched DELETE and add a filtered index on ReadingDate.
Show answer & debrief
Answer: B

B1T1T8

First-of-month boundaries require RANGE RIGHT so the first instant of a month lands with that month. Partitioning on the date column is what delivers elimination for T1. ALL TO ([PRIMARY]) satisfies T8 — and is the only legal option on Azure SQL Database anyway.

A fails twice: end-of-month boundaries on a datetime2 column send anything after midnight on the last day into the next month, and multiple filegroups are impossible here. C partitions on the wrong column, so date filters get no elimination. D doesn't change the cost of deleting hundreds of millions of rows.

Note the PK is already (ReadingID, ReadingDate) in the exhibit — someone anticipated the unique-index rule. That's one collision this study doesn't set for you; question 2 sets a different one.

2
After partitioning is implemented, the monthly archive job fails. What is the cause?
  • A. The primary key does not include the partitioning column.
  • B. The partition function uses RANGE RIGHT.
  • C. IX_Reading_Meter was created ON [PRIMARY] rather than on the partition scheme, so it is not aligned.
  • D. Statistics on Meter.Reading are out of date.
Show answer & debrief
Answer: C

B1 — and the evidence is in the Existing environment tab, not the requirements.

SWITCH requires every index to be aligned — built on the same partition scheme with the same partitioning column. The developer's index sits on a single filegroup, so it isn't aligned, and one non-aligned index blocks the whole operation.

Read for what changed. Two separate sentences plant this: "a developer recently added a nonclustered index" and "it was created with ON [PRIMARY]". Case studies hide the deciding fact in the environment narrative and expect you to connect it to a requirement three tabs away.

A is a genuine SWITCH prerequisite and therefore a strong distractor — but the exhibit shows the PK already includes ReadingDate. B is correct configuration, not a fault.

3
How should you enforce the meter serial number format?
  • A. CHECK (SerialCode LIKE '[A-Z][A-Z]-[0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[A-Z]')
  • B. CHECK (REGEXP_LIKE(SerialCode, '^[A-Z]{2}-[0-9]{7}-[A-Z]$'))
  • C. An AFTER INSERT trigger that calls REGEXP_LIKE and rolls back on failure.
  • D. A scheduled job that reports malformed serials each night.
Show answer & debrief
Answer: B

T2 — "must be rejected at insert" is the deciding phrase.

A CHECK constraint with REGEXP_LIKE is declarative, evaluated at write time, and expresses the pattern precisely. REGEXP_LIKE is a predicate, so a CHECK constraint is exactly where it belongs.

A is the interesting distractor — that LIKE pattern would work, and before SQL Server 2025 it was the only option. But it's unreadable, doesn't scale to any real pattern, and when regex is available it's the expected answer. If an exam offers both, take regex. C works but is heavier and fires after the write. D reports rather than rejects.

Two things to check while you're here The existing table already holds 0.4% malformed rows from the legacy import — so adding the constraint will fail unless you clean them first or use WITH NOCHECK. And REGEXP_LIKE needs compatibility level 170.
4
Which clauses satisfy the two halves of the compliance requirement?
  • A. (a) FOR SYSTEM_TIME AS OF · (b) FOR SYSTEM_TIME FROM … TO …
  • B. (a) FOR SYSTEM_TIME ALL · (b) FOR SYSTEM_TIME BETWEEN … AND …
  • C. (a) FOR SYSTEM_TIME AS OF · (b) FOR SYSTEM_TIME CONTAINED IN (…, …)
  • D. Change Data Capture for both.
Show answer & debrief
Answer: C

T3 — two questions, two clauses.

(a) "as it stood on any past date" is a point-in-time snapshot → AS OF.
(b) "versions that both began and were replaced within a quarter" → CONTAINED IN, the only clause that requires the row's entire lifespan to fall inside the window.

This is the one people most often get wrong. FROM…TO and BETWEEN…AND both mean "alive at any point during the window" — they'd include a tariff that started in 2024 and is still current. Only CONTAINED IN means born and died inside it, which is why a currently-active row can never match it.

D can't reconstruct a point-in-time state without you building that logic yourself.

5
How should you protect BankAccountNumber?
  • A. Always Encrypted with deterministic encryption
  • B. Always Encrypted with randomized encryption
  • C. Transparent Data Encryption with a customer-managed key
  • D. Dynamic Data Masking with default()
Show answer & debrief
Answer: B

B3 — plus a sentence in the Existing environment tab that most people skim past.

"Never readable in plaintext by database administrators" → Always Encrypted, because the keys live outside the database. Then: "it is never used in a WHERE clause, a JOIN, or a GROUP BY" tells you there is no query requirement — so you take the stronger type, randomized.

★ The inversion from the last case study In Lakeside Outdoors (case study 1), the answer was deterministic, because a requirement demanded exact lookup. Here it's randomized, because a sentence explicitly rules lookup out. Same technology, opposite answer, decided by one clause.

The design rule underneath: use the weakest encryption type that still satisfies the query requirement. Deterministic leaks frequency, so you only accept it when forced.

C decrypts transparently for anyone who authenticates. D is a display control and defeatable by inference.

6
What should you implement for call centre agents viewing ContactPhone?
  • A. Dynamic Data Masking with partial(0,"XXX-XXX-",4), and do not grant UNMASK to the agent role
  • B. Always Encrypted with deterministic encryption
  • C. An RLS filter predicate on dbo.Customer
  • D. DENY SELECT on ContactPhone to the agent role
Show answer & debrief
Answer: A

B4 — and two phrases make this clean.

"Not a compliance control" lowers the bar to exactly where DDM belongs. And the environment tab states the phone number is not encrypted — which is what makes masking technically possible at all.

★ Why this question is deliberately on a different column from Q5 Dynamic Data Masking cannot be applied to an Always Encrypted column. DDM works by having the SQL engine rewrite the value in the result set — but with Always Encrypted the engine only ever holds ciphertext. It cannot produce "the last four characters" of a value it cannot read.

So the two controls live on separate columns: BankAccountNumber is encrypted and simply unreadable by anyone without the key; ContactPhone is unencrypted and masked for presentation. That's how a real design satisfies both kinds of requirement.

C filters rows, not columns. D hides it entirely — agents need the last four digits.

7
How should you enforce regional isolation on dbo.EngineerJob?
  • A. A security policy with a filter predicate on RegionID
  • B. A security policy with a filter predicate on RegionID, plus block predicates AFTER INSERT and AFTER UPDATE
  • C. Eight views, one per region, with SELECT granted per role
  • D. A CHECK constraint comparing RegionID to SESSION_CONTEXT
Show answer & debrief
Answer: B

B2 — one requirement, three verbs.

"See only jobs in their own region" → filter predicate. "Not create a job for another region" → AFTER INSERT. "Not reassign an existing job to another region" → AFTER UPDATE.

The partial answer. Option A satisfies one third of B2 and is the most-chosen wrong option in this shape. A filter alone would let an engineer successfully insert a job stamped with another region's ID — and then watch it vanish from their own view.

C is bypassable via the base table and unmanageable at eight regions. D can't reference session state reliably and doesn't control reads.

8
Which two actions should you perform for the partner API and the engineer assistant? (Choose two.)
  • A. Use Data API Builder exposing the GraphQL endpoint, with relationships defined between Tariff, Region and Price
  • B. Use Data API Builder exposing the REST endpoint only
  • C. Chunk the maintenance reports, embed each chunk, build a DiskANN vector index and query with VECTOR_SEARCH
  • D. Embed each maintenance report as a single vector and query with VECTOR_DISTANCE
  • E. Build an Azure Function that queries OpsDB and returns JSON to partners
Show answer & debrief
Answer: A and C

T4B6

A: "tariff, its region, and the current unit price in a single request" is GraphQL relationship traversal. And "no server-side application code will be written" (Existing environment → Applications) eliminates E.

C: two numbers decide it. 2,500 words per report forces chunking — a single vector for a long report is a blurred average. 400,000 reports under one second forces approximate search with a DiskANN index.

D is the near-miss: right idea, wrong on both counts — no chunking and an exhaustive scan that won't hit one second.

9
Select the appropriate option for each requirement. (Three dropdowns.)

Dropdown 1 — to satisfy T5 (compare each reading to the immediately preceding reading for the same meter):

  • A. LAG(ReadingValue) OVER (PARTITION BY MeterID ORDER BY ReadingDate)  ·  B. LEAD(...)  ·  C. FIRST_VALUE(...)  ·  D. MIN(...) OVER ()

Dropdown 2 — to satisfy T6 (totals by region, by region and tariff, and a grand total, in one query):

  • A. GROUP BY CUBE (RegionID, TariffID)  ·  B. GROUP BY ROLLUP (RegionID, TariffID)  ·  C. GROUP BY RegionID, TariffID  ·  D. NTILE(3) OVER (...)

Dropdown 3 — to satisfy the first half of T7 (validate before merging to main):

  • A. dotnet build  ·  B. dotnet publish  ·  C. sqlpackage /Action:Import  ·  D. sqlpackage /Action:Export
Show answer & debrief
Answer: 1 = A (LAG) · 2 = B (ROLLUP) · 3 = A (dotnet build)

T5T6T7 — each dropdown scores separately, so partial credit is available. Never leave one on its default.

1. "Immediately preceding" → LAG. LEAD looks forward, which inverts the sign and shifts every row by one period.

2. Three levels requested: (Region, Tariff), (Region), and (). That's exactly ROLLUP — n+1 = 3 groupings, dropping columns from the right. CUBE would give 2ⁿ = 4, adding an unrequested "by tariff across all regions" level. C gives only the detail level with no subtotals.

3. Validation happens before the merge, against no database — dotnet build compiles the project into a dacpac and surfaces unresolved references offline. Publishing the dacpac (SqlPackage /Action:Publish) is the deployment half of T7, which happens after.

Why CUBE is the sharpest distractor here It's not wrong in any obvious way — it produces everything ROLLUP does and more. But the requirement listed exactly three levels, and returning a fourth uninvited grouping means the report doesn't match its specification. "More than asked for" is still not "what was asked for."

Debrief Score the kind of mistake, not just the count.

If you missed…BecauseThe habit to fix it
Q2The deciding fact was in Existing environment, not RequirementsRead the environment tab for what recently changed — that's where faults are planted
Q5You reached for deterministic out of habit"Never used in WHERE/JOIN/GROUP BY" removes the query requirement → take the stronger type
Q7You answered the first verb onlyUnderline every "and" and "or" in a requirement — B2 had three obligations
Q8You missed the "no server-side code" lineConstraints hide under Applications, not just under Requirements
Q9 part 2CUBE looked safe because it includes everythingCount the levels the requirement lists. ROLLUP = n+1, CUBE = 2ⁿ
Q3You picked the LIKE versionBoth work; when regex is available it's the intended answer
The pattern to notice across both case studies In Lakeside Outdoors, every question was decided by information the question itself didn't mention. Same here — Q2 by a sentence about a developer, Q5 by a sentence about how the column is used, Q8 by a sentence under Applications. That is the format, not a quirk of my writing.

Which is why the three minutes tagging requirements at the start is the highest-return three minutes of the exam — and why 22 minutes is the right budget, not 12.
🔑 Method, once more: requirements tabs first · note the platform · tag everything B1…Bn / T1…Tn · for each question find which requirements are in play (usually one isn't mentioned) · check every option against all of them · finish the block before you leave it.