Skill 2 · 35–40%

Secure, optimize, and deploy database solutions

The other 35–40% area, and the widest. It joins four jobs a database developer owns end to end: protect the data, keep queries fast and correct under concurrency, ship schema changes through a pipeline, and connect the database to Azure — Data API builder, Azure Monitor and the right change-handling mechanism.

Skills measured as of March 12, 2026

What this skill tests

Four objective groups: data security and compliance, database performance, CI/CD with SQL Database Projects and integration with Azure services. Most items are "which feature meets this requirement" — and the distractors are features that sound right but protect a different thing (masking is not encryption; Change Tracking does not keep old values).

Data security and compliance

flowchart TB
  CALLER["Caller<br/>app · user · agent"] --> NET["Network<br/>firewall · private endpoint"]
  NET --> AUTHN["Authentication<br/>Entra ID · managed identity · passwordless"]
  AUTHN --> AUTHZ["Authorization<br/>roles · GRANT / DENY · RLS"]
  AUTHZ --> DATA["Data protection<br/>Always Encrypted · DDM · column encryption · TDE"]
  DATA --> AUDIT["Auditing<br/>who did what, when"]
Security in depth. Each layer answers a different question; an exam scenario usually names the layer through its requirement — 'administrators must not see', 'tenants must only see their rows', 'no passwords in the app'.

Encryption: Always Encrypted, column-level and TDE

Always EncryptedColumn-level encryptionTransparent data encryption (TDE)
Where encryption happensIn the client driverIn the engine, by your T-SQLIn the engine, at the storage layer
Who can see plaintextOnly clients holding the column master key — not DBAs or the cloud operatorAnyone who can open the keyAnyone who can query the table
Protects againstPrivileged insiders, compromised serversCasual access to selected columnsStolen files and backups (data at rest)
Application changesConnection string setting, parameterized queriesQueries must call ENCRYPTBYKEY/DECRYPTBYKEYNone
PlatformsSQL Server, Azure SQL DB, MI — not FabricAll fourOn by default in Azure SQL; Fabric encrypts storage with service-managed or workspace keys
T-SQL
-- Always Encrypted: the driver encrypts on the client; the engine never sees plaintext.
-- Keys: a column master key (in Azure Key Vault) protects column encryption keys.
CREATE TABLE dbo.Patient
(
    PatientID  int IDENTITY PRIMARY KEY,
    NationalId char(11) COLLATE Latin1_General_BIN2
        ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = CEK_Patient,
                        ENCRYPTION_TYPE = DETERMINISTIC,          -- equality lookups, joins, grouping
                        ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL,
    Diagnosis  nvarchar(400)
        ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = CEK_Patient,
                        ENCRYPTION_TYPE = RANDOMIZED,             -- stronger; no operations without an enclave
                        ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL
);
-- Client connection string: Column Encryption Setting=Enabled
-- Queries must be parameterized so the driver can encrypt the parameter values.
  • Deterministic encryption allows equality, joins, GROUP BY and indexes, but leaks repeating values; needs a BIN2 collation on strings.
  • Randomized is stronger but allows no operations — unless you use Always Encrypted with secure enclaves, which lets the server evaluate comparisons, LIKE and range queries inside a protected enclave.
  • Keys: the column master key (CMK) lives outside the database, typically in Azure Key Vault; it encrypts the column encryption keys (CEKs) stored in the database.
T-SQL
-- Column-level (cell-level) encryption: the engine encrypts with a symmetric key.
CREATE MASTER KEY ENCRYPTION BY PASSWORD = N'<strong password>';
CREATE CERTIFICATE CertPayroll WITH SUBJECT = 'Payroll column key protector';
CREATE SYMMETRIC KEY SymPayroll WITH ALGORITHM = AES_256 ENCRYPTION BY CERTIFICATE CertPayroll;

OPEN SYMMETRIC KEY SymPayroll DECRYPTION BY CERTIFICATE CertPayroll;
UPDATE dbo.Employee SET SalaryEnc = ENCRYPTBYKEY(KEY_GUID('SymPayroll'), CAST(Salary AS varchar(20)));
SELECT EmployeeID, CAST(DECRYPTBYKEY(SalaryEnc) AS varchar(20)) AS Salary FROM dbo.Employee;
CLOSE SYMMETRIC KEY SymPayroll;

Dynamic data masking

