PL/pgSQL, PostgreSQL'in yerleşik prosedürel dilidir: SQL sorgularının yanına değişken, IF/CASE koşulları, LOOP/FOR/WHILE/FOREACH döngüleri ve hata yakalama ekler. Kodu bir fonksiyon, procedure ya da tek seferlik bir DO bloğu içinde yazarsınız; her gövde DECLARE ... BEGIN ... END biçiminde bir bloktur. Dil her yeni veritabanında hazır gelir, ayrıca kurmanız gerekmez.
Bu yazıda PL/pgSQL'in temel yapı taşlarını çalışan örneklerle ele alıyoruz: blok yapısı ve DO, değişken tanımlama, %TYPE ve %ROWTYPE, SELECT INTO ve FOUND, koşullar, döngüler, RETURN QUERY ile küme döndürme, EXECUTE ... USING ile dinamik SQL, GET DIAGNOSTICS ve en sık karşılaşılan hatalar. CREATE FUNCTION sözdiziminin kendisi (oynaklık, SECURITY DEFINER vb.) CREATE FUNCTION ve PROCEDURE yazısında, hata yakalama ve RAISE ayrıntıları PL/pgSQL hata yakalama yazısında. Serinin geneli için PostgreSQL öğrenme rehberine bakın.
Blok yapısı ve DO komutu
PL/pgSQL blok yapılı bir dildir. Fonksiyon gövdesinin tamamı tek bir blok olmalıdır ve blok şu şekildedir:
[ <<etiket>> ]
[ DECLARE
tanımlar ]
BEGIN
komutlar
END [ etiket ];Her tanım ve komut noktalı virgülle biter. İç içe bir bloğun END ifadesinden sonra noktalı virgül gerekir, gövdeyi kapatan son END için gerekmez. Anahtar sözcükler büyük/küçük harfe duyarlı değildir; tanımlayıcılar, normal SQL'de olduğu gibi çift tırnak içinde değilse küçük harfe çevrilir. Yorumlar SQL'deki gibidir: -- satır sonuna kadar, /* */ blok yorum.
Kılavuzun özellikle altını çizdiği bir ayrım var: PL/pgSQL içindeki BEGIN/END yalnızca komutları gruplar, transaction başlatmaz veya bitirmez. Transaction kontrolü procedure ve DO bloklarında COMMIT/ROLLBACK ile yapılır; bu konu procedure yazısında örneklendi. Bir de EXCEPTION yan tümcesi içeren blok bir alt transaction oluşturur; ayrıntısı hata yakalama yazısında.
DO komutu, parametresiz ve void döndüren geçici bir fonksiyon gibi davranan anonim bir kod bloğunu bir kez ayrıştırıp çalıştırır. Dil belirtilmezse plpgsql varsayılır. Tek seferlik bakım işleri, deneme ve öğrenme için idealdir; bu yazıdaki ilk örnek de bir DO bloğu. Hem gövde metni hem DO kodu bir metin sabiti olduğu için dolar tırnağı ($$ ... $$) kullanmak, içerideki tek tırnakları ikileme zahmetini ortadan kaldırır.
Değişkenler: DECLARE, %TYPE, %ROWTYPE, RECORD

