Ücretsiz3 dakikalık yazılım sağlık testi: önce neyi düzeltmeniz gerektiğini görün
Blog

Doğru veri modeli: normalizasyon nerede durmalı

18 dk okuma
  • PostgreSQL
  • Veritabanı
  • Prisma
Seri: Bölüm 1 · 1/12 bölüm yayındaPostgreSQL’i üretimde doğru kullanmakTüm bölümler
İçindekiler

Bir veritabanında sonradan değiştirmesi en pahalı şey veri modelidir. Yavaş bir sorguya dakikalar içinde indeks eklersiniz, yanlış bir ayarı bir satırla düzeltirsiniz. Ama bir tablonun neyi temsil ettiği, hangi bilginin nerede durduğu, uygulamadaki her sorguya ve veritabanındaki her satıra işler. Onu değiştirmek, sistem çalışırken hem kodu hem veriyi birlikte taşımak demektir.

Bu yüzden serinin ilk bölümünü normalizasyona ayırıyorum. Normal formların tanımlarını tekrar etmeyeceğim; onlar her veritabanı kitabında var. Bu yazıda, kendi sistemlerimde bir bilgiyi tek yerde tutmanın neyi kazandırdığını, ne zaman bilerek kopyaladığımı ve kopyanın bedelini nasıl ödediğimi anlatacağım.

Normalizasyonun asıl kazandırdığı

Normalizasyonu tek cümleyle özetlemem gerekse şunu derdim: her bilgi tek bir yerde durur. Bir müşterinin adresi müşteri tablosundadır; siparişler müşteriye kimliğiyle bağlanır. Adres değişince tek satır güncellenir ve bütün siparişler yeni adresi görür.

Bunun değeri, bilgi tek yerde durmadığında anlaşılıyor. Okuduğunuz bu blogdan bir örnek vereyim. Yazıların etiketleri ayrı bir tabloda değil, yazının kendisinde bir dizi olarak duruyor:

model Post {
  id   String   @id @default(uuid())
  // ...
  tags String[]
}

PostgreSQL'de bu bir text[] sütunu. Sorgulaması kolay:

SELECT title FROM posts WHERE tags @> ARRAY['Node.js'];

Ama etiketin kendisi hiçbir yerde tanımlı değil. Her yazı kendi etiket listesini serbest metin olarak taşıyor. Bir yazıya Node.js, diğerine node.js yazarsam veritabanı bunların aynı şey olduğunu bilmiyor:

SELECT tag, count(*) FROM posts, unnest(tags) AS tag GROUP BY tag;
    tag     | count
------------+-------
 node.js    |     1
 Node.js    |     1
 PostgreSQL |     1

Bu blogda bu sorunu uygulama tarafında çözüyorum. Admin panelindeki etiket önerileri, aynı etiketin farklı yazılışlarını tek sayıyor ve en çok kullanılan yazılışı öne çıkarıyor. Yani normalizasyonun veritabanında vereceği garantiyi, kodda elle veriyorum.

Normalize edilmiş hâli şöyle olurdu:

CREATE TABLE tags (
  id   serial PRIMARY KEY,
  name text NOT NULL
);
CREATE UNIQUE INDEX tags_name_key ON tags (lower(name));

CREATE TABLE post_tags (
  post_id int REFERENCES posts ON DELETE CASCADE,
  tag_id  int REFERENCES tags  ON DELETE CASCADE,
  PRIMARY KEY (post_id, tag_id)
);

Bu şemada node.js eklemeye çalıştığınız anda veritabanı itiraz eder:

ERROR:  duplicate key value violates unique constraint "tags_name_key"
DETAIL:  Key (lower(name))=(node.js) already exists.

Bir etiketi yeniden adlandırmak tek satırlık bir UPDATE olur. Etikete açıklama, sayfa adresi ya da logo eklemek istediğinizde de koyacak bir yeriniz vardır.

