SQL Performance: Complete Gids voor Database Optimalisatie
Leer geavanceerde SQL performance optimalisatie technieken voor snellere queries, betere schaalbaarheid en efficiënter resource gebruik. Met praktische voorbeelden en best practices.
Zoek je SQL Performance Experts?
Vind ervaren Database Performance Specialists voor je kritieke database projecten
Inhoudsopgave
- Performance Fundamentals en Metrics
- Query Execution Plans Analyseren
- Geavanceerde Index Optimalisatie
- Query Rewriting Technieken
- JOIN Performance Optimalisatie
- Subquery en CTE Performance
- Temp Tables en Table Variables
- Parallel Query Execution
- Statistics en Maintenance
- Monitoring Tools en Scripts
- Praktijk Case Studies
1. Performance Fundamentals en Metrics
Kritieke Performance Metrics
SQL Performance monitoring begint met het begrijpen van key metrics die de gezondheid en efficiëntie van je database bepalen.
CPU Usage
Query compilatie en execution kosten. High CPU kan duiden op missing indexes of inefficiënte queries.
I/O Operations
Logical reads, physical reads en write operations. I/O is vaak de grootste bottleneck.
Memory Pressure
Buffer cache hit ratio, page life expectancy en memory grants.
Wait Statistics
Wat queries wachten op (locks, latches, I/O). Cruciaal voor bottleneck identificatie.
| Performance Issue | Symptomen | Oorzaken | Oplossingen |
|---|---|---|---|
| High CPU | Trage queries, CPU > 80% | Missing indexes, parameter sniffing, compilaties | Index optimalisatie, plan guides, optimize for ad hoc |
| High I/O | Disk queue length > 2, hoge read/write | Table scans, inefficient queries, fragmentation | Proper indexes, query rewriting, partitioning |
| Memory Pressure | Page life expectancy < 300, cache hit ratio < 90% | Memory grants, large sorts, hash joins | Memory optimization, query hints, tempdb tuning |
| Blocking Chains | Queries timeouts, deadlocks | Long transactions, missing indexes, lock escalation | Transaction isolation, index includes, lock timeouts |
| Parameter Sniffing | Sporadisch trage queries | Suboptimal plans voor bepaalde parameters | OPTIMIZE FOR, RECOMPILE, plan guides |
2. Query Execution Plans Analyseren
Execution Plan Analysis
Execution plans tonen hoe de database engine je query uitvoert. Ze zijn essentieel voor performance troubleshooting.
Execution Plan Basics
-- ❌ SLECHTE PRACTICE: Geen plan analyse
SELECT * FROM large_table WHERE column1 = 'value';
-- ✅ GOEDE PRACTICE: Plan analysis voor alle queries
-- SQL Server: Actual execution plan
SET STATISTICS PROFILE ON;
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
SELECT
c.CustomerID,
c.CompanyName,
COUNT(o.OrderID) AS OrderCount,
SUM(od.Quantity * od.UnitPrice) AS TotalSpent
FROM Customers c
INNER JOIN Orders o ON c.CustomerID = o.CustomerID
INNER JOIN [Order Details] od ON o.OrderID = od.OrderID
WHERE o.OrderDate >= '1998-01-01'
GROUP BY c.CustomerID, c.CompanyName
HAVING COUNT(o.OrderID) > 10;
-- PostgreSQL: EXPLAIN ANALYZE met details
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, FORMAT JSON)
SELECT
c.customer_id,
c.company_name,
COUNT(o.order_id) AS order_count,
SUM(od.quantity * od.unit_price) AS total_spent
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
INNER JOIN order_details od ON o.order_id = od.order_id
WHERE o.order_date >= '1998-01-01'
GROUP BY c.customer_id, c.company_name
HAVING COUNT(o.order_id) > 10;
Plan Analysis Tips
-- ✅ GOEDE PRACTICE: Plan warning indicators check
-- SQL Server: Find queries with plan warnings
SELECT
qs.plan_handle,
qs.query_hash,
qs.query_plan_hash,
st.text AS query_text,
qp.query_plan,
qs.execution_count,
qs.total_worker_time / qs.execution_count AS avg_cpu_time,
qs.total_logical_reads / qs.execution_count AS avg_logical_reads,
qs.total_elapsed_time / qs.execution_count AS avg_elapsed_time
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
WHERE qp.query_plan.value('declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan";
count(//p:Warnings)', 'int') > 0
ORDER BY qs.total_worker_time DESC;
-- PostgreSQL: Analyze plan efficiency
SELECT
query,
calls,
total_exec_time,
mean_exec_time,
rows,
100.0 * shared_blks_hit /
NULLIF(shared_blks_hit + shared_blks_read, 0) AS hit_percent,
CASE
WHEN 100.0 * shared_blks_hit /
NULLIF(shared_blks_hit + shared_blks_read, 0) < 90
THEN 'POOR'
WHEN 100.0 * shared_blks_hit /
NULLIF(shared_blks_hit + shared_blks_read, 0) < 95
THEN 'FAIR'
ELSE 'GOOD'
END AS cache_efficiency
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;
3. Geavanceerde Index Optimalisatie
Advanced Index Strategies
Index optimalisatie gaat verder dan basic index creatie. Leer filtered indexes, included columns, en index maintenance.
Strategic Index Design
-- ✅ GOEDE PRACTICE: Geavanceerde index strategieën
-- 1. Filtered indexes voor partiële data
CREATE INDEX idx_orders_active
ON orders (customer_id, order_date)
WHERE status = 'active';
-- 2. Included columns voor covering indexes
CREATE INDEX idx_customers_search
ON customers (last_name, first_name)
INCLUDE (email, phone, city);
-- 3. Columnstore indexes voor analytics
CREATE COLUMNSTORE INDEX idx_columnstore_orders
ON order_history (order_id, product_id, quantity, price, order_date);
-- 4. Partitioned indexes voor grote tabellen
CREATE INDEX idx_orders_partitioned
ON orders (order_date)
ON ps_order_date (order_date); -- Partition scheme
-- 5. Index met computed columns
ALTER TABLE products
ADD name_lower AS LOWER(product_name);
CREATE INDEX idx_products_name_lower
ON products (name_lower);
-- 6. Geospatial indexes
CREATE SPATIAL INDEX idx_customers_location
ON customers (location);
Index Maintenance Automation
-- ✅ GOEDE PRACTICE: Geautomatiseerde index maintenance
-- Dynamische index reorganisatie/rebuild
DECLARE @threshold_fragmentation FLOAT = 30.0; -- Rebuild boven 30%
DECLARE @threshold_reorganize FLOAT = 10.0; -- Reorganize 10-30%
DECLARE @sql NVARCHAR(MAX) = '';
SELECT @sql = @sql +
CASE
WHEN avg_fragmentation_in_percent > @threshold_fragmentation
THEN
'ALTER INDEX ' + QUOTENAME(i.name) +
' ON ' + QUOTENAME(OBJECT_NAME(ips.object_id)) +
' REBUILD WITH (ONLINE = ON); '
WHEN avg_fragmentation_in_percent > @threshold_reorganize
THEN
'ALTER INDEX ' + QUOTENAME(i.name) +
' ON ' + QUOTENAME(OBJECT_NAME(ips.object_id)) +
' REORGANIZE; '
END
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') ips
INNER JOIN sys.indexes i ON ips.object_id = i.object_id
AND ips.index_id = i.index_id
WHERE ips.avg_fragmentation_in_percent > @threshold_reorganize
AND ips.page_count > 1000 -- Alleen voor grote indexes
AND i.name IS NOT NULL
AND ips.alloc_unit_type_desc = 'IN_ROW_DATA';
-- Uitvoeren als er werk te doen is
IF LEN(@sql) > 0
BEGIN
PRINT 'Executing index maintenance...';
EXEC sp_executesql @sql;
END;
-- PostgreSQL: Index bloat monitoring
SELECT
schemaname,
tablename,
indexname,
pg_size_pretty(pg_relation_size(indexname::regclass)) AS index_size,
idx_scan AS number_of_scans,
idx_tup_read AS tuples_read,
idx_tup_fetch AS tuples_fetched,
CASE
WHEN idx_scan = 0 THEN 'UNUSED'
WHEN idx_tup_read > idx_tup_fetch * 100 THEN 'INEFFICIENT'
ELSE 'HEALTHY'
END AS index_status
FROM pg_stat_user_indexes
ORDER BY pg_relation_size(indexname::regclass) DESC;
Database Performance Experts Nodig?
Vind ervaren Database Performance Specialists voor je kritieke SQL optimalisatie projecten
4. Query Rewriting Technieken
Efficient Query Patterns
Query rewriting kan performance verbeteren door inefficient patterns te vervangen met optimalere alternatieven.
Common Anti-Patterns en Solutions
-- ❌ ANTI-PATTERN: Correlated subquery in SELECT
SELECT
c.customer_id,
c.company_name,
(
SELECT COUNT(*)
FROM orders o
WHERE o.customer_id = c.customer_id
) AS order_count
FROM customers c;
-- ✅ SOLUTION: Gebruik JOIN met GROUP BY
SELECT
c.customer_id,
c.company_name,
COUNT(o.order_id) AS order_count
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.company_name;
-- ❌ ANTI-PATTERN: OR conditions met verschillende columns
SELECT *
FROM products
WHERE category_id = 5
OR supplier_id = 10;
-- ✅ SOLUTION: Gebruik UNION voor betere index usage
SELECT *
FROM products
WHERE category_id = 5
UNION
SELECT *
FROM products
WHERE supplier_id = 10;
-- ❌ ANTI-PATTERN: Functions in WHERE clause
SELECT *
FROM orders
WHERE YEAR(order_date) = 2024
AND MONTH(order_date) = 6;
-- ✅ SOLUTION: Range queries gebruiken
SELECT *
FROM orders
WHERE order_date >= '2024-06-01'
AND order_date < '2024-07-01';
-- ❌ ANTI-PATTERN: NOT IN met NULL values
SELECT *
FROM customers
WHERE customer_id NOT IN (
SELECT customer_id
FROM orders
WHERE status = 'cancelled'
);
-- ✅ SOLUTION: Gebruik NOT EXISTS of LEFT JOIN
SELECT *
FROM customers c
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
AND o.status = 'cancelled'
);
5. JOIN Performance Optimalisatie
Advanced JOIN Techniques
JOIN performance kan dramatisch verbeteren met de juiste strategieën voor join volgorde, types en hints.
JOIN Strategy Optimization
-- Scenario: Complex multi-join query
-- ❌ SUBOPTIMAAL: Default join volgorde
SELECT
c.company_name,
o.order_date,
od.product_id,
p.product_name,
od.quantity,
od.unit_price,
s.company_name AS supplier_name
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
INNER JOIN order_details od ON o.order_id = od.order_id
INNER JOIN products p ON od.product_id = p.product_id
INNER JOIN suppliers s ON p.supplier_id = s.supplier_id
WHERE o.order_date >= '2024-01-01'
AND c.country = 'Netherlands'
ORDER BY o.order_date DESC;
-- ✅ OPTIMAAL: Strategic join volgorde
SELECT
c.company_name,
o.order_date,
od.product_id,
p.product_name,
od.quantity,
od.unit_price,
s.company_name AS supplier_name
FROM customers c -- Start met filtered table
INNER JOIN orders o ON c.customer_id = o.customer_id
AND o.order_date >= '2024-01-01' -- Filter early
INNER JOIN order_details od ON o.order_id = od.order_id
INNER JOIN products p ON od.product_id = p.product_id
INNER JOIN suppliers s ON p.supplier_id = s.supplier_id
WHERE c.country = 'Netherlands' -- Al gefilterd
ORDER BY o.order_date DESC;
-- ✅ OPTIMAAL: Gebruik van query hints (alleen als nodig)
SELECT
c.company_name,
o.order_date,
od.product_id,
p.product_name,
od.quantity,
od.unit_price
FROM customers c WITH (INDEX(idx_customers_country))
INNER HASH JOIN orders o ON c.customer_id = o.customer_id
INNER LOOP JOIN order_details od ON o.order_id = od.order_id
WHERE c.country = 'Netherlands'
AND o.order_date >= '2024-01-01'
OPTION (
RECOMPILE, -- Fresh plan voor parameters
MAXDOP 4 -- Limit parallelism
);
6. Subquery en CTE Performance
Subquery Performance Patterns
Subqueries en CTEs hebben specifieke performance characteristics. Leer wanneer ze optimal zijn en wanneer te vermijden.
CTE vs Subquery vs Derived Table
-- Scenario: Complexe data aggregatie
-- OPTIE 1: CTE (Common Table Expression)
-- Goed voor leesbaarheid, maar kan meermaals geëvalueerd worden
WITH MonthlySales AS (
SELECT
customer_id,
DATE_TRUNC('month', order_date) AS sales_month,
SUM(total_amount) AS monthly_total,
COUNT(*) AS order_count
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY customer_id, DATE_TRUNC('month', order_date)
),
CustomerStats AS (
SELECT
customer_id,
AVG(monthly_total) AS avg_monthly_spend,
SUM(monthly_total) AS total_spend,
COUNT(DISTINCT sales_month) AS active_months
FROM MonthlySales
GROUP BY customer_id
)
SELECT
c.customer_id,
c.company_name,
cs.avg_monthly_spend,
cs.total_spend,
cs.active_months
FROM customers c
INNER JOIN CustomerStats cs ON c.customer_id = cs.customer_id
WHERE cs.active_months >= 3
ORDER BY cs.total_spend DESC;
-- OPTIE 2: Derived Table (Inline View)
-- Soms beter voor performance in complexe scenario's
SELECT
c.customer_id,
c.company_name,
dt.avg_monthly_spend,
dt.total_spend,
dt.active_months
FROM customers c
INNER JOIN (
SELECT
o.customer_id,
AVG(monthly.monthly_total) AS avg_monthly_spend,
SUM(monthly.monthly_total) AS total_spend,
COUNT(DISTINCT monthly.sales_month) AS active_months
FROM orders o
INNER JOIN (
SELECT
customer_id,
DATE_TRUNC('month', order_date) AS sales_month,
SUM(total_amount) AS monthly_total
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY customer_id, DATE_TRUNC('month', order_date)
) monthly ON o.customer_id = monthly.customer_id
GROUP BY o.customer_id
) dt ON c.customer_id = dt.customer_id
WHERE dt.active_months >= 3
ORDER BY dt.total_spend DESC;
-- OPTIE 3: Lateral Join (PostgreSQL)
-- Voor complexe correlated subqueries
SELECT
c.customer_id,
c.company_name,
recent_orders.order_count,
recent_orders.total_spent
FROM customers c
CROSS JOIN LATERAL (
SELECT
COUNT(*) AS order_count,
SUM(total_amount) AS total_spent
FROM orders o
WHERE o.customer_id = c.customer_id
AND o.order_date >= CURRENT_DATE - '90 days'::INTERVAL
) recent_orders
WHERE recent_orders.order_count > 0;
7. Temp Tables en Table Variables
Temporary Object Performance
Temporary objects hebben verschillende performance characteristics. Kies het juiste type voor je use case.
Temp Table vs Table Variable
-- Scenario: Complex data processing met tussentijdse resultaten
-- ✅ TEMP TABLE: Goed voor grote datasets, ondersteunt indexes en statistics
CREATE TABLE #OrderSummary (
customer_id INT NOT NULL,
order_year INT NOT NULL,
order_month INT NOT NULL,
total_amount DECIMAL(10, 2) NOT NULL,
order_count INT NOT NULL,
PRIMARY KEY (customer_id, order_year, order_month)
);
-- Voeg index toe voor betere join performance
CREATE INDEX idx_temp_order_summary
ON #OrderSummary (order_year, order_month);
-- Vul temp table met geaggregeerde data
INSERT INTO #OrderSummary
SELECT
customer_id,
YEAR(order_date) AS order_year,
MONTH(order_date) AS order_month,
SUM(total_amount) AS total_amount,
COUNT(*) AS order_count
FROM orders
WHERE order_date >= '2023-01-01'
GROUP BY
customer_id,
YEAR(order_date),
MONTH(order_date);
-- Gebruik temp table in complexe query
SELECT
c.customer_id,
c.company_name,
os.order_year,
os.order_month,
os.total_amount,
os.order_count,
LAG(os.total_amount) OVER (
PARTITION BY c.customer_id
ORDER BY os.order_year, os.order_month
) AS prev_month_amount
FROM customers c
INNER JOIN #OrderSummary os ON c.customer_id = os.customer_id
ORDER BY c.customer_id, os.order_year, os.order_month;
-- Cleanup
DROP TABLE #OrderSummary;
-- ✅ TABLE VARIABLE: Goed voor kleine datasets (< 1000 rows)
DECLARE @RecentOrders TABLE (
order_id INT PRIMARY KEY,
customer_id INT NOT NULL,
order_date DATE NOT NULL,
total_amount DECIMAL(10, 2) NOT NULL
);
-- Insert recent orders
INSERT INTO @RecentOrders
SELECT
order_id,
customer_id,
order_date,
total_amount
FROM orders
WHERE order_date >= DATEADD(day, -30, GETDATE());
-- Gebruik in query (geen cleanup nodig)
SELECT
c.company_name,
ro.order_date,
ro.total_amount,
SUM(ro.total_amount) OVER (
PARTITION BY c.customer_id
) AS customer_total
FROM customers c
INNER JOIN @RecentOrders ro ON c.customer_id = ro.customer_id;
-- ✅ CTE met MATERIALIZED hint (PostgreSQL 12+)
-- Forceert materialization van CTE resultaat
WITH MATERIALIZED CustomerOrders AS (
SELECT
customer_id,
COUNT(*) AS order_count,
SUM(total_amount) AS total_spent
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY customer_id
)
SELECT
c.customer_id,
c.company_name,
co.order_count,
co.total_spent
FROM customers c
INNER JOIN CustomerOrders co ON c.customer_id = co.customer_id
WHERE co.order_count > 5;
8. Parallel Query Execution
Parallelism Control
Parallel query execution kan performance verbeteren voor grote queries, maar vereist zorgvuldige configuratie.
Parallel Query Optimization
-- ✅ GOEDE PRACTICE: Parallel query configuration
-- Check huidige parallelism settings
SELECT
name,
value,
value_in_use,
description
FROM sys.configurations
WHERE name LIKE '%parallel%';
-- Find queries using parallelism
SELECT
qs.sql_handle,
qs.query_hash,
SUBSTRING(st.text, (qs.statement_start_offset/2)+1,
((CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(st.text)
ELSE qs.statement_end_offset
END - qs.statement_start_offset)/2)+1) AS query_text,
qp.query_plan,
qs.execution_count,
qs.total_worker_time / qs.execution_count AS avg_cpu_time,
qs.total_elapsed_time / qs.execution_count AS avg_elapsed_time,
qs.total_physical_reads / qs.execution_count AS avg_physical_reads
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
WHERE qp.query_plan.value('declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan";
count(//p:RelOp[@Parallel = "1"])', 'int') > 0
ORDER BY qs.total_worker_time DESC;
-- Voorbeeld: Parallel query met cost threshold
SELECT
c.customer_id,
c.company_name,
o.order_id,
o.order_date,
od.product_id,
od.quantity,
od.unit_price,
p.product_name
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
INNER JOIN order_details od ON o.order_id = od.order_id
INNER JOIN products p ON od.product_id = p.product_id
WHERE o.order_date >= '2023-01-01'
AND c.country = 'Netherlands'
ORDER BY o.order_date DESC;
-- Gebruik MAXDOP hint om parallelism te controleren
SELECT
c.customer_id,
c.company_name,
COUNT(*) AS order_count,
SUM(o.total_amount) AS total_spent
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_date >= '2023-01-01'
GROUP BY c.customer_id, c.company_name
HAVING COUNT(*) > 10
OPTION (
MAXDOP 4, -- Maximum degree of parallelism
USE HINT ('ENABLE_PARALLEL_PLAN_PREFERENCE')
);
-- Disable parallelism voor kleine queries
SELECT
c.customer_id,
c.company_name,
o.order_date,
o.total_amount
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
WHERE c.customer_id = 12345
OPTION (MAXDOP 1); -- Forceer serial execution
-- PostgreSQL: Parallel query hints
SET max_parallel_workers_per_gather = 4;
SET parallel_setup_cost = 1000;
SET parallel_tuple_cost = 0.1;
SELECT /*+ Parallel(customers 4) */
c.customer_id,
c.company_name,
COUNT(o.order_id) AS order_count,
SUM(o.total_amount) AS total_spent
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_date >= '2023-01-01'
GROUP BY c.customer_id, c.company_name;
9. Statistics en Maintenance
Statistics Management
Database statistics zijn essentieel voor de query optimizer. Verouderde statistics leiden tot suboptimale execution plans.
Statistics Monitoring en Maintenance
-- ✅ GOEDE PRACTICE: Statistics monitoring en update
-- Check verouderde statistics
SELECT
OBJECT_NAME(s.object_id) AS table_name,
s.name AS statistics_name,
sp.last_updated,
sp.rows,
sp.rows_sampled,
sp.modification_counter,
CASE
WHEN sp.modification_counter > sp.rows * 0.20
THEN 'UPDATE NEEDED'
ELSE 'OK'
END AS update_status
FROM sys.stats s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp
WHERE OBJECT_NAME(s.object_id) NOT LIKE 'sys%'
ORDER BY sp.modification_counter DESC;
-- Automatische statistics update voor specifieke tabel
DECLARE @sql NVARCHAR(MAX) = '';
SELECT @sql = @sql +
'UPDATE STATISTICS ' + QUOTENAME(OBJECT_NAME(s.object_id)) +
' ' + QUOTENAME(s.name) +
' WITH FULLSCAN; '
FROM sys.stats s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp
WHERE OBJECT_NAME(s.object_id) = 'orders'
AND sp.modification_counter > sp.rows * 0.20;
EXEC sp_executesql @sql;
-- Create filtered statistics voor partitioned data
CREATE STATISTICS stats_orders_2024
ON orders (customer_id, order_date, total_amount)
WHERE order_date >= '2024-01-01';
-- Update statistics met sample rate
UPDATE STATISTICS orders
WITH SAMPLE 50 PERCENT,
PERSIST_SAMPLE_PERCENT = ON;
-- PostgreSQL: Statistics management
-- Check statistics
SELECT
schemaname,
tablename,
attname,
n_distinct,
correlation,
most_common_vals,
most_common_freqs
FROM pg_stats
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
AND tablename = 'orders';
-- Manual statistics update
ANALYZE VERBOSE orders;
-- Extended statistics voor correlated columns
CREATE STATISTICS orders_customer_date (customer_id, order_date)
ON orders;
-- Statistics voor expressies
CREATE STATISTICS orders_year_month (EXTRACT(YEAR FROM order_date), EXTRACT(MONTH FROM order_date))
ON orders;
10. Monitoring Tools en Scripts
Performance Monitoring Toolkit
Monitoring tools en scripts helpen bij het proactief identificeren en oplossen van performance issues.
Essentiële Monitoring Scripts
-- ✅ GOEDE PRACTICE: Comprehensive monitoring toolkit
-- 1. Top 10 langst lopende queries
SELECT TOP 10
qs.total_elapsed_time / 1000000.0 AS total_elapsed_time_sec,
qs.total_worker_time / 1000000.0 AS total_worker_time_sec,
qs.total_logical_reads,
qs.total_physical_reads,
qs.execution_count,
qs.total_elapsed_time / qs.execution_count / 1000000.0 AS avg_elapsed_time_sec,
SUBSTRING(st.text, (qs.statement_start_offset/2)+1,
((CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(st.text)
ELSE qs.statement_end_offset
END - qs.statement_start_offset)/2)+1) AS query_text,
qp.query_plan
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
WHERE qs.execution_count > 0
ORDER BY qs.total_elapsed_time DESC;
-- 2. Wait statistics analysis
SELECT
wait_type,
waiting_tasks_count,
wait_time_ms,
max_wait_time_ms,
signal_wait_time_ms,
wait_time_ms / waiting_tasks_count AS avg_wait_time_ms,
100.0 * wait_time_ms / SUM(wait_time_ms) OVER() AS pct_total_waits
FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN (
'CLR_SEMAPHORE', 'LAZYWRITER_SLEEP', 'RESOURCE_QUEUE',
'SLEEP_TASK', 'SLEEP_SYSTEMTASK', 'WAITFOR',
'BROKER_EVENTHANDLER', 'BROKER_RECEIVE_WAITFOR',
'CHECKPOINT_QUEUE', 'CHKPT', 'XE_TIMER_EVENT'
)
AND waiting_tasks_count > 0
ORDER BY wait_time_ms DESC;
-- 3. Missing indexes analysis
SELECT
migs.avg_total_user_cost * (migs.avg_user_impact / 100.0) * (migs.user_seeks + migs.user_scans) AS improvement_measure,
DB_NAME(mid.database_id) AS database_name,
OBJECT_NAME(mid.object_id, mid.database_id) AS table_name,
mid.equality_columns,
mid.inequality_columns,
mid.included_columns,
migs.user_seeks,
migs.user_scans,
migs.last_user_seek,
migs.avg_total_user_cost,
migs.avg_user_impact
FROM sys.dm_db_missing_index_details mid
INNER JOIN sys.dm_db_missing_index_groups mig
ON mid.index_handle = mig.index_handle
INNER JOIN sys.dm_db_missing_index_group_stats migs
ON mig.index_group_handle = migs.group_handle
WHERE mid.database_id = DB_ID()
ORDER BY improvement_measure DESC;
-- 4. Blocking chain analysis
SELECT
blocking.session_id AS blocking_session_id,
blocked.session_id AS blocked_session_id,
blocking_text.text AS blocking_text,
blocked_text.text AS blocked_text,
blocking.wait_type AS blocking_wait_type,
blocked.wait_type AS blocked_wait_type,
blocking.wait_time AS blocking_wait_time,
blocked.wait_time AS blocked_wait_time
FROM sys.dm_exec_requests blocked
INNER JOIN sys.dm_exec_requests blocking
ON blocked.blocking_session_id = blocking.session_id
CROSS APPLY sys.dm_exec_sql_text(blocked.sql_handle) blocked_text
CROSS APPLY sys.dm_exec_sql_text(blocking.sql_handle) blocking_text
WHERE blocked.blocking_session_id > 0;
-- 5. Memory usage monitoring
SELECT
type,
name,
pages_kb / 1024.0 AS pages_mb,
virtual_memory_reserved_kb / 1024.0 AS vm_reserved_mb,
virtual_memory_committed_kb / 1024.0 AS vm_committed_mb,
awe_allocated_kb / 1024.0 AS awe_mb
FROM sys.dm_os_memory_clerks
WHERE pages_kb > 0
ORDER BY pages_kb DESC;
11. Praktijk Case Studies
Real-World Performance Problemen
Praktijk case studies tonen hoe performance issues worden geïdentificeerd en opgelost.
Case Study 1: E-commerce Platform
Probleem: Trage product search queries tijdens piekuren (15+ seconden).
Root Cause: Volledige tabel scans op products tabel met 10M+ rows, geen geschikte indexes.
Oplossing:
-- Originele trage query
SELECT *
FROM products
WHERE category_id = @category_id
AND (product_name LIKE '%' + @search_term + '%'
OR description LIKE '%' + @search_term + '%')
AND price BETWEEN @min_price AND @max_price
ORDER BY
CASE @sort_by
WHEN 'price' THEN price
WHEN 'name' THEN product_name
END
OFFSET @page_size * (@page_number - 1) ROWS
FETCH NEXT @page_size ROWS ONLY;
-- Oplossing 1: Full-text search index
CREATE FULLTEXT CATALOG ft_catalog AS DEFAULT;
CREATE FULLTEXT INDEX ON products(product_name, description)
KEY INDEX pk_products;
-- Oplossing 2: Filtered index voor actieve producten
CREATE INDEX idx_products_active
ON products(category_id, price)
INCLUDE (product_name, stock_quantity)
WHERE is_active = 1 AND stock_quantity > 0;
-- Oplossing 3: Geoptimaliseerde query met full-text search
SELECT
p.product_id,
p.product_name,
p.price,
p.stock_quantity,
KEY_TBL.RANK
FROM products p
INNER JOIN CONTAINSTABLE(products, (product_name, description), @search_term) AS KEY_TBL
ON p.product_id = KEY_TBL.[KEY]
WHERE p.category_id = @category_id
AND p.is_active = 1
AND p.stock_quantity > 0
AND p.price BETWEEN @min_price AND @max_price
ORDER BY
CASE @sort_by
WHEN 'relevance' THEN KEY_TBL.RANK
WHEN 'price' THEN p.price
WHEN 'name' THEN p.product_name
END
OFFSET @page_size * (@page_number - 1) ROWS
FETCH NEXT @page_size ROWS ONLY;
Resultaat: Query tijd gereduceerd van 15+ seconden naar < 100ms. CPU usage gereduceerd met 70%.
Case Study 2: Financial Reporting System
Probleem: Maandelijkse rapportage queries timeouts na 30 minuten.
Root Cause: Complexe aggregaties over 100M+ rows zonder partitionering.
Oplossing:
-- Originele trage aggregatie query
SELECT
account_id,
YEAR(transaction_date) AS transaction_year,
MONTH(transaction_date) AS transaction_month,
transaction_type,
COUNT(*) AS transaction_count,
SUM(amount) AS total_amount,
AVG(amount) AS avg_amount,
MIN(transaction_date) AS first_transaction,
MAX(transaction_date) AS last_transaction
FROM transactions
WHERE transaction_date >= DATEADD(year, -2, GETDATE())
AND status = 'completed'
GROUP BY
account_id,
YEAR(transaction_date),
MONTH(transaction_date),
transaction_type
HAVING COUNT(*) > 1;
-- Oplossing 1: Table partitionering
CREATE PARTITION FUNCTION pf_transaction_date (DATE)
AS RANGE RIGHT FOR VALUES (
'2023-01-01',
'2023-04-01',
'2023-07-01',
'2023-10-01',
'2024-01-01',
'2024-04-01'
);
-- Oplossing 2: Columnstore index voor analytics
CREATE COLUMNSTORE INDEX idx_columnstore_transactions
ON transactions (
account_id,
transaction_date,
transaction_type,
amount,
status
)
WHERE transaction_date >= DATEADD(year, -2, GETDATE());
-- Oplossing 3: Materialized view voor veelgebruikte aggregaties
CREATE MATERIALIZED VIEW mv_monthly_transaction_summary
WITH (DISTRIBUTION = HASH(account_id))
AS
SELECT
account_id,
YEAR(transaction_date) AS transaction_year,
MONTH(transaction_date) AS transaction_month,
transaction_type,
COUNT_BIG(*) AS transaction_count,
SUM(amount) AS total_amount,
AVG(amount) AS avg_amount,
MIN(transaction_date) AS first_transaction,
MAX(transaction_date) AS last_transaction
FROM transactions
WHERE status = 'completed'
GROUP BY
account_id,
YEAR(transaction_date),
MONTH(transaction_date),
transaction_type;
Resultaat: Query tijd gereduceerd van 30+ minuten naar 45 seconden. Storage gebruik gereduceerd met 60% door compressie.
Klaar voor Database Performance Projecten?
Vind de juiste SQL performance experts of plaats je database vacature
Conclusie en Best Practices Checklist
SQL Performance optimalisatie is een continue proces van monitoring, analyse en verbetering. Hier is je checklist:
Daily Monitoring
- Check slow query logs
- Monitor wait statistics
- Review blocking chains
- Check disk I/O latency
- Monitor memory pressure
Weekly Maintenance
- Update statistics voor actieve tabellen
- Reorganize/rebuild fragmented indexes
- Review execution plan changes
- Validate backup en recovery
Monthly Optimization
- Analyze query performance trends
- Implement missing indexes
- Review en update maintenance plans
- Capacity planning en forecasting
- Security audit en compliance check
Quick Performance Tips
Immediate Wins
- Gebruik parameterized queries
- Vermijd SELECT *
- Index foreign keys
- Gebruik pagination voor grote result sets
- Vermijd functions in WHERE clauses
Medium Term
- Implement query store
- Set up automated monitoring
- Create performance baselines
- Implement CI/CD voor database changes
- Regular query performance reviews
Long Term
- Database partitioning strategy
- Query performance testing framework
- Advanced indexing strategy
- Disaster recovery planning
- Performance SLA definition
Veelgestelde Vragen (FAQ)
Q: Hoe identificeer ik de meest kritieke queries om te optimaliseren?
A: Focus op queries met: 1) Hoogste execution frequency, 2) Langste execution time, 3) Meeste CPU/I/O gebruik, 4) Impact op gebruikerservaring, 5) Business critical functies.
Q: Wanneer moet ik query hints gebruiken?
A: Gebruik hints alleen als laatste optie wanneer: 1) Query optimizer suboptimale plannen produceert, 2) Je het gedrag consistent wilt forceren, 3) Je performance problemen hebt die niet anders opgelost kunnen worden, 4) Je een workaround nodig hebt voor optimizer limitations.
Q: Hoe monitor ik database performance in real-time?
A: Implementeer een combinatie van: 1) Built-in DMVs en system views, 2) Third-party monitoring tools, 3) Custom alerting op thresholds, 4) Dashboard met key metrics, 5) Automated reporting.
Q: Wat zijn de meest voorkomende index misvattingen?
A: 1) Meer indexes = beter performance (fout), 2) Alle queries hebben indexes nodig (fout), 3) Primary key is altijd genoeg (fout), 4) Index maintenance is niet nodig (fout), 5) Alle columns moeten geïndexeerd worden (fout).