Mert Özen Her Satırda Daha İleri
Veritabanı PostgreSQL — #02

PostgreSQL Lab'ı: Docker'da PostgreSQL 18, psql, pgAdmin ve Bookstore'u Taşımak

Mert Özen 14 Kas 2026 16 dk 2 görüntülenme
PostgreSQL Lab'ı: Docker'da PostgreSQL 18, psql, pgAdmin ve Bookstore'u Taşımak

PostgreSQL 18 image'ında volume artık /var/lib/postgresql'e bağlanır ve veri 18/docker alt klasöründe durur; eski /data yolundaki veriyle image hiç açılmaz. Bookstore için kalıcı bir lab: isimli volume, owner/uygulama/superuser rollerini ayırmak, ALTER DEFAULT PRIVILEGES, snake_case ve IDENTITY ile taşınmış 1 milyon siparişlik şema, psql meta komutları ve psqlrc, pg_hba.conf, ALTER SYSTEM ile reload/restart farkı ve EF Core'un tırnaklı "Books" tablosu tuzağı.

Sürüm notu: Bu yazıdaki örnekler PostgreSQL 18.7 (resmî postgres:18 Docker image'ı), psql 18, .NET 10, Npgsql 10.0.3 ve Npgsql.EntityFrameworkCore.PostgreSQL 10.0.3 ile çalıştırılmıştır.

Önce Tahmin Et

Bir ekip arkadaşın Bookstore'un (sitedeki serilerin ortak örnek uygulaması, bir online kitapçı) geliştirme ortamında PostgreSQL 17 kullanıyor. Volume yıllardır internetteki örneklerde gördüğümüz yere bağlı: /var/lib/postgresql/data. İçinde birkaç tablo ve test verisi var. Bir gün image etiketini 18'e çekip container'ı aynı volume ile yeniden başlatıyor:

# Dün: PostgreSQL 17, veri bu volume'da
docker run -d --name bookstore-pg -e POSTGRES_PASSWORD=$env:PG_SUPERUSER_PASSWORD `
  -v bookstore-pgdata:/var/lib/postgresql/data postgres:17

# Bugün: aynı volume, image 18
docker rm -f bookstore-pg
docker run -d --name bookstore-pg -e POSTGRES_PASSWORD=$env:PG_SUPERUSER_PASSWORD `
  -v bookstore-pgdata:/var/lib/postgresql/data postgres:18

Ne olur? PostgreSQL 18 eski veriyi görüp otomatik upgrade mi eder, boş bir veritabanıyla mı açılır, yoksa hiç açılmaz mı?

docker logs bookstore-pg
Error: in 18+, these Docker images are configured to store database data in a
       format which is compatible with "pg_ctlcluster" (specifically, using
       major-version-specific directory names).  This better reflects how
       PostgreSQL itself works, and how upgrades are to be performed.

       See also https://github.com/docker-library/postgres/pull/1259

       Counter to that, there appears to be PostgreSQL data in:
         /var/lib/postgresql/data

       This is usually the result of upgrading the Docker image without
       upgrading the underlying database using "pg_upgrade" (which requires both
       versions).

Container hiç açılmadı ve exit code 1 ile durdu. Bu iyi bir sonuç: image eski yoldaki veriyi fark edip çalışmayı reddediyor. Major sürüm değişikliği, veri dosyalarının formatı değiştiği için pg_upgrade ya da dump/restore ister; Docker image bunu senin yerine yapmaz. Hatayı "çözmek" için volume'u yeni yola, ama yeni ve boş bir volume ile bağlarsan container mutlu mutlu açılır ve uygulaman boş bir veritabanı görür. Veri kaybolmamıştır, eski volume'da duruyordur; ama o an öyle hissettirir. Bu yazıda doğru kurulumla bir lab hazırlıyoruz: image ve volume yolu, roller, Bookstore şemasının PostgreSQL sürümü, psql ve configuration dosyaları.

postgres:18 Image'ı: Volume Yolu, PGDATA ve Parola

PostgreSQL 18 ile resmî image'ın düzeni değişti. Image'ın tanımladığı volume artık /var/lib/postgresql, veri ise sürüme özgü bir alt klasörde: PGDATA=/var/lib/postgresql/18/docker. Amaç, Debian paketlerinin kullandığı düzene yaklaşmak ve major upgrade'i kolaylaştırmak. pg_upgrade --link eski ve yeni veri klasörünün aynı dosya sistemi üzerinde olmasını ister; tek bir mount altında 17/docker ve 18/docker yan yana durunca bu mümkün oluyor. 17 ve öncesinde ise doğru yer hâlâ /var/lib/postgresql/data; image sürümüne göre yolu değiştirmen gerekiyor. Lab'ı PowerShell'de şöyle kuruyoruz:

# Superuser parolası yalnızca ilk açılışta, veri klasörü boşken kullanılır
$env:PG_SUPERUSER_PASSWORD = "<strong-password>"
docker volume create bookstore-pgdata
docker run -d --name bookstore-pg `
  -e POSTGRES_PASSWORD=$env:PG_SUPERUSER_PASSWORD `
  -p 5432:5432 `
  -v bookstore-pgdata:/var/lib/postgresql `
  -v "${PWD}\db:/lab:ro" `
  postgres:18

docker logs bookstore-pg
The database cluster will be initialized with locale "en_US.utf8".
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".
Data page checksums are enabled.
fixing permissions on existing directory /var/lib/postgresql/18/docker ... ok
...
initdb: warning: enabling "trust" authentication for local connections
...
PostgreSQL init process complete; ready for start up.
LOG:  listening on IPv4 address "0.0.0.0", port 5432
LOG:  database system is ready to accept connections

Log'da üç satır önemli. Birincisi Data page checksums are enabled: PostgreSQL 18'den itibaren initdb data checksum'ları varsayılan olarak açıyor; diskte bozulan bir page okunduğunda sessizce yanlış veri dönmek yerine hata alırsın. İkincisi locale: cluster en_US.utf8 ile kuruldu; Türkçe sıralama ve İ harfi 03'ün konusu. Üçüncüsü trust uyarısı: container'ın içinden yapılan bağlantılar parola sormaz. Aşağıda pg_hba.conf'a bakınca bunun ne demek olduğunu göreceğiz.

İkinci -v, repodaki db klasörünü container'ın içine salt okunur olarak bağlıyor. Şema ve deneme script'lerini docker exec ile /lab/... yolundan çalıştıracağız; okurların çoğunun Windows'ta psql'i ayrıca kurmasına gerek kalmıyor.

Image etiketi seçimi de bir karar. postgres:latest bir gün major sürüm değiştirir ve tam olarak "Önce Tahmin Et"teki duruma düşersin. postgres:18 18'in en son minor sürümünü takip eder; minor sürümler yalnızca hata ve güvenlik düzeltmesi getirdiği için çoğu ortamda doğru tercih bu. Ortamları birebir aynı tutmak istiyorsan postgres:18.7 gibi tam sürüme sabitler, güncellemeyi bilinçli yaparsın. Parola tarafında POSTGRES_PASSWORD yalnızca veri klasörü boşken, yani ilk açılışta okunur; sonradan değiştirmenin etkisi yoktur. Production'a benzeyen ortamlarda POSTGRES_PASSWORD_FILE ile parolayı bir Docker secret dosyasından vermek daha doğru.

Rolleri Ayırmak: Owner, Uygulama ve Superuser

Image'ın oluşturduğu postgres rolü superuser. SQL Server'daki sa gibi her şeyi yapabilir: her tabloyu okur, her rolü değiştirir, COPY ... FROM PROGRAM ile sunucuda komut çalıştırır. Uygulamanın bu rolle bağlanması, uygulamadaki bir SQL injection açığının veritabanı sunucusunun tamamına yayılması demek. Lab'da baştan üç rol kullanıyoruz: yönetim için postgres, şemanın sahibi ve migration'ları çalıştıran bookstore_owner, uygulamanın bağlandığı bookstore_app. PostgreSQL'de login ve user ayrımı yok; ikisi de role, bağlanabilmesi için LOGIN yetkisi yeterli.

CREATE ROLE bookstore_owner LOGIN;
CREATE ROLE bookstore_app LOGIN;
\password bookstore_owner
\password bookstore_app

CREATE DATABASE bookstore OWNER bookstore_owner;
REVOKE ALL ON DATABASE bookstore FROM PUBLIC;
GRANT CONNECT, TEMPORARY ON DATABASE bookstore TO bookstore_app;

\connect bookstore
/* bookstore_owner'ın ileride oluşturacağı tablo ve sequence'lar uygulama rolüne açık olsun */
ALTER DEFAULT PRIVILEGES FOR ROLE bookstore_owner IN SCHEMA public
    GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO bookstore_app;
ALTER DEFAULT PRIVILEGES FOR ROLE bookstore_owner IN SCHEMA public
    GRANT USAGE, SELECT ON SEQUENCES TO bookstore_app;
docker exec -it bookstore-pg psql -U postgres -f /lab/00-roles.sql

\password parolayı ekranda göstermeden iki kez sorar ve sunucuya SCRAM (parolanın kendisini değil, ondan türetilen bir doğrulayıcıyı saklayan kimlik doğrulama yöntemi) ile hash'lenmiş hâlini gönderir; parola ne script'e ne de komut geçmişine düşer. SQL Server'da GRANT SELECT ON SCHEMA::dbo gibi schema seviyesinde bir yetki, schema'ya sonradan eklenen tabloları da kapsar. PostgreSQL'de GRANT ... ON ALL TABLES IN SCHEMA yalnızca o an var olan tabloları kapsar; ileride oluşturulacaklar için ALTER DEFAULT PRIVILEGES gerekir. Bu sayede owner yeni bir tablo oluşturduğunda uygulama rolü o tabloda okuma ve yazma (SELECT, INSERT, UPDATE, DELETE) yetkisini kendiliğinden alır, her migration'dan sonra GRANT yazmak gerekmez. PostgreSQL 15'ten beri public schema'da herkesin tablo oluşturma yetkisi yok; schema'nın sahibi database'in sahibi. Bu yüzden uygulama rolü tablo oluşturamıyor:

docker exec -it bookstore-pg psql -U bookstore_app -d bookstore -c "CREATE TABLE wishlist (book_id int)"
ERROR:  permission denied for schema public
LINE 1: CREATE TABLE wishlist (book_id int);
                     ^

İstediğimiz tam olarak bu: uygulama veriyi okur ve yazar, şemayı değiştiremez. Yetki modelinin ayrıntıları (schema bazında yetki, pg_read_all_data gibi hazır roller, RLS (Row-Level Security; satır bazında erişim kuralları)) 14. yazıda.

Bookstore Şemasını PostgreSQL'e Taşımak

SQL Server Derinlemesine serisinde kullandığımız Bookstore şemasını PostgreSQL'e taşıdık: aynı sekiz tablo, aynı satır sayıları, aynı deterministik veri üretimi. random() kullanılmıyor; her satırın değeri satır numarasından türetilen bir hash'ten geliyor, bu yüzden iki yükleme birebir aynı veriyi üretiyor ve yazılardaki sayıları sen de aynen görebiliyorsun. İki tablo örnek:

CREATE TABLE books
(
    book_id        int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    isbn           varchar(13)   NOT NULL,
    title          text          NOT NULL,
    author_id      int           NOT NULL,
    category       text          NOT NULL,
    price          numeric(10,2) NOT NULL,
    published_year smallint      NOT NULL
);

CREATE TABLE orders
(
    order_id     int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    order_number varchar(20)   NOT NULL,
    customer_id  int           NOT NULL,
    status       text          NOT NULL,
    created_at   timestamptz   NOT NULL,
    total_amount numeric(12,2) NOT NULL,
    currency     char(3)       NOT NULL
);

Tip eşlemesinde verdiğimiz kararlar şunlar:

SQL ServerPostgreSQLNot
nvarchar(n), nvarchar(max)textPostgreSQL'de metin her zaman database encoding'iyle (UTF8) saklanır; N öneki yok.
int IDENTITY(1,1)int GENERATED ALWAYS AS IDENTITYSQL standardı sözdizimi; eski serial yerine.
datetime2 (UTC)timestamptzBir zaman anını saklar; 03'te ayrıntılı.
decimal(10,2)numeric(10,2)Aynı kavram, iki isim.
bitbooleanGerçek bir boolean tipi.
uniqueidentifieruuid18'de uuidv7() geliyor.
PascalCase isimlersnake_case isimlerTırnaksız isimler küçük harfe çevrilir.

Son satır en önemlisi; tuzaklar bölümünde göreceğiz. Lehçe farklarının genel özeti için SQL Temelleri serisinin "Aynı Sorgu Dört Lehçede" yazısına bakabilirsin. GENERATED ALWAYS seçimi de bilinçli: uygulamanın id değerini elle vermesini engelliyor.

INSERT INTO books (book_id, isbn, title, author_id, category, price, published_year)
VALUES (40001, '9780000040001', 'Dune #40001', 1, 'SciFi', 19.90, 1965);
ERROR:  cannot insert a non-DEFAULT value into column "book_id"
DETAIL:  Column "book_id" is an identity column defined as GENERATED ALWAYS.
HINT:  Use OVERRIDING SYSTEM VALUE to override.

Veri yükleyen script bu yüzden OVERRIDING SYSTEM VALUE kullanıyor ve sonunda identity sequence'larını setval ile en büyük id'ye ilerletiyor; SQL Server'daki SET IDENTITY_INSERT ON ve DBCC CHECKIDENT ikilisinin karşılığı. Script'i owner rolüyle çalıştırıyoruz:

docker exec -it bookstore-pg psql -U bookstore_owner -d bookstore -f /lab/schema.sql
 table_name  |  rows   | total_size
-------------+---------+------------
 authors     |    5000 | 432 kB
 books       |   40000 | 6144 kB
 customers   |  100000 | 16 MB
 order_items | 2500496 | 234 MB
 orders      | 1000000 | 141 MB
 payments    | 1000000 | 118 MB
 shipments   |  954755 | 111 MB
 stock_items |   40000 | 2968 kB
(8 rows)

Yükleme bu lab'da 11 saniye sürdü. Toplam yaklaşık 630 MB; 1 milyon sipariş ve 2,5 milyon sipariş satırı, ileride index ve EXPLAIN denemelerinde planların gerçekçi olması için yeterli. Script idempotent: tabloları silip baştan oluşturuyor, yani bir denemeden sonra lab'ı temiz hâline döndürmek için aynı komutu tekrar çalıştırman yeterli.

psql'in Günlük Komutları

psql, SQL Server tarafındaki sqlcmd'nin karşılığı ama çok daha fazlası. Ters bölü ile başlayan meta komutlar sunucuya gitmez, psql'in kendisi çalıştırır ve çoğu catalog'u okuyan hazır sorgulardır. Uygulama rolüyle bağlanıp en sık kullanılanlara bakalım:

docker exec -it bookstore-pg psql -U bookstore_app -d bookstore
\conninfo
\dt
\d+ books
           Connection Information
      Parameter       |        Value
----------------------+---------------------
 Database             | bookstore
 Client User          | bookstore_app
 Socket Directory     | /var/run/postgresql
 Server Port          | 5432
 Password Used        | false
 Backend PID          | 135
 SSL Connection       | false
 Superuser            | off

                 List of tables
 Schema |    Name     | Type  |      Owner
--------+-------------+-------+-----------------
 public | authors     | table | bookstore_owner
 public | books       | table | bookstore_owner
 public | customers   | table | bookstore_owner
 public | order_items | table | bookstore_owner
 public | orders      | table | bookstore_owner
 public | payments    | table | bookstore_owner
 public | shipments   | table | bookstore_owner
 public | stock_items | table | bookstore_owner
(8 rows)

                         Table "public.books"
     Column     |         Type          | Nullable |           Default
----------------+-----------------------+----------+------------------------------
 book_id        | integer               | not null | generated always as identity
 isbn           | character varying(13) | not null |
 title          | text                  | not null |
 author_id      | integer               | not null |
 category       | text                  | not null |
 price          | numeric(10,2)         | not null |
 published_year | smallint              | not null |
Indexes:
    "books_pkey" PRIMARY KEY, btree (book_id)
    "ix_books_author_id" btree (author_id)
    "ux_books_isbn" UNIQUE, btree (isbn)
Foreign-key constraints:
    "fk_books_authors" FOREIGN KEY (author_id) REFERENCES authors(author_id)
Referenced by:
    TABLE "order_items" CONSTRAINT "fk_order_items_books" FOREIGN KEY (book_id) REFERENCES books(book_id)
    TABLE "stock_items" CONSTRAINT "fk_stock_items_books" FOREIGN KEY (book_id) REFERENCES books(book_id)
Not-null constraints:
    "books_book_id_not_null" NOT NULL "book_id"
    "books_isbn_not_null" NOT NULL "isbn"
    ...
Access method: heap

Çıktıyı kısalttık (\d+ storage ve istatistik kolonlarını da gösterir). İki ayrıntı dikkat çekiyor. Password Used false: container'ın içinden Unix socket ile bağlandık ve trust kuralı parola sormadı. Not-null constraints bölümü ise PostgreSQL 18 ile geldi; NOT NULL artık adı olan gerçek bir constraint olarak catalog'da tutuluyor. \d+, SQL Server'daki sp_help'in daha okunur hâli; index'ler, foreign key'ler ve tabloya başka tabloların referansları tek yerde.

Diğer günlük komutlar: \l database'leri, \dn schema'ları, \du rolleri listeler. \x geniş satırları dikey gösterir, \x auto bunu yalnızca satır ekrana sığmadığında yapar. \timing on her sorgunun süresini yazar. \e son sorguyu editörde açar, kaydedip çıkınca çalıştırır; container içinde editör kısıtlı olduğu için bu komutu kendi makinene kurduğun psql ile kullanmak daha rahat. \gset sorgu sonucunu psql değişkenine yazar, \watch 2 son sorguyu iki saniyede bir tekrarlar; 01'de WAL (Write-Ahead Log; değişikliklerin önce yazıldığı log) ölçümünde \gset kullanmıştık. Her bağlantıda aynı ayarları yazmamak için bir psqlrc dosyası hazırladık ve repodaki db klasörüne koyduk:

\set QUIET 1
\pset null '(null)'
\set COMP_KEYWORD_CASE upper
\set HISTCONTROL ignoredups
\x auto
\timing on
\set PROMPT1 '%n@%/%R%# '
\unset QUIET
# psql varsayılan olarak ~/.psqlrc okur; PSQLRC ile dosyayı biz gösteriyoruz
docker exec -it -e PSQLRC=/lab/psqlrc bookstore-pg psql -U bookstore_app -d bookstore
bookstore_app@bookstore=> SELECT book_id, title, price FROM books WHERE book_id <= 2;
 book_id |     title     | price
---------+---------------+-------
       1 | Emma #1       | 32.77
       2 | The Hobbit #2 | 39.00
(2 rows)

Time: 0.578 ms

NULL değerin boş string'den ayırt edilmesi ((null)), anahtar kelimelerin büyük harfle tamamlanması ve prompt'ta hangi rolle hangi database'e bağlı olduğunu görmek, özellikle aynı anda lab ve production'a bağlıyken hata riskini azaltır.

postgresql.conf, pg_hba.conf ve ALTER SYSTEM

PostgreSQL'in ayarları iki ana dosyada durur. postgresql.conf sunucu parametrelerini (bellek, log, bağlantı sayısı), pg_hba.conf ise kimin, nereden, hangi database'e, hangi yöntemle bağlanabileceğini tutar. SQL Server'da bu iki iş sp_configure ve login/endpoint ayarları arasında dağılmış durumda. Dosyaların yerini ve pg_hba.conf'un okunmuş hâlini SQL ile görebilirsin:

SHOW hba_file;
SELECT line_number, type, database, user_name, address, auth_method FROM pg_hba_file_rules;
                 hba_file
-------------------------------------------
 /var/lib/postgresql/18/docker/pg_hba.conf

 line_number | type  |   database    | user_name |  address  |  auth_method
-------------+-------+---------------+-----------+-----------+---------------
         117 | local | {all}         | {all}     |           | trust
         119 | host  | {all}         | {all}     | 127.0.0.1 | trust
         121 | host  | {all}         | {all}     | ::1       | trust
         124 | local | {replication} | {all}     |           | trust
         125 | host  | {replication} | {all}     | 127.0.0.1 | trust
         126 | host  | {replication} | {all}     | ::1       | trust
         128 | host  | {all}         | {all}     | all       | scram-sha-256
(7 rows)

Kurallar yukarıdan aşağı okunur ve ilk eşleşen uygulanır. Container'ın içinden gelen bağlantılar (Unix socket ve 127.0.0.1) trust, yani parolasız. Dışarıdan, örneğin Windows'taki uygulamandan port 5432 üzerinden gelen bağlantı ise son satıra düşer ve SCRAM-SHA-256 ile parola ister. Lab için makul; production'da trust satırı olmamalı. pg_hba.conf'un ayrıntıları ve şifreli bağlantı (TLS) 14. yazıda.

Parametreleri değiştirmek için dosyayı elle düzenlemek yerine ALTER SYSTEM kullanılabilir. Değer postgresql.auto.conf dosyasına yazılır ve postgresql.conf'tan sonra okunduğu için onu ezer:

ALTER SYSTEM SET log_min_duration_statement = '250ms';
ALTER SYSTEM SET shared_buffers = '512MB';
SELECT pg_reload_conf();

SELECT name, setting, unit, pending_restart FROM pg_settings
WHERE name IN ('log_min_duration_statement', 'shared_buffers');
            name            | setting | unit | pending_restart
----------------------------+---------+------+-----------------
 log_min_duration_statement | 250     | ms   | f
 shared_buffers             | 16384   | 8kB  | t

İki parametre iki farklı davranış gösteriyor. log_min_duration_statement reload ile hemen geçerli oldu: 250 ms'den uzun süren her sorgu artık log'a düşüyor. shared_buffers ise hâlâ eski değerde (16384 × 8 kB = 128MB) ve pending_restart = t diyor; shared memory yalnızca sunucu açılırken ayrıldığı için restart gerekiyor. pg_settings'teki context kolonu her parametrenin ne zaman değiştirilebileceğini söyler (postmaster restart, sighup reload, user oturum içinde). Bir not daha: pg_reload_conf() yalnızca sinyal gönderir ve hemen döner; aynı komut satırında hemen arkasından okursan eski değeri görebilirsin. docker restart bookstore-pg sonrasında SHOW shared_buffers 512MB döndü. Lab'ın sonraki yazılarda varsayılanlarla çalışması için denemeden sonra ALTER SYSTEM RESET ALL ile ayarları geri aldık.

Bağlantı Dizesi ve GUI Araçları

.NET tarafında Npgsql bağlantı dizesi SQL Server'dakine benzer ama anahtarlar farklı:

Host=localhost;Port=5432;Database=bookstore;Username=bookstore_app;Password=<app-password>

Server yerine Host, User Id yerine Username; Windows authentication karşılığı olarak Kerberos/GSSAPI desteği var ama lab'da parola kullanıyoruz. Parolayı repoya değil, geliştirmede user-secrets'a ya da ortam değişkenine koy. 01'deki denemeleri bu kez lab'a ve uygulama rolüyle çalıştırmak için BOOKSTORE_PG ortam değişkenini bu diziye çevirmen yeterli.

Grafik araç olarak üç seçenek öne çıkıyor. pgAdmin 4 PostgreSQL topluluğunun resmî yönetim aracı (yazı tarihinde 9.19); masaüstü uygulaması ya da container olarak çalışır, plan görselleştirme ve rol yönetimi gibi PostgreSQL'e özgü ekranları var. Microsoft'un Visual Studio Code için PostgreSQL extension'ı Kasım 2025'te genel kullanıma açıldı; SQL Server tarafında Visual Studio Code'un mssql extension'ını kullanıyorsan en tanıdık deneyim bu. DBeaver ise birden fazla veritabanıyla aynı anda çalışanlar için iyi bir genel araç. Hangisini seçersen seç, bağlantıyı bookstore_app ile değil, okuma için ayrı bir rolle ya da owner ile kur ve production'a bağlanırken renk ya da isimle ortamı ayırt et.

Üretimde Dikkat

Bu lab bir geliştirme ortamı. Production'a giderken şunları ayrıca düşün: image etiketini en azından major sürüme sabitle ve minor güncellemeleri planlı uygula; veri volume'unun yedeğini ayrı al (volume yedeği ile tutarlı veritabanı yedeği aynı şey değil, pg_dump ve pg_basebackup 14'te). shared_buffers, work_mem ve max_connections'ı container'a verdiğin bellek limitine göre ayarla; container'ın bellek limiti PostgreSQL'e ayrıca bildirilmez, limit aşılınca kernel process'i öldürür. pg_hba.conf'ta trust bırakma, uygulama rolünü yalnızca ihtiyaç duyduğu database'e bağla. Kubernetes'te PostgreSQL işletmek ayrı bir konu; Kubernetes serisinin storage ve stateful uygulamalar yazısında veritabanını cluster'ın içinde mi dışında mı çalıştıracağına dair kararı tartışmıştık.

Sık Düşülen Tuzaklar

Tırnaklı isimler. Entity Framework Core (EF Core), SQL Server için yazılmış bir model'den PostgreSQL'de ne üretir? Npgsql provider'ı ile varsayılan ayarlarda Book ve Author entity'leri için ürettiği script:

public sealed class BookstoreContext(DbContextOptions<BookstoreContext> options) : DbContext(options)
{
    public DbSet<Book> Books => Set<Book>();
    public DbSet<Author> Authors => Set<Author>();
}

DbContextOptions<BookstoreContext> options = new DbContextOptionsBuilder<BookstoreContext>()
    .UseNpgsql(connectionString)
    .Options;
using BookstoreContext db = new(options);
Console.WriteLine(db.Database.GenerateCreateScript());
CREATE TABLE "Books" (
    "Id" integer GENERATED BY DEFAULT AS IDENTITY,
    "Isbn" text NOT NULL,
    "Title" text NOT NULL,
    "Price" numeric NOT NULL,
    "PublishedAt" timestamp with time zone NOT NULL,
    "AuthorId" integer NOT NULL,
    CONSTRAINT "PK_Books" PRIMARY KEY ("Id"),
    CONSTRAINT "FK_Books_Authors_AuthorId" FOREIGN KEY ("AuthorId") REFERENCES "Authors" ("Id") ON DELETE CASCADE
);

Bütün isimler çift tırnak içinde ve PascalCase. PostgreSQL tırnaksız isimleri küçük harfe çevirir, tırnaklı isimleri olduğu gibi saklar; yani "Books" ile books iki ayrı tablo. Bu tabloyu lab'daki books'un yanına oluşturunca ne olduğuna bak:

SELECT count(*) FROM Books;
SELECT count(*) FROM "Books";
SELECT "Title" FROM Books LIMIT 1;
 count
-------
 40000

 count
-------
     0

ERROR:  column "Title" does not exist
LINE 1: SELECT "Title" FROM Books LIMIT 1;
               ^
HINT:  Perhaps you meant to reference the column "books.title".

Tırnaksız Books sessizce books'a gitti ve 40.000 döndü; EF Core'un tablosu ise boş. Raporlama için elle yazılan SQL'de "Books" tırnağını unutmak, hata bile vermeden yanlış tablodan okumak demek. Ayrıca Price kolonu hassasiyetsiz numeric olarak geldi (model'de precision verilmemiş) ve DateTime, timestamp with time zone'a eşlendi. İsimlendirmeyi snake_case'e çevirmeyi (EFCore.NamingConventions) ve Npgsql'in tip eşlemesini 11 ve 12. yazılarda göreceğiz; Bookstore şemasında baştan snake_case seçmemizin nedeni bu.

Uygulamayı postgres ile bağlamak. Hızlı başlamak için cazip, ama superuser 01'de gördüğümüz, superuser'a ayrılmış 3 bağlantıyı da kullanır; bağlantı sızdıran bir uygulama, yöneticinin acil durumda bağlanacağı yeri de tüketir. Ayrıca superuser RLS'yi atlar ve sunucuda dosya okuyup komut çalıştırabilir. İlk günden ayrı rol.

Volume'suz container. -v vermezsen image'ın tanımladığı volume anonim olarak oluşturulur; docker rm -v ya da --rm ile container'la birlikte silinir. Lab'da bile isimli volume kullan.

POSTGRES_PASSWORD'u değiştirip parolanın değiştiğini sanmak: veri klasörü doluysa image bu değişkeni okumaz. Parolayı \password postgres ile değiştir.

Küçük Bir Deneme

★☆☆ docker exec bookstore-pg ls /var/lib/postgresql /var/lib/postgresql/18/docker ile volume'un içine bak. postgresql.auto.conf, pg_hba.conf ve pg_wal nerede duruyor?

★★☆ Kendi makinene psql kurup (PostgreSQL'in Windows kurulumunda yalnızca "Command Line Tools" bileşenini seçmen yeterli) psql -h localhost -U bookstore_app -d bookstore ile bağlan. Bu kez parola sorulmasının nedenini pg_hba_file_rules çıktısında hangi satıra denk geldiğini bularak açıkla.

★★★ Yeni bir rol oluştur: bookstore_readonly, yalnızca SELECT yapabilsin ve owner'ın ileride oluşturacağı tablolarda da otomatik yetki alsın. Ardından schema.sql'i yeniden çalıştırıp (tabloları silip yeniden oluşturur) bu rolün yeni tabloları hâlâ okuyabildiğini doğrula.

Lab hazır. Bir sonraki yazıda bu tablolardaki tiplere yakından bakıyoruz: text ile varchar, timestamptz'nin aslında ne sakladığı, uuidv7(), enum, array, range ve Türkçe karakterlerle collation.