Peki bu blog neden hâlâ diziyle çalışıyor? Çünkü burada bedel küçük. Tek bir yazar var, yazı sayısı az, etiketin kendine ait bir verisi yok. Dizi, bir join tablosundan ve onu yöneten koddan daha az iş çıkarıyor. Etiketler kendi sayfalarına, açıklamalarına ya da birden fazla yazara kavuştuğu gün bu hesap değişir. Normalizasyonu bir kural olarak değil, böyle bir hesap olarak görüyorum: tutarsızlığın bedeli, ek tablonun bedelinden büyük olduğunda normalize ederim.

Bilerek kopyalamak

Denormalizasyon, aynı bilgiyi ikinci bir yerde tutmaktır. Genellikle okumayı hızlandırmak için yapılır: her seferinde hesaplamak yerine sonucu bir kenara yazarsınız. Bunu ben de yapıyorum, ama her kopya için kendime iki soru soruyorum:

  1. Asıl kaynak hangisi? Kopya bozulduğunda neye bakarak düzelteceğim?
  2. Kopyayı kim, nerede güncelliyor? Bunun cevabı tek bir yer olmalı.

Asıl kaynağı olan bir sayaç

Bu blogdaki okunma sayısı iyi bir örnek. Her yazının views sütununda bir sayı duruyor. Ama bu sayı asıl bilgi değil. Asıl bilgi, post_views tablosunda:

CREATE TABLE post_views (
  post_id    text REFERENCES posts ON DELETE CASCADE,
  ip_hash    text,
  created_at timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (post_id, ip_hash)
);

Bir okunmayı kaydederken önce bu tabloya satır ekliyorum, satır gerçekten eklendiyse sayacı artırıyorum. İkisi aynı transaction içinde. Birincil anahtar (post_id, ip_hash) olduğu için aynı IP aynı yazıyı ikinci kez açtığında ekleme hiçbir şey yapmıyor, sayaç da artmıyor. Prisma'da bu iş şöyle:

return db.$transaction(async (tx) => {
  const { count } = await tx.postView.createMany({
    data: [{ postId: post.id, ipHash: hashIp(ip, secret) }],
    skipDuplicates: true,
  });
  if (count === 0) return post.views;
  const updated = await tx.post.update({
    where: { id: post.id },
    data: { views: { increment: 1 } },
    select: { views: true },
  });
  return updated.views;
});

Sayaç bir gün bozulursa ne yapacağımı biliyorum, çünkü asıl kaynak duruyor:

UPDATE posts p
SET views = (SELECT count(*) FROM post_views v WHERE v.post_id = p.id);

Kopyanın kabul edilebilir olmasını sağlayan şey bu: her an asıl kaynaktan yeniden üretilebiliyor.

Kopyanın gerçek riski: yazma yollarının çoğalması

KillReport'ta aynı fikir çok daha büyük ölçekte çalışıyor. Bir killmail kaydedildiğinde liderlik tablolarındaki günlük sayaçlar, kaydın kendisiyle aynı transaction içinde artırılıyor:

INSERT INTO character_kill_stats (kill_date, character_id, kill_count)
VALUES ($1::date, $2, 1)
ON CONFLICT (kill_date, character_id)
DO UPDATE SET kill_count = character_kill_stats.kill_count + 1;

Birincil anahtar (kill_date, character_id). Gün içindeki ilk kayıt satırı açıyor, sonrakiler sayacı artırıyor. Ayrı bir zamanlanmış işe gerek kalmıyor.

SQL tarafında bir sorun yoktu. Sorun, killmail'i yazan kodun tek bir yerde olmamasıydı. Zamanla yedi farklı dosya killmail yazar hâle gelmişti ve bunlar birbirinden ayrışmıştı. İkisi liderlik sayaçlarını hiç güncellemiyordu. Bunu fark ettiğimde 3.847 killmail hiçbir liderlik tablosunda sayılmıyordu.

