PostgreSQL'de CSV dosyasını tabloya aktarmanın en hızlı yolu COPY komutudur: COPY urun FROM '/yol/urunler.csv' WITH (FORMAT csv, HEADER true) dosyayı sunucu tarafında okur, psql'deki \copy urun FROM 'urunler.csv' WITH (FORMAT csv, HEADER true) ise dosyayı sizin bilgisayarınızdan okuyup sunucuya gönderir. Dışa aktarma için COPY (SELECT ...) TO STDOUT WITH (FORMAT csv, HEADER true) veya \copy (...) TO 'dosya.csv' kullanılır. İkisi arasındaki fark, dosyanın hangi makinede durduğu ve hangi yetkiyle okunduğudur.
Bu yazıda COPY ile \copy farkını, CSV seçeneklerini (HEADER, DELIMITER, NULL, QUOTE, FORCE_QUOTE, ENCODING, DEFAULT), hatalı satırları atlama seçeneklerini (ON_ERROR, PostgreSQL 18 ile gelen REJECT_LIMIT, LOG_VERBOSITY) ve büyük veri yüklerken kılavuzun önerdiği adımları anlatıyoruz. Örneklerin hepsi PostgreSQL 18.1 üzerinde, Türkçe karakterli küçük CSV dosyalarıyla çalıştırıldı. CSV biçiminin kendisi (RFC 4180, tırnak kuralları, JSON'a dönüşüm) için CSV formatı ve JSON dönüşümü yazısına bakabilirsiniz.
COPY ve \copy: dosya nerede, kim okuyor?

COPY bir SQL komutudur ve dosya adı verildiğinde dosyayı PostgreSQL sunucu süreci okur veya yazar. Bu üç sonuç doğurur: dosya veritabanı sunucusunun makinesinde olmalıdır, sunucunun çalıştığı işletim sistemi kullanıcısı tarafından erişilebilir olmalıdır ve komutu çalıştıran rol süper kullanıcı ya da pg_read_server_files, pg_write_server_files veya (PROGRAM için) pg_execute_server_program rollerinden birinin yetkisine sahip olmalıdır. Bu kısıt keyfi değildir: dosya adı alan bir COPY, sunucunun erişebildiği her dosyayı okumaya izin verir.
psql'in \copy meta komutu ise arka planda COPY ... FROM STDIN veya COPY ... TO STDOUT çalıştırır ve dosyayı psql'in çalıştığı makinede, sizin kullanıcınızın yetkileriyle okur veya yazar. Süper kullanıcı gerekmez, yalnızca tabloda INSERT veya SELECT yetkisi yeterlidir. Uzak bir veritabanına kendi bilgisayarınızdan veri yüklüyorsanız doğru araç budur. Kılavuzun notuna göre veri istemci ile sunucu arasındaki bağlantıdan geçtiği için \copy, sunucu tarafındaki dosyalı COPY kadar verimli değildir.
| COPY ... dosya | \copy | COPY ... STDIN/STDOUT | |
|---|---|---|---|
| Dosyayı okuyan | Sunucu süreci | psql (istemci) | İstemci uygulaması |
| Dosyanın yeri | Sunucu makinesi | psql'in çalıştığı makine | Uygulamaya bağlı |
| Gereken yetki | Süper kullanıcı veya pg_read/write_server_files | Tabloda INSERT/SELECT | Tabloda INSERT/SELECT |
| Göreli yol | FROM için veri dizinine göre, TO için yasak | psql'in çalışma dizinine göre | Yok |
| Çok satıra yayılabilir mi | Evet | Hayır, tek satır | Evet |
Yetkisiz bir rolle sunucu tarafında dosya okumaya çalıştığımızda PostgreSQL ne yapmamız gerektiğini de söyledi:
SET ROLE raporcu;
COPY urun (ad, fiyat) FROM 'C:/Windows/win.ini' WITH (FORMAT csv);ERROR: permission denied to COPY from a file
DETAIL: Only roles with privileges of the "pg_read_server_files" role may COPY from a file.
HINT: Anyone can COPY to stdout or from stdin. psql's \copy command also works for anyone.CSV dosyasını \copy ile içe aktarmak
Örnek tablo ve dosya şöyle. Dosyada bir değer virgül içerdiği için tırnaklı, bir kategori boş, bir kategori ise iki tırnakla ("") yazılmış, bir stok değeri de boş:
CREATE TABLE urun (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
ad text NOT NULL,
kategori text,
fiyat numeric(10,2) NOT NULL,
stok integer,
eklenme date DEFAULT current_date
);ad,kategori,fiyat,stok
Çay bardağı,Mutfak,45.90,120
"Kahve fincanı, 6'lı",Mutfak,349.00,15
Bakır cezve,,520.00,8
Kolonya,"",89.50,\copy urun (ad, kategori, fiyat, stok) FROM 'urunler.csv' WITH (FORMAT csv, HEADER true)
-- COPY 4
SELECT id, ad, kategori, kategori IS NULL AS kategori_null, fiyat, stok FROM urun ORDER BY id; id | ad | kategori | kategori_null | fiyat | stok
----+---------------------+----------+---------------+--------+------
1 | Çay bardağı | Mutfak | f | 45.90 | 120
2 | Kahve fincanı, 6'lı | Mutfak | f | 349.00 | 15
3 | Bakır cezve | | t | 520.00 | 8
4 | Kolonya | | f | 89.50 |
(4 rows)Bu çıktıda üç önemli kural görünüyor. Birincisi, sütun listesi verdiğimiz için dosyadaki alanlar sırayla ad, kategori, fiyat, stok sütunlarına yazıldı; listede olmayan id ve eklenme varsayılan değerlerini aldı. İkincisi, HEADER true ilk satırı yalnızca atladı; başlıktaki adlar kontrol edilmedi. Üçüncüsü, CSV biçiminde NULL ile boş metin tırnakla ayırt edilir: tırnaksız boş alan (Bakır cezve'nin kategorisi) NULL oldu, "" ise boş metin. Bu ayrımı değiştirmek için FORCE_NOT_NULL (tırnaksız boşları da boş metin say) ve FORCE_NULL (tırnaklı boşları da NULL say) seçenekleri var.
Bir uyarı: tabloda identity sütunu olsa da, sütun listesinde id'yi verirseniz COPY FROM dosyadaki değerleri olduğu gibi yazar; kılavuz bunu INSERT'teki OVERRIDING SYSTEM VALUE davranışına benzetiyor. Sonradan eklenen satırların aynı numaraları almaması için sequence'i ileri almanız gerekir (identity ve sequence).
Sık kullanılan seçenekler

| Seçenek | Varsayılan | Açıklama |
|---|---|---|
| FORMAT | text | text, csv veya binary |
| HEADER | false | true: ilk satırı atla / başlık yaz; MATCH: başlığı sütun adlarıyla karşılaştır (yalnızca FROM) |
| DELIMITER | text: sekme, csv: virgül | Tek baytlık ayraç karakteri |
| NULL | text: \N, csv: tırnaksız boş | NULL'u temsil eden metin |
| QUOTE / ESCAPE | çift tırnak | Yalnızca CSV; ESCAPE varsayılanı QUOTE ile aynı |
| FORCE_QUOTE | yok | TO'da seçilen sütunların NULL olmayan değerlerini hep tırnakla |
| ENCODING | istemci kodlaması | Dosyanın karakter kodlaması |
| DEFAULT | yok | Bu metni gördüğü yerde sütunun varsayılanını kullan (yalnızca FROM) |
| ON_ERROR | stop | ignore: tip dönüşümü başarısız satırı atla |
| REJECT_LIMIT | sınırsız | ON_ERROR ignore ile en fazla kaç hatalı satır |
| FREEZE | false | Aynı transaction'da oluşturulan/boşaltılan tabloya dondurulmuş satır yükle |
HEADER MATCH ile başlık kontrolü
Başlığın gerçekten beklediğiniz sütunlara karşılık geldiğinden emin olmak için HEADER match kullanın. Başlığında urun_adi yazan bir dosyayla denediğimizde:
\copy urun (ad, kategori, fiyat, stok) FROM 'baslik.csv' WITH (FORMAT csv, HEADER match)ERROR: column name mismatch in header line field 1: got "urun_adi", expected "ad"
CONTEXT: COPY urun, line 1: "urun_adi,kategori,fiyat,stok"Sütunların sırası değişmiş bir dosyanın sessizce yanlış sütunlara yazılmasını engellemenin en basit yolu budur.
Ayraç ve karakter kodlaması
Eski bir sistemden noktalı virgülle ayrılmış ve Windows Türkçe kodlamasıyla (WIN1254) kaydedilmiş bir dosya aldığınızı düşünün. Önce yalnızca ayracı belirterek denedik:
\copy urun (ad, kategori, fiyat, stok) FROM 'eski_sistem.csv' WITH (FORMAT csv, HEADER true, DELIMITER ';')ERROR: invalid byte sequence for encoding "UTF8": 0xde 0x61
CONTEXT: COPY urun, line 20xde, WIN1254'te "Ş" harfidir; dosya UTF-8 sanılarak okunduğu için geçersiz sayıldı. ENCODING seçeneği eklenince iki satır da doğru yüklendi:
\copy urun (ad, kategori, fiyat, stok) FROM 'eski_sistem.csv' WITH (FORMAT csv, HEADER true, DELIMITER ';', ENCODING 'WIN1254')
-- COPY 2ENCODING verilmezse dosya istemci kodlamasıyla (client_encoding) yorumlanır; bu kural, dosyayı sunucu doğrudan okusa bile geçerlidir. Kodlamalar ve Türkçe sıralama için collation ve Türkçe sıralama yazısına bakabilirsiniz.
DEFAULT seçeneği
Dosyanın bazı satırlarında bir sütun için "varsayılanı kullan" demek istiyorsanız DEFAULT seçeneği işe yarar. Aşağıdaki dosyada ikinci satırın tarih alanında yok yazıyor:
ad,fiyat,eklenme
Nar ekşisi,140.00,2026-01-15
Sumak,60.00,yok\copy urun (ad, fiyat, eklenme) FROM 'varsayilan.csv' WITH (FORMAT csv, HEADER true, DEFAULT 'yok')
-- COPY 2Nar ekşisi dosyadaki tarihi aldı, Sumak ise yok yerine sütunun varsayılanı olan current_date değerini. Bu, NULL'dan farklıdır: NULL yazmak varsayılanı devreye sokmaz.
Hatalı satırlar: ON_ERROR, REJECT_LIMIT, LOG_VERBOSITY
COPY varsayılan olarak ilk hatada durur ve hiçbir satırı yüklemez. İki hatalı satır içeren şu dosyayı deneyelim:
ad,kategori,fiyat,stok
Lokum,Gıda,120.00,40
Pestil,Gıda,on beş,10
Tarhana,Gıda,75.00,çok
Pekmez,Gıda,95.00,25\copy urun (ad, kategori, fiyat, stok) FROM 'bozuk.csv' WITH (FORMAT csv, HEADER true)ERROR: invalid input syntax for type numeric: "on beş"
CONTEXT: COPY urun, line 3, column fiyat: "on beş"Hata iletisi satır numarasını ve sütunu tam olarak gösteriyor; tablodaki satır sayısı değişmedi. COPY FROM tek bir komut olduğu için ya hepsi ya hiçbiri yüklenir. Ancak kılavuz şunu da not ediyor: başarısız bir COPY o ana kadar yazdığı satırları silinmiş olarak bırakır; görünmezler ama diskte yer kaplarlar ve VACUUM ile geri kazanılır. Biz de sonraki denemelerde identity değerlerinde boşluklar gördük, çünkü başarısız satırlar da sequence'ten numara almıştı.
Hatalı satırları atlayıp geri kalanını yüklemek için ON_ERROR ignore kullanılır. LOG_VERBOSITY verbose eklenince hangi satırın neden atlandığı da yazılır:
\copy urun (ad, kategori, fiyat, stok) FROM 'bozuk.csv' WITH (FORMAT csv, HEADER true, ON_ERROR ignore, LOG_VERBOSITY verbose)NOTICE: skipping row due to data type incompatibility at line 3 for column "fiyat": "on beş"
NOTICE: skipping row due to data type incompatibility at line 4 for column "stok": "çok"
NOTICE: 2 rows were skipped due to data type incompatibility
COPY 2Atlanabilecek hata sayısına üst sınır koymak için PostgreSQL 18 ile gelen REJECT_LIMIT kullanılır. Sınırı 1 yaptığımızda, ikinci hatalı satırda komut tamamen başarısız oldu:
\copy urun (ad, kategori, fiyat, stok) FROM 'bozuk.csv' WITH (FORMAT csv, HEADER true, ON_ERROR ignore, REJECT_LIMIT 1)ERROR: skipped more than REJECT_LIMIT (1) rows due to data type incompatibility
CONTEXT: COPY urun, line 4, column stok: "çok"Bu ikili, "birkaç bozuk satır kabul edilebilir ama dosyanın yarısı bozuksa dur" kuralını uygulamanın kolay yoludur. PostgreSQL: Release 18 notlarına göre REJECT_LIMIT ve LOG_VERBOSITY silent PostgreSQL 18 ile eklendi. Önemli sınır: ON_ERROR yalnızca bir değerin sütun tipine dönüştürülemediği hataları kapsar ve yalnızca text ile CSV biçiminde çalışır. Kısıt ihlalleri atlanmaz. Fiyatı boş bir satırla denediğimizde ON_ERROR ignore açık olduğu halde komut durdu:
ERROR: null value in column "fiyat" of relation "urun" violates not-null constraint
DETAIL: Failing row contains (19, Ayran, İçecek, null, 50, 2026-11-07).
CONTEXT: COPY urun, line 3: "Ayran,İçecek,,50"Kısıt ihlali, yinelenen anahtar veya yabancı anahtar hatası bekliyorsanız veriyi önce kısıtsız bir ara tabloya yükleyip sonra INSERT ... SELECT ile süzmek daha güvenli bir yoldur; çakışan anahtarlar için ON CONFLICT ve MERGE yazısındaki teknikler kullanılabilir. COPY FROM tablodaki tetikleyicileri ve CHECK kısıtlarını çalıştırır, kuralları (rule) çalıştırmaz. Ayrıca WHERE yan tümcesiyle dosyadan yalnızca belirli satırları yükleyebilirsiniz: \copy urun (...) FROM 'urunler.csv' WITH (FORMAT csv, HEADER true) WHERE fiyat > 100 komutu dört satırdan yalnızca fiyatı 100'ün üzerindeki ikisini yükledi.
Dışa aktarma: COPY TO ve sorgu sonucu
Bir tablonun ya da herhangi bir sorgunun sonucunu CSV olarak almak için sorguyu parantez içine yazarsınız:
COPY (SELECT ad, kategori, fiyat FROM urun WHERE id <= 4 ORDER BY id)
TO STDOUT WITH (FORMAT csv, HEADER true);ad,kategori,fiyat
Çay bardağı,Mutfak,45.90
"Kahve fincanı, 6'lı",Mutfak,349.00
Bakır cezve,,520.00
Kolonya,"",89.50Çıktı içe aktardığımız dosyanın aynısı: virgül içeren değer tırnaklandı, NULL tırnaksız boş, boş metin "" olarak yazıldı. Aynı sorguyu FORCE_QUOTE (ad) ve NULL 'NULL' ile çalıştırınca bütün adlar tırnaklandı ve NULL kategori açıkça NULL yazıldı. Varsayılan text biçimi ise sekmeyle ayırır ve NULL için \N kullanır:
Çay bardağı Mutfak 120
Kahve fincanı, 6'lı Mutfak 15
Bakır cezve \N 8
Kolonya \NDosyaya yazmak için yine iki yol var. \copy (SELECT ad, fiyat FROM urun WHERE fiyat > 300 ORDER BY id) TO 'pahali.csv' WITH (FORMAT csv, HEADER true) dosyayı sizin makinenize yazar. Sunucu tarafında COPY ... TO 'dosya' ise mutlak yol ister; göreli yol verdiğimizde ERROR: relative path not allowed for COPY to file hatası aldık. Windows'ta yolu C:/klasor/dosya.csv gibi düz eğik çizgiyle yazabilir ya da kılavuzun önerdiği gibi E'' dizesi kullanıp ters eğik çizgileri çiftleyebilirsiniz. Kılavuzun belirttiği bir Windows ayrıntısını da doğruladık: Windows'ta çalışan sunucu, dosyaya yazarken satırları \r\n ile bitirir; COPY TO STDOUT ise platformdan bağımsız olarak \n gönderir.
COPY tablo TO, SELECT * FROM ONLY tablo ile aynı satırları verir; bölümlenmiş tabloların veya view'ların tüm satırları için COPY (SELECT * FROM tablo) TO biçimini kullanın. PostgreSQL 18 ile COPY TO doldurulmuş materialized view'lardan da doğrudan çalışır. Tarih sütunları içeren verileri başka sistemlere aktarırken DateStyle ayarının çıktıyı etkilediğini, taşınabilirlik için ISO kullanılmasının önerildiğini unutmayın.
Büyük veri yüklemek için öneriler

Kılavuzun "Populating a Database" bölümü, ilk kez büyük miktarda veri yüklerken uygulanabilecek adımları sıralıyor. Bunlar ayar değeri önerisi değil, yöntem önerileridir:
- Çok sayıda
INSERTyerine tek birCOPYkullanın.COPYtek komut olduğu için autocommit'i kapatmaya da gerek kalmaz;INSERTkullanmak zorundaysanız hepsini tek transaction içinde yapın. - Yeni bir tabloya yüklüyorsanız önce tabloyu oluşturun, veriyi
COPYile yükleyin, indeksleri en son kurun. Var olan veriye indeks kurmak, her satırda indeksi güncellemekten hızlıdır. - Yabancı anahtarları yükleme sırasında kaldırıp sonra eklemeyi düşünün. Milyonlarca satırda her satır için bekleyen tetikleyici kaydı belleği zorlayabilir.
maintenance_work_mem'i geçici olarak artırmak indeks ve yabancı anahtar oluşturmayı hızlandırır, amaCOPY'nin kendisine pek etkisi yoktur.max_wal_size'ı geçici olarak artırmak yükleme sırasındaki checkpoint sayısını azaltır.- Yüklemeden sonra mutlaka
ANALYZEçalıştırın.
FREEZE seçeneği, aynı transaction içinde oluşturulmuş veya TRUNCATE edilmiş bir tabloya satırları baştan dondurulmuş olarak yazar ve sonraki VACUUM işini azaltır. Kılavuz bunun normal MVCC görünürlük kurallarını çiğnediğini, verinin yükleme biter bitmez diğer oturumlara görüneceğini hatırlatıyor. Genel performans ayarları için PostgreSQL performans optimizasyonu, pg_dump yedeklerinin geri yüklenmesi için pg_dump ve pg_restore yazılarına bakabilirsiniz.
PostgreSQL 18 ile gelen değişiklikler
PostgreSQL: Release 18 notlarına göre bu sürümde COPY ile ilgili şu değişiklikler var: REJECT_LIMIT ve LOG_VERBOSITY silent eklendi, COPY TO doldurulmuş materialized view'larla çalışır hale geldi, yabancı tablolarda COPY FREEZE artık reddediliyor. Uyumluluğu etkileyen bir değişiklik de var: COPY FROM CSV dosyalarını okurken \. satırını artık veri sonu işareti saymıyor. psql, STDIN'den CSV okurken bu satırı hâlâ son işareti olarak kabul ediyor ve sürüm notları eski psql istemcilerinin PostgreSQL 18 sunucularıyla \copy kullanırken sorun yaşayabileceğini belirtiyor.
Sık yapılan hatalar
- Sunucu tarafı
COPY FROM 'C:/...'ile kendi bilgisayarınızdaki dosyayı okumaya çalışmak; dosya sunucuda aranır. Uzak sunucuda\copykullanın. \copykomutunu birden fazla satıra bölmek; meta komut satır sonunda biter. Çok satırlı yazmak istiyorsanızCOPY ... TO STDOUT \g dosyabiçimini kullanın.HEADER true'nun başlığı kontrol ettiğini sanmak; yalnızca atlar. Kontrol içinHEADER match.- Dosya kodlamasını belirtmemek; Türkçe karakterlerde
invalid byte sequencehatası alırsınız. ON_ERROR ignore'un kısıt ihlallerini de atlayacağını sanmak.- Ondalık ayracı virgül olan sayıları doğrudan
numericsütuna yüklemek;SELECT '30,00'::numericdenediğimizdeinvalid input syntax for type numeric: "30,00"hatası aldık. Böyle dosyaları ara tabloya metin olarak alıp dönüştürün. - Yüklemeden sonra
ANALYZEçalıştırmamak.
Sık Sorulan Sorular
COPY mi INSERT mi?
Çok satır yüklüyorsanız COPY. Kılavuza göre COPY ile çok sayıda satır yüklemek, PREPARE kullanılsa ve satırlar tek transaction'da toplansa bile neredeyse her zaman INSERT'ten hızlıdır. Az sayıda satır ve satır başına mantık gerekiyorsa INSERT daha esnektir.
CSV'deki boş alan neden NULL oldu?
CSV biçiminde varsayılan NULL dizesi tırnaksız boş alandır. Boş metin istiyorsanız alanı "" yazın ya da FORCE_NOT_NULL kullanın.
Excel'de açılan CSV'de Türkçe karakterler bozuk çıkıyor, ne yapmalıyım?
Dışa aktarmada ENCODING seçeneğiyle dosyayı hedef programın beklediği kodlamada yazabilirsiniz (örneğin ENCODING 'WIN1254'). Hangi kodlamanın gerektiği, dosyayı açan programın ayarlarına bağlıdır.
COPY kaç satır yüklediğini nasıl anlarım?
Başarılı bir COPY, COPY 4 gibi bir komut etiketi döndürür; sayı yüklenen veya yazılan satır sayısıdır. psql, TO STDOUT durumunda veriyle karışmaması için bu etiketi yazdırmaz. Uzun süren yüklemelerin ilerlemesini pg_stat_progress_copy görünümünden izleyebilirsiniz.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: PostgreSQL öğrenme rehberi · CSV formatı ve JSON dönüşümü · pg_dump ve pg_restore · psql komutları · UPSERT ve MERGE · Performans optimizasyonu