Masking changes what query results show to users without the UNMASK permission; the stored data is unchanged. It reduces exposure in applications and support tools but is not a security boundary — a user with ad hoc query access can infer values with predicates such as WHERE CreditLimit > 10000.

T-SQL
-- Dynamic data masking: obfuscates results for users without UNMASK. Data is unchanged.
ALTER TABLE dbo.Customer ALTER COLUMN Email
    ADD MASKED WITH (FUNCTION = 'email()');                -- aXXX@XXXX.com
ALTER TABLE dbo.Customer ALTER COLUMN Phone
    ADD MASKED WITH (FUNCTION = 'partial(0, "XXX-XXX-", 4)');
ALTER TABLE dbo.Customer ALTER COLUMN CreditLimit
    ADD MASKED WITH (FUNCTION = 'default()');              -- 0 for numbers, XXXX for strings
ALTER TABLE dbo.Customer ALTER COLUMN BirthDate
    ADD MASKED WITH (FUNCTION = 'datetime("Y")');          -- masks the year

-- Granular UNMASK (SQL Server 2022+, Azure SQL, Fabric): database, schema, table or column
GRANT UNMASK ON dbo.Customer(Email) TO SupportAgent;
Applies to SQL Server 2025 Azure SQL Database Azure SQL MI SQL database in Fabric Granular UNMASK at schema, table or column level needs SQL Server 2022 or later, Azure SQL or Fabric.

Row-level security

RLS filters rows by the caller's context — a user name, a role, or a value the app puts in SESSION_CONTEXT. A filter predicate silently hides rows from SELECT, UPDATE and DELETE; a block predicate stops writes that would violate the rule (AFTER INSERT, AFTER UPDATE, BEFORE UPDATE, BEFORE DELETE).

T-SQL
-- Row-level security: an inline TVF predicate bound to a security policy
CREATE SCHEMA Security;
GO
CREATE FUNCTION Security.fn_TenantPredicate (@TenantID int)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS allowed
       WHERE @TenantID = CAST(SESSION_CONTEXT(N'TenantID') AS int)
          OR IS_MEMBER(N'db_owner') = 1;
GO
CREATE SECURITY POLICY Security.TenantPolicy
    ADD FILTER PREDICATE Security.fn_TenantPredicate(TenantID) ON dbo.Orders,               -- hides rows from reads
    ADD BLOCK  PREDICATE Security.fn_TenantPredicate(TenantID) ON dbo.Orders AFTER INSERT   -- stops writing other tenants' rows
WITH (STATE = ON);
GO
-- The application sets the tenant once per connection
EXECUTE sys.sp_set_session_context @key = N'TenantID', @value = 42, @read_only = 1;

RLS traps

  • The predicate is an inline table-valued function created WITH SCHEMABINDING.
  • Without a block predicate, a user can insert rows they can then no longer see.
  • With a shared connection pool, set SESSION_CONTEXT with @read_only = 1 on every connection open, or tenants can impersonate each other.
  • Index the predicate column — the filter is added to every query.

Object-level permissions

Grant to roles, at the schema level where you can, and prefer EXECUTE on procedures over table permissions. DENY overrides any GRANT; REVOKE removes a grant or a deny. Ownership chaining lets a procedure read tables its caller has no rights on when both have the same owner, and EXECUTE AS runs a module under another principal (not available in Fabric).

T-SQL
-- Least privilege through roles and schemas, not individual users and objects
CREATE ROLE app_reader;
GRANT SELECT ON SCHEMA::Sales TO app_reader;                -- every current and future object in the schema
CREATE ROLE app_writer;
GRANT EXECUTE ON SCHEMA::Api TO app_writer;                 -- procedures only; no direct table access
DENY  SELECT ON dbo.Salary TO app_reader;                   -- DENY wins over any GRANT

-- Passwordless: a contained user for an app's managed identity (Entra ID)
CREATE USER [contoso-orders-api] FROM EXTERNAL PROVIDER;
ALTER ROLE app_writer ADD MEMBER [contoso-orders-api];

Secure, passwordless database access

  • Microsoft Entra authentication for people and workloads; Entra-only authentication turns SQL authentication off at the server or instance (and is the only option in Fabric).
  • Managed identities for Azure-hosted apps (App Service, Container Apps, Functions): create a contained user FROM EXTERNAL PROVIDER and connect with Authentication=Active Directory Managed Identity or Active Directory Default — no secret in the connection string or in Key Vault.
  • Network: server firewall rules, private endpoints, and "deny public network access" when only private traffic should reach the database.

Auditing