Sayaçları asıl kayıtlardan yeniden ürettim, ama asıl düzeltme veritabanında değil kodda oldu: killmail yazan tek bir fonksiyon. Killmail'i, kurbanı, saldırganları ve sayaçları o fonksiyon, tek bir transaction içinde yazıyor. Başka hiçbir yer killmails tablosuna doğrudan dokunmuyor.

Buradan çıkardığım ders şu: denormalizasyonun riski çoğu zaman SQL'de değil, aynı veriyi yazan yolların sayısında. Kopya tutacaksanız, onu güncelleyen yolu da tek yere toplayın.

Aynı transaction'ın bir sonucunu da hatırlatayım. PostgreSQL'de transaction içinde bir komut hata verirse, transaction'ın geri kalanı iptal olur:

ERROR:  current transaction is aborted, commands ignored until end of transaction block

Yani sayacı artıran komutun hatasını uygulama kodunda yakalayıp "önemli değil, devam et" diyemezsiniz. Bu güvenceyi zaten istediğiniz için aynı transaction'ı seçtiniz: ya killmail de sayaç da yazılır, ya ikisi de yazılmaz. Bir parçanın başarısız olmasına izin vermek istiyorsanız o parça için SAVEPOINT kullanmanız gerekir. Ama o zaman sayacın asıl kayıtla tutarlı kalacağı garantisinden vazgeçmiş olursunuz.

Türetilmiş veriyi kopyalamak, türetme mantığını da kopyalamaktır

KillReport'ta filtreleme için geniş bir okuma tablosu var: killmail_filters. Her killmail için kurbanın ve saldırganların kimlikleri, bölge, değer gibi filtrelenen her şey tek satırda duruyor. Saldırganların kimlikleri int[] dizilerinde ve GIN indeksli.

Bu tablo ilk başta bir materialized view'du. Görünümün tanımı, bölge ve takımyıldız bilgisini başka tablolarla join yaparak buluyordu. Görünümü her seferinde yenilemek yerine yeni killmail'leri tek tek eklemek için onu normal bir tabloya çevirdim. Ekleme kodunu yazarken o join'i taşımayı unuttum.

Görünüm tanımı ortadan kalkınca, bölge bilgisinin nereden geldiğini söyleyen tek yer de kalkmış oldu. Yeni eklenen satırlarda bölge ve takımyıldız boş geldi. Bunu aylar sonra fark ettiğimde 44.493 satırın 37.781'inde bu iki alan NULL idi.

Bir veriyi türetip kopyaladığınızda, onu türeten mantığı da kopyalamış olursunuz. Mantık kodda iki yerde yaşıyorsa ayrışır.

Bu olaydan sonra, kopyaladığım her sütun için bir soruyu açıkça cevaplıyorum: bu değer değişebilir mi? KillReport'taki onarım migration'ında iki farklı cevap yan yana duruyor:

  • Bir gemi tipinin grubu hiç değişmez. Bu kopya bir kez doğru yazıldıysa sonsuza kadar doğrudur, senkronizasyona gerek yoktur.
  • Bir killmail'in ISK değeri piyasa fiyatlarına göre değişir. Bu kopyayı güncel tutan ayrı bir işçi var.

İlki ucuz bir kopya. İkincisi, sahibi olan bir süreç gerektiriyor. Bu ayrımı yapmadan kopyalanan sütun, ne zaman eskidiğini kimsenin bilmediği bir sütundur.

Kopya değil, o anın kaydı

Her tekrar eden bilgi denormalizasyon değildir. KillReport'ta bir saldırganın kurum kimliği, killmail'in içinde ESI'nin o an gönderdiği hâliyle saklanıyor. Pilot ertesi gün kurum değiştirse de kayıt değişmiyor, değişmemeli de. Burada saklanan şey "pilotun kurumu" değil, "pilotun o savaştaki kurumu".

