SQL Engineering
Quote
“A database is only as good as the integrity constraints that protect it.”
— C.J. Date, An Introduction to Database Systems (2003)
Summary
SQL Engineering is the second notebook in this SQL Server query series for data engineering: it shifts from ad-hoc querying into reusable database objects, physical design choices, and operational pipeline patterns, all demonstrated in a disposable
demoschema with explicit cleanup.Reusable database objects
- covers views, stored procedures, inline table-valued functions,
TRY/CATCHerror handling, dynamic SQL boundaries, and the trade-offs between reusable query surfacesPerformance and physical design
- covers index strategy, covering indexes, execution plan reading, parameter sniffing, partitioning, and why scalar UDFs and non-SARGable access patterns degrade throughput
Pipeline state and intermediate data
- covers SCD Type 1 vs Type 2 dimensions,
LAG-based gap detection,ROW_NUMBER()deduplication, temp-table materialization, and audit-column lineage patternsLoad and concurrency operations
- covers transaction isolation levels, reader-writer blocking behavior, bulk loading patterns, and safe object-lifecycle cleanup for notebook reruns
Operations and safety
- Warnings: object-creating sections, lab-only credentials, parameter sniffing, scalar UDF row-by-row execution,
MERGEconcurrency bugs,NOLOCK/READ UNCOMMITTED, Type 1 history loss, repeated CTE execution, and over-indexing- Recommendations table: 7 defaults covering object selection, guarded
TRY/CATCH, sniffing mitigation, staging-load flow, RCSI for analytics, demo schema isolation, and audit columns- Troubleshooting: 6 failure modes covering unstable stored procedure performance, slow views,
MERGEduplicate-key races, unindexed#temptables, scalar-UDF timeouts, and missing partition elimination
Glossary
View
A named query stored in the database that exposes a virtual table-shaped interface without persisting separate data by default.
It matters because views are the lightest reusable abstraction in this note for sharing query logic across dashboards, notebooks, and downstream SQL objects.
Views are not caches
A regular view reruns its underlying query whenever it is referenced. Only indexed views persist results, and they carry strict design rules plus write-time maintenance cost.
Stored procedure
A named T-SQL program stored in the database, usually parameterized and capable of control flow, transactions, and error handling.
It matters because the note uses stored procedures for multi-step pipeline behavior that needs encapsulation, plan reuse, and a stable execution surface.
Cached plans can mislead
The first parameter values seen by a procedure can shape its cached plan. That makes stored procedures operationally convenient but performance-sensitive when input sizes vary wildly.
Inline table-valued function
A function that returns a table from a single
SELECTexpression and can usually be inlined by the optimizer into the calling query.It matters because iTVFs give the note a parameterized, reusable alternative to views without the row-by-row penalty of scalar functions.
Parameterized view mental model
An iTVF behaves much closer to a reusable query template than to a procedural routine. That is why it often optimizes well and stays composable in larger statements.
Execution plan
The physical operator tree SQL Server chooses to execute a statement, including scans, seeks, joins, sorts, memory grants, and row estimates.
It matters because plan reading is the note’s main diagnostic lens for explaining why one version of a query is fast and another is not.
Estimated is not actual
Estimated plans show what the optimizer predicted. Real troubleshooting often depends on the actual plan and runtime counters such as
STATISTICS IOandSTATISTICS TIME.
SCD Type 1 / Type 2
Two slowly changing dimension strategies: Type 1 overwrites prior values, while Type 2 closes the old row and inserts a new version with validity metadata.
It matters because the note shows how dimensional corrections change analytical history depending on whether the pipeline preserves or destroys prior states.
Type 1 rewrites history
If an attribute affects calculations, a Type 1 update can silently change historical outputs. Type 2 exists precisely to avoid that loss of analytical truth.
MERGE
A T-SQL statement that combines match detection and data modification so one command can insert, update, or delete against a target table from a source dataset.
It matters because the note positions
MERGEas a compact upsert pattern for incremental pipeline loads.Concurrency needs locking
SQL Server
MERGEhas known race and correctness issues under concurrent access. If it is used at all, the target should be protected withWITH (HOLDLOCK)and tested carefully.
Transaction isolation level
The rule set that governs how one transaction can see data modified by other concurrent transactions.
It matters because the note compares isolation levels to decide when analytics should block writers, read row versions, or avoid unsafe dirty-read shortcuts.
NOLOCKis not harmless
READ UNCOMMITTEDcan read rows twice, miss rows, or return rolled-back data. It is a correctness trade-off, not a free speed boost.
Temp table /
#temp
A session-scoped table stored in
tempdbthat supports indexes, statistics, and reuse across multiple statements in the same session.It matters because the note recommends temp tables when intermediate results must be referenced repeatedly or tuned with their own indexes.
Materialization is sometimes the optimization
Recomputing a complex CTE several times can be more expensive than writing it once to
#temp. Materialization is not just a convenience; it can be the performance fix.
Table variable /
@table
A table-shaped variable scoped to the batch, procedure, or function that stores rows without behaving like a fully statistics-driven temp table.
It matters because the note contrasts table variables with temp tables to show why the simpler syntax often loses on anything but tiny rowsets.
Cardinality guesses stay tiny
SQL Server often optimizes table variables as if they contain about one row. That guess can wreck join choices and memory grants once the real row count grows.
Covering index
An index whose key and included columns satisfy a query without forcing additional lookups to the base table.
It matters because many of the note’s dashboard and pipeline reads become cheaper when the access path already contains the projected and filtered columns.
Read wins become write tax
Covering an important query can help latency dramatically, but every extra index still has to be maintained during data modification. The right answer depends on workload frequency, not on elegance.
Partition elimination
The optimizer’s ability to skip whole physical partitions when a predicate proves they cannot contain qualifying rows.
It matters because partitioning only pays off when queries filter on the partition key in a form the optimizer can actually exploit.
Functions defeat pruning
If the filter wraps the partition column in
YEAR()or another function, SQL Server usually cannot eliminate partitions efficiently. The same anti-pattern also harms ordinary index seeks.
Parameter sniffing
SQL Server’s plan-caching behavior where the first parameter values used during compilation influence the shape of the cached plan reused later.
It matters because stored procedure performance in this note can swing sharply depending on whether the compiled-for inputs resemble typical runtime inputs.
Recompile is not free
OPTION (RECOMPILE)can fix a bad cached plan, but it also forces new optimization work on every execution. Use it deliberately on the statements that actually vary by input shape.
Demo schema
A non-production SQL Server schema used to isolate experimental tables, views, and procedures from the main application objects.
It matters because the note intentionally creates objects during examples and needs those objects to stay safe to rerun and easy to clean up.
Isolation helps idempotence
Putting notebook objects under
demomakes cleanup straightforward and reduces the risk of colliding with real pipeline assets. It is an operational pattern, not just a naming choice.
RCSI /
READ_COMMITTED_SNAPSHOT
A database setting that changes
READ COMMITTEDbehavior to use row versions so readers stop blocking writers and vice versa.It matters because the recommendations section presents RCSI as the lowest-friction way to improve analytical read concurrency across an entire database.
It is a database-level choice
RCSI is not a per-query hint. Enabling it changes read semantics for the database and should be treated as an operational decision that needs environment-level review.
Some Sections CREATE Database Objects
All objects are created in a
demoschema or use temp tables to avoid modifying the production stoxx schema.
Safe Pattern
All persistent objects in this notebook use a dedicated
demoschema (CREATE OR ALTER ... demo.object_name) and are dropped in the Cleanup section at the end. Always use a non-production schema for experimental objects, and include a cleanup block to ensure idempotent re-runs.
Load the jupysql extension and configure display settings for notebook SQL execution.
%load_ext sql
%config SqlMagic.displaycon = False
%config SqlMagic.displaylimit = 0Connect to the local SQL Server stoxx database via ODBC.
%sql mssql+pyodbc://sa:EsgDev2026Pass1@localhost:1434/stoxx?driver=ODBC+Driver+18+for+SQL+Server&TrustServerCertificate=yes&MARS_Connection=yesConnecting to 'mssql+pyodbc://sa:***@localhost:1434/stoxx?MARS_Connection=yes&TrustServerCertificate=yes&driver=ODBC+Driver+18+for+SQL+Server'Lab-Only Credentials
The connection string above contains a plaintext password for a local lab environment. In production, credentials are stored in GCP Secret Manager and fetched at runtime — never hardcoded. See secrets-management > Access from Python.
Safe Pattern
In production, retrieve the connection string from GCP Secret Manager at runtime:
secretmanager.SecretManagerServiceClient().access_secret_version(name=...). Never hardcode passwords in notebooks, scripts, or source control. Use environment variables or secret injection via Cloud Run / GKE secrets.
The demo schema isolates all objects created in this file from the production stoxx schemas. The IF NOT EXISTS guard makes this idempotent — safe to re-run.
Create the demo schema if it does not already exist (idempotent).
IF NOT EXISTS (SELECT 1 FROM sys.schemas WHERE name = 'demo')
EXEC('CREATE SCHEMA demo');Views
SQL Server views encapsulate reusable queries as named database objects. They simplify complex query logic for consumers while centralizing maintenance — when the underlying table structure changes, only the view definition needs updating. SQL Server expands a view inline at query time: the optimizer merges the view definition with the outer query into a single execution plan, so a well-written view carries no extra cost over writing the query directly. Indexed views (created with SCHEMABINDING) pre-compute and persist the result set, trading storage for instant read access on expensive aggregations.
Cross-Engine: Views
SQL Server expands views inline — no performance penalty vs. writing the query directly. Indexed views persist pre-computed results for expensive aggregations. BigQuery supports logical views (inline) and materialized views (with a configurable refresh schedule). Firestore has no view concept — queries always run against raw document collections; reuse is achieved through query abstraction in application code.
Regular Views — Simplify Complex Queries
A view is a saved query. It doesn’t store data — it runs the query every time you SELECT from it. Use case: wrap the “latest price per stock” pattern so downstream queries are simple.
Create a view wrapping ROW_NUMBER deduplication logic
Create a view that returns the most recent OHLCV row per stock using ROW_NUMBER deduplication.
CREATE OR ALTER VIEW demo.v_latest_prices AS
SELECT symbol, date, [open], high, low, [close], volume
FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn
FROM silver.eurostoxx50_ohlcv
) sub
WHERE rn = 1;Once the view is created, the ROW_NUMBER deduplication logic is hidden — consumers write a simple SELECT against the view.
Query the view with a simple SELECT
Query the view — the complex dedup logic is now hidden behind a simple SELECT.
SELECT TOP 10 * FROM demo.v_latest_prices ORDER BY [close] DESC| symbol | date | open | high | low | close | volume |
|---|---|---|---|---|---|---|
| RMS.PA | 2026-03-12 | 1900.0 | 1918.5 | 1894.0 | 1906.0 | 18681 |
| RHM.DE | 2026-03-12 | 1536.0 | 1588.0 | 1535.0 | 1551.5 | 158741 |
| ASML.AS | 2026-03-12 | 1194.8 | 1202.2 | 1187.8 | 1190.8 | 128223 |
| ADYEN.AS | 2026-03-12 | 920.7 | 933.4 | 917.3 | 925.7 | 27887 |
| ARGX.BR | 2026-03-12 | 629.0 | 631.6 | 625.6 | 626.6 | 14083 |
| MUV2.DE | 2026-03-12 | 524.4 | 528.8 | 523.6 | 526.2 | 86783 |
| MC.PA | 2026-03-12 | 495.3 | 497.4 | 491.6 | 494.35 | 171997 |
| OR.PA | 2026-03-12 | 361.1 | 362.3 | 357.8 | 360.8 | 82621 |
| ALV.DE | 2026-03-12 | 349.6 | 351.6 | 347.9 | 348.7 | 182426 |
| SAF.PA | 2026-03-12 | 319.3 | 320.2 | 314.9 | 315.4 | 160065 |
Views — Cross-Layer Dashboard View
Join multiple tables into a single business-friendly view. Dashboards query this instead of raw tables.
Create a cross-layer dashboard view
Create a cross-layer dashboard view joining gold scores with silver dimension metadata.
CREATE OR ALTER VIEW demo.v_stock_dashboard AS
SELECT
s.composite_rank AS [rank],
s.symbol,
d.short_name,
d.sector,
d.country,
s.current_price,
ROUND(s.composite_score, 4) AS composite_score,
ROUND(s.relative_value_score, 3) AS value_score,
ROUND(s.momentum_score, 3) AS momentum_score,
ROUND(s.index_weight * 100, 2) AS weight_pct,
s._index,
s.score_date
FROM gold.scores_daily s
JOIN silver.index_dim d ON s.symbol = d.symbol AND d._index = s._index AND d.is_current = 1;Query the dashboard view for the latest rankings
Query the dashboard view for the latest Euro Stoxx 50 scores ordered by rank.
SELECT TOP 10 * FROM demo.v_stock_dashboard
WHERE _index = 'euro_stoxx_50'
AND score_date = (SELECT MAX(score_date) FROM gold.scores_daily WHERE _index = 'euro_stoxx_50')
ORDER BY [rank]| rank | symbol | short_name | sector | country | current_price | composite_score | value_score | momentum_score | weight_pct | _index | score_date |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | BNP.PA | BNP PARIBAS ACT.A | Financial Services | France | 87.44 | 0.6796 | 1.497 | 0.46 | 1.94 | euro_stoxx_50 | 2026-03-12 |
| 2 | VOW.DE | VOLKSWAGEN AG | Consumer Cyclical | Germany | 92.85 | 0.5756 | 1.028 | -0.382 | 0.93 | euro_stoxx_50 | 2026-03-12 |
| 3 | DTE.DE | DEUTSCHE TELEKOM AG | Communication Services | Germany | 32.55 | 0.487 | 0.226 | 0.706 | 3.13 | euro_stoxx_50 | 2026-03-12 |
| 4 | TTE.PA | TOTALENERGIES | Energy | France | 69.8 | 0.3913 | 0.585 | 1.307 | 2.95 | euro_stoxx_50 | 2026-03-12 |
| 5 | ABI.BR | AB INBEV | Consumer Defensive | Belgium | 62.76 | 0.3852 | 0.251 | 0.537 | 2.43 | euro_stoxx_50 | 2026-03-12 |
| 6 | IFX.DE | INFINEON TECHNOLOGIES AG | Technology | Germany | 40.735 | 0.3487 | 0.084 | 0.302 | 1.06 | euro_stoxx_50 | 2026-03-12 |
| 7 | SAN.MC | BANCO SANTANDER S.A. | Financial Services | Spain | 9.617 | 0.3106 | -0.037 | 0.45 | 2.78 | euro_stoxx_50 | 2026-03-12 |
| 8 | DG.PA | VINCI | Industrials | France | 129.9 | 0.2928 | 0.957 | 0.489 | 1.43 | euro_stoxx_50 | 2026-03-12 |
| 9 | ISP.MI | INTESA SANPAOLO | Financial Services | Italy | 5.204 | 0.2852 | 0.553 | -0.208 | 1.8 | euro_stoxx_50 | 2026-03-12 |
| 10 | BAYN.DE | Bayer AG | Healthcare | Germany | 39.475 | 0.2724 | 0.349 | 0.642 | 0.77 | euro_stoxx_50 | 2026-03-12 |
Stored Procedures
Stored procedures encapsulate reusable T-SQL logic as named database objects with optional input/output parameters. SQL Server compiles and caches the execution plan on first execution — subsequent calls reuse the cached plan, eliminating parse and optimization overhead. This plan caching comes with a trade-off: parameter sniffing means the optimizer builds the plan around the first set of parameter values it sees. A plan optimized for a small result set (@top_n = 5) can perform catastrophically when the same SP is called with a large result set (@top_n = 10000), because the plan was compiled with row estimates tuned to the original parameters. See the parameter sniffing callout in the section below.
Cross-Engine: Stored Procedures
SQL Server stored procedures are compiled, parameterized objects with plan caching, output parameters, and full ACID transaction support. BigQuery supports scripting procedures (
CREATE PROCEDURE) introduced in 2021, but there is no plan caching — every call incurs full query compilation. Firestore delegates server-side logic to Cloud Functions, which run outside the database engine entirely.
Related pattern
The dbt-sqlserver-adapter generates parameterized queries and materialization logic similar to these stored procedures, providing a version-controlled alternative to hand-written SPs.
Stored Procedures — Basic SP with Parameters
A stored procedure is precompiled SQL that lives in the database. Use case: pipeline steps as SPs — each step has consistent parameters and error handling.
Dynamic SQL is an injection vector
EXEC('SELECT * FROM ' + @tableName)is vulnerable to SQL injection if@tableNamecomes from user input. Always usesp_executesqlwith parameterized queries for values. For dynamic object names, validate againstsys.tables/sys.columnsbefore building the string.
Safe Pattern
Use
sp_executesqlwith typed parameters for all variable values:EXEC sp_executesql N'SELECT ... WHERE symbol = @sym', N'@sym VARCHAR(20)', @sym = @input. For dynamic object names (table/column names), always validate the input againstsys.tablesorsys.columnsbefore concatenating it into SQL — never trust caller input directly.
Create a parameterized top-N stored procedure
Create a parameterized stored procedure that returns the top N stocks by composite rank for a given index.
CREATE OR ALTER PROCEDURE demo.sp_top_stocks
@index_key NVARCHAR(50),
@top_n INT = 10
AS
BEGIN
SET NOCOUNT ON;
SELECT TOP (@top_n)
[rank], symbol, short_name,
composite_score AS score,
current_price
FROM demo.v_stock_dashboard
WHERE _index = @index_key
AND score_date = (
SELECT MAX(score_date) FROM gold.scores_daily WHERE _index = @index_key
)
ORDER BY [rank];
END;Execute the stored procedure for Euro Stoxx 50
Execute the stored procedure for the Euro Stoxx 50 index, returning the top 5 stocks.
EXEC demo.sp_top_stocks @index_key = 'euro_stoxx_50', @top_n = 5| rank | symbol | short_name | score | current_price |
|---|---|---|---|---|
| 1 | BNP.PA | BNP PARIBAS ACT.A | 0.6796 | 87.44 |
| 2 | VOW.DE | VOLKSWAGEN AG | 0.5756 | 92.85 |
| 3 | DTE.DE | DEUTSCHE TELEKOM AG | 0.487 | 32.55 |
| 4 | TTE.PA | TOTALENERGIES | 0.3913 | 69.8 |
| 5 | ABI.BR | AB INBEV | 0.3852 | 62.76 |
Stored Procedures — Error Handling with TRY/CATCH
Production SPs wrap logic in TRY/CATCH with explicit transactions. If anything fails, the entire operation rolls back — no partial loads. The @@TRANCOUNT > 0 guard before ROLLBACK is essential: if the error occurred outside an open transaction (e.g., in a trigger), calling ROLLBACK unconditionally would raise an additional error.
Parameter Sniffing
SQL Server sniffs parameter values on first SP execution and optimizes the plan for those specific values. A plan compiled for
@top_n = 5may perform catastrophically when called with@top_n = 10000— the optimizer chose a nested loops join expecting 5 rows, but now processes 10,000. Plan cache invalidation (after an index rebuild orsp_recompile) resets the sniffed values.
Mitigations
Three options in order of preference: (1)
OPTION (RECOMPILE)on the statement — recompiles every call using the actual parameter values, best for plans that vary dramatically by input; (2)OPTION (OPTIMIZE FOR (@param UNKNOWN))— uses average statistics rather than the sniffed value; (3) reassign to a local variable inside the SP (DECLARE @local = @param) — prevents sniffing but may produce suboptimal plans for all inputs.
Create an SP with TRY/CATCH, transaction, and OUTPUT parameter
Create an SP with TRY/CATCH error handling, explicit transaction, and an OUTPUT parameter for row count.
CREATE OR ALTER PROCEDURE demo.sp_load_scores
@index_key NVARCHAR(50),
@rows_loaded INT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
SET @rows_loaded = 0;
BEGIN TRY
BEGIN TRANSACTION;
SELECT @rows_loaded = COUNT(*)
FROM gold.scores_daily
WHERE _index = @index_key;
COMMIT TRANSACTION;
PRINT 'Load completed: ' + CAST(@rows_loaded AS VARCHAR) + ' rows';
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
DECLARE @msg NVARCHAR(4000) = ERROR_MESSAGE();
DECLARE @sev INT = ERROR_SEVERITY();
RAISERROR(@msg, @sev, 1);
END CATCH
END;User-Defined Functions
SQL Server supports three types of user-defined functions: scalar functions (return a single value), inline table-valued functions (iTVFs, return a table via a single SELECT), and multi-statement table-valued functions (MSTVFs, build a result set row by row). iTVFs are the only type the optimizer can inline and parallelize — always prefer them. Scalar UDFs and MSTVFs force row-by-row execution and disable parallelism. SQL Server 2019 introduced scalar UDF inlining, but many patterns remain ineligible (functions with TRY/CATCH, RAND, NEWID, recursion, or side effects); verify with sys.sql_modules.is_inlineable = 1.
Scalar UDFs Kill Performance
Scalar UDFs Force Row-by-Row Execution. T-SQL scalar UDFs (non-inlineable) disable parallelism and force SQL Server to call the function once per row. A simple scalar UDF on a 10M-row table can turn a 2-second query into a 2-minute query. Always use inline table-valued functions (iTVFs) instead — the optimizer can fold them into the outer query plan. SQL Server 2019+ has “scalar UDF inlining,” but many patterns are still not eligible.
Safe Pattern
Replace scalar UDFs with inline table-valued functions (
RETURNS TABLE AS RETURN (SELECT ...)). The optimizer can fold an iTVF into the outer query plan and parallelize it. If you must retain a scalar UDF, verify it qualifies for SQL Server 2019+ scalar UDF inlining by checkingsys.sql_modules.is_inlineable = 1and test withSET STATISTICS IO, TIME ONto confirm the plan is not row-by-row.
User-Defined Functions — Inline Table-Valued Function
An iTVF is like a parameterized view — the optimizer inlines it into the outer query. Always prefer iTVFs over scalar UDFs or multi-statement TVFs.
Create an inline table-valued function for price history
Create an inline table-valued function that returns OHLCV data for a given symbol and date range.
CREATE OR ALTER FUNCTION demo.fn_price_history(
@symbol VARCHAR(20),
@from_date DATE,
@to_date DATE
)
RETURNS TABLE
AS RETURN (
SELECT symbol, date, [open], high, low, [close], volume
FROM silver.eurostoxx50_ohlcv
WHERE symbol = @symbol AND date BETWEEN @from_date AND @to_date
);The iTVF is called in the FROM clause exactly like a table — the optimizer inlines it into the outer query plan.
Call the iTVF from a SELECT statement
Call the iTVF for ASML March 2026 data — the optimizer inlines it into the outer query plan.
SELECT TOP 10 * FROM demo.fn_price_history('ASML.AS', '2026-03-01', '2026-03-31')
ORDER BY date DESC| symbol | date | open | high | low | close | volume |
|---|---|---|---|---|---|---|
| ASML.AS | 2026-03-12 | 1194.8 | 1202.2 | 1187.8 | 1190.8 | 128223 |
| ASML.AS | 2026-03-11 | 1188.4 | 1210.8 | 1174.0 | 1198.8 | 562904 |
| ASML.AS | 2026-03-10 | 1188.4 | 1208.4 | 1172.2 | 1200.0 | 800815 |
| ASML.AS | 2026-03-09 | 1072.0 | 1147.6 | 1060.2 | 1147.6 | 689086 |
| ASML.AS | 2026-03-06 | 1186.0 | 1192.6 | 1112.8 | 1147.0 | 857271 |
| ASML.AS | 2026-03-05 | 1198.6 | 1220.0 | 1183.0 | 1186.0 | 778081 |
| ASML.AS | 2026-03-04 | 1171.0 | 1210.8 | 1167.6 | 1199.8 | 714587 |
| ASML.AS | 2026-03-03 | 1186.6 | 1187.4 | 1144.0 | 1161.8 | 941945 |
| ASML.AS | 2026-03-02 | 1192.8 | 1231.4 | 1180.0 | 1210.4 | 871267 |
Indexes
Index selection is the single highest-leverage performance decision in SQL Server. The right index can turn a multi-second table scan into a sub-millisecond seek; the wrong index imposes unnecessary write overhead on every INSERT, UPDATE, and DELETE. Index design for data pipelines requires balancing read-query patterns (equality filters, range scans, analytical aggregations) against write throughput. See index-types-and-strategy for columnstore internals, fragmentation maintenance, and missing index DMV analysis.
Covering Indexes for Pipeline Queries
A covering index includes all columns needed by a query in the index leaf pages, eliminating key lookups back to the clustered index. For the
fn_price_historypattern —WHERE symbol = @symbol AND date BETWEEN @from AND @to, selectingsymbol, date, open, high, low, close, volume— a covering index(symbol, date) INCLUDE (open, high, low, close, volume)satisfies the entire query from the index alone. Usesys.dm_db_missing_index_detailsto identify queries that would benefit from a covering index.
Indexes — Types and When to Use Each
The table below summarizes SQL Server index types and their primary use cases for time-series financial data. Index selection depends on the dominant query pattern for each table.
| Type | What | When |
|---|---|---|
| Clustered | Physical row order. One per table. | PK (symbol, date) for time-series |
| Non-clustered | Separate B-tree pointing to rows. | Filter/sort columns (sector, _index) |
| Covering | Includes extra columns in leaf. | Avoids key lookups for SELECT columns |
| Filtered | Index only subset of rows. | WHERE is_current = 1 on dims |
| Columnstore | Columnar storage, batch processing. | Analytical aggregations on OHLCV |
The query below inspects existing indexes on the silver.eurostoxx50_ohlcv table using catalog views. STRING_AGG aggregates the key column names in ordinal order to show the composite key layout.
Inspect existing indexes on a table via catalog views
Inspect existing indexes on the OHLCV table: name, type, uniqueness, and key columns.
SELECT
i.name AS index_name,
i.type_desc,
i.is_unique,
STRING_AGG(c.name, ', ') WITHIN GROUP (ORDER BY ic.key_ordinal) AS columns
FROM sys.indexes i
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
WHERE i.object_id = OBJECT_ID('silver.eurostoxx50_ohlcv')
GROUP BY i.name, i.type_desc, i.is_unique
ORDER BY i.type_desc| index_name | type_desc | is_unique | columns |
|---|---|---|---|
| PK__eurostox__3213E83FDF67D274 | CLUSTERED | True | id |
| IX_silver_eurostoxx50_ohlcv_symbol_date | NONCLUSTERED | True | symbol, date |
Indexes — Design Principles for Data Pipelines
Query-pattern-first design: identify the three or four most common predicates and projections for each table before creating any index.
- Equality columns first in composite keys:
WHERE _index = 'X' AND date >= '2026-01-01'→ index on(_index, date) - Include columns to avoid lookups:
INCLUDE (close, volume)if you SELECT those - Don’t over-index: each index slows writes. Monitor with
sys.dm_db_index_usage_stats - Filtered indexes for hot subsets:
WHERE is_current = 1on dimension tables
Redundancy Note
This section covers index usage patterns for query tuning. For full index internals — B-tree structure, columnstore encodings, fragmentation mechanics, and automated maintenance scripts — see index-types-and-strategy in Chapter 04.
Slowly Changing Dimensions (SCD)
The MERGE patterns used for SCD Type 2 below are a key building block for idempotent-pipeline-design, where every load can be safely re-run without duplicating or corrupting data.
Slowly Changing Dimensions — SCD Type 1 Overwrite
Simply UPDATE the row. History is lost. Use when you don’t care about old values. Example: fix a typo in a company name.
Simulate an SCD Type 1 overwrite on a temp table
Simulate an SCD Type 1 overwrite: copy 5 rows into a temp table, then UPDATE ASML’s sector in place.
SELECT TOP 5 symbol, short_name, sector, is_current
INTO #scd_demo
FROM silver.index_dim
WHERE _index = 'euro_stoxx_50' AND is_current = 1;
UPDATE #scd_demo SET sector = 'Information Technology' WHERE symbol = 'ASML.AS';
SELECT * FROM #scd_demo5 rows affected.1 rows affected.| symbol | short_name | sector | is_current |
|---|---|---|---|
| ASML.AS | ASML HOLDING | Information Technology | True |
| MC.PA | LVMH | Consumer Cyclical | True |
| RMS.PA | HERMES INTL | Consumer Cyclical | True |
| OR.PA | L'OREAL | Consumer Defensive | True |
| SAP.DE | SAP SE | Technology | True |
Slowly Changing Dimensions — SCD Type 2 History Tracking
Expire the old row (is_current=0, valid_to=NOW) and insert a new row (is_current=1).
This is how silver.index_dim works — it has valid_from, valid_to, is_current columns.
Query SCD Type 2 validity ranges
Query SCD Type 2 history: show valid_from/valid_to ranges for Euro Stoxx 50 dimension rows.
SELECT TOP 10
symbol, short_name, sector,
is_current,
CAST(valid_from AS DATE) AS valid_from,
CAST(valid_to AS DATE) AS valid_to
FROM silver.index_dim
WHERE _index = 'euro_stoxx_50'
ORDER BY symbol, valid_from| symbol | short_name | sector | is_current | valid_from | valid_to |
|---|---|---|---|---|---|
| ABI.BR | AB INBEV | Consumer Defensive | True | 2026-03-04 | None |
| AD.AS | KONINKLIJKE AHOLD DELHAIZE N.V. | Consumer Defensive | True | 2026-03-04 | None |
| ADS.DE | adidas AG | Consumer Cyclical | True | 2026-03-04 | None |
| ADYEN.AS | ADYEN | Technology | True | 2026-03-04 | None |
| AI.PA | AIR LIQUIDE | Basic Materials | True | 2026-03-04 | None |
| AIR.PA | AIRBUS SE | Industrials | True | 2026-03-04 | None |
| ALV.DE | Allianz SE | Financial Services | True | 2026-03-04 | None |
| ARGX.BR | ARGENX SE | Healthcare | True | 2026-03-04 | None |
| ASML.AS | ASML HOLDING | Technology | True | 2026-03-04 | None |
| BAS.DE | BASF SE | Basic Materials | True | 2026-03-04 | None |
Gap Detection & Gap Filling
Time-series data in financial pipelines frequently contains gaps: missing trading days due to market holidays, exchange closures, or ingestion failures. Detecting and classifying these gaps is a prerequisite for accurate signal computation — undetected gaps produce incorrect rolling averages, momentum scores, and drawdown calculations. SQL Server provides two primary techniques: the LAG/DATEDIFF approach (detect a gap by comparing each row’s date to the previous row’s date within the same symbol partition) and the classical islands-and-gaps pattern (use ROW_NUMBER minus the date value to assign the same group number to consecutive days, then find the spaces between groups).
Gap Detection & Gap Filling — Detect Gaps with LAG
Uses LAG() to compare each trading date to the previous date for the same symbol. A gap larger than 3 calendar days (accounting for weekends) signals a missing trading session or ingestion failure.
Detect calendar gaps with LAG and DATEDIFF
Detect time-series gaps: compare each date to the previous date using LAG and flag gaps > 3 days.
SELECT TOP 10
symbol, date,
LAG(date) OVER (PARTITION BY symbol ORDER BY date) AS prev_date,
DATEDIFF(DAY, LAG(date) OVER (PARTITION BY symbol ORDER BY date), date) AS gap_days,
CASE WHEN DATEDIFF(DAY, LAG(date) OVER (PARTITION BY symbol ORDER BY date), date) > 3
THEN 'UNUSUAL GAP' ELSE 'normal' END AS status
FROM silver.eurostoxx50_ohlcv
WHERE symbol = 'ASML.AS' AND date >= '2025-01-01'
ORDER BY date DESC| symbol | date | prev_date | gap_days | status |
|---|---|---|---|---|
| ASML.AS | 2026-03-12 | 2026-03-11 | 1 | normal |
| ASML.AS | 2026-03-11 | 2026-03-10 | 1 | normal |
| ASML.AS | 2026-03-10 | 2026-03-09 | 1 | normal |
| ASML.AS | 2026-03-09 | 2026-03-06 | 3 | normal |
| ASML.AS | 2026-03-06 | 2026-03-05 | 1 | normal |
| ASML.AS | 2026-03-05 | 2026-03-04 | 1 | normal |
| ASML.AS | 2026-03-04 | 2026-03-03 | 1 | normal |
| ASML.AS | 2026-03-03 | 2026-03-02 | 1 | normal |
| ASML.AS | 2026-03-02 | 2026-02-27 | 3 | normal |
| ASML.AS | 2026-02-27 | 2026-02-26 | 1 | normal |
Deduplication Strategies
Duplicate rows in source data are one of the most common data quality issues in financial pipelines: broker feeds retry failed deliveries, ETL jobs re-run after failures, and UNION operations occasionally double-count rows. SQL Server’s ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) window function is the standard deduplication tool — assign rank 1 to the row to keep within each duplicate group, then delete or exclude the rest. The ORDER BY clause controls which duplicate survives: highest volume, latest ingestion timestamp, or most complete record.
Deduplication Strategies — ROW_NUMBER Pattern
Assigns ROW_NUMBER() within each (symbol, date) group ordered by descending volume. Rows with rn = 1 are the canonical records; rows with rn > 1 are duplicates to remove. The COUNT(*) OVER window simultaneously flags which keys have multiple rows, so you can isolate only the affected dates for inspection.
The CTE simulates a duplicate by UNION ALL-ing the same latest-date row with a slightly modified close and volume. ROW_NUMBER() partitioned by (symbol, date) and ordered by descending volume assigns rn = 1 to the row with the highest volume (the tie-breaking rule). COUNT(*) OVER counts how many copies exist per key — the outer WHERE copies > 1 isolates only the duplicated dates for inspection.
Identify duplicates with ROW_NUMBER and tie-breaking
Simulate a duplicate row and identify it using ROW_NUMBER with volume-based tie-breaking.
WITH raw_data AS (
SELECT symbol, date, [close], volume, 'original' AS source
FROM silver.eurostoxx50_ohlcv
WHERE symbol = 'ASML.AS' AND date >= '2026-03-10'
UNION ALL
SELECT symbol, date, [close] + 0.5, volume + 999, 'duplicate'
FROM silver.eurostoxx50_ohlcv
WHERE symbol = 'ASML.AS' AND date = (SELECT MAX(date) FROM silver.eurostoxx50_ohlcv WHERE symbol = 'ASML.AS')
),
numbered AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY symbol, date ORDER BY volume DESC) AS rn,
COUNT(*) OVER (PARTITION BY symbol, date) AS copies
FROM raw_data
)
SELECT TOP 10 symbol, date, ROUND([close], 2) AS [close], volume, source, rn, copies
FROM numbered
WHERE copies > 1
ORDER BY date DESC, rn| symbol | date | close | volume | source | rn | copies |
|---|---|---|---|---|---|---|
| ASML.AS | 2026-03-12 | 1191.3 | 129222 | duplicate | 1 | 2 |
| ASML.AS | 2026-03-12 | 1190.8 | 128223 | original | 2 | 2 |
Execution Plans & Query Optimization
SQL Server’s cost-based optimizer compiles a query into an execution plan that specifies the physical operations (seeks, scans, joins, sorts) and their estimated costs. The plan is cached and reused for subsequent identical queries. Use SET STATISTICS IO, TIME ON to measure actual logical reads and elapsed time; query sys.dm_exec_query_stats to identify the most expensive cached plans. Understanding the common anti-patterns below — non-sargable predicates, implicit type conversions, missing indexes — is the first step in pipeline performance tuning.
flowchart TD A[T-SQL Query Submitted] --> B[Parse & Tokenize] B --> C[Bind / Algebrize] C --> D{Plan Cache Lookup} D -->|Cache Hit| E[Reuse Cached Plan] D -->|Cache Miss| F[Optimization Phase] F --> G{Trivial Plan?} G -->|Yes - single table<br/>no joins/aggs| H[Use Trivial Plan<br/>no cost estimation] G -->|No| I[Cost-Based Optimization<br/>estimate rows + cost per op] I --> J[Select Lowest-Cost Plan<br/>cached for reuse] H --> K[Execute Plan] J --> K E --> K K --> L[Return Result Set] style A fill:#1a1b26,stroke:#565f89,color:#c0caf5 style D fill:#292e42,stroke:#bb9af7,color:#c0caf5 style F fill:#292e42,stroke:#7aa2f7,color:#c0caf5 style I fill:#24283b,stroke:#7aa2f7,color:#c0caf5 style K fill:#1a1b26,stroke:#9ece6a,color:#c0caf5 style L fill:#1a1b26,stroke:#9ece6a,color:#c0caf5
Execution Plans & Query Optimization — Common Anti-Patterns
The following patterns prevent SQL Server from using indexes efficiently. Each forces a table scan where an index seek would suffice, often increasing query cost by orders of magnitude on large tables.
| Anti-Pattern | Problem | Fix |
|---|---|---|
WHERE YEAR(date) = 2025 | Function on column prevents index seek | WHERE date >= '2025-01-01' AND date < '2026-01-01' |
SELECT * | Reads all columns, can’t use covering index | Select only needed columns |
WHERE col = NULL | Always FALSE (NULL != NULL) | WHERE col IS NULL |
| Implicit conversion | VARCHAR compared to NVARCHAR causes scan | Match data types in predicates |
| Missing index | Table scan on large table | Add non-clustered index on filter columns |
Both queries return the same count, but the non-sargable version (YEAR(date) = 2025) wraps the column in a function, preventing the index seek — SQL Server must evaluate YEAR() for every row. The sargable version (date >= '2025-01-01' AND date < '2026-01-01') expresses the same filter as a range predicate the index can seek directly.
Compare non-SARGable vs SARGable predicates
Compare non-SARGable (function-on-column) vs SARGable (range) predicates — same result, different plans.
SELECT
(SELECT COUNT(*) FROM silver.eurostoxx50_ohlcv
WHERE YEAR(date) = 2025) AS bad_function_on_column,
(SELECT COUNT(*) FROM silver.eurostoxx50_ohlcv
WHERE date >= '2025-01-01' AND date < '2026-01-01') AS good_sargable| bad_function_on_column | good_sargable |
|---|---|
| 12698 | 12698 |
Transaction Isolation Levels
SQL Server’s transaction isolation levels control how reads interact with concurrent writes — the trade-off between data consistency and blocking. The default READ COMMITTED blocks readers when a writer holds a row lock. For analytics reads in a data pipeline, SNAPSHOT isolation provides point-in-time consistency with no blocking by reading row versions stored in tempdb. Read Committed Snapshot Isolation (RCSI) extends snapshot behavior automatically to all READ COMMITTED statements database-wide — enable it with ALTER DATABASE stoxx SET READ_COMMITTED_SNAPSHOT ON — eliminating reader/writer blocking without changing any application code.
Cross-Engine: Isolation Levels
SQL Server implements all ANSI isolation levels plus
SNAPSHOT(optimistic, row-versioned via tempdb) and RCSI. BigQuery uses serializable isolation for multi-statement transactions by default; single statements are always atomic and isolated. Firestore transactions are serializable and limited to 500 documents per transaction; reads outside a transaction use strong consistency by default for server-side reads, eventual consistency for mobile/web clients.
Transaction Isolation Levels — Guide for Data Engineering
Each isolation level is a commitment about which read anomalies the engine prevents. Higher levels prevent more anomalies but increase blocking — lower levels scale better but may return stale or inconsistent reads.
| Level | Dirty Reads | Non-Repeatable | Phantoms | Use Case |
|---|---|---|---|---|
| READ UNCOMMITTED | Yes | Yes | Yes | Stale-tolerant dashboards, quick counts |
| READ COMMITTED (default) | No | Yes | Yes | Most pipeline reads |
| REPEATABLE READ | No | No | Yes | Financial calculations |
| SERIALIZABLE | No | No | No | Critical writes (score computation) |
| SNAPSHOT | No | No | No | Analytics reads (no blocking, uses tempdb) |
Recommendation for pipelines: READ COMMITTED for writes, SNAPSHOT for reads.
NOLOCK Can Return Wrong Data
READ UNCOMMITTED (NOLOCK) Can Return Wrong Data.
NOLOCK/READ UNCOMMITTEDcan read rows that are being moved by a page split, causing the same row to appear twice or not at all in the result. It can also read uncommitted data that is later rolled back. Never use NOLOCK for counts, sums, or any calculation where accuracy matters — even for “approximate” dashboards, the error can be larger than expected.
Safe Pattern
Use
SNAPSHOTisolation for analytics reads instead ofNOLOCK:SET TRANSACTION ISOLATION LEVEL SNAPSHOT. SNAPSHOT provides point-in-time read consistency with no blocking, using row versions from tempdb rather than dirty reads. Enable it at the database level withALTER DATABASE stoxx SET ALLOW_SNAPSHOT_ISOLATION ON.
Bulk Loading Patterns
Bulk data loading is the performance-critical path for bronze-layer ingestion and silver-layer transforms. SQL Server provides several insertion strategies spanning orders of magnitude in throughput — from simple INSERT INTO ... SELECT to minimally-logged BULK INSERT from flat files. The choice depends on data source (query result vs. file), load size, recovery model (FULL vs. SIMPLE/BULK_LOGGED), and whether you need checkpointing for loads that exceed available transaction log space.
Bulk Loading Strategies — Insert Method Comparison
Choose an insert strategy based on data source, batch size, and recovery model. Minimal logging (requires SIMPLE or BULK_LOGGED recovery model) is needed to achieve the fastest throughput with INSERT ... WITH (TABLOCK) and BULK INSERT.
| Strategy | Speed | When |
|---|---|---|
INSERT INTO ... SELECT | Medium | Small-medium loads from staging |
INSERT ... WITH (TABLOCK) | Fast | Minimal logging in SIMPLE/BULK_LOGGED |
BULK INSERT | Fastest | Loading from CSV files on disk |
| Batched inserts (TOP N loop) | Controlled | Large loads with checkpoints |
| Drop indexes → load → rebuild | Fastest | Full table reloads |
Pipeline pattern: load to staging table → validate → MERGE to target → truncate staging.
Data Lineage & Audit Columns
The stoxx database implements audit columns on every table to support data lineage tracking: when each row was ingested, computed, and last modified. These columns enable freshness checks (is the data stale?), replay detection (has this batch already been loaded?), and pipeline debugging (which layer introduced a discrepancy?). The query below checks the latest timestamp across all four layers of the medallion architecture to confirm a successful end-to-end pipeline run.
Data Lineage & Audit — Standard Audit Columns
Every table in the stoxx database has audit columns:
| Column | Type | Purpose |
|---|---|---|
_ingested_at | DATETIME2 | When the row was loaded (bronze) |
_scored_at | DATETIME2 | When the score was computed (gold) |
_computed_at | DATETIME2 | When the performance was calculated |
is_filled | BIT | Whether the row was gap-filled (silver) |
is_current | BIT | SCD Type 2 current flag (dimension) |
Check data freshness across all medallion layers
Check data freshness across all four medallion layers — the latest timestamp per layer.
SELECT 'bronze.eurostoxx50_ohlcv' AS [table], MAX(_ingested_at) AS last_update
FROM bronze.eurostoxx50_ohlcv
UNION ALL
SELECT 'silver.signals_daily', MAX(signal_date) FROM silver.signals_daily
WHERE _index = 'euro_stoxx_50'
UNION ALL
SELECT 'gold.scores_daily', MAX(score_date) FROM gold.scores_daily
WHERE _index = 'euro_stoxx_50'
UNION ALL
SELECT 'gold.index_performance', MAX(perf_date) FROM gold.index_performance
WHERE _index = 'euro_stoxx_50'
ORDER BY last_update DESC| table | last_update |
|---|---|
| bronze.eurostoxx50_ohlcv | 2026-03-12 12:45:00.021478 |
| gold.index_performance | 2026-03-12 00:00:00 |
| gold.scores_daily | 2026-03-12 00:00:00 |
| silver.signals_daily | 2026-03-12 00:00:00 |
Partitioning Strategies
Table partitioning divides a large table’s data into physically separate segments based on a column value range (typically a date). SQL Server’s partition elimination allows the query optimizer to skip entire partitions that cannot satisfy the WHERE clause predicate — equivalent to a physical shard filter at the storage level. Partitioning also enables instant data archival via SWITCH: moving an entire partition between tables is a metadata-only operation requiring no row movement. See partitioning-strategies for full implementation details including partition functions, schemes, sliding windows, and maintenance scripts.
Partitioning Strategies — When to Partition
Partition large tables (millions of rows) by a date column for:
- Faster queries: partition elimination skips irrelevant months/years
- Easier maintenance: rebuild one partition, not the whole table
- Instant archival: SWITCH old partitions to archive table
The OHLCV tables (~65K rows each) are too small to benefit. In production with 100M+ rows, partition by year or month.
The schema below shows how a partition function and scheme would be defined — for reference only; do not run in the lab environment.
Define a yearly partition function and scheme
Define a yearly partition function and scheme for a partitioned OHLCV table (reference only — not executed in lab).
CREATE PARTITION FUNCTION pf_yearly(DATE)
AS RANGE RIGHT FOR VALUES ('2022-01-01', '2023-01-01', '2024-01-01', '2025-01-01', '2026-01-01');
CREATE PARTITION SCHEME ps_yearly
AS PARTITION pf_yearly ALL TO ([PRIMARY]);
CREATE TABLE silver.ohlcv_partitioned (
symbol VARCHAR(20), date DATE, [close] FLOAT, ...
) ON ps_yearly(date);Cleanup
Drop all objects created in the demo schema by this notebook. Running the cleanup leaves the database in its original state and makes the notebook safe to re-run from a clean slate.
Demo object cleanup
Every view, stored procedure, and function created earlier must be dropped in reverse dependency order before the schema itself can be removed.
Drop all demo objects and the demo schema
Drop all demo objects and the demo schema to leave the database clean.
DROP VIEW IF EXISTS demo.v_latest_prices;
DROP VIEW IF EXISTS demo.v_stock_dashboard;
DROP PROCEDURE IF EXISTS demo.sp_top_stocks;
DROP PROCEDURE IF EXISTS demo.sp_load_scores;
DROP FUNCTION IF EXISTS demo.fn_price_history;
DROP SCHEMA IF EXISTS demo;
SELECT 'Demo objects cleaned up' AS status| status |
|---|
| Demo objects cleaned up |
SQL Server SQL Engineering Warnings
The table below lists the highest-impact production pitfalls associated with the database objects and patterns covered in this note. Each entry corresponds to a warning or danger callout earlier in the page.
| Topic | Warning |
|---|---|
| Parameter sniffing | An SP’s cached plan is optimized for the first parameter values. A plan compiled for 5 rows can be catastrophic for 10,000 rows. Monitor with sys.dm_exec_query_stats. |
| Scalar UDFs | Non-inlineable scalar UDFs disable parallelism and force row-by-row execution. A simple scalar UDF on 10M rows can turn a 2-second query into a 2-minute query. |
| MERGE concurrency bugs | Microsoft has documented multiple bugs: missing rows, duplicate key violations, incorrect results under concurrent access. Always add WITH (HOLDLOCK) on the target. |
| NOLOCK / READ UNCOMMITTED | Can return rows twice, skip rows, or read rolled-back data during page splits. Never use for counts, sums, or any calculation where accuracy matters. |
| SCD Type 1 in financial pipelines | Destroys history permanently. A sector change applied via Type 1 retroactively alters historical portfolio returns without any audit trail. |
| CTE re-execution | A CTE referenced 3 times runs 3 times. Check the execution plan for repeated subtrees — switch to #temp if cost is significant. |
| Over-indexing | Each index slows INSERT/UPDATE/DELETE. Monitor index usage with sys.dm_db_index_usage_stats and drop unused indexes. |
SQL Server SQL Engineering Recommendations
Standing guidance for designing, writing, and operating the database objects covered above. Apply these as defaults unless a specific workload has a documented reason to deviate.
| Area | Recommendation |
|---|---|
| View vs SP vs iTVF | Use views for static logic, iTVFs for parameterized reads, SPs for multi-step procedural logic with transactions. |
| Error handling | Always use TRY/CATCH with @@TRANCOUNT > 0 guard before ROLLBACK. Without the guard, rolling back a non-existent transaction raises an additional error. |
| Parameter sniffing mitigation | Use OPTION (RECOMPILE) on specific statements (not the whole SP) for plans that genuinely vary by input. Use OPTIMIZE FOR UNKNOWN for stable average behavior. |
| Bulk loading | Load to staging table → validate with quality checks → MERGE to target → truncate staging. Drop non-clustered indexes before large loads, rebuild after. |
| Isolation for analytics | Enable RCSI (ALTER DATABASE stoxx SET READ_COMMITTED_SNAPSHOT ON) to eliminate reader/writer blocking database-wide without changing application code. |
| Demo schema pattern | Always create experimental objects in a dedicated schema (demo). Include a cleanup block at the end to ensure idempotent re-runs. |
| Audit columns | Every pipeline table should have _ingested_at (bronze), _scored_at (gold), and is_filled / is_current flags for lineage tracking. |
SQL Server SQL Engineering Troubleshooting
Symptoms you will encounter when a database object or query misbehaves, mapped to the most likely cause and the fix that resolves it in practice.
| Symptom | Likely cause | Fix |
|---|---|---|
| SP runs fast the first time, slow on subsequent calls | Parameter sniffing — plan cached for atypical first values | Add OPTION (RECOMPILE) or WITH RECOMPILE on the SP, or use sp_recompile to flush the plan. |
| View query is unexpectedly slow | View references a CTE or subquery that is re-evaluated, or the base table lacks an index on the filter column | Check the execution plan. Add a covering index on the most-used predicate columns. |
| MERGE raises duplicate key violation | Race condition between MATCHED check and INSERT under concurrent access | Add WITH (HOLDLOCK) on the target table in the MERGE statement, or switch to explicit INSERT/UPDATE in a transaction with UPDLOCK. |
#temp table query slow despite small size | Missing index on the join/filter column in the temp table | Add CREATE INDEX ix ON #temp (key_col) after populating the temp table. |
| Scalar UDF causes query timeout | UDF is non-inlineable — forces row-by-row execution | Rewrite as an iTVF or inline the logic directly into the query. Check sys.sql_modules.is_inlineable. |
| Partition elimination not working | WHERE clause uses a function on the partition column, or the filter column doesn’t match the partition function | Rewrite as a range predicate on the raw partition column. Verify with execution plan’s “Actual Partition Count”. |
SQL Server SQL Engineering Cross-References
Related notes that extend or depend on the patterns covered here.
- index-types-and-strategy — full index internals, columnstore, fragmentation maintenance, missing index DMV analysis
- partitioning-strategies — partition functions, schemes, sliding windows, maintenance scripts
- sargable-queries — deep dive on SARGable vs non-SARGable predicates
- idempotent-pipeline-design — how MERGE fits into re-runnable load strategies
- bronze-layer-loading — ingestion pipeline feeding the medallion architecture
- silver-transforms — production daily return and gap-fill transforms
- gold-transforms — production z-score normalization and composite ranking
- dbt-sqlserver-adapter — dbt alternative to hand-written stored procedures
- secrets-management — production credential management (GCP Secret Manager)