Blokta kullanılan tüm değişkenler DECLARE bölümünde tanımlanır. İki istisna vardır: tamsayı aralığında dönen FOR döngüsünün değişkeni otomatik olarak integer tanımlanır, imleç üzerinde dönen FOR döngüsünün değişkeni de otomatik olarak record olur. Genel sözdizimi şöyledir:
ad [ CONSTANT ] tip [ COLLATE collation_adi ] [ NOT NULL ] [ { DEFAULT | := | = } ifade ];- Başlangıç değeri verilmezse değişken
NULLile başlar. CONSTANTdeğişkene ilk değerden sonra atama yapılmasını engeller.NOT NULLverilen değişkeninNULLolmayan bir varsayılanı olmalıdır; sonradanNULLatamak çalışma anı hatası verir.- Varsayılan değer fonksiyon çağrısında bir kez değil, bloğa her girişte yeniden hesaplanır. Bu yüzden
now()ile başlatılan bir değişken o anki çağrının zamanını taşır. tablo.sutun%TYPEbir sütunun tipini kopyalar; sütun tipi ileride değişirse fonksiyonu değiştirmeniz gerekmeyebilir.tablo%ROWTYPEbir tablonun tüm satırını tutan satır değişkeni tanımlar; alanlarap.adgibi nokta ile erişilir.RECORDönceden yapısı olmayan bir yer tutucudur; ilk atandığı satırın yapısını alır. Atanmadan önce bir alanına erişmek çalışma anı hatası verir.
Atama := (ya da =) ile yapılır. Arka planda ifade bir SQL SELECT olarak ana motora gönderilir ve tek bir değer üretmelidir. Sonuç tipi değişkenin tipinden farklıysa atama dönüşümü uygulanır; uygun dönüşüm yoksa değer metne çevrilip değişken tipine okunmaya çalışılır, bu da çalışma anı hatasına yol açabilir.
SELECT INTO, FOUND ve IF
Tek satır döndüren bir sorgunun sonucunu değişkene almak için sorguya INTO eklenir. Kılavuz bunun SQL'deki SELECT INTO komutuyla (yeni tablo oluşturan) karıştırılmaması gerektiğini vurgular; fonksiyon içinde tablo oluşturmak için CREATE TABLE ... AS SELECT kullanılmalıdır. STRICT yazılmazsa değişkene ilk satır atanır, satır yoksa değişkenler NULL olur ve özel FOUND değişkeni false olur. Aşağıdaki DO bloğu bu üç parçayı birleştiriyor:
CREATE TABLE personel (
id integer PRIMARY KEY,
ad text NOT NULL,
birim text NOT NULL,
maas numeric(10,2) NOT NULL
);
INSERT INTO personel VALUES
(1, 'Deniz Acar', 'yazılım', 72000),
(2, 'Ece Polat', 'yazılım', 81000),
(3, 'Kerem Şahin', 'destek', 48000),
(4, 'Leyla Koç', 'satış', 55000);
DO $$
DECLARE
p personel%ROWTYPE;
aranan personel.ad%TYPE := 'Ece Polat';
zam_orani CONSTANT numeric := 0.10;
seviye text;
BEGIN
SELECT * INTO p FROM personel WHERE ad = aranan;
IF NOT FOUND THEN
RAISE NOTICE '% bulunamadı', aranan;
RETURN;
END IF;
IF p.maas >= 80000 THEN
seviye := 'kıdemli';
ELSIF p.maas >= 60000 THEN
seviye := 'orta';
ELSE
seviye := 'başlangıç';
END IF;
RAISE NOTICE '%: birim=%, seviye=%, zamlı maaş=%',
p.ad, p.birim, seviye, round(p.maas * (1 + zam_orani), 2);
END
$$;NOTICE: Ece Polat: birim=yazılım, seviye=kıdemli, zamlı maaş=89100.00
DORAISE NOTICE metindeki her % işaretinin yerine sıradaki argümanı koyar ve mesajı istemciye bir bildirim olarak gönderir; psql bunu NOTICE: satırı olarak gösterir. IF ... ELSIF ... ELSE ... END IF koşul zinciri ilk doğru dalı çalıştırır. Arama değerini değiştirip olmayan bir ad verirseniz FOUND false olur ve blok erken RETURN ile biter.
FOUND her fonksiyon çağrısında false ile başlar ve yalnızca belirli komutlar tarafından ayarlanır: SELECT INTO, PERFORM, en az bir satırı etkileyen INSERT/UPDATE/DELETE/MERGE, FETCH, MOVE, en az bir kez dönen FOR/FOREACH ve RETURN QUERY. Dikkat: dinamik EXECUTE, FOUND değerini değiştirmez; onun sonucunu GET DIAGNOSTICS ile okumalısınız.
CASE ve döngüler
![CASE ve döngüler: LOOP ... END LOOP, EXIT [etiket] [WHEN koşul], CONTINUE [etiket] [WHEN koşul], WHILE koşul LOOP](/assets/blog/postgresql-plpgsql-temelleri-b4.webp)
PL/pgSQL'de iki CASE biçimi vardır. Basit biçim bir ifadeyi sırayla WHEN değerleriyle eşitlik açısından karşılaştırır; aramalı biçim her WHEN koşulunu sırayla değerlendirip ilk doğru olanı çalıştırır. Önemli fark şudur: hiçbir dal eşleşmez ve ELSE de yoksa, IF sessizce hiçbir şey yapmazken komut olarak CASE bir CASE_NOT_FOUND hatası yükseltir. Aşağıdaki örnekteki CASE ise SQL ifadesi olarak kullanılan CASE ... END'dir, komut değildir.
Döngü komutları şunlardır:
| Komut | Ne yapar |
|---|---|
| LOOP ... END LOOP | EXIT veya RETURN gelene kadar koşulsuz tekrar |
| EXIT [etiket] [WHEN koşul] | Döngüden (etiketle bir bloktan) çıkar |
| CONTINUE [etiket] [WHEN koşul] | Sonraki tura geçer |
| WHILE koşul LOOP | Her turdan önce koşulu denetler |
| FOR i IN [REVERSE] a..b [BY n] LOOP | Tamsayı aralığında döner, i otomatik integer |
| FOR kayit IN sorgu LOOP | Sorgunun her satırında döner |
| FOR kayit IN EXECUTE metin LOOP | Dinamik sorgunun satırlarında döner |
| FOREACH x IN ARRAY dizi LOOP | Dizinin elemanlarında döner (SLICE ile dilimlerde) |
Tamsayı FOR döngüsünde sınırlar döngüye girerken bir kez hesaplanır; alt sınır üst sınırdan büyükse (REVERSE ile tersi) gövde hiç çalışmaz ve hata da verilmez. Sorgu üzerinde dönen FOR döngüsünde EXIT ile çıkılırsa, son atanan satır döngüden sonra da erişilebilir kalır. FOREACH çok boyutlu dizilerde de elemanları depolama sırasıyla dolaşır.
Küme döndürme: RETURN QUERY ve RETURN NEXT
RETURNS SETOF veya RETURNS TABLE ile tanımlanan PL/pgSQL fonksiyonu sonuç satırlarını adım adım biriktirir. RETURN NEXT ifade sonuca bir satır ekler, RETURN QUERY sorgu bir sorgunun tüm satırlarını ekler. İkisi de fonksiyondan çıkmaz; yürütme bir sonraki komutla devam eder. Sonunda argümansız bir RETURN yazılır ya da akış fonksiyonun sonuna ulaşır. İkisini aynı fonksiyonda karıştırabilirsiniz, sonuçlar art arda eklenir.
CREATE FUNCTION birim_raporu(p_birimler text[])
RETURNS TABLE (birim text, kisi integer, toplam_maas numeric)
LANGUAGE plpgsql STABLE
AS $$
DECLARE
b text;
BEGIN
FOREACH b IN ARRAY p_birimler LOOP
RETURN QUERY
SELECT b, count(*)::integer, coalesce(sum(p.maas), 0)
FROM personel p
WHERE p.birim = b;
END LOOP;
END
$$;
SELECT * FROM birim_raporu(ARRAY['yazılım', 'satış', 'muhasebe']);
CREATE FUNCTION taksitler(tutar numeric, adet integer)
RETURNS SETOF text
LANGUAGE plpgsql IMMUTABLE
AS $$
DECLARE
kalan numeric := tutar;
pay numeric;
BEGIN
FOR i IN 1..adet LOOP
pay := CASE WHEN i = adet THEN kalan ELSE round(tutar / adet, 2) END;
kalan := kalan - pay;
RETURN NEXT format('%s. taksit: %s', i, pay);
END LOOP;
RETURN;
END
$$;
SELECT * FROM taksitler(100, 3); birim | kisi | toplam_maas
----------+------+-------------
yazılım | 2 | 153000.00
satış | 1 | 55000.00
muhasebe | 0 | 0
(3 rows)
taksitler
------------------
1. taksit: 33.33
2. taksit: 33.33
3. taksit: 33.34
(3 rows)İlk fonksiyonda FOREACH her birim için bir RETURN QUERY çalıştırıyor. Sorguda tablo takma adı p ve p.birim kullanmamızın sebebi, RETURNS TABLE içindeki birim sütun adının fonksiyon içinde bir değişken gibi davranmasıdır; nitelemeden birim yazsaydık aşağıda göreceğimiz belirsizlik hatası çıkardı. İkinci fonksiyon FOR ile taksitleri hesaplayıp her birini RETURN NEXT ile ekliyor; kuruş farkı son taksite yazılıyor.
Dinamik SQL: EXECUTE, format() ve USING
PL/pgSQL, gövdede doğrudan yazılan sorguların planlarını hazırlayıp önbelleğe alabilir. Bunun bir sonucu, doğrudan yazılan bir SQL komutunda tablo veya sütun adının değişken olamamasıdır. Tablo adı çalışma anında belirleniyorsa, ya da CREATE gibi parametre kabul etmeyen bir yardımcı komut çalıştırıyorsanız, komut metnini oluşturup EXECUTE ile çalıştırırsınız. EXECUTE ile çalışan komutun planı önbelleğe alınmaz, her seferinde yeniden planlanır.
Kılavuzun önerdiği güvenli düzen iki parçalıdır: tanımlayıcıları (tablo, sütun adı) format() fonksiyonunun %I belirteciyle, veri değerlerini ise $1, $2 yer tutucuları ve USING ile vermek. %I otomatik olarak quote_ident uygular. USING değerleri metne çevirip tırnaklamaya gerek bırakmaz; kılavuz bunu hem daha verimli hem de SQL enjeksiyonuna karşı çok daha dayanıklı bulur. Yer tutucular yalnızca veri değerleri için kullanılabilir.
CREATE TABLE arsiv_2025 (id integer, not_metni text);
CREATE TABLE arsiv_2026 (id integer, not_metni text);
INSERT INTO arsiv_2026 VALUES (1, 'ilk'), (2, 'ikinci'), (3, 'üçüncü');
CREATE FUNCTION arsiv_temizle(yil integer, en_kucuk_id integer)
RETURNS integer
LANGUAGE plpgsql
AS $$
DECLARE
silinen integer;
BEGIN
EXECUTE format('DELETE FROM %I WHERE id >= $1', 'arsiv_' || yil)
USING en_kucuk_id;
GET DIAGNOSTICS silinen = ROW_COUNT;
RETURN silinen;
END
$$;
SELECT arsiv_temizle(2026, 2) AS silinen;
SELECT arsiv_temizle(2024, 1) AS silinen; silinen
---------
2
(1 row)
ERROR: relation "arsiv_2024" does not exist
LINE 1: DELETE FROM arsiv_2024 WHERE id >= $1
^
QUERY: DELETE FROM arsiv_2024 WHERE id >= $1
CONTEXT: PL/pgSQL function arsiv_temizle(integer,integer) line 5 at EXECUTEİlk çağrı iki satırı sildi; GET DIAGNOSTICS silinen = ROW_COUNT son komutun etkilediği satır sayısını okudu. İkinci çağrıda arsiv_2024 tablosu olmadığı için hata alındı; QUERY: satırı EXECUTE'un çalıştırdığı gerçek metni, CONTEXT: satırı da hatanın fonksiyonun kaçıncı satırında çıktığını gösteriyor. Bu ikisi dinamik SQL hatalarını ayıklarken en değerli bilgidir.
Kılavuz bir performans ayrıntısını da not eder: parametreli sabit bir komutu EXECUTE ile çalıştırmak, aynı komutu doğrudan yazmakla işlevsel olarak eşdeğerdir; fark, EXECUTE'un her seferinde o anki değerlere özel bir plan üretmesi, doğrudan yazılan komutun ise genel bir planın önbelleğe alınmasına izin vermesidir. En iyi plan parametre değerine çok bağlıysa EXECUTE bilinçli bir seçim olabilir.
Procedure çağırmak ve INOUT parametreler
PL/pgSQL içinden bir procedure'ü CALL ile çağırdığınızda çıkış parametreleri düz SQL'den farklı işlenir: her OUT veya INOUT parametresine bir değişken karşılık gelmeli ve procedure dönünce değer o değişkene yazılır. Düz SQL'deki gibi NULL yazmak burada işe yaramaz.
CREATE PROCEDURE zam_uygula(p_birim text, oran numeric, INOUT etkilenen integer DEFAULT 0)
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE personel SET maas = round(maas * (1 + oran), 2) WHERE birim = p_birim;
GET DIAGNOSTICS etkilenen = ROW_COUNT;
END
$$;
DO $$
DECLARE n integer;
BEGIN
CALL zam_uygula('yazılım', 0.05, n);
RAISE NOTICE '% kişiye zam yapıldı', n;
END
$$;
SELECT ad, maas FROM personel WHERE birim = 'yazılım' ORDER BY id;NOTICE: 2 kişiye zam yapıldı
DO
ad | maas
------------+----------
Deniz Acar | 75600.00
Ece Polat | 85050.00
(2 rows)Procedure'ün dönüş değeri olmadığı için RETURN zorunlu değildir; erken çıkmak isterseniz argümansız RETURN yazarsınız. Çıkış parametrelerinin son değerleri çağırana döner.
Sık yapılan hatalar ve mesajları