E-ticaretteki karşılığı, sipariş satırına yazılan fiyattır. Ürünün fiyatı sonradan değişir, ama siparişin tutarı değişmez. Bu sütunu ürünler tablosuna bir join'le değiştirmeye kalkarsanız geçmişi bozarsınız.

Biocidal'da bu tür sütunlar çok ve her birinin yanında neden kopyalandığını söyleyen bir not var. Abonelik olaylarında plan ve şirket adı kayıt anında kopyalanıyor; plan sonradan yeniden adlandırılsa ya da silinse de geçmiş okunabilir kalıyor. Araçların bir bölgeye girip çıktığı olaylarda bölgenin adı kopyalanıyor; bölge sonradan silinse de döküm anlamını kaybetmiyor. Bildirim kuyruğunda alıcının adresi kopyalanıyor; personel silinse de "geçen ay bu adrese ne gitti?" sorusu cevaplanabiliyor. Bazı kayıtların şirkete foreign key'i bile bilerek yok: şirketin verisi dışa aktarıldıktan sonra şirket silinse de arşiv kaydı durmalı.

Ortak nokta şu: bu sütunların hiçbiri "güncel değer" taşımıyor ve hiçbiri senkronize edilmiyor. Bir olayın ne zaman, kime, hangi adla olduğunu kaydediyorlar. Burada normalizasyon, bilgiyi kaybetmek anlamına gelirdi.

Bu iki anlam aynı sistemde yan yana yaşayabilir. Önemli olan, her sütunun hangisini taşıdığını bilmek. KillReport'ta killmail'in bir fraksiyona ait olup olmadığı, killmail'in üzerindeki o anki fraksiyon kimliğine göre belirleniyor. Fraksiyon sayaçları ise karakterlerin ve kurumların bugünkü fraksiyonunu kullanıyor. Kodda bunu açıklayan bir not olmasaydı, iki ekranda farklı sayılar gören biri hata olduğunu düşünürdü.

Kısıtlamalar: veritabanına kuralı söylemek

Tip de bir kısıtlamadır

Sütunun tipi, veritabanına verdiğiniz ilk kuraldır. Yanlış seçildiğinde de en sinsi hataları üretir. Biocidal'da taleplerdeki cep telefonu numarası uzun süre bigint bir sütunda durdu. Kulağa mantıklı geliyor: numara rakamlardan oluşuyor. Ama telefon numarası bir sayı değil; onunla toplama ya da karşılaştırma yapmıyorsunuz.

Bu sütuna + ile başlayan bir numara yazmaya çalışın:

UPDATE request SET mobile = '+905321234567' WHERE id = 1;

Hata almazsınız. PostgreSQL metni sayıya çevirirken + işaretini sessizce atar ve sütuna 905321234567 yazar. Ülke kodunu saklamanın bir yolu kalmaz, uluslararası numara desteği de bu sütunla mümkün olmaz.

Daha kötüsü, aynı numaranın birden fazla yazımı vardı. Kullanıcıların telefonu başka bir tabloda serbest metin olarak duruyordu ve kimi kayıtta 5321234567, kimi kayıtta +905321234567 yazıyordu. Uygulama aynı numaranın iki kez kaydedilmesini engellediğini düşünüyordu. Ama bu tekillik veritabanında hiç tanımlı değildi ve iki farklı yazım, uygulamanın kontrolünden geçip ayrı kayıt sayılıyordu.

Düzeltme dört adımda oldu. Önce sütunun tipini veriyi silmeden değiştirdim:

ALTER TABLE request ALTER COLUMN mobile TYPE text USING mobile::text;

Sonra bütün numaraları tek bir biçime, E.164'e (+905321234567) çevirdim. Bu kurala uymayan değerleri ise NULL yaptım. 238 ya da on üç haneli bir sayı telefon numarası değil; onları biçime zorlasaydım var olmayan numaralar üretecektim. Bozuk veriyi yanlış veriye çevirmektense "bilmiyorum" demeyi seçtim. Kaç kaydın temizlendiğini migration çalışırken ekrana yazdırdım. En son, uygulamanın baştan beri varsaydığı kuralı veritabanına söyledim:

