SQL Server'da Partitioning: Partition Function, Scheme, Aligned Index, Partition Elimination ve SWITCH
SQL Server'da partitioning'in asıl kazancı SWITCH ile milisaniyede arşivlemek; sorgu kazancı ise ancak partition elimination çalışırsa gelir. Lab'da on bir ayı ay başına 10 ms'nin altında arşive taşıdık; buna karşılık AddWithValue(DateTime), Scale = 0 ve EF Core'un datetime2(7) parametreleri datetime2(0) kolonda elimination'ı kapattı ve 52 partition'ın hepsi okundu. RANGE LEFT/RIGHT, aligned index, SPLIT/MERGE maliyeti ve LOCK_ESCALATION = AUTO.
Sürüm notu: Bu yazıdaki örnekler SQL Server 2025 (CU10, 17.0.5035.5), .NET 10, Microsoft.Data.SqlClient 7.1.2 ve Microsoft.EntityFrameworkCore.SqlServer 10.0.15 ile tek makinelik bir lab'da çalıştırılmıştır. Süreleri mutlak hız olarak okuma; karşılaştırmada önemli olan logical reads, okunan partition sayısı ve log miktarı.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı), 07'de PostgreSQL'de yaptığını SQL Server'da da yaptı: dbo.Orders'ın aynısını CreatedAt kolonuna göre aylık partition'lara bölen dbo.OrdersP, 52 partition, aynı 1 milyon sipariş. Muhasebe ekibinin ekim ayı cirosu sorgusu:
SELECT COUNT(*), SUM(TotalAmount) FROM dbo.OrdersP WHERE CreatedAt >= '2026-10-01' AND CreatedAt < '2026-11-01';
Ekim ayının bütün satırları tek bir partition'da. Tahmin et: SQL Server bu sorgu için kaç partition okur? SET STATISTICS IO çıktısı ve gerçek execution plan'ın XML'indeki RunTimePartitionSummary değeri, yanında partition'sız tablodaki aynı sorgu:
'OrdersP'. Scan count 52, logical reads 5799, physical reads 0
partitions accessed: 52
'Orders'. Scan count 11, logical reads 5785, physical reads 0
52 partition'ın hepsi. Partition'lı tablo, partition'sız tabloyla neredeyse aynı sayıda sayfa okudu. Sebep partitioning'de değil, sorgunun yazılışında: SQL Server bu basit sorguyu otomatik olarak parametrelendirdi ve tarih literal'lerini varchar(8000) tipinde parametreye çevirdi. Tip uyuşmazlığı partition elimination'ı kapattı. Aynı tuzağa .NET'ten de düşülüyor; bu yazıda önce partition function, scheme ve aligned index'i kuruyor, sonra elimination'ın hangi koşulda çalıştığını ölçüyor, en sonda da 03'teki arşivlemeyi sliding window ile milisaniyelere indiriyoruz.
Partition Function, Scheme ve RANGE LEFT/RIGHT
SQL Server'da partitioning iki nesneyle kurulur. Partition function bir değerin hangi partition numarasına düştüğünü belirler; partition scheme bu numaraları filegroup'lara eşler. Sınır değerinin hangi tarafa düştüğünü RANGE LEFT ya da RANGE RIGHT belirler. İki sınırlı iki fonksiyon ve $PARTITION fonksiyonu ile farkı görmek kolay:
CREATE PARTITION FUNCTION pf_left (datetime2(0)) AS RANGE LEFT FOR VALUES ('2026-10-01', '2026-11-01');
CREATE PARTITION FUNCTION pf_right (datetime2(0)) AS RANGE RIGHT FOR VALUES ('2026-10-01', '2026-11-01');
GO
SELECT v.CreatedAt, $PARTITION.pf_left(v.CreatedAt) AS LeftPartition, $PARTITION.pf_right(v.CreatedAt) AS RightPartition
FROM (VALUES (CAST('2026-09-30 23:59:59' AS datetime2(0))), ('2026-10-01 00:00:00'), ('2026-10-31 23:59:59'), ('2026-11-01 00:00:00')) AS v(CreatedAt);
CreatedAt|LeftPartition|RightPartition
---------|-------------|--------------
2026-09-30 23:59:59|1|1
2026-10-01 00:00:00|1|2
2026-10-31 23:59:59|2|2
2026-11-01 00:00:00|2|3
RANGE LEFT'te 1 Ekim gece yarısı eylül partition'ına düşüyor; ekimin ilk anı yanlış ayda. RANGE RIGHT'ta sınır değeri sağdaki partition'ın alt sınırı oluyor ve her ay kendi gece yarısıyla başlıyor. Tarih kolonlarında RANGE RIGHT bu yüzden pratik; Microsoft'un dokümanı da tarih tipleri için aynı öneriyi yapıyor. Belirtilmezse varsayılan LEFT, bunu unutmak sık yapılan bir hata.
Bookstore'un aylık fonksiyonu 2022 Kasım ile 2027 Ocak arasındaki 51 sınırı GENERATE_SERIES ile üretiyor:
DECLARE @boundaries nvarchar(max) = (
SELECT STRING_AGG(CONCAT('''', CONVERT(char(10), DATEADD(MONTH, value, '2022-11-01'), 23), ''''), ', ')
WITHIN GROUP (ORDER BY value)
FROM GENERATE_SERIES(0, 50));
EXEC (N'CREATE PARTITION FUNCTION pf_orders_month (datetime2(0)) AS RANGE RIGHT FOR VALUES (' + @boundaries + N')');
GO
CREATE PARTITION SCHEME ps_orders_month AS PARTITION pf_orders_month ALL TO ([PRIMARY]);
51 sınır 52 partition demek: en soldaki (2022 Kasım'dan önce) ve en sağdaki (2027 Ocak'tan sonra) boş kalıyor; sliding window'da bu boş uçlar önemli. Bütün partition'lar PRIMARY filegroup'unda. Dokümana göre ayrı filegroup'lar yalnız eski partition'ları daha ucuz diske koymak ya da filegroup bazında backup/restore yapmak istiyorsan anlamlı; gerisinde yönetim yükü getiriyor. GENERATE_SERIES SQL Server 2022'den beri var; 2019'da sınır listesini bir döngüyle ya da elle yazmak gerekiyor. Partitioning'in kendisi SQL Server 2016 Service Pack 1'den beri bütün edition'larda.
Tablo, Aligned Index ve Foreign Key
Tablo ve index'ler scheme üzerinde, partition kolonu verilerek oluşturulur:
CREATE TABLE dbo.OrdersP (
OrderId bigint IDENTITY NOT NULL,
CustomerId int NOT NULL,
Status varchar(20) NOT NULL,
TotalAmount decimal(12,2) NOT NULL,
Currency char(3) NOT NULL CONSTRAINT DF_OrdersP_Currency DEFAULT 'TRY',
CreatedAt datetime2(0) NOT NULL,
CONSTRAINT PK_OrdersP PRIMARY KEY CLUSTERED (OrderId, CreatedAt)
) ON ps_orders_month (CreatedAt);
CREATE TABLE dbo.OrderItemsP (
OrderId bigint NOT NULL,
LineNumber smallint NOT NULL,
CreatedAt datetime2(0) NOT NULL,
BookId int NOT NULL,
Quantity int NOT NULL,
UnitPrice decimal(10,2) NOT NULL,
CONSTRAINT PK_OrderItemsP PRIMARY KEY CLUSTERED (OrderId, LineNumber, CreatedAt)
) ON ps_orders_month (CreatedAt);
CREATE INDEX IX_OrdersP_CustomerId_CreatedAt ON dbo.OrdersP (CustomerId, CreatedAt DESC)
INCLUDE (Status, TotalAmount) ON ps_orders_month (CreatedAt);
PostgreSQL'deki kural burada da geçerli: unique bir clustered index ya da primary key partition kolonunu içermek zorunda. Aynı scheme üzerinde kurulan index'e aligned index denir. Aligned olmayan bir index (örneğin OrderId üzerinde tek parça bir unique index) OrderId'nin tekilliğini garanti eder ama partition'ı tablodan ayırmayı imkânsız kılar; SWITCH için tablonun bütün index'leri aligned olmalı. Sipariş kalemleri de aynı fonksiyonla bölündü ve CreatedAt kolonu kalemlere taşındı. İlk denemede iki tablo arasına bir foreign key koyduk ve eski ayı tablodan çıkarmayı denedik:
ALTER TABLE dbo.OrderItemsP ADD CONSTRAINT FK_OrderItemsP_OrdersP
FOREIGN KEY (OrderId, CreatedAt) REFERENCES dbo.OrdersP (OrderId, CreatedAt);
GO
ALTER TABLE dbo.OrdersP SWITCH PARTITION 2 TO dbo.OrdersArchiveStage;
Msg 4967, Level 16, State 1, Server sql1, Line 1
ALTER TABLE SWITCH statement failed. SWITCH is not allowed because source table 'dbscaleseries_08.dbo.OrdersP' contains primary key for constraint 'FK_OrderItemsP_OrdersP'.
PostgreSQL'de önce kalemleri ayırıp foreign key'i düşürerek ilerleyebilmiştik. SQL Server'da referans alınan tablo, foreign key etkin olduğu sürece hiç SWITCH edilemiyor; kalemlerin partition'ı boşalmış olsa bile. Foreign key'i NOCHECK ile devre dışı bırakınca SWITCH çalıştı, ama devre dışı bir foreign key is_not_trusted = 1 olur ve hiçbir şeyi garanti etmez. Bookstore'da foreign key'i kaldırdık: sipariş ve kalemleri Ordering module'ü tek bir transaction'da yazıyor, bütünlük oradan geliyor. Bu bilinçli bir takas; büyük partition'lı tablolarda yaygın bir seçim.
Partition Elimination'ı Plan'da Görmek
Yazının başındaki sorunun kaynağı parametre tipi. Sorgu planındaki parametre listesi bunu gösteriyor: ParameterDataType="varchar(8000)". Aynı sorgu OPTION (RECOMPILE) ile literal değerlerle derlenince tek partition okunuyor:
'OrdersP'. Scan count 1, logical reads 239, physical reads 0
partitions accessed: 1
partition range: 49 - 49
5.799 yerine 239 logical read. Uygulamadan gelen sorgular hep parametreli olduğu için asıl soru, .NET'in hangi tipte parametre gönderdiği. Aynı ciro sorgusunu SqlClient ve EF Core ile çalıştırıp her seferinde plan XML'ini okuduk:
await RunAsync(connection, "AddWithValue(DateTime)", command =>
{
command.Parameters.AddWithValue("@from", from);
command.Parameters.AddWithValue("@to", to);
});
await RunAsync(connection, "SqlDbType.DateTime2, Scale = 0", command =>
{
command.Parameters.Add(new SqlParameter("@from", SqlDbType.DateTime2) { Scale = 0, Value = from });
command.Parameters.Add(new SqlParameter("@to", SqlDbType.DateTime2) { Scale = 0, Value = to });
});
AddWithValue(DateTime)
Table 'OrdersP'. Scan count 52, logical reads 5799
parameter types: datetime
partitions accessed: 52
SqlDbType.DateTime2, Scale = 0
Table 'OrdersP'. Scan count 52, logical reads 5799
parameter types: datetime2(7)
partitions accessed: 52
DateTime2 + CAST(@p AS datetime2(0)) in SQL
Table 'OrdersP'. Scan count 2, logical reads 340
parameter types: datetime2(7)
partitions accessed: 2
EF Core LINQ (HasColumnType datetime2(0)): revenue=60212828.00
parameters: (@from datetime2(7),@to datetime2(7)) logical reads: 5799
Dört sonuç var. AddWithValue bir DateTime'ı eski datetime tipiyle gönderir. Scale = 0 vermek işe yaramadı: SqlClient parametreyi yine datetime2(7) olarak bildirdi. EF Core da kolonun tipini datetime2(0) olarak bildiği hâlde parametreyi datetime2(7) gönderdi. Üçünde de elimination çalışmadı. Yalnız SQL tarafında CAST(@from AS datetime2(0)) yazınca 2 partition okundu. Sebebi tip önceliği: datetime2(7) bir değerle datetime2(0) kolon karşılaştırılınca dönüşüm kolon tarafına uygulanıyor ve SQL Server partition'ı kolonun değerinden hesaplayamıyor. Aynı sorguyu sp_executesql ile, SqlClient'ın gönderdiği biçimde farklı parametre tipleriyle ve partition kolonu datetime2(7) olan ikinci bir tabloda (OrdersP7) tekrarladık:
| Kolon tipi | datetime parametre | datetime2(7) parametre | datetime2(0) parametre |
|---|---|---|---|
datetime2(0) | 52 partition, 5.799 read | 52 partition, 5.799 read | 2 partition, 340 read |
datetime2(7) | 52 partition, 4.818 read | 2 partition, 284 read | 2 partition, 284 read |
Pratik sonuç: partition kolonunu .NET'in varsayılan olarak gönderdiği tiple aynı seç (datetime2(7), EF Core'un DateTime için varsayılanı) ve AddWithValue kullanma. Mevcut bir datetime2(0) kolonunda ise sorguyu CAST ile yaz ya da parametreyi açıkça bildir ve plan'da elimination'ı doğrula. Bunu bilmeden yapılan partitioning'in bütün sorgu kazancı sessizce kaybolur.
Bir ayrıntı daha: parametreli sorgular 1 değil 2 partition okudu. Üst sınır < @to olsa da SQL Server çalışma anında @to'nun düştüğü partition'ı da aralığa kattı (plan'da partition range: 49 - 50); literal'li ve RECOMPILE'lı sorgu yalnız 49'u okudu. Okunan ikinci partition'ın ilk sayfaları dışında maliyeti küçük, ama aşağıdaki lock örneğinde önemli olacak.
Partition Kolonu Olmayan Sorgular
Elimination'ın olmadığı ikinci durum, sorgunun partition kolonunu hiç içermemesi:
SELECT TOP (20) OrderId, Status, TotalAmount, CreatedAt FROM dbo.OrdersP WHERE CustomerId = 4242 ORDER BY CreatedAt DESC;
SELECT Status, TotalAmount FROM dbo.OrdersP WHERE OrderId = 750000;
'OrdersP'. Scan count 52, logical reads 96, physical reads 0
'Orders'. Scan count 1, logical reads 4, physical reads 0
'OrdersP'. Scan count 52, logical reads 96, physical reads 0
'Orders'. Scan count 0, logical reads 3, physical reads 0
"Siparişlerim" 4 yerine 96, sipariş numarasıyla tek satır okuma 3 yerine 96 logical read. Microsoft'un dokümanı da partition kolonunu içermeyen tek satır seek'lerin partition sayısı kadar seek yaptığını ve bu yüzden partitioning'in OLTP (Online Transaction Processing; çok sayıda kısa okuma ve yazma yapan işlem sistemleri) sistemlerinde nadiren hız kazandırdığını söylüyor. Bookstore'da sipariş detayı sayfası URL'de sipariş numarasıyla açılıyor; bu sorguyu CreatedAt ile birlikte yazabilmek için URL'ye ya da önceki ekrandan gelen veriye tarih eklemek gerekiyor. Aksi hâlde her sipariş detayı 42–52 seek demek.
Sliding Window: SPLIT, SWITCH ve MERGE
Sliding window, tabloda sabit bir pencere tutmaktır: her ay sağ uçta yeni bir boş partition açılır, sol uçtaki en eski ay tablodan çıkarılır. Üç işlemin maliyeti, partition'ın boş olup olmamasına bağlı. Aynı transaction içinde ürettikleri log'u ölçtük:
ALTER PARTITION SCHEME ps_orders_month NEXT USED [PRIMARY];
ALTER PARTITION FUNCTION pf_orders_month() SPLIT RANGE ('2027-02-01');
SELECT 'split empty' AS step, database_transaction_log_bytes_used / 1024 AS log_kb, DATEDIFF(ms, @t, SYSDATETIME()) AS ms
FROM sys.dm_tran_database_transactions WHERE database_id = DB_ID() AND transaction_id = CURRENT_TRANSACTION_ID();
step|log_kb|ms
----|------|--
split empty|6|6
step|log_kb|ms
----|------|--
split populated|13080|448
step|log_kb|ms
----|------|--
merge populated|3851|180
Kodda boş sağ ucu bölen adım var; aynı ölçümü ekim ayının dolu partition'ını SPLIT RANGE ('2026-10-16') ile bölüp MERGE RANGE ile geri birleştirerek de yaptık. Boş partition'ı bölmek yalnız metadata değişikliği: 6 KB log. Dolu partition'ı bölmek 13 MB log üretti, çünkü SQL Server satırları yeni partition'a taşıdı; geri birleştirmek 3,8 MB daha. Bu sırada tablo üzerinde schema modification lock'u tutulur, yani tablo bütün okuma ve yazmalara kapanır. 42 bin siparişlik bir ayda 448 ms; 4 milyon siparişlik bir ayda dakikalar. Kural: yalnız boş partition'ları böl, yeni ayları önceden aç.

En eski ayı çıkarmanın yolu: partition'ın verisi aynı filegroup'ta duran, aynı yapıda ve aynı index'lere sahip boş bir tabloya yalnız metadata değişikliğiyle geçer (SWITCH). Ardından MERGE RANGE iki boş partition'ı birleştirir. Bütün adımları bir procedure'e koyduk; çekirdeği:
BEGIN TRANSACTION;
ALTER TABLE dbo.OrderItemsP SWITCH PARTITION 2 TO dbo.OrderItemsArchiveStage
WITH (WAIT_AT_LOW_PRIORITY (MAX_DURATION = 1 MINUTES, ABORT_AFTER_WAIT = SELF));
ALTER TABLE dbo.OrdersP SWITCH PARTITION 2 TO dbo.OrdersArchiveStage
WITH (WAIT_AT_LOW_PRIORITY (MAX_DURATION = 1 MINUTES, ABORT_AFTER_WAIT = SELF));
ALTER PARTITION FUNCTION pf_orders_month() MERGE RANGE (@oldest);
COMMIT;
SET @switchMs = DATEDIFF(MILLISECOND, @t, SYSDATETIME());
BEGIN TRANSACTION;
INSERT INTO dbo.OrdersArchive (OrderId, CustomerId, Status, TotalAmount, Currency, CreatedAt)
SELECT OrderId, CustomerId, Status, TotalAmount, Currency, CreatedAt FROM dbo.OrdersArchiveStage;
SET @orders = @@ROWCOUNT;
INSERT INTO dbo.OrderItemsArchive (OrderId, LineNumber, BookId, Quantity, UnitPrice)
SELECT OrderId, LineNumber, BookId, Quantity, UnitPrice FROM dbo.OrderItemsArchiveStage;
TRUNCATE TABLE dbo.OrdersArchiveStage;
TRUNCATE TABLE dbo.OrderItemsArchiveStage;
COMMIT;
SWITCH her iki tabloda da schema modification lock'u ister; uzun bir sorgu tabloyu tutuyorsa bekler ve beklerken arkasındaki herkesi bekletir. WAIT_AT_LOW_PRIORITY bekleme sırasında diğer sorguların önüne geçmemesini, ABORT_AFTER_WAIT = SELF ise bir dakika içinde lock alamazsa kendisinin vazgeçmesini sağlar; job ertesi gün tekrar dener. Üç adımın aynı transaction'da olması, kalemler çıkarılıp siparişler çıkarılamazsa her şeyin geri alınmasını garanti eder. 03'teki dbo.OrdersArchive tablolarına kopyalama ikinci, ayrı bir transaction; canlı tabloyu etkilemiyor. Procedure'ü 3 yıllık pencereyle çalıştırdık:
EXEC dbo.SlideOrdersWindow @keepFrom = '2023-11-01', @openUntil = '2027-02-01';
archived 2022-12: 944 orders, switch+merge 10 ms, copy 10 ms
archived 2023-01: 1844 orders, switch+merge 5 ms, copy 20 ms
archived 2023-02: 2440 orders, switch+merge 10 ms, copy 20 ms
archived 2023-03: 3558 orders, switch+merge 5 ms, copy 30 ms
archived 2023-04: 4301 orders, switch+merge 10 ms, copy 30 ms
archived 2023-05: 5330 orders, switch+merge 10 ms, copy 35 ms
archived 2023-06: 6016 orders, switch+merge 5 ms, copy 39 ms
archived 2023-07: 7101 orders, switch+merge 5 ms, copy 40 ms
archived 2023-08: 8003 orders, switch+merge 5 ms, copy 50 ms
archived 2023-09: 8601 orders, switch+merge 5 ms, copy 55 ms
archived 2023-10: 9774 orders, switch+merge 6 ms, copy 63 ms
Kasım 2022'yi foreign key denemesinde elle çıkarmıştık; kalan on bir ay, canlı tabloya dokunan kısmı ay başına 10 ms'nin altında olacak şekilde arşive taşındı. Toplam 58.047 sipariş; 03'teki keyset batch job'unun aynı iş için ürettiği log ve lock'ların hiçbiri canlı tabloda yok. Arşive kopyalamaya gerek yoksa TRUNCATE TABLE dbo.OrdersP WITH (PARTITIONS (2 TO 4)) partition'ları doğrudan boşaltır; lab'da ayrı bir tabloda 2.923 satırı sildi. Procedure, @openUntil'e kadar boş ayları da açıyor; bir sonraki çalışmada @openUntil = '2027-03-01' ile opened partition from 2027-03-01 yazdı.
Lock Escalation'ı Partition Seviyesine Almak
03'te 5.000 lock'u aşan bir silmenin bütün tabloyu kilitlediğini görmüştük. Partition'lı tabloda LOCK_ESCALATION = AUTO bu escalation'ı tablo yerine partition (HoBT, Heap or B-Tree; bir index'in bir partition'ı) seviyesinde yapar. Bir session ekim ayının 41 bin siparişini güncelleyip transaction'ı açık tutarken başka bir session eylül ayını okudu:
ALTER TABLE dbo.OrdersP SET (LOCK_ESCALATION = AUTO);
== LOCK_ESCALATION = AUTO
resource_type|request_mode|locks
-------------|------------|-----
HOBT|IX|1
OBJECT|IX|1
HOBT|X|1
(3 rows affected)
SeptemberOrders
---------------
39410
(1 rows affected)
== LOCK_ESCALATION = TABLE
resource_type|request_mode|locks
-------------|------------|-----
OBJECT|X|1
(1 rows affected)
Msg 1222, Level 16, State 56, Server sql1, Line 6
Lock request time out period exceeded.
Varsayılan TABLE ile güncelleme tabloyu X ile kilitledi ve eylül okuması 3 saniyelik lock timeout'a takıldı. AUTO ile yalnız ekimin partition'ı X ile kilitlendi ve okuma tamamlandı. Bir koşulla: okuma ekim partition'ına hiç dokunmamalı. Aynı okumayı parametreli yazdığımızda, yukarıda gördüğümüz "bir sonraki partition'ı da kat" davranışı yüzünden ekim partition'ına uğradı ve yine bekledi. Microsoft'un dokümanı AUTO'nun farklı partition'larda escalation yapan session'lar arasında deadlock riskini artırabileceğini de belirtiyor; bu yüzden varsayılan değil.
SQL Server ve PostgreSQL: Kıyas
| Konu | PostgreSQL (07) | SQL Server (08) |
|---|---|---|
| Tanım | Parent tablo + PARTITION OF child tablolar | Partition function + scheme, tablo scheme üzerinde |
| Sınır | Alt sınır dahil, üst sınır hariç | RANGE LEFT ya da RIGHT seçilir (varsayılan LEFT) |
| Arşiv | DETACH PARTITION ... CONCURRENTLY | SWITCH + MERGE RANGE |
| Foreign key | Önce referans veren tarafı ayır, foreign key'i düşür | Referans alınan tablo, foreign key etkinken SWITCH edilemez |
| Yeni dönem | Boş partition'ı önceden aç; default partition'dan kaçın | Yalnız boş partition'ı SPLIT et |
| Elimination tuzağı | Partition key'i fonksiyona sokmak | Parametre tipinin kolon tipiyle uyuşmaması |
Üretimde Dikkat
Elimination'ı plan'da doğrula. Plan XML'indeki RunTimePartitionSummary ve SET STATISTICS IO'daki Scan count partition sayısını açıkça gösterir. Query Store'da en çok logical read yapan sorgular arasında partition'lı tabloya giden bir sorgu varsa ilk bakılacak yer parametre tipi.
Pencereyi her zaman boş uçlarla tut. En sağda en az bir, tercihen birkaç boş partition olsun; job birkaç gün çalışmazsa yeni siparişler son partition'a yığılır ve o partition'ı bölmek dolu partition bölmeye döner. En sağdaki partition'ın satır sayısı için alarm kur.
Lock beklemesini sınırla. SWITCH, SPLIT ve MERGE schema modification lock'u ister; WAIT_AT_LOW_PRIORITY ve job'un kendi timeout'u olmadan uzun bir rapor sorgusu bütün tabloyu kilitler. Job'u en sakin saatte çalıştır.
Partition sayısını makul tut. SQL Server tablo başına 15.000 partition'a kadar destekliyor, ama doküman yüzlerce ve binlerce partition'ın bellek, index işlemleri ve DBCC CHECKDB süresi üzerindeki etkisi konusunda uyarıyor ve 1.000'den fazla partition'lı tabloda aligned olmayan index oluşturmayı desteklemiyor. Aylık partition ve 3 yıllık pencere 40 civarında partition demek.
Sık Düşülen Tuzaklar
RANGE tipini yazmamak: varsayılan LEFT olduğu için ayın ilk gece yarısı önceki aya düşer.
Dolu partition'da SPLIT yapmak: lab'da 41 bin siparişlik bir ayda 13 MB log ve yarım saniyelik tablo kilidi; üretimdeki ayların boyutunda dakikalar.
Aligned olmayan bir index eklemek: tek bir aligned olmayan index bütün SWITCH akışını durdurur. Index'leri her zaman scheme üzerinde oluştur.
AddWithValue(DateTime) ile partition'lı tabloyu sorgulamak: parametre datetime gider ve 52 partition'ın hepsi okunur.
Partitioning'i index tasarımının yerine koymak: "Siparişlerim" ve sipariş detayı partition'lı tabloda 24–32 kat fazla logical read yaptı. Partitioning'in kazancı arşiv ve bakımda; sorgu hızı index'ten gelir.
Küçük Bir Deneme
★☆☆ OrdersP7'deki gibi partition kolonu datetime2(7) olan bir tabloyla AddWithValue(DateTime) deneyini tekrarla. Kaç partition okunuyor? Plan'daki CONVERT_IMPLICIT'in hangi tarafa uygulandığına bak.
★★☆ Sipariş detayı sorgusunu WHERE OrderId = @id AND CreatedAt = @createdAt olarak yaz ve logical read sayısını ölç. Bookstore'un sipariş detayı URL'sine tarihi eklemeden aynı kazancı sağlamanın bir yolunu bul (ipucu: OrderId'den ay hesaplanabilir mi?).
★★★ SlideOrdersWindow'u bir .NET background job'undan çağır: job her gece çalışsın, procedure WAIT_AT_LOW_PRIORITY nedeniyle vazgeçerse bunu loglayıp alarm üretsin. Uzun süren bir rapor sorgusu açıkken job'u çalıştırıp davranışı doğrula.
Partitioning tek sunucunun içinde veriyi böldü. Sonraki yazıda veriyi sunucular arasında bölüyoruz: sharding. Shard key seçimi, .NET'te uygulama seviyesinde bir shard resolver, Citus ile PostgreSQL'i dağıtık hâle getirmek ve cross-shard sorguların bedeli.