PL/pgSQL, fonksiyonu ilk çağrıldığında ayrıştırır, ancak içindeki SQL ifadelerini o satıra ilk ulaşıldığında analiz eder. Kılavuz bunun bir dezavantajını açıkça yazar: basit sözdizimi hataları ilk ayrıştırmada yakalanır, daha derin hatalar ise ancak ilgili satır çalıştığında ortaya çıkar. Yeni başlayanların en sık gördüğü beş hatayı art arda üretelim:
-- 1) BEGIN sonrasında noktalı virgül
DO $$ BEGIN; RAISE NOTICE 'merhaba'; END $$;
-- 2) INTO olmadan SELECT
DO $$ BEGIN SELECT count(*) FROM personel; END $$;
-- 3) INTO STRICT ve satır yok
DO $$
DECLARE m numeric;
BEGIN
SELECT maas INTO STRICT m FROM personel WHERE birim = 'muhasebe';
END $$;
-- 4) Değişken ile sütun adı çakışması
CREATE FUNCTION birim_maasi(birim text) RETURNS numeric
LANGUAGE plpgsql AS $$
BEGIN
RETURN (SELECT sum(maas) FROM personel WHERE personel.birim = birim);
END $$;
SELECT birim_maasi('yazılım');
-- 5) RETURN unutulmuş fonksiyon
CREATE FUNCTION iki_kati(n integer) RETURNS integer
LANGUAGE plpgsql AS $$
BEGIN
IF n > 0 THEN
RETURN n * 2;
END IF;
END $$;
SELECT iki_kati(-1);ERROR: syntax error at or near ";"
LINE 1: DO $$ BEGIN; RAISE NOTICE 'merhaba'; END $$;
^
ERROR: query has no destination for result data
HINT: If you want to discard the results of a SELECT, use PERFORM instead.
CONTEXT: PL/pgSQL function inline_code_block line 1 at SQL statement
ERROR: query returned no rows
CONTEXT: PL/pgSQL function inline_code_block line 4 at SQL statement
CREATE FUNCTION
ERROR: column reference "birim" is ambiguous
LINE 1: ...SELECT sum(maas) FROM personel WHERE personel.birim = birim)
^
DETAIL: It could refer to either a PL/pgSQL variable or a table column.
QUERY: (SELECT sum(maas) FROM personel WHERE personel.birim = birim)
CONTEXT: PL/pgSQL function birim_maasi(text) line 3 at RETURN
CREATE FUNCTION
ERROR: control reached end of function without RETURN
CONTEXT: PL/pgSQL function iki_kati(integer)- BEGIN; yazmak: kılavuz bunu sık yapılan bir hata olarak anar. PL/pgSQL'de
BEGIN'den sonra noktalı virgül konmaz. - Sonucu atılan SELECT: satır döndüren bir komut
INTOolmadan yazılırsa reddedilir; ipucu satırı daPERFORMkullanmanızı söylüyor. - INTO STRICT ve sıfır satır:
STRICTtam olarak bir satır bekler; satır yoksaNO_DATA_FOUND, birden fazlaysaTOO_MANY_ROWShatası verir. Bu hataları yakalamak EXCEPTION bloğu ile yapılır. - Değişken ile sütun adı çakışması: varsayılan olarak PL/pgSQL, bir ad hem değişkeni hem sütunu gösterebiliyorsa hata verir.
plpgsql.variable_conflictayarı bunuuse_variableveyause_columnolarak değiştirebilir, ancak kılavuz en temiz çözümün adı değiştirmek ya da nitelemek olduğunu gösterir. - RETURN unutmak: skaler değer döndüren bir fonksiyonda akış
RETURNgörmeden sona ulaşırsa çalışma anı hatası olur.OUTparametreli vevoidfonksiyonlarda bu kural geçerli değildir.
Dördüncü hatanın düzeltilmiş hâli: tabloya takma ad verip sütunu p.birim, parametreyi de fonksiyon adıyla birim_maasi.birim olarak nitelemek. Kılavuz, her fonksiyon gövdesinin fonksiyon adıyla etiketlenmiş gizli bir dış blok içinde olduğunu, bu yüzden parametrelerin fonksiyon adıyla nitelenebildiğini açıklar.
CREATE OR REPLACE FUNCTION birim_maasi(birim text) RETURNS numeric
LANGUAGE plpgsql AS $$
BEGIN
RETURN (SELECT sum(p.maas) FROM personel p WHERE p.birim = birim_maasi.birim);
END $$;
SELECT birim_maasi('yazılım'); birim_maasi
-------------
160650.00
(1 row)Sonuç, önceki örnekte zam yapılmış maaşların toplamıdır. Plan önbelleğinin bir başka yan etkisi de record değişkenleriyle ilgilidir: bir ifadede kullanılan record alanlarının tipi çağrıdan çağrıya değişmemelidir, çünkü ifade ilk ulaşıldığı andaki tiple analiz edilir. Gerekirse EXECUTE bu sorunu aşar.
PL/pgSQL ne zaman, SQL fonksiyonu ne zaman?
Tek bir sorgu veya ifade yeterliyse SQL dilinde fonksiyon yazmak daha sadedir. Değişken, ara sonuç, koşullu akış, döngü, dinamik SQL veya hata yakalama gerektiğinde PL/pgSQL'e geçin. Trigger fonksiyonları da düz SQL ile yazılamadığı için genellikle PL/pgSQL kullanılır; konu PostgreSQL trigger oluşturma yazısında. Dizi ve FOREACH ile çalışırken dizi fonksiyonlarına PostgreSQL dizi tipi yazısından bakabilirsiniz.
PostgreSQL 18 sürüm notları PL/pgSQL için imleç (cursor) argümanlarında => ile adlı argüman sözdiziminin desteklendiğini belirtir; imleçler bu temel yazının kapsamı dışında. Diğer yenilikler PostgreSQL 18 yenilikleri yazısında.
Sık Sorulan Sorular
PL/pgSQL kurmam gerekir mi?
Hayır. DO komutunun belgesine göre plpgsql varsayılan olarak kuruludur; diğer prosedürel diller ise CREATE EXTENSION ile eklenmelidir.
DO bloğu bir değer döndürebilir mi?
Hayır. DO bloğu void döndüren parametresiz bir fonksiyon gibi davranır. Sonuç görmek için RAISE NOTICE kullanabilir ya da bir tabloya yazabilirsiniz; değer döndürmek gerekiyorsa fonksiyon tanımlayın.
= ile := arasında fark var mı?
Atama ve değişken tanımında ikisi de kabul edilir. := PL/SQL uyumlu biçimdir; kılavuz = kullanımına da izin verir.
Fonksiyonun hangi satırında hata çıktığını nasıl anlarım?
Hata mesajının CONTEXT: satırı fonksiyon adını ve satır numarasını verir; yukarıdaki örneklerde "line 5 at EXECUTE" gibi. Kendi bağlam bilginizi üretmek için GET DIAGNOSTICS ... PG_CONTEXT kullanılabilir; hata yakalama yazısında anlatılıyor.
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 · CREATE FUNCTION ve PROCEDURE · PL/pgSQL hata yakalama · Trigger oluşturma · SQL injection önleme