CREATE UNIQUE INDEX user_company_id_phone_key ON "user" (company_id, phone);

Bir kural yalnızca uygulamadaysa, veri o kurala uymayan bir yoldan girdiği anda kural ortadan kalkar. Normalize edilmiş bir biçim ve veritabanındaki bir unique kısıtlama birlikte olmadan "bu numara zaten kayıtlı" kontrolü güvenilir değil.

Bileşik foreign key

Normalizasyonun bir diğer faydası, kuralları veritabanına söyleyebilmek. KillReport'ta her ay bir gezegene, her gezegen bir yıldız sistemine ait. Ayda da hız için solar_system_id tutuluyor. Ama yalnızca planet_id üzerinden bir foreign key, ayın sisteminin gezegenin sistemiyle aynı olduğunu garanti etmiyor. Çözüm bileşik bir foreign key:

CREATE TABLE planets (
  id              int PRIMARY KEY,
  solar_system_id int NOT NULL,
  UNIQUE (id, solar_system_id)
);

CREATE TABLE moons (
  id              int PRIMARY KEY,
  planet_id       int NOT NULL,
  solar_system_id int NOT NULL,
  FOREIGN KEY (planet_id, solar_system_id)
    REFERENCES planets (id, solar_system_id) ON DELETE CASCADE
);

Artık yanlış sistemdeki bir ayı eklemek mümkün değil:

ERROR:  insert or update on table "moons" violates foreign key constraint "moons_planet_id_solar_system_id_fkey"
DETAIL:  Key (planet_id, solar_system_id)=(1, 200) is not present in table "planets".

Kopyaladığınız bir sütunu bu şekilde kaynağına bağlayabiliyorsanız, kopyanın tutarlılığını uygulama kodu yerine veritabanı korur. Bu, denormalizasyonun en güvenli hâli.

Şemayı sonradan değiştirmek neden pahalı

Buraya kadar anlattıklarımın ortak noktası, hepsinin sistem çalışırken fark edilmiş olması. Sonradan yapılan bir şema değişikliği, boş bir veritabanında yaptığınız değişiklikten çok daha fazlasını gerektirir. Bende en çok tekrar eden üç durum şunlar.

Yeniden adlandırma, silip yeniden oluşturmaya dönüşebilir

Bir tabloyu yeniden adlandırmak isteyin; ORM'iniz şemadaki farkı nasıl yorumlar? Prisma'nın migrate diff komutu bir tablonun adı değiştiğinde bunu DROP TABLE ve ardından CREATE TABLE olarak ifade ediyor. Boş bir geliştirme veritabanında fark etmez. Üretimde, o tablodaki bütün veriyi siler.

KillReport'ta saatlik sistem verilerini tutan tabloyu yeniden adlandırırken migration'ı elle yazdım:

ALTER TABLE system_kills RENAME TO system_activity;

Bu, verinin yerinde kalmasını sağlıyor ama işi bitirmiyor. PostgreSQL tabloyu yeniden adlandırdığında ona bağlı nesnelerin adlarına dokunmaz:

                    Table "public.system_activity"
 Column |  Type   | Nullable |                 Default
--------+---------+----------+------------------------------------------
 id     | integer | not null | nextval('system_kills_id_seq'::regclass)
Indexes:
    "system_kills_pkey" PRIMARY KEY, btree (id)

Sequence, birincil anahtar ve indeksler hâlâ eski adı taşıyor. ORM bir sonraki karşılaştırmada bunları fark olarak görüyor. Bu yüzden onları da tek tek yeniden adlandırmak gerekiyor.

Yeni sütunun varsayılan değeri bir iddiadır

