psql ile Günlük Çalışma: Meta-Command'lar, \x, \timing, \copy, \watch, \gexec, .psqlrc, Schema ve search_path
psql'de COPY dosyayı sunucu diskine yazar ve özel yetki ister; \copy aynı işi istemci makinesinde, senin yetkilerinle yapar. \d ailesi, \x ve \timing, \gset ve \gexec ile üretilen SQL, \copy ile CSV, \watch, ON_ERROR_STOP'lu script, .psqlrc, schema ve search_path tuzağı, PostgreSQL 15'te public schema yetkisi ve pg_dump ile tek tabloyu dev ortamına almak.
Sürüm notu: Bu yazıdaki örnekler PostgreSQL 18.7 ve onunla gelen psql 18 (resmî postgres:18 Docker image'ı) üzerinde, Bookstore şeması yüklendikten hemen sonra çalıştırılmıştır.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı), 02. yazıda kurduğumuz lab'da duruyor. İki geliştirici, uygulama rolüyle bağlı psql'de en çok satan beş kitabı bir dosyaya yazmak istiyor. Emre COPY, Ayşe \copy yazıyor; ikisi de aynı sorguyu, aynı dosya yolunu veriyor:
COPY (SELECT book_id, title, price FROM books ORDER BY book_id LIMIT 5) TO '/tmp/top5.csv' WITH (FORMAT csv, HEADER);
\copy (SELECT book_id, title, price FROM books ORDER BY book_id LIMIT 5) TO '/tmp/top5.csv' WITH (FORMAT csv, HEADER)
İkisi de çalışır mı? Çalışırsa dosya nereye yazılır?
ERROR: permission denied to COPY to a file
DETAIL: Only roles with privileges of the "pg_write_server_files" role may COPY to a file.
HINT: Anyone can COPY to stdout or from stdin. psql's \copy command also works for anyone.
COPY 5
COPY bir SQL komutu: dosyayı sunucu süreci, sunucu makinesinin diskine yazıyor ve bunun için özel bir yetki (pg_write_server_files) istiyor; uygulama rolünde o yetki yok. \copy ise psql'in meta-command'ı: aynı veriyi sunucudan stdin/stdout üzerinden alıp dosyayı psql'in çalıştığı makinede, senin yetkilerinle yazıyor. Yani Emre'nin komutu yetki hatası verdi, Ayşe'nin komutu kendi makinesine beş satırlık CSV (virgülle ayrılmış değerler dosyası) yazdı. Managed bir veritabanında (Azure Database for PostgreSQL ya da Amazon RDS (AWS'nin yönetilen veritabanı servisi)) sunucu diskine zaten erişemezsin; oradaki tek yol \copy. Bu yazıda psql'i gündelik işte verimli kullanmayı görüyoruz: meta-command'lar, \x ve \timing, \copy, \watch, \gexec, .psqlrc, schema'lar ve search_path. Bu, PostgreSQL serisinin şimdilik son yazısı.
Meta-Command'lar ve \d Ailesi
psql, SQL Server tarafındaki sqlcmd'nin karşılığı ama çok daha fazlası. Ters bölü ile başlayan meta-command'lar sunucuya gitmeden psql'in kendisi tarafından çalıştırılıyor; \d ailesi bunların en çok kullanılanı ve hepsi aslında catalog'u okuyan hazır sorgular. 02'de \dt ve \d+'e bakmıştık; ailenin tamamı bir nesne türünü listeliyor:
\dt
\di books*
\df public.book_available
\dv
\dn
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
List of indexes
Schema | Name | Type | Owner | Table
--------+------------+-------+-----------------+-------
public | books_pkey | index | bookstore_owner | books
List of functions
Schema | Name | Result data type | Argument data types | Type
--------+----------------+------------------+---------------------+------
public | book_available | integer | p_book_id integer | func
List of views
Schema | Name | Type | Owner
--------+-------------+------+-----------------
public | v_book_list | view | bookstore_owner
List of schemas
Name | Owner
--------+-------------------
public | pg_database_owner
\dt tabloları, \di index'leri, \df function'ları, \dv view'ları, \dn schema'ları, \du rolleri listeliyor; \l veritabanlarını. Hepsi bir pattern alıyor: \di books* yalnızca adı books ile başlayan index'leri, \df public.* public schema'sındaki function'ları gösteriyor. Sonuna + eklemek ayrıntıyı artırıyor (\dt+ tablo boyutunu da yazıyor). \d <nesne> (harfsiz) bir tablonun ya da view'ın kolonlarını, index'lerini, constraint'lerini ve ona bağlı nesneleri tek yerde veriyor; \sf book_available bir function'ın, \sv v_book_list bir view'ın tanımını basıyor. Hangi meta-command'ın arkasında hangi sorgu olduğunu görmek için psql'i -E (ya da oturum içinde \set ECHO_HIDDEN on) ile çalıştır; bu, kendi yönetim sorgularını yazarken catalog'u öğrenmenin en hızlı yolu.
\x, \timing ve NULL Görünümü
Çok kolonlu bir satır ekrana sığmayınca tablo biçimi okunmaz oluyor. \x çıktıyı dikey (her kolon bir satır) gösteriyor, \x auto bunu yalnızca satır ekrana sığmadığında yapıyor. \timing her komutun süresini yazıyor:
\timing on
\x on
SELECT book_id, title, price, published_year FROM books WHERE book_id = 1;
-[ RECORD 1 ]--+--------
book_id | 1
title | Emma #1
price | 32.77
published_year | 1885
Time: 1.292 ms
\timing'in yazdığı süre istemci tarafında, ağ gidiş-dönüşü dahil ölçülüyor; sorgunun sunucudaki gerçek maliyeti için 07. yazıdaki EXPLAIN (ANALYZE, BUFFERS) gerekiyor. \pset null '(null)' boş string ile NULL'ı ayırt ediyor: ayar olmadan ikisi de boş görünüyor ve bu özellikle outer join sonuçlarında yanlış okumaya yol açıyor. \pset çıktının biçimini de değiştiriyor: \pset format csv, \pset border 2, \a hizalamayı kapatıyor. Bir sorguyu çalıştırdıktan sonra biçimi değiştirmek için \g son sorguyu yeniden çalıştırıyor.
Değişkenler, \gset ve \gexec
psql'in kendi değişkenleri var. \set ile tanımlanıyor, :name ile metne, :'name' ile tırnaklı string'e, :"name" ile tırnaklı tanımlayıcıya gömülüyor:
\set low_stock 10
SELECT count(*) AS below FROM stock_items WHERE quantity_on_hand < :low_stock;
SELECT count(*) AS total_books, max(book_id) AS max_id FROM books \gset
\echo total books: :total_books max id: :max_id
below
-------
2021
total books: 40000 max id: 40000
\gset bir sorgunun sonucunu kolon adlarıyla değişkenlere yazıyor; 01. yazıda WAL (Write-Ahead Log; değişikliklerin veri dosyalarından önce yazıldığı log) ölçümünde bunu kullanmıştık. Tek satır döndürmesi şart: birden çok satırda more than one row returned for \gset hatası veriyor. \gexec ise daha güçlü: çalıştırdığı sorgunun döndürdüğü her satırı birer SQL komutu olarak çalıştırıyor. Bir şablondan toplu komut üretmenin en temiz yolu bu. Sipariş tablolarını tek tek yazmadan ANALYZE etmek (bookstore_owner ile):
SELECT format('ANALYZE %I;', tablename) FROM pg_tables WHERE schemaname = 'public' AND tablename LIKE 'order%' \gexec
ANALYZE order_items;
ANALYZE
ANALYZE orders;
ANALYZE
Önce \gexec'i çıkarıp sorguyu çalıştır, üreteceği komutları gör, sonra \gexec ile uygula; üretilen SQL doğrudan çalışacağı için şablondaki tablo/kolon adlarını 18. yazıdaki format(..., %I) ile ver. Aynı kalıp "bütün tablolara aynı index'i ekle", "bir pattern'e uyan tabloları DROP et", "her müşteri için bir satır üret" gibi işlerde işe yarıyor. Çok satırlı \gset ya da tehlikeli \gexec yazmadan önce SELECT'i tek başına görmek alışkanlık olmalı.
Veri Taşımak: \copy ve pg_dump
"Önce Tahmin Et"teki \copy, dosyayı psql'in çalıştığı makineye yazıyor. Bir dışa aktarma ve bir içe aktarma:
\copy (SELECT book_id, title, price FROM books ORDER BY book_id LIMIT 5) TO 'top5.csv' WITH (FORMAT csv, HEADER)
\copy staging_books FROM 'top5.csv' WITH (FORMAT csv, HEADER)
book_id,title,price
1,Emma #1,32.77
2,The Hobbit #2,39.00
3,Middlemarch #3,35.69
4,Frankenstein #4,42.80
5,Foundation #5,15.34
Yol görelidir: top5.csv, psql'i başlattığın dizine yazılıyor. \copy ... TO bir sorgunun ya da tablonun sonucunu dışa, \copy ... FROM bir CSV'yi bir tabloya aktarıyor; ikisi de satır satır stdin/stdout üzerinden gittiği için büyük veride sunucu COPY'sinden yavaş ama her yerde çalışıyor. Toplu yüklemenin uygulama tarafındaki en hızlı yolu 11. yazıdaki binary COPY'ydi; \copy onun elle, tek seferlik karşılığı. En sık hata ikisini karıştırıp sunucu COPY'siyle yazdığın dosyayı kendi makinende aramak; dosya sunucunun (bu lab'da container'ın) diskinde duruyor.
Bir Tabloyu Geliştirme Ortamına Almak
Bir hatayı yerelde üretmek için çoğu zaman production'ın tamamı değil, birkaç tablosu gerekiyor. 14. yazıdaki pg_dump/pg_restore operasyon tarafında ayrıntılı; gündelik kullanımda tek tabloyu almak bir satır. Önce tek tablonun şemasına bakalım, sonra iki tabloyu bir dev veritabanına taşıyalım:
pg_dump -U bookstore_owner -d bookstore -t public.stock_items --schema-only
pg_dump -U bookstore_owner -d bookstore -t public.authors -t public.books --no-owner --no-privileges | psql -X -q -U bookstore_owner -d bookstore_dev
CREATE TABLE public.stock_items (
book_id integer NOT NULL,
quantity_on_hand integer NOT NULL,
quantity_reserved integer NOT NULL,
updated_at timestamp with time zone NOT NULL,
CONSTRAINT ck_stock_items_quantity CHECK (((quantity_on_hand >= 0) AND (quantity_reserved >= 0)))
);
ALTER TABLE public.stock_items OWNER TO bookstore_owner;
ALTER TABLE ONLY public.stock_items
ADD CONSTRAINT stock_items_pkey PRIMARY KEY (book_id);
ALTER TABLE ONLY public.stock_items
ADD CONSTRAINT fk_stock_items_books FOREIGN KEY (book_id) REFERENCES public.books(book_id);
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE public.stock_items TO bookstore_app;
-t bir tabloyu (ya da bir pattern'e uyan tabloları) seçiyor; birden çok -t verilebiliyor. --schema-only yalnızca yapıyı, --data-only yalnızca veriyi döküyor. İkinci komutta pg_dump'ın çıktısını doğrudan başka bir veritabanına bağlı psql'e verdik: books ve authors 40.000 ve 5.000 satırıyla bookstore_dev'e taşındı. Dikkat edilecek nokta foreign key'ler: stock_items'ı tek başına alırsan onun referans verdiği books dev'de yoksa restore constraint'te takılır; ilişkili tabloları birlikte al ya da --disable-triggers ile sırayı gevşet. psql 18'in dump çıktısına eklediği \restrict satırını burada kısalttık; o, dump'ın araya komut enjekte edilmeden güvenle yüklenmesini sağlayan yeni bir mekanizma.
\watch: Bir Sorguyu İzlemek
Bir siparişin durumunun değişmesini ya da bir kuyruğun boşalmasını beklerken aynı sorguyu elle tekrar çalıştırmak yerine \watch var. Son siparişlerin durum dağılımını saniyede bir, iki kez çalıştırdık:
SELECT status, count(*) FROM orders WHERE order_id > 999990 GROUP BY status ORDER BY status \watch i=1 c=2
Sun 15 Nov 2026 12:58:54 AM UTC (every 1s)
status | count
---------+-------
Pending | 10
Sun 15 Nov 2026 12:58:55 AM UTC (every 1s)
status | count
---------+-------
Pending | 10
\watch son sorguyu belirli aralıkla yeniden çalıştırıyor; i (interval) saniye cinsinden aralık, c (count) kaç kez çalışacağı. Sayı vermezsen Ctrl+C'ye kadar sürüyor. Başka bir oturumda 22. yazıdaki reserve_stock'u çağırıp bu ekranda Pending sayısının düşmesini canlı izleyebilirsin. Çıktıdaki saat sunucunun saati ve TimeZone ayarına bağlı (19. yazı). \watch bir izleme aracı değil; dakikalarca açık bir dashboard için uygun değil, ama bir iş biterken ya da bir migration ilerlerken bakmak için pratik.
Script Çalıştırmak ve .psqlrc
psql, SQL dosyalarını çalıştıran bir araç olarak da kullanılıyor. \i dosya.sql bir script'i oturumun içinden çalıştırıyor, \o cikti.txt sonraki sorguların çıktısını bir dosyaya yönlendiriyor (\o ile kapatılıyor), \e son sorguyu bir editörde açıp kaydedince çalıştırıyor. Script çalıştırırken en önemli ayar ON_ERROR_STOP:
psql -X -v ON_ERROR_STOP=on -U bookstore_app -d bookstore
SELECT 1/0;
SELECT 'never reached';
ERROR: division by zero
ON_ERROR_STOP açık olmadan psql bir hatadan sonra script'in kalanını çalıştırmaya devam eder; migration ya da veri yükleme script'inde bu, yarı uygulanmış ve fark edilmeyen bir değişiklik demek. Açıkken ilk hatada durup sıfırdan farklı bir exit code ile çıkıyor, böylece onu çalıştıran shell script hatayı görüyor. Komut satırında -v ON_ERROR_STOP=on ya da script'in başında \set ON_ERROR_STOP on. Şema dosyalarımızın başındaki satır da buydu. Tek bir komutu bağlanmadan çalıştırmak için -c "SQL", bir dosyayı çalıştırıp çıkmak için -f dosya.sql var; ikisi de 02. ve sonraki yazılarda docker exec ... psql ile kullandığımız biçim.
.psqlrc ile Varsayılanlar
Her bağlantıda aynı ayarları yazmamak için psql açılışta bir .psqlrc dosyası okuyor. 02'de lab için hazırladığımız dosya:
\set QUIET 1
\pset null '(null)'
\set COMP_KEYWORD_CASE upper
\set HISTCONTROL ignoredups
\x auto
\timing on
\set PROMPT1 '%n@%/%R%# '
\unset QUIET
Dosya kullanıcının ev dizinindeki ~/.psqlrc'den okunuyor; başka bir dosyayı PSQLRC ortam değişkeniyle ya da psql'i -X ile başlatıp hiç okutmadan gösterebilirsin. Bu yazıdaki bütün örnekleri ve serinin ölçümlerini -X ile çalıştırdık ki kimsenin .psqlrc'si sonucu değiştirmesin; kendi ölçümlerini alırken sen de öyle yap. PROMPT1'deki %n rolü, %/ veritabanını, %# rolün superuser olup olmadığını (# ya da >) gösteriyor; aynı anda lab ve production'a bağlıyken hangisinde olduğunu prompt'tan görmek hata riskini azaltıyor. İki ortam için farklı .psqlrc tutup production'da prompt'u kırmızıya boyamak yaygın bir alışkanlık.
Schema'lar ve search_path
Bir sorguda tabloyu public.books diye tam yazmazsan PostgreSQL onu search_path'teki schema'larda sırayla arıyor. Aynı adı iki schema'da tutmak, bu yüzden sessiz bir tuzak:
CREATE SCHEMA staging;
CREATE TABLE staging.books (book_id int, title text, note text);
INSERT INTO staging.books VALUES (1, 'Emma #1 (draft)', 'import test');
SHOW search_path;
SELECT title FROM books WHERE book_id = 1;
SET search_path = staging, public;
SELECT title FROM books WHERE book_id = 1;
SELECT title FROM public.books WHERE book_id = 1;
search_path
-----------------
"$user", public
title
---------
Emma #1
title
-----------------
Emma #1 (draft)
title
---------
Emma #1
Varsayılan search_path, "$user", public: PostgreSQL önce kullanıcının adıyla aynı schema'ya (varsa) bakıyor, sonra public'e. staging öne alınınca books artık taslak tabloyu gösterdi; aynı sorgu, aynı ad, farklı sonuç. Tam nitelemeli public.books her zaman doğru tabloyu getiriyor. SET search_path yalnızca oturumu etkiliyor; kalıcı yapmak için ALTER ROLE ... SET search_path ya da ALTER DATABASE .... 22. yazıda SECURITY DEFINER function'larında search_path'i sabitlemenin neden güvenlik meselesi olduğunu görmüştük; buradaki tuzak onun gündelik hâli.
Bir ayrıntı daha 14. yazıya bağlanıyor. PostgreSQL 15'ten beri public schema'da nesne oluşturma yetkisi artık herkese (PUBLIC) açık değil:
SELECT nspname, pg_catalog.has_schema_privilege('bookstore_app', nspname, 'CREATE') AS app_can_create
FROM pg_namespace WHERE nspname = 'public';
nspname | app_can_create
---------+----------------
public | f
Uygulama rolü public'te tablo oluşturamıyor; bu, 02'de \dt ile gördüğümüz davranışla tutarlı ve eski bir güvenlik açığına karşı 2018'den beri önerilen güvenli schema kullanımının varsayılan hâle gelmesi. Eski bir cluster'ı yükselttiğinde ya da bir dump'ı geri yüklediğinde eski (açık) yetkiler korunuyor; yeni veritabanlarında kapalı geliyor.
Üretimde Dikkat
Production veritabanına psql ile bağlanırken iki alışkanlık işe yarıyor. Birincisi, salt okunur bir rolle bağlan (14. yazıdaki pg_read_all_data gibi); yanlışlıkla yazmak imkânsız hâle gelir. İkincisi, bir komutu çalıştırmadan önce transaction'a al: BEGIN; yazıp komutu çalıştır, sonucu gör, doğruysa COMMIT; yanlışsa ROLLBACK;. psql'i -1 (tek transaction) ile çalıştırmak bir script'in tamamını tek transaction'a alıyor.
Uzun çıktılı sorgular psql'de bir pager'dan (genelde less) geçiyor. Script içinde ya da çıktıyı bir dosyaya/komuta boru ile verirken \pset pager off ya da PAGER ortam değişkeni bunu kapatıyor; aksi hâlde komut pager'ı bekleyerek takılıyor gibi görünüyor.
psql komut geçmişini ~/.psql_history dosyasında tutuyor. Parola ya da başka hassas bir değer içeren bir komut yazdıysan geçmişte kalır; .psqlrc'ye \set HISTFILE /dev/null ya da komut başına bir boşluk (HISTCONTROL=ignorespace ile) bunu engelliyor. Parolayı hiçbir zaman komuta ya da connection string'e yazma; ~/.pgpass dosyası ya da ortam değişkenleri bu iş için var (14. yazı).
Sık Düşülen Tuzaklar
\copy ile COPY'yi karıştırmak: sunucu COPY'si yazdığın dosyayı sunucunun diskinde arar ve yetkisiz rollerde hata verir. İstemci tarafında dosya işi her zaman \copy.
search_path'e güvenip yanlış schema'daki tabloyu sorgulamak: aynı ada sahip bir staging ya da eski bir schema varsa sessizce yanlış veri gelir. Script'lerde ve function'larda tam nitelemeli ad kullan ya da search_path'i başta açıkça ayarla.
Script'i ON_ERROR_STOP olmadan çalıştırmak: ilk hatadan sonra kalan komutlar da çalışır ve yarı uygulanmış bir değişiklik fark edilmez. Migration ve veri yükleme script'lerinde her zaman aç.
Uzun çıktıda pager'ın takıldığını sanmak ya da production'da hangi veritabanına bağlı olduğunu prompt'tan görmemek. .psqlrc'de anlamlı bir PROMPT1 ve gerektiğinde \pset pager off.
Küçük Bir Deneme
★☆☆ \gexec ile public schema'daki bütün tablolar için satır ve toplam boyutu veren bir rapor üret: önce pg_tables'tan SELECT 'SELECT ''' || tablename || ''' ...' biçiminde bir şablon yaz, \gexec olmadan çıktısını gör, sonra çalıştır. 08. yazıdaki pg_total_relation_size'ı kullan.
★★☆ Kendi .psqlrc'ni yaz: \x auto, \timing on, \pset null ve prompt'ta veritabanı adını göster. Sonra bir \set ile :top5 adında, en çok satan beş kitabı getiren bir sorguyu değişkene koy ve oturumda :top5 yazarak çalıştır (22. yazıdaki mv_top_books ya da 07'deki bestseller sorgusu).
★★★ Bir siparişin yaşam döngüsünü \watch ile izle: bir oturumda seçtiğin bir Pending siparişin durumunu ve rezervasyonlarını gösteren bir sorguyu \watch i=2 ile aç, başka bir oturumda 22. yazıdaki reserve_stock'u o siparişe çağır. İlk ekranda PaymentPending'e geçişi ve stock_items'taki quantity_reserved artışını canlı gör.
Bu yazıyla PostgreSQL serisinin şimdilik son yazısına geldik: mimariden (01) başlayıp lab, data type'lar, JSONB (PostgreSQL'in binary JSON tipi), index'ler, sorgu planı okuma (EXPLAIN), MVCC (çok sürümlü eşzamanlılık kontrolü) ve VACUUM (ölü satır temizliği), transaction'lar, full-text search, Npgsql ve Entity Framework Core (EF Core), extension'lar, güvenlik ve operasyon, ardından bu son blokta string, tarih ve aggregate function'ları, view ve materialized view, function ve procedure, trigger ve psql ile Bookstore'u uçtan uca PostgreSQL'e taşıdık. Geriye bir yazı kaldı: PostgreSQL 19'un getirdikleri. 19 şu an release candidate aşamasında; GA (genel kullanıma açık kararlı sürüm) olduğunda seriye "PostgreSQL 19 Yenilikleri" yazısını ekleyeceğiz.