SQL Veri Tipleri ve CREATE TABLE: Sayılar, Metin, Tarih/Saat ve Collation'daki Türkçe İ Tuzağı
Para için DECIMAL, insan dilindeki metin için NVARCHAR (ya da UTF-8 collation'lı VARCHAR), zaman için UTC'de DATETIME2 seç; Türkçe veride Latin1 collation UPPER('i')'yi 'I' yapar ve 'ince' araması İnce Memed'i bulamaz. FLOAT toplam hatasını, VARCHAR'da sessizce kaybolan İ ve ğ harflerini, DATETIME'ın 23:59:59.999'u ertesi güne yuvarlamasını, collation çakışması hatasını ve kendi schema'nla CREATE TABLE'ı gerçek çıktılarla gösteriyoruz.
Sürüm notu: Bu yazıdaki SQL (Structured Query Language) sorguları SQL Server 2025 (17.0, Cumulative Update 10) üzerinde, PostgreSQL karşılaştırmaları PostgreSQL 18.6 üzerinde çalıştırılmıştır.
Önce Tahmin Et: "Saatleri Ayarlama Enstitüsü" Büyük Harfle Nasıl Yazılır?
Bookstore kataloğunda dört Türkçe kitap var. İkisinin adını büyük harfe çeviriyoruz. Çalıştırmadan önce ikinci row'un çıktısını tahmin et:
SELECT title,
UPPER(title) AS upper_title
FROM dbo.books
WHERE book_id IN (75, 77);
Bir de arama kutusundan gelen klasik bir sorgu: kullanıcı "ince" yazıyor ve title case-insensitive bir column. İnce Memed listede çıkar mı?
SELECT book_id, title
FROM dbo.books
WHERE title LIKE N'ince%';
Gerçek çıktılar:
title upper_title
--------------------------- ---------------------------
İnce Memed İNCE MEMED
Saatleri Ayarlama Enstitüsü SAATLERI AYARLAMA ENSTITÜSÜ
(2 rows affected)
book_id title
------- -----
(0 rows affected)
"SAATLERI ... ENSTITÜSÜ": noktalı i'ler noktasız I olmuş. Arama ise hiç sonuç döndürmedi. Sebep tek bir kelime: collation. Veritabanının varsayılan collation'ı SQL_Latin1_General_CP1_CI_AS ve o kurallara göre "i"nin büyüğü "I", "İ" ise "i" ile aynı harf değil. Bu yazıda bu tuzağa, onu doğuran veri tiplerine ve CREATE TABLE ile bu tiplerin nasıl seçildiğine bakacağız.
Standart Tipler ve T-SQL Karşılıkları
ISO (International Organization for Standardization) SQL standardı tipleri ailelere ayırır: tam sayılar, kesin ondalık sayılar (exact numeric), yaklaşık sayılar (approximate numeric), karakter dizileri, tarih ve saat, boolean ve binary. Her ürün bu aileleri kendi isimleriyle uygular ve isimler her zaman örtüşmez:
| Standart | T-SQL | PostgreSQL | Not |
|---|---|---|---|
INTEGER, SMALLINT, BIGINT | INT, SMALLINT, BIGINT, TINYINT | aynı, TINYINT yok | TINYINT 0–255, T-SQL'e özgü |
DECIMAL(p,s), NUMERIC(p,s) | aynı | aynı | Para için doğru tip |
REAL, DOUBLE PRECISION | REAL, FLOAT | real, double precision | İkili kayan nokta, yaklaşık |
CHARACTER, VARCHAR, NATIONAL CHARACTER | CHAR, VARCHAR, NCHAR, NVARCHAR | char, varchar, text | PostgreSQL'de "national" ayrımı yok, hepsi veritabanı encoding'inde |
DATE, TIME, TIMESTAMP | DATE, TIME, DATETIME2 | date, time, timestamp | T-SQL'de TIMESTAMP tarih değil |
TIMESTAMP WITH TIME ZONE | DATETIMEOFFSET | timestamptz | PostgreSQL offset'i saklamaz, UTC'ye (Coordinated Universal Time) çevirir |
BOOLEAN | BIT | boolean | T-SQL'de boolean column tipi yok |
Son satırdaki "T-SQL'de TIMESTAMP tarih değil" notu bir tuzak. Standartta TIMESTAMP tarih ve saat demek; T-SQL'de ise rowversion'ın eski adı, her güncellemede değişen 8 byte'lık bir sayaç:
CREATE TABLE dbo.ts_demo (id INT, changed TIMESTAMP);
INSERT INTO dbo.ts_demo (id) VALUES (1);
SELECT id, changed FROM dbo.ts_demo;
id changed
-- ------------------
1 0x00000000000007D1
PostgreSQL'den taşınan bir created TIMESTAMP column'ı SQL Server'da hata vermeden oluşur ve tarih yerine bu binary değeri tutar. Bu sayaç optimistic concurrency için kullanılır; Entity Framework Core (EF Core) serisinin "EF Core'da Concurrency: Token, rowversion ve DbUpdateConcurrencyException" yazısında ele alındı.
Sayılar: INT, BIGINT, DECIMAL ve FLOAT
INT 4 byte'tır ve 2.147.483.647'de biter. Taşınca SQL Server sessizce sarmaz, hata verir:
DECLARE @stock INT = 2147483647;
SELECT @stock + 1 AS next_value;
Msg 8115, Level 16, State 2, Server b8be943222fa, Line 2
Arithmetic overflow error converting expression to data type int.
Aynı değişken BIGINT olunca sonuç 2147483648. Günde 800 siparişle orders.order_id'nin INT'i doldurması binlerce yıl sürer; ama log, event ya da tıklama table'larında identity değeri birkaç yılda bu sınıra dayanabilir. Kural basit: row sayısının milyarları geçme ihtimali varsa baştan BIGINT. Sonradan bir primary key'in tipini değiştirmek, ona bağlı bütün foreign key'leri de değiştirmek demek.
Para için tek doğru seçim DECIMAL(p,s): p toplam basamak sayısı (precision), s virgülden sonraki basamak sayısı (scale). list_price DECIMAL(10,2) en fazla 99.999.999,99 tutabilir ve değerleri ondalık olarak tam saklar. FLOAT ise ikili tabanda yaklaşık bir sayıdır; 0,1 ikili tabanda tam yazılamaz:
SELECT CASE WHEN CAST(0.1 AS FLOAT) + CAST(0.2 AS FLOAT) = CAST(0.3 AS FLOAT) THEN 'equal' ELSE 'not equal' END AS float_check,
CASE WHEN CAST(0.1 AS DECIMAL(10,2)) + CAST(0.2 AS DECIMAL(10,2)) = 0.3 THEN 'equal' ELSE 'not equal' END AS decimal_check;
float_check decimal_check
----------- -------------
not equal equal
(1 rows affected)
Toplamlarda hata birikiyor. 379 siparişin her birine 0,10 TL'lik bir ücret eklediğini düşün:
SELECT SUM(CAST(0.1 AS FLOAT)) AS float_sum, SUM(CAST(0.1 AS DECIMAL(10,2))) AS decimal_sum
FROM dbo.orders;
float_sum decimal_sum
------------------ -----------
37.900000000000269 37.90
(1 rows affected)
Raporda bu fark yuvarlamayla kaybolur, ama WHERE total = 37.90 gibi bir eşitlik kontrolü ya da muhasebe mutabakatı tutmaz. FLOAT bilimsel ölçümler, koordinatlar ve istatistik için var; para, miktar ve stok için değil. DECIMAL'in de bir disiplini var: scale'i aşan değer sessizce yuvarlanır, precision'ı aşan değer hata verir.
SELECT CAST(19.999 AS DECIMAL(10,2)) AS rounded,
CAST(10 AS DECIMAL(10,2)) / 3 AS divided;
SELECT CAST(123456789.50 AS DECIMAL(10,2)) AS too_big;
rounded divided
------- --------
20.00 3.333333
(1 rows affected)
Msg 8115, Level 16, State 8, Server b8be943222fa, Line 1
Arithmetic overflow error converting numeric to data type numeric.
Bölmenin sonucu DECIMAL(10,2) değil; SQL Server sonuç tipinin precision ve scale'ini işlenenlerden hesaplar ve burada 6 ondalık hane çıktı. Sonucu bir column'a yazarken hangi tipe ve hangi yuvarlamaya düşeceğini bilerek yaz.
Metin: CHAR, VARCHAR, NVARCHAR ve UTF-8
CHAR(n) sabit uzunluktur ve eksik kalan yeri boşlukla doldurur; VARCHAR(n) değişken uzunluktur; başındaki N ise "national" demek ve Unicode (UTF-16) saklar, karakter başına en az 2 byte:
SELECT DATALENGTH(CAST('Dune' AS CHAR(10))) AS char10,
DATALENGTH(CAST('Dune' AS VARCHAR(10))) AS varchar10,
DATALENGTH(CAST(N'Dune' AS NVARCHAR(10))) AS nvarchar10,
'[' + CAST('Dune' AS CHAR(10)) + ']' AS char_padded;
char10 varchar10 nvarchar10 char_padded
------ --------- ---------- ------------
10 4 8 [Dune ]
(1 rows affected)
CHAR yalnız uzunluğu gerçekten sabit olan değerler için anlamlı: Bookstore'da isbn CHAR(13), language_code CHAR(2), country_code CHAR(2). Asıl karar VARCHAR ile NVARCHAR arasında. Klasik VARCHAR bir code page'e bağlıdır ve code page'i collation belirler. Veritabanımızın collation'ı code page 1252 kullanıyor ve 1252'de "ü" var ama "İ" ile "ğ" yok:
SELECT CAST(N'İnce Memed' AS VARCHAR(50)) AS as_varchar,
CAST(N'Saatleri Ayarlama Enstitüsü' AS VARCHAR(50)) AS as_varchar2,
CAST(N'Oğuz Atay' AS VARCHAR(50)) AS as_varchar3;
as_varchar as_varchar2 as_varchar3
---------- --------------------------- -----------
Ince Memed Saatleri Ayarlama Enstitüsü Oguz Atay
(1 rows affected)
Hata yok, uyarı yok: "İ" sessizce "I", "ğ" sessizce "g" oldu ve bu veri bir daha geri gelmez. Aynı kayıp, string literal'ın başına N koymayı unuttuğunda da olur, çünkü N'siz literal da VARCHAR'dır:
SELECT 'Oğuz' AS without_n, N'Oğuz' AS with_n;
without_n with_n
--------- ------
Oguz Oğuz
(1 rows affected)
Bu yüzden Bookstore'da insan dilinde yazılan her şey NVARCHAR: title, first_name, city. email ve phone ise VARCHAR, çünkü içerikleri ASCII (American Standard Code for Information Interchange) karakterlerle sınırlı. SQL Server 2019'dan beri üçüncü bir yol var: adı _UTF8 ile biten bir collation'la VARCHAR UTF-8 (Unicode Transformation Format, 8 bit) saklar:
SELECT CAST(N'Oğuz Atay' COLLATE Latin1_General_100_CI_AS_SC_UTF8 AS VARCHAR(50)) AS utf8_varchar,
DATALENGTH(CAST(N'Oğuz Atay' COLLATE Latin1_General_100_CI_AS_SC_UTF8 AS VARCHAR(50))) AS utf8_bytes,
DATALENGTH(N'Oğuz Atay') AS nvarchar_bytes;
utf8_varchar utf8_bytes nvarchar_bytes
------------ ---------- --------------
Oğuz Atay 10 18
(1 rows affected)
"ğ" UTF-8'de 2 byte, diğer harfler 1 byte; toplam 10 byte. Aynı metin NVARCHAR'da 18 byte. Ağırlıklı olarak Latin harfli metinde UTF-8 yer kazandırır; ama VARCHAR(50)'deki 50 artık karakter değil byte sayısıdır ve Türkçe karakter yoğun bir metin beklediğinden önce dolabilir.
Bir de uzunluk meselesi var. "Nasılsa sığar" diye her column'u NVARCHAR(MAX) yapmak cazip, ama MAX column'lar büyük değerlerde row'un dışında saklanabilir, index key'i olamaz ve uzunluğu bir iş kuralı olmaktan çıkar. title NVARCHAR(200) hem belge hem doğrulama: 200 karakterden uzun bir kitap adı muhtemelen bir hatadır.
Tarih ve Saat: DATE, DATETIME2 ve DATETIMEOFFSET
Yeni bir column için seçenekler şunlar: yalnız tarih için DATE (3 byte), tarih ve saat için DATETIME2(n), offset'i de saklaman gerekiyorsa DATETIMEOFFSET(n). n saniyenin kesirli basamak sayısı, 0 ile 7 arası:
SELECT CAST('2025-12-31 23:59:59.1234567' AS DATETIME2(7)) AS dt2_7,
CAST('2025-12-31 23:59:59.1234567' AS DATETIME2(0)) AS dt2_0,
CAST('2025-12-31 23:59:59.1234567 +03:00' AS DATETIMEOFFSET(0)) AS dto,
CAST('2025-12-31' AS DATE) AS d;
dt2_7 dt2_0 dto d
--------------------------- ------------------- -------------------------- ----------
2025-12-31 23:59:59.1234567 2025-12-31 23:59:59 2025-12-31 23:59:59 +03:00 2025-12-31
(1 rows affected)
Eski DATETIME tipini yeni koda koyma. Hassasiyeti saniyenin 1/300'ü kadardır ve değerleri .000, .003, .007 ile biten anlara yuvarlar. Gün sonunu "23:59:59.999" diye yazan klasik kod bu yüzden yanlış güne düşer:
SELECT CAST('2025-12-31 23:59:59.997' AS DATETIME) AS dt_997,
CAST('2025-12-31 23:59:59.998' AS DATETIME) AS dt_998,
CAST('2025-12-31 23:59:59.999' AS DATETIME) AS dt_999,
CAST('2025-12-31 23:59:59.999' AS DATETIME2(3)) AS dt2_999;
dt_997 dt_998 dt_999 dt2_999
----------------------- ----------------------- ----------------------- -----------------------
2025-12-31 23:59:59.997 2025-12-31 23:59:59.997 2026-01-01 00:00:00.000 2025-12-31 23:59:59.999
(1 rows affected)
"31 Aralık'ın son anı" diye yazılan değer 1 Ocak 2026 gece yarısı oldu. Bir yıllık raporun BETWEEN ile yazılmış üst sınırı ertesi yılın ilk siparişlerini de toplar. Doğru çözüm tip değiştirmek değil, aralıkları yarı açık yazmak (>= başlangıç ve < bitiş); bunu 6. yazıda yapacağız. Boyut da DATETIME2'nin lehine: DATETIME 8 byte, DATETIME2(0) 6 byte, DATETIME2(7) 8 byte.
Bookstore'da bütün zaman damgaları DATETIME2(0) ve UTC. Kural şu: anı (instant) UTC olarak sakla, kullanıcıya gösterirken yerel saate çevir. Yılbaşı gecesi verilen 263 numaralı sipariş buna iyi bir örnek:
SELECT o.order_id, o.ordered_at AS utc,
o.ordered_at AT TIME ZONE 'UTC' AT TIME ZONE 'Turkey Standard Time' AS istanbul
FROM dbo.orders AS o
WHERE o.order_id = 263;
order_id utc istanbul
-------- ------------------- --------------------------
263 2025-12-31 23:06:27 2026-01-01 02:06:27 +03:00
(1 rows affected)
İlk AT TIME ZONE 'UTC', offset'siz değere "bu bir UTC değeri" etiketini koyar; ikincisi onu İstanbul saatine çevirir. Sipariş UTC'ye göre 2025'te, İstanbul'a göre 2026'da. "2025 satışları" raporunun hangi saat dilimine göre kesileceği bir iş kararıdır ve sorguda açıkça yazılmalıdır. Zaman dilimi adları Windows isimleridir, Linux'taki SQL Server'da da öyle. Bu konunun tasarım tarafı Solution Pattern serisinin "Zaman ve Time Zone: UTC Instant, Yerel İş Günü, DateOnly ve TimeProvider" yazısında.
BIT ve UNIQUEIDENTIFIER
T-SQL'de boolean column yok; yerine BIT var ve 0, 1 ya da NULL alır. Dönüşümde sıfır olmayan her sayı 1 olur, 'true' ve 'false' string'leri de kabul edilir:
SELECT CAST('true' AS BIT) AS from_true, CAST(5 AS BIT) AS from_five,
CAST('6F9619FF-8B86-D011-B42D-00C04FC964FF' AS UNIQUEIDENTIFIER) AS guid_value,
DATALENGTH(NEWID()) AS guid_bytes;
from_true from_five guid_value guid_bytes
--------- --------- ------------------------------------ ----------
1 1 6F9619FF-8B86-D011-B42D-00C04FC964FF 16
(1 rows affected)
UNIQUEIDENTIFIER 16 byte'lık bir GUID (Globally Unique Identifier). Dışarıya açılan id'ler ve birden fazla sistemde üretilen kayıtlar için kullanışlı, ama rastgele GUID'i clustered primary key yapmanın index üzerindeki etkisi bir tasarım konusu. Kısa cevabı bir sonraki yazıda, uzun cevabı "SQL Server Derinlemesine" serisinde.
Collation ve Türkçe İ Tuzağı
Collation, metnin nasıl karşılaştırılacağını ve sıralanacağını belirleyen kurallar bütünü: hangi harfin büyüğü hangisi, büyük-küçük harf farkı önemli mi, yani CI (case-insensitive) mı CS (case-sensitive) mi, aksan farkı önemli mi, yani AI (accent-insensitive) mı AS (accent-sensitive) mi, VARCHAR hangi code page'i kullanıyor. Türkçe'nin farkı dört harfte: i ↔ İ ve ı ↔ I. Üç karşılaştırma bunu net gösteriyor:
SELECT CASE WHEN N'istanbul' = N'ISTANBUL' COLLATE Latin1_General_100_CI_AS THEN 'equal' ELSE 'different' END AS latin1,
CASE WHEN N'istanbul' = N'ISTANBUL' COLLATE Turkish_100_CI_AS THEN 'equal' ELSE 'different' END AS turkish,
CASE WHEN N'istanbul' = N'İSTANBUL' COLLATE Turkish_100_CI_AS THEN 'equal' ELSE 'different' END AS turkish_dotted;
latin1 turkish turkish_dotted
------ --------- --------------
equal different equal
(1 rows affected)
Türkçe collation'da "istanbul" ile "ISTANBUL" farklı kelimeler, çünkü "i"nin büyüğü "İ". İkisi de doğru; hangisinin doğru olduğu verinin diline bağlı. Açılıştaki iki sorguyu Türkçe kurallarla yeniden çalıştıralım. COLLATE bir ifadeye sorgu içinde başka bir collation uygular:
SELECT title,
UPPER(title COLLATE Turkish_100_CI_AS) AS upper_title
FROM dbo.books
WHERE book_id IN (75, 77);
SELECT book_id, title
FROM dbo.books
WHERE title COLLATE Turkish_100_CI_AS LIKE N'ince%';
title upper_title
--------------------------- ---------------------------
İnce Memed İNCE MEMED
Saatleri Ayarlama Enstitüsü SAATLERİ AYARLAMA ENSTİTÜSÜ
(2 rows affected)
book_id title
------- ----------
75 İnce Memed
(1 rows affected)
Doğru sonuçlar. Ama sorgu içinde COLLATE yazmanın bir bedeli var: column'un kendi collation'ından farklı bir kuralla karşılaştırma yapıldığında o column üzerindeki bir index arama için kullanılamaz. Kalıcı çözüm, column'u (ya da veritabanını) baştan doğru collation'la oluşturmak. PostgreSQL'de durum benzer; varsayılan collation Türkçe kuralları bilmez, ICU (International Components for Unicode) tabanlı Türkçe collation'ı açıkça istemen gerekir:
SELECT upper('Saatleri Ayarlama Enstitüsü') AS default_collation,
upper('Saatleri Ayarlama Enstitüsü' COLLATE "tr-TR-x-icu") AS turkish_icu;
default_collation | turkish_icu
-----------------------------+-----------------------------
SAATLERI AYARLAMA ENSTITÜSÜ | SAATLERİ AYARLAMA ENSTİTÜSÜ
(1 row)
Farklı collation'lar aynı sorguda karşılaşınca bir de hata çıkar. Bir import table'ını Türkçe collation'la oluşturup katalogla join'leyelim:
CREATE TABLE dbo.import_titles (
title NVARCHAR(200) COLLATE Turkish_100_CI_AS NOT NULL
);
INSERT INTO dbo.import_titles (title) VALUES (N'İnce Memed'), (N'Dune');
SELECT b.book_id, b.title
FROM dbo.books AS b
JOIN dbo.import_titles AS i
ON i.title = b.title;
Msg 468, Level 16, State 9, Server b8be943222fa, Line 4
Cannot resolve the collation conflict between "SQL_Latin1_General_CP1_CI_AS" and "Turkish_100_CI_AS" in the equal to operation.
SQL Server iki farklı kuraldan hangisini kullanacağına karar veremiyor. Karşılaştırmanın bir tarafına COLLATE Turkish_100_CI_AS yazınca join çalışıyor ve iki kitabı da buluyor. Bu hatayı en sık, veritabanı collation'ı sunucunun collation'ından farklı olduğunda tempdb'deki temporary table'larla karşılaşırsın: temporary table'ın column'ları tempdb'nin, yani sunucunun collation'ını alır.
CREATE TABLE ve Schema
Şimdi bu kararları bir araya getirip Bookstore'a kitap baskılarını tutan bir table ekleyelim. Önce kendi schema'mızı açıyoruz. Schema, table'ları gruplayan bir isim alanı ve yetki sınırı; dbo varsayılan schema'dır:
DROP TABLE IF EXISTS catalog.book_editions;
DROP SCHEMA IF EXISTS catalog;
GO
CREATE SCHEMA catalog;
GO
CREATE TABLE catalog.book_editions (
edition_id INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
book_id INT NOT NULL,
edition_label NVARCHAR(100) NOT NULL,
language_code CHAR(2) NOT NULL,
isbn CHAR(13) NOT NULL,
page_count SMALLINT NULL,
list_price DECIMAL(10,2) NOT NULL,
weight_grams INT NULL,
is_hardcover BIT NOT NULL DEFAULT (0),
published_on DATE NULL,
created_at DATETIME2(0) NOT NULL DEFAULT (SYSUTCDATETIME())
);
INSERT INTO catalog.book_editions (book_id, edition_label, language_code, isbn, page_count, list_price, published_on)
VALUES (75, N'İnce Memed 1, pocket edition', 'tr', '9798900000751', 436, 289.90, '2024-03-01');
SELECT edition_id, edition_label, list_price, is_hardcover, created_at
FROM catalog.book_editions;
edition_id edition_label list_price is_hardcover created_at
---------- ---------------------------- ---------- ------------ -------------------
1 İnce Memed 1, pocket edition 289.90 0 2026-11-08 12:00:27
(1 rows affected)
Birkaç kural okunuyor. CREATE SCHEMA kendi batch'inde olmak zorunda; GO bir T-SQL komutu değil, sqlcmd, SSMS (SQL Server Management Studio) ve Visual Studio Code'un script'i batch'lere bölmek için tanıdığı bir ayraç. DROP TABLE IF EXISTS SQL Server 2016'dan beri var ve script'leri tekrar çalıştırılabilir yapar. IDENTITY(1,1) değeri sunucuya ürettirir, DEFAULT'lar da yazılmayan column'ları doldurur: is_hardcover 0, created_at o anın UTC zamanı. İsimlendirmede Bookstore'un kuralı snake_case, çoğul table adı ve <table>_id biçiminde primary key. Hangi kuralı seçtiğinden çok, bir kez seçip her yerde uygulaman önemli. Bu table'da constraint'ler eksik: book_id gerçekten var olan bir kitabı göstermek zorunda değil, list_price negatif olabilir. Onları bir sonraki yazıda ekleyeceğiz.
Üretimde Dikkat
Collation'ı veritabanını oluştururken seç; sonradan değiştirmek, var olan column'ları değiştirmez. ALTER DATABASE ... COLLATE yalnız veritabanının varsayılanını ve yeni column'ları etkiler, eski column'lar eski collation'la kalır ve tam olarak bu yazıdaki çakışma hatasının kaynağı olur. Sunucunun collation'ı container'da MSSQL_COLLATION environment variable'ıyla kurulumda belirlenir. Türkçe ağırlıklı bir uygulamada Turkish_100_CI_AS mantıklı bir varsayılan; çok dilli bir uygulamada Latin1 tabanlı bir collation'ı varsayılan bırakıp Türkçe sıralama ve arama gereken column'lara column seviyesinde collation vermek daha esnek.
Tip değiştirmek pahalı. INT'ten BIGINT'e, VARCHAR'dan NVARCHAR'a geçmek büyük bir table'da her row'u yeniden yazmak ve tamamlanana kadar table'ı kilitlemek demek. Tipi, row sayısının ve verinin dilinin üç yıl sonraki halini düşünerek seç. EF Core kullanıyorsan .NET tiplerinin varsayılan karşılıklarını (string için nvarchar(max) gibi) bilerek override et; bu konu EF Core serisinin "EF Core'da Model ve Convention'lar: Key, Nullable, Kolon Tipleri ve Shadow Property" yazısında.
Sık Düşülen Tuzaklar
- Para için
FLOATya daREALkullanmak. 379 küçük toplamada bile hata görünür hale geldi. - Türkçe veri için
VARCHARve code page 1252: "İ" ve "ğ" hatasız, uyarısız kaybolur. N'siz string literal da aynı kaybı yaşatır. - Her column'u
NVARCHAR(MAX)yapmak; uzunluk bir iş kuralıdır ve index key'i olamayan column'lar ileride sorun çıkarır. - Tarihleri string olarak saklamak.
'31.12.2025'ile'2025-12-31'sıralanamaz, doğrulanamaz, tarih aritmetiğine girmez. - Eski
DATETIMEile "23:59:59.999" yazmak ve ertesi güne yuvarlanmak. - Yerel saati offset'siz saklamak ve hangi saat dilimine ait olduğunu kaybetmek. UTC sakla ya da
DATETIMEOFFSETkullan. - Latin1 collation'da Türkçe metin aramak ve "ince" ile "İnce Memed"i bulamamak.
Küçük Bir Deneme
★☆☆ customers table'ının column tiplerini INFORMATION_SCHEMA.COLUMNS'tan listele ve her biri için tipin neden seçildiğini bir cümleyle yaz. email neden VARCHAR(254)?
★★☆ Turkish_100_CI_AS ile Turkish_100_CS_AS arasında N'ırmak' = N'IRMAK' ve N'ırmak' = N'irmak' karşılaştırmalarının sonuçlarını önce tahmin et, sonra çalıştır.
★★★ Latin1_General_100_CI_AS_SC_UTF8 collation'lı bir VARCHAR(27) column oluştur ve içine 27 karakterlik "Saatleri Ayarlama Enstitüsü"yü yazmaya çalış. Hata alıyorsan neden? Aynı denemeyi NVARCHAR(27) ile yap ve LEN ile DATALENGTH değerlerini karşılaştır.
catalog.book_editions şu haliyle her şeyi kabul ediyor: var olmayan bir kitap, negatif fiyat, aynı ISBN (International Standard Book Number) ile iki baskı. Bir sonraki yazıda primary key, foreign key, UNIQUE, CHECK ve DEFAULT constraint'leriyle bu table'ı kurallı hale getirecek ve ALTER TABLE ile production'da şemayı güvenle değiştirmenin sırasını konuşacağız.