Aynı tabloya geçiş verisi için yeni bir sütun ekledim. İlk aklıma gelen DEFAULT 0 idi. Ama o tabloda sunucunun o güne kadar topladığı saatlik kayıtlar vardı ve o saatlerin geçiş verisini bilmiyordum. DEFAULT 0 bütün eski saatlere "o saatte hiç geçiş olmadı" yazacaktı. Yedi günlük grafikte de olmayan bir düz çizgi görünecekti.

ALTER TABLE system_activity ADD COLUMN ship_jumps integer;

Sütunu varsayılan değersiz ve NULL olabilir şekilde ekledim. NULL "bilmiyorum" demek, 0 ise "biliyorum, sıfır" demek. Eski satırlar için doğru cevap birincisiydi.

Sütun gerçekten zorunluysa ve tablo büyükse, PostgreSQL'de bunu tabloyu uzun süre kilitlemeden yapmanın bilinen bir yolu var. Önce sütunu NULL olabilir ekleyin ve eski satırları doldurun. Sonra kısıtlamayı doğrulanmamış olarak ekleyip ayrı bir adımda doğrulayın:

ALTER TABLE posts ADD COLUMN slug text;
-- eski satırları doldurun (büyük tabloda parça parça)
UPDATE posts SET slug = 'p-' || id WHERE slug IS NULL;

ALTER TABLE posts ADD CONSTRAINT posts_slug_not_null
  CHECK (slug IS NOT NULL) NOT VALID;
ALTER TABLE posts VALIDATE CONSTRAINT posts_slug_not_null;

ALTER TABLE posts ALTER COLUMN slug SET NOT NULL;
ALTER TABLE posts DROP CONSTRAINT posts_slug_not_null;

NOT VALID kısıtlamayı yalnızca yeni yazılan satırlara uygular ve tabloyu taramaz. VALIDATE taramayı yapar ama yazmaları engellemez. Son adımda SET NOT NULL, doğrulanmış kısıtlamayı görür ve tabloyu ikinci kez taramaz. Kilitleri serinin altıncı bölümünde ayrıntılı anlatacağım.

Sonradan eklenen bir sütun, kuralları da değiştirir

Şemayı sonradan değiştirmenin en büyük örneğini Biocidal'da yaşadım. Sistemin ilk altı yılında veri modelinde "kurum" diye bir kavram yoktu. Birden fazla kurumu aynı veritabanında ağırlamaya karar verdiğimde on iki tabloya kurum kimliği (company_id) eklemem gerekti.

Sütunu eklemek kolay kısımdı. Önce NULL olabilir şekilde ekledim, bütün mevcut satırları bir varsayılan kuruma atadım, sonra zorunlu yaptım. Zor kısım, "benzersiz" kelimesinin anlamının değişmesiydi. Tek kurumlu sistemde bir araç plakası tüm tabloda benzersizdi. Çok kurumlu sistemde iki farklı firmanın aynı plakayı kaydetmesi mümkün olmalıydı, aynı firmanın iki kez kaydetmesi ise olmamalıydı:

DROP INDEX vehicle_plate_key;
CREATE UNIQUE INDEX vehicle_plate_company_id_key ON vehicle (plate, company_id);

Bunu her unique kısıtlama için tek tek düşünmek gerekiyor. Ben ilk denemede fazlasını kurumlara böldüm. Ertesi gün bir kısmını geri aldım: biyosidal ürünler gibi bütün kurumların ortak kullandığı referans tablolarından kurum kimliğini kaldırdım. E-posta adresinin de kurum bazında değil, tüm sistemde benzersiz kalması gerektiğine karar verdim. Bu kararların hiçbiri teknik değildi; hepsi "bu veri kime ait?" sorusunun cevabıydı.

Çok kiracılı mimariyi, verinin kurumlar arasında nasıl ayrıldığını ve bu ayrımın nerelerde delindiğini serinin onuncu bölümünde ayrıntılı anlatacağım. Bu bölüm için ders şu: veri modelindeki bir varsayımı değiştirdiğinizde, o varsayıma dayanan bütün kısıtlamaları da yeniden düşünmeniz gerekir.