PlatformFeatureDestinations
SQL Server 2025SQL Server Audit — server audit plus server or database audit specificationsFile, Windows Application or Security log
Azure SQL MISQL Server AuditAzure Blob storage, Log Analytics, Event Hubs
Azure SQL DatabaseAzure SQL auditing, at server level (all databases) or database levelAzure Storage, Log Analytics, Event Hubs
SQL database in FabricSQL auditing, configured in Security → Manage SQL auditingThe item's read-only OneLake folder; read with sys.fn_get_audit_file_v2
T-SQL · SQL Server / MI
-- SQL Server / Azure SQL MI: server audit + database audit specification
CREATE SERVER AUDIT AuditToFile TO FILE (FILEPATH = N'D:\Audit\');
ALTER SERVER AUDIT AuditToFile WITH (STATE = ON);

CREATE DATABASE AUDIT SPECIFICATION PatientAccess
FOR SERVER AUDIT AuditToFile
    ADD (SELECT, UPDATE ON dbo.Patient BY public),
    ADD (DATABASE_ROLE_MEMBER_CHANGE_GROUP)
WITH (STATE = ON);

-- Read the logs (SQL Server, MI, Azure SQL Database and Fabric all expose this function family)
SELECT event_time, action_id, server_principal_name, statement
FROM sys.fn_get_audit_file(N'D:\Audit\*.sqlaudit', DEFAULT, DEFAULT);

Secure model endpoints with managed identity

An external model call leaves the database over HTTPS, so its credential matters. Prefer a database scoped credential with IDENTITY = 'Managed Identity' over an API key in HTTPEndpointHeaders; grant the database's identity an RBAC role on the model resource; and grant EXECUTE on the external model only to the roles that need it. The credential name must be the endpoint URL (or a prefix of it).

T-SQL
-- Secure a model endpoint with the database's managed identity (no API key in the database).
-- Grant the identity the "Cognitive Services OpenAI User" role on the Azure OpenAI / Foundry resource first.
-- Azure SQL Database and Fabric: the logical server's / item's identity.
-- SQL Server 2025: connect the server to Azure Arc and enable its primary managed identity, then
--   EXECUTE sp_configure 'allow server scoped db credentials', 1; RECONFIGURE;
CREATE DATABASE SCOPED CREDENTIAL [https://contoso-ai.openai.azure.com/]
    WITH IDENTITY = 'Managed Identity',
         SECRET   = '{"resourceid":"https://cognitiveservices.azure.com"}';

CREATE EXTERNAL MODEL EmbeddingModel
WITH (
    LOCATION   = 'https://contoso-ai.openai.azure.com/openai/deployments/text-embedding-3-small/embeddings?api-version=2024-02-01',
    API_FORMAT = 'Azure OpenAI',
    MODEL_TYPE = EMBEDDINGS,
    MODEL      = 'text-embedding-3-small',
    CREDENTIAL = [https://contoso-ai.openai.azure.com/]
);
GRANT EXECUTE ON EXTERNAL MODEL::EmbeddingModel TO app_writer;
Applies to SQL Server 2025 Azure SQL Database Azure SQL MI SQL database in Fabric Managed identity on SQL Server 2025 requires Azure Arc. CREATE EXTERNAL MODEL on MI needs the Always-up-to-date update policy.

Secure GraphQL, REST and MCP endpoints

Data API builder serves all three surfaces from one configuration, so the same controls protect all three:

Authentication provider (runtime.host.authentication.provider)
EntraId (formerly AzureAd) or Custom validate JWT bearer tokens; AppService trusts EasyAuth headers; Unauthenticated (the default in DAB 2.x) treats every call as anonymous; Simulator works only in development mode.
Roles and permissions
No role has permissions by default. Each entity lists roles (anonymous, authenticated or a custom role chosen with the X-MS-API-ROLE header) and their actions — create, read, update, delete, execute — with field-level include/exclude.
Database policies
Row filters such as @item.OwnerId eq @claims.oid appended to the query for a role and action.
Identity to the database
DAB connects with a managed identity; with on-behalf-of (OBO) it passes the user's identity so RLS and auditing see the real user.
MCP specifically
Disable DML tools you do not need (runtime.mcp.dml-tools.delete-record: false) and opt sensitive entities out with "mcp": false.

Samples for each are in the Configuration reference.

Optimize database performance

Recommend database configurations

SettingRecommendationWhy
Compatibility levelLatest (170) after testingUnlocks new functions and intelligent query processing; use Query Store to catch regressions
Query StoreOn, READ_WRITEHistory of plans and runtime stats; needed for plan forcing and hints
READ_COMMITTED_SNAPSHOTOn for OLTPReaders stop blocking writers (default in Azure SQL Database)
Accelerated database recovery + optimized lockingOnFast rollback; fewer locks held, less blocking and no lock escalation (on by default in Azure SQL Database)
MAXDOP (database scoped)8 or fewer, by workloadCaps parallelism per query
Automatic tuningFORCE_LAST_GOOD_PLAN onReverts plan-choice regressions automatically (Azure SQL; SQL Server via Query Store)
StatisticsAuto create and auto update on, async update for latency-sensitive OLTPFresh cardinality estimates
Azure SQL service tierServerless for intermittent use, Hyperscale for large or fast-growing data, Business Critical for low-latency I/O and In-Memory OLTPCapacity matches the workload

Isolation levels and concurrency controls

Isolation levelDirty readsNon-repeatable readsPhantomsHow it works
READ UNCOMMITTEDYesYesYesNo shared locks (NOLOCK)
READ COMMITTED (locking)NoYesYesShort shared locks; readers wait for writers
READ COMMITTED with RCSINoYesYesStatement-level row versions; readers don't block
REPEATABLE READNoNoYesShared locks held to the end of the transaction
SNAPSHOTNoNoNoTransaction-level row versions; update conflicts raise error 3960
SERIALIZABLENoNoNoKey-range locks (HOLDLOCK); most blocking
T-SQL
-- Reader sees the last committed version instead of waiting for writers
ALTER DATABASE CURRENT SET READ_COMMITTED_SNAPSHOT ON;      -- on by default in Azure SQL Database

-- Transaction-level SNAPSHOT: one consistent view for the whole transaction
ALTER DATABASE CURRENT SET ALLOW_SNAPSHOT_ISOLATION ON;
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
    SELECT SUM(Balance) FROM dbo.Account;                   -- stable for the whole transaction
COMMIT;

-- Pessimistic read-then-update without a lost update: lock the row you read
BEGIN TRANSACTION;
    SELECT Quantity FROM dbo.Stock WITH (UPDLOCK, HOLDLOCK) WHERE ProductID = 42;
    UPDATE dbo.Stock SET Quantity -= 1 WHERE ProductID = 42;
COMMIT;

-- Optimistic concurrency with rowversion
UPDATE dbo.Product SET ListPrice = @NewPrice
WHERE ProductID = @ID AND RowVer = @RowVerReadEarlier;
IF @@ROWCOUNT = 0 THROW 50001, 'The row was changed by someone else.', 1;

Optimized locking

Optimized locking (requires accelerated database recovery) replaces row and page locks held to the end of a transaction with a single transaction ID lock, and with RCSI adds lock after qualification — rows are evaluated against the latest committed version before any lock is taken. The result is less blocking, lower lock memory and no lock escalation.

Applies to SQL Server 2025 Azure SQL Database Azure SQL MI SQL database in Fabric Enabled by default in Azure SQL Database. MI needs the SQL Server 2025 or Always-up-to-date update policy; on SQL Server 2025 you turn it on per database.

Evaluate query performance

  • Execution plans: the estimated plan shows the optimizer's choice; the actual plan adds row counts, warnings and spills. Look for estimate-versus-actual gaps, key lookups, scans where seeks were expected, implicit conversions (a CONVERT_IMPLICIT on a column defeats an index), and tempdb spills.
  • Statement statistics: SET STATISTICS IO, TIME ON for logical reads and CPU; Live Query Statistics for a running query.
  • DMVs for what is happening now and since the last restart: sys.dm_exec_requests, sys.dm_exec_sessions, sys.dm_exec_query_stats, sys.dm_os_wait_stats (or sys.dm_db_wait_stats in Azure SQL Database), sys.dm_tran_locks, sys.dm_db_index_usage_stats and the missing-index DMVs.
  • Query Store for history that survives restarts: regressed queries, top resource consumers, plan forcing and Query Store hints.
  • Query Performance Insight — Azure SQL Database only — is a portal view over Query Store: top queries by CPU, duration and execution count. In Fabric the equivalent is the Performance Dashboard.
T-SQL
-- What is running now, and who is blocking whom?
SELECT r.session_id, r.blocking_session_id, r.wait_type, r.wait_time, r.cpu_time,
       t.text AS sql_text
FROM sys.dm_exec_requests AS r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS t
WHERE r.session_id <> @@SPID;

-- Most expensive cached statements by CPU
SELECT TOP (10) qs.total_worker_time / qs.execution_count AS avg_cpu, qs.execution_count,
       SUBSTRING(t.text, qs.statement_start_offset / 2 + 1, 200) AS stmt, qp.query_plan
FROM sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS t
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) AS qp
ORDER BY avg_cpu DESC;

-- Azure SQL Database: resource use in 15-second intervals for the last hour
SELECT end_time, avg_cpu_percent, avg_data_io_percent, avg_log_write_percent
FROM sys.dm_db_resource_stats ORDER BY end_time DESC;
T-SQL
-- Query Store: on by default in Azure SQL Database and new SQL Server 2022+ databases
ALTER DATABASE CURRENT SET QUERY_STORE = ON (OPERATION_MODE = READ_WRITE);

-- Find a regressed query and its plans
SELECT q.query_id, p.plan_id, rs.avg_duration, rs.last_execution_time
FROM sys.query_store_query AS q
JOIN sys.query_store_plan AS p           ON p.query_id = q.query_id
JOIN sys.query_store_runtime_stats AS rs ON rs.plan_id = p.plan_id
WHERE q.query_id = 1234
ORDER BY rs.avg_duration;

-- Fix it without touching the application
EXECUTE sp_query_store_force_plan @query_id = 1234, @plan_id = 17;                     -- pin the good plan
EXECUTE sp_query_store_set_hints  @query_id = 1234, @query_hints = N'OPTION (RECOMPILE)'; -- or add a hint

Blocking and deadlocks

Blocking is one session waiting for a lock another holds — find the head blocker through blocking_session_id chains. A deadlock is a cycle; the engine kills the cheapest victim with error 1205. Capture deadlock graphs from the system_health Extended Events session (SQL Server, MI), a database-scoped XE session or the Deadlocks diagnostic setting (Azure SQL Database).

  1. Keep transactions short and never wait for user input inside one.
  2. Access objects in the same order in every code path.
  3. Index to turn scans into seeks, so fewer rows are locked.
  4. Turn on RCSI (and optimized locking) so readers stop taking shared locks.
  5. Retry on 1205 in the application, and use SET DEADLOCK_PRIORITY or SET LOCK_TIMEOUT where one side should always yield.

CI/CD with SQL Database Projects

A SQL database project is the declarative source of truth: one CREATE script per object, built into a .dacpac model. Deployment compares the model with the target and generates the ALTER script — you never hand-write migrations for schema.

flowchart TB
  PR["Pull request<br/>feature branch → main"] --> BUILD["Build job<br/>dotnet build · code analysis"]
  BUILD --> TEST["Test job<br/>SQL Server container · unit and integration tests"]
  TEST --> ART["Artifact<br/>.dacpac"]
  ART --> DEVENV["Deploy to test<br/>environment: test"]
  DEVENV --> REPORT["DeployReport and script<br/>review the plan"]
  REPORT --> GATE["Environment gate<br/>required reviewers · branch rule"]
  GATE --> PROD["Publish to production<br/>azure/sql-action · OIDC login"]
A typical GitHub Actions flow: validate and test on every pull request, deploy the same .dacpac artifact to each environment, and gate production with an environment that requires reviewers.

Create, build and validate SDK-style projects

.sqlproj
<!-- An SDK-style SQL project: every .sql file under the folder is included by default -->
<Project DefaultTargets="Build">
  <Sdk Name="Microsoft.Build.Sql" Version="2.3.0" />
  <PropertyGroup>
    <Name>Contoso.Database</Name>
    <DSP>Microsoft.Data.Tools.Schema.Sql.SqlAzureV12DatabaseSchemaProvider</DSP>
    <RunSqlCodeAnalysis>True</RunSqlCodeAnalysis>
  </PropertyGroup>
  <ItemGroup>
    <PostDeploy Include="Scripts\PostDeployment.sql" />
  </ItemGroup>
</Project>
  • Microsoft.Build.Sql (2.3.0 at the time of writing) is the SDK-style format; it builds cross-platform with dotnet build and uses NuGet package references for database references.
  • The target platform is the DSP property: Sql170DatabaseSchemaProvider (SQL Server 2025), SqlAzureV12DatabaseSchemaProvider (Azure SQL Database), SqlDbFabricDatabaseSchemaProvider (SQL database in Fabric, preview). Publishing to a different platform fails unless you pass /p:AllowIncompatiblePlatform=true.
  • Build validates every reference and syntax error; RunSqlCodeAnalysis adds static analysis rules.
  • VS Code (SQL Database Projects in the MSSQL extension) supports SDK-style projects GA; Visual Studio 2022 has them in preview, and Visual Studio 2026 only supports the original format.
  • SQL database in Fabric's built-in Git integration stores the database as an SDK-style project, one file per object under schema/ObjectType/.

Testing strategy

Test levelWhat it provesHow
Build and static analysisThe model compiles; no broken references or rule violationsdotnet build with code analysis, on every PR
Unit testsOne procedure, function or trigger behaves correctly in isolationtSQLt: fake tables, act, assert, inside a rolled-back transaction
Integration testsThe deployed schema works with the app and real data shapesPublish the .dacpac to an ephemeral database (a SQL Server container service in the workflow), then run the app's tests
Deployment testsThe upgrade path is safe for existing dataDeployReport/Script against a copy of production; BlockOnPossibleDataLoss
T-SQL
-- A tSQLt unit test: arrange with a fake table, act, assert
EXEC tSQLt.NewTestClass 'OrderTests';
GO
CREATE PROCEDURE OrderTests.[test usp_PlaceOrder inserts one header]
AS
BEGIN
    EXEC tSQLt.FakeTable 'dbo.OrderHeader';
    DECLARE @no bigint;
    EXEC dbo.usp_PlaceOrder @CustomerID = 7, @OrderNo = @no OUTPUT;
    DECLARE @rows int = (SELECT COUNT(*) FROM dbo.OrderHeader WHERE CustomerID = 7);
    EXEC tSQLt.AssertEquals @Expected = 1, @Actual = @rows;
END;
GO
EXEC tSQLt.Run 'OrderTests';

Reference and static data in source control

Lookup values (status codes, countries) belong in the project as an idempotent post-deployment script — usually a MERGE from a VALUES list — so every environment converges on the same rows. A pre-deployment script handles data that must move before schema changes (splitting a column, for example). Transactional data never belongs in the project.

T-SQL
-- Scripts/PostDeployment.sql — idempotent reference data, runs after every publish
MERGE dbo.OrderStatus AS target
USING (VALUES (1, N'Placed'), (2, N'Shipped'), (3, N'Delivered'), (4, N'Cancelled'))
      AS source (StatusID, Name)
ON target.StatusID = source.StatusID
WHEN MATCHED AND target.Name <> source.Name THEN UPDATE SET Name = source.Name
WHEN NOT MATCHED BY TARGET THEN INSERT (StatusID, Name) VALUES (source.StatusID, source.Name)
WHEN NOT MATCHED BY SOURCE THEN DELETE;

Source control, branching and pull requests

  • Commit the .sqlproj, the object scripts, pre/post-deployment scripts and publish profiles without secrets; ignore bin/ and obj/.
  • Branch per feature, merge to main through a pull request that must pass the build and tests.
  • Conflicts are rare because each object is its own file; when two branches change the same object, resolve the script, rebuild and re-run tests. Renames go through the project's refactor so a .refactorlog tells deployment to sp_rename instead of drop-and-create.

Secrets management

  • Authenticate the workflow to Azure with OpenID Connect (azure/login with a federated credential) — no client secret stored at all.
  • Keep any remaining values in GitHub secrets, preferably environment secrets so only the gated job can read them; or in Azure Key Vault.
  • Deploy with Entra authentication to the database; never put a password in a publish profile or .sqlproj, and use SQLCMD variables for per-environment values.

Detect schema drift

Drift is a target that no longer matches what the pipeline last deployed — a hotfix applied by hand, an index added in production.

sqlpackage /Action:DriftReport
Reports changes made to a registered database since it was last registered (publish with /p:RegisterDataTierApplication=true).
/p:BlockWhenDriftDetected=true (default)
Blocks a publish to a registered database whose schema no longer matches its registration.
DeployReport and schema compare
Compare the project with any target — the usual drift check in a scheduled workflow for unregistered databases.

Update a project and deploy changes

Change the object scripts (or apply a schema compare from a database into the project), build, then publish. In GitHub Actions azure/sql-action@v2 builds a .sqlproj or takes a .dacpac and runs SqlPackage for you. The full workflow is in the Configuration reference.

bash
# Build the model and validate it
dotnet build Contoso.Database.sqlproj -c Release      # → bin/Release/Contoso.Database.dacpac

# What would a publish change? (XML report and a reviewable script)
sqlpackage /Action:DeployReport /SourceFile:bin/Release/Contoso.Database.dacpac \
  /TargetConnectionString:"$TARGET" /OutputPath:deployreport.xml
sqlpackage /Action:Script /SourceFile:bin/Release/Contoso.Database.dacpac \
  /TargetConnectionString:"$TARGET" /OutputPath:deploy.sql

# Has anyone changed a registered database outside the pipeline?
sqlpackage /Action:DriftReport /TargetConnectionString:"$TARGET" /OutputPath:drift.xml

# Publish, refusing anything that could lose data
sqlpackage /Action:Publish /SourceFile:bin/Release/Contoso.Database.dacpac \
  /TargetConnectionString:"$TARGET" /p:BlockOnPossibleDataLoss=true

Controls for deployment pipelines

ControlWhereWhat it enforces
Branch protection rules or rulesetsRepository settingsChanges reach main only through a PR with required status checks and approvals; no force-push
CODEOWNERS.github/CODEOWNERSThe DBA team must review any change under, say, /database/
Triggerson: in the workflowpull_request to validate, push to main to deploy, paths filters, workflow_dispatch for manual runs
Environments with approvalsSettings → EnvironmentsRequired reviewers, wait timer, deployment branch rules and environment-scoped secrets before a job targeting production runs
Authenticationpermissions: id-token: write + azure/loginShort-lived OIDC tokens scoped to the environment

An odd phrase in the study guide

The objective lists "branching policies, triggers in approvals, authentication tables, and code owners". There is no GitHub feature called an authentication table; read it as the authentication used by the pipeline (OIDC federated credentials, environment secrets) and you will match what the course teaches.

Integrate SQL solutions with Azure services

Data API builder

Data API builder (DAB) is an open-source, configuration-driven engine that exposes tables, views and stored procedures as REST (/api), GraphQL (/graphql) and MCP (/mcp) endpoints — no API code. Everything lives in dab-config.json, created and edited with the dab CLI.

ObjectiveWhat to configure
Create the configuration filedab init --database-type mssql --connection-string "@env('SQL_CONN')"; never a literal connection string
Entities for REST and GraphQLdab add Product --source dbo.Product --permissions "anonymous:read"; rest.path, graphql.type singular/plural, mappings to rename columns
Cachingruntime.cache plus per-entity cache.enabled and ttl-seconds for read results
Pagination$first/$after (REST) and first/after (GraphQL); runtime.pagination.default-page-size (100) and max-page-size (100,000); -1 asks for the maximum
Searching and filteringREST $filter (eq, ne, gt, ge, lt, le, and, or, not), $orderby, $select; GraphQL filter with operators such as contains, startsWith, isNull
Stored procedures and viewsViews need key-fields; procedures set rest.methods and graphql.operation (query or mutation)
GraphQL relationshipsrelationships with cardinality one/many, target.entity, source/target fields, and a linking.object for many-to-many

Deploy Data API builder

  • Azure Container Apps (the common choice) or App Service, running the DAB container image with the config file mounted or baked in.
  • Set runtime.host.mode to production (no Simulator, no GraphQL introspection UI by default), configure CORS and the authentication provider.
  • Give the container a managed identity and connect to SQL with it; read any remaining values from environment variables or Key Vault references.
  • Turn on Application Insights or OpenTelemetry in runtime.telemetry.

Azure Monitor configurations

NeedRecommend
Database metrics and alerts (CPU, DTU/vCore %, storage, deadlocks, availability)Azure Monitor metrics with metric alert rules and action groups
Query and wait diagnostics retained and queryable with KQLDiagnostic settings sending SQLInsights, QueryStoreRuntimeStatistics, QueryStoreWaitStatistics, Errors, Timeouts, Blocks and Deadlocks to a Log Analytics workspace
Application-side telemetry (API latency, failures, dependency calls to SQL)Application Insights on the app or on Data API builder
Fleet-wide, near-real-time SQL monitoringDatabase watcher (Azure SQL Database and MI), storing data in Azure Data Explorer or a Fabric Eventhouse
Long-term or compliance archiveDiagnostic settings to a Storage account; streaming to Event Hubs for a SIEM

Handle changes

flowchart TB
  Q["Something must react to row changes"] --> A{"Push each change as an<br/>event to a stream?"}
  A -->|"yes"| CES["Change event streaming<br/>Event Hubs · Fabric Eventstream"]
  A -->|"no"| B{"Need the old and new<br/>values of every change?"}
  B -->|"yes"| CDC["Change data capture<br/>change tables · LSN ranges"]
  B -->|"no, just which rows"| C{"Run code when<br/>rows change?"}
  C -->|"serverless function"| FN["Azure Functions<br/>SQL trigger binding"]
  C -->|"low-code workflow"| LA["Azure Logic Apps<br/>SQL connector trigger"]
  C -->|"sync on my own schedule"| CT["Change Tracking<br/>CHANGETABLE · versions"]
Pick a mechanism from what the consumer needs: a pushed event, the before-and-after values, or simply which rows changed and some code to run.
MechanismCapturesDeliveryPlatform line
Change event streaming (CES)Each row change with old and new values, as a CloudEvent (JSON or Avro)Pushed in near real time to Azure Event Hubs or Fabric Eventstream (Kafka protocol; AMQP deprecated from 15 August 2026)Preview on all four
Change data capture (CDC)Every change, before and after images, in change tablesPulled by LSN range (cdc.fn_cdc_get_all_changes_…); capture and cleanup jobsSQL Server, MI, Azure SQL DB S3+; not Fabric
Change TrackingWhich rows changed (key and operation), not old valuesPulled with CHANGETABLE(CHANGES …) since a saved versionAll
Azure Functions SQL trigger bindingRow changes detected through Change TrackingFunction invoked with batches of changed rows; leases kept in an az_func schemaNeeds Change Tracking on the database and table
Azure Logic Apps SQL connectorNew or modified rows, by pollingWorkflow trigger "When an item is created" or "modified"Needs an identity column (created) or a rowversion column (modified)
Table triggerSynchronous, inside the transactionRuns T-SQL immediatelyAll — but adds latency to every write
T-SQL
-- Change Tracking: which rows changed since version N (no old values)
ALTER DATABASE CURRENT SET CHANGE_TRACKING = ON (CHANGE_RETENTION = 2 DAYS, AUTO_CLEANUP = ON);
ALTER TABLE dbo.Product ENABLE CHANGE_TRACKING WITH (TRACK_COLUMNS_UPDATED = ON);

DECLARE @last bigint = 1520;                                  -- stored by the consumer after its last sync
SELECT ct.ProductID, ct.SYS_CHANGE_OPERATION, ct.SYS_CHANGE_VERSION, p.Name, p.ListPrice
FROM CHANGETABLE(CHANGES dbo.Product, @last) AS ct
LEFT JOIN dbo.Product AS p ON p.ProductID = ct.ProductID;     -- current values; deleted rows return NULL
SELECT CHANGE_TRACKING_CURRENT_VERSION();                     -- save as the next @last

-- Change data capture: every change with before and after images
EXEC sys.sp_cdc_enable_db;
EXEC sys.sp_cdc_enable_table @source_schema = N'dbo', @source_name = N'Product', @role_name = NULL;
SELECT * FROM cdc.fn_cdc_get_all_changes_dbo_Product(
    sys.fn_cdc_get_min_lsn('dbo_Product'), sys.fn_cdc_get_max_lsn(), N'all update old');
T-SQL · CES preview
-- Change event streaming (preview): publish row changes as CloudEvents
-- SQL Server 2025 needs PREVIEW_FEATURES and the FULL recovery model; Azure SQL is always FULL.
EXEC sys.sp_enable_event_stream;
EXEC sys.sp_create_event_stream_group
     @stream_group_name      = N'ordersStream',
     @destination_type       = N'AzureEventHubsApacheKafka',   -- SQL Server 2025 / MI; 'AzureEventHubs' on Azure SQL Database and Fabric
     @destination_location   = N'contoso-ns.servicebus.windows.net:9093/orders',
     @destination_credential = CesEventHubsCredential,          -- a database scoped credential
     @partition_key_scheme   = N'Table',
     @encoding               = N'JSON';
EXEC sys.sp_add_object_to_event_stream_group
     @stream_group_name = N'ordersStream', @object_name = N'dbo.Orders';
SELECT * FROM sys.dm_change_feed_errors;   -- delivery errors

The classic distractor

"Downstream needs the previous value of the price" rules out Change Tracking and the Functions SQL trigger (both report that a row changed and give you its current values). Choose CDC to pull history, or CES to push each change as an event.