ORM'in bilmediği tablolar

KillReport'ta beş tablo bilerek Prisma şemasının dışında duruyor: killmail_filters, üç sayaç tablosu ve bir kayıt tablosu. Bunlar elle yazılmış SQL'le yönetiliyor, çünkü Prisma'nın ifade edemediği şeyler kullanıyorlar.

Sorun şu: Prisma bu tabloları veritabanında görüyor ama şemasında bulamıyor. Bunu bir sapma olarak yorumluyor ve prisma migrate dev ilk fırsatta bu tabloları silmeyi öneriyor. Bir noktada bu öneri 99.724 satıra karşılık geliyordu. Daha da kötüsü, prisma migrate status aynı anda "Database schema is up to date!" diyor; yani durum komutuna bakarak bu tehlikeyi göremiyorsunuz.

Bunun için iki önlem aldım. Birincisi, her migration'ı prisma migrate diff ile üretip içindeki DROP TABLE satırlarını elle çıkarmak ve uygulamadan önce dosyada hiç DROP kalmadığını kontrol etmek. İkincisi, yazılı kuralın yetmediğini görünce bu projede birlikte çalıştığım kodlama asistanının migrate dev, migrate reset ve veri kaybını kabul eden db push komutlarını çalıştırmasını bir kancayla (hook) engellemek. Yazılı bir kural, çiğnenmesi mümkün olduğu sürece bir gün çiğnenir; hele komutları yazan her zaman siz değilseniz.

Aynı şey Biocidal'da tablolar yerine indekslerle oluyor. PostGIS geometri sütunları Prisma şemasında Unsupported(...) olarak tanımlı ve üzerlerindeki GiST indekslerini Prisma göremiyor. migrate dev her seferinde bu indeksleri silen satırlar üretiyor; migration dosyalarının başındaki notlar, bu satırları her seferinde ayıklamayı hatırlatıyor.

ORM'le çalışıyorsanız ve veritabanında ORM'in bilmediği tek bir nesne bile varsa, migration aracınızın onu nasıl gördüğünü bugün kontrol edin.

Bir sütun eklerken sorduğum sorular

Bütün bunları bir kontrol listesine indirmem gerekse şöyle olurdu:

  • Bu bilginin asıl yeri neresi? Başka bir tabloda zaten varsa, burada neden tekrar tutuyorum?
  • Kopyaysa, kim güncelliyor? Cevap tek bir fonksiyon ya da tek bir transaction değilse, zamanla ayrışacak.
  • Bu değer değişebilir mi? Değişmiyorsa kopya ucuzdur. Değişiyorsa, onu güncel tutan bir süreç gerekir.
  • Kopya mı, o anın kaydı mı? Sipariş satırındaki fiyat bir kopya değildir. İkisini karıştırmak geçmişi bozar.
  • Tipi verinin anlamına uyuyor mu? Telefon numarası, posta kodu ya da kimlik numarası rakamlardan oluşsa da bir sayı değildir.
  • Veritabanı bu kuralı koruyabilir mi? Foreign key, unique ya da check ile söyleyebildiğim bir kuralı uygulama koduna bırakmıyorum.
  • Eski satırlar için doğru değer ne? Bilmiyorsam NULL, uydurma bir varsayılan değil.

Sırada ne var

Veri modeli doğru olsa da sorgular yavaş olabilir. Bir sonraki bölümde indeksleri anlatacağım: B-tree'nin neden çoğu zaman yettiğini, GIN'in diziler ve JSON için ne zaman gerektiğini, GiST'in mekânsal sorgulardaki yerini ve yalnızca bir koşulu sağlayan satırları indeksleyen partial indekslerin ne zaman işe yaradığını.

Yeni yazılar e-postanıza gelsin

Yeni bir yazı yayımladığımda kısa bir e-posta alırsınız. İstediğiniz zaman tek tıkla çıkabilirsiniz. Gizlilik

İlgili yazılar