PostgreSQL'de metinleri || operatörü ya da concat() ile birleştirir, substring, left, split_part ile parçalar, replace ve regexp_replace ile değiştirir, LIKE, ILIKE ve ~ operatörleriyle desen arar. Bu fonksiyonların çoğu text, varchar ve char tiplerini kabul eder ve text döndürür; char değerlerinin sondaki boşlukları işlemden önce atılır.

Bu yazıda en sık kullanılan metin fonksiyonlarını, NULL ile birleştirme tuzağını, format() ile güvenli SQL metni üretmeyi, üç desen eşleme yöntemi olan LIKE, SIMILAR TO ve POSIX düzenli ifadeleri, Türkçe I/i harflerinin büyük-küçük harf dönüşümündeki davranışını çalıştırılmış örneklerle göreceksiniz. Örnekler PostgreSQL 18.1 üzerinde, builtin sağlayıcılı C.UTF-8 yerel ayarıyla kurulmuş bir veritabanında çalıştırıldı. Serinin diğer yazıları için PostgreSQL öğrenme rehberi sayfasına bakabilirsiniz.

Örnek tablo

Örnek tablo

Bütün örnekler şu küçük müşteri tablosu üzerinde çalışır. Bir müşterinin soyadı bilerek boş bırakıldı, e-posta ve telefon alanları da farklı yazım biçimleri içeriyor:

sql
CREATE TABLE musteri (
    id      int PRIMARY KEY,
    ad      text,
    soyad   text,
    eposta  text,
    telefon text,
    sehir   text
);
INSERT INTO musteri VALUES
  (1, 'Ayşe',   'Yılmaz', 'ayse.yilmaz@ornek.com.tr', '0532 111 22 33', 'İstanbul'),
  (2, 'Mehmet', NULL,     'MEHMET@Ornek.com',         '(0505) 444-55-66', 'izmir'),
  (3, 'Zeynep', 'Demir',  'zeynep_d@posta.net',       '+90 555 777 88 99', 'Ankara');

Birleştirme: ||, concat ve concat_ws

Metin birleştirmenin üç yolu vardır ve NULL karşısında farklı davranırlar. || operatöründe taraflardan biri NULL ise sonuç da NULL olur. concat() NULL argümanları yok sayar. concat_ws() ilk argümanı ayırıcı olarak kullanır ve NULL değerleri ayırıcısıyla birlikte atlar:

sql
SELECT ad || ' ' || soyad AS birlesik,
       concat(ad, ' ', soyad) AS concat_ile,
       concat_ws(' ', ad, soyad) AS concat_ws_ile
FROM musteri ORDER BY id;
text
   birlesik   |  concat_ile  | concat_ws_ile 
--------------+--------------+---------------
 Ayşe Yılmaz  | Ayşe Yılmaz  | Ayşe Yılmaz
              | Mehmet       | Mehmet
 Zeynep Demir | Zeynep Demir | Zeynep Demir
(3 rows)

İkinci satırda || ile yapılan birleştirme tamamen boş kaldı. Raporlarda bir müşterinin adı birden kaybolursa ilk bakmanız gereken yer budur. concat() sonucunun sonunda ayrıca bir boşluk kalır ('Mehmet '), çünkü ayırıcıyı siz elle eklediniz; concat_ws() bu sorunu da çözer. Alternatif olarak coalesce(soyad, '') ile NULL değeri boş metne çevirebilirsiniz.

|| operatörü taraflardan en az biri metin olduğu sürece sayı gibi metin olmayan değerleri de kabul eder ve onları metne çevirir: 'Sipariş no: ' || 42 sonucu Sipariş no: 42 olur. Belgelere göre tek istisna dizilerdir; dizi ile || kullanıldığında dizi birleştirme operatörü devreye girer, bu yüzden bir diziyi metin olarak eklemek istiyorsanız önce açıkça text tipine çevirmeniz gerekir. Satırları tek bir metinde toplamak için string_agg kullanılır; o konu GROUP BY ve aggregate fonksiyonlar yazısında.

Uzunluk: karakter mi bayt mı?

length() ve char_length() karakter sayısını, octet_length() bayt sayısını verir. UTF-8 kodlamasında Türkçe karakterler iki bayt tuttuğu için bu iki değer farklıdır. char(n) tipinde ise sondaki dolgu boşlukları uzunluğa katılmaz:

sql
SELECT length('Şişli') AS karakter, octet_length('Şişli') AS bayt,
       char_length('abc  '::char(5)) AS char_tipi;
text
 karakter | bayt | char_tipi 
----------+------+-----------
        5 |    7 |         3
(1 row)

Bir sütuna varchar(50) gibi bir sınır koyduğunuzda sınır karakter cinsindendir, bayt cinsinden değil. Tip seçimi PostgreSQL veri tipleri yazısında anlatılıyor.

Parça alma: substring, left, right, position, split_part

Konum hesabında PostgreSQL 1'den sayar. substring(metin FROM baslangic FOR adet) SQL standardındaki yazımdır; substr(metin, baslangic, adet) aynı işi virgüllü sözdizimiyle yapar. left ve right baştan ya da sondan belirli sayıda karakter alır; negatif sayı verirseniz "son n karakter hariç hepsi" anlamına gelir. position(alt IN metin) ve strpos(metin, alt) ilk bulunduğu yeri, bulunamazsa 0 döndürür; iki fonksiyonun argüman sırasının ters olduğuna dikkat edin.

sql
SELECT substring('KEYDAL-2026-0042' FROM 8 FOR 4) AS yil,
       substr('KEYDAL-2026-0042', 13) AS sira,
       left('KEYDAL-2026-0042', 6) AS onek,
       right('KEYDAL-2026-0042', 4) AS son4,
       position('-' IN 'KEYDAL-2026-0042') AS ilk_tire,
       strpos('KEYDAL-2026-0042', '2026') AS yil_yeri;
text
 yil  | sira |  onek  | son4 | ilk_tire | yil_yeri 
------+------+--------+------+----------+----------
 2026 | 0042 | KEYDAL | 0042 |        7 |        8
(1 row)

Ayırıcıyla bölünmüş alanlar için split_part(metin, ayirici, n) en pratik araçtır. Alanlar 1'den numaralanır; negatif n sondan saymayı sağlar. Böylece bir e-posta adresinin kullanıcı ve alan adı kısmını ya da bir alan adının son parçasını tek fonksiyonla alırsınız:

sql
SELECT eposta,
       split_part(eposta, '@', 1) AS kullanici,
       split_part(eposta, '@', 2) AS alan,
       split_part(eposta, '.', -1) AS son_parca
FROM musteri ORDER BY id;
text
          eposta          |  kullanici  |     alan     | son_parca 
--------------------------+-------------+--------------+-----------
 ayse.yilmaz@ornek.com.tr | ayse.yilmaz | ornek.com.tr | tr
 MEHMET@Ornek.com         | MEHMET      | Ornek.com    | com
 zeynep_d@posta.net       | zeynep_d    | posta.net    | net
(3 rows)

Bütün parçaları bir dizi olarak almak isterseniz string_to_array(metin, ayirici) kullanın. Üçüncü argüman verilirse o metne eşit alanlar NULL olur. Dizilerle çalışmanın ayrıntısı PostgreSQL array (dizi) tipi yazısında.

sql
SELECT string_to_array('a,b,,c', ',') AS dizi, string_to_array('a,b,,c', ',', '') AS bos_null;
text
    dizi    |   bos_null   
------------+--------------
 {a,b,"",c} | {a,b,NULL,c}
(1 row)

Değiştirme ve temizleme

Değiştirme ve temizleme

replace(metin, eski, yeni) alt metnin bütün geçişlerini değiştirir. translate(metin, kaynak, hedef) karakter karakter eşleme yapar: kaynak kümedeki her karakter hedef kümede aynı sıradaki karakterle değişir, hedef kısa kalırsa fazla karakterler silinir. Bu, Türkçe karakterleri sadeleştirip URL için kısa ad (slug) üretmek gibi işlerde kullanışlıdır. trim, ltrim, rtrim ve btrim baştaki ve sondaki boşlukları ya da verdiğiniz karakterleri atar; lpad ve rpad metni istenen uzunluğa doldurur; initcap her kelimenin ilk harfini büyütür.

sql
SELECT replace('ürün-adı-yeni', '-', ' ') AS replace_ile,
       translate('Çağrı Işık', 'çÇğĞıIİöÖşŞüÜ', 'cCgGiIIoOsSuU') AS translate_ile,
       trim('  boşluklu metin  ') AS trim_ile,
       btrim('xxabcxx', 'x') AS btrim_ile,
       lpad('42', 6, '0') AS lpad_ile,
       initcap('ali VELİ kaya') AS initcap_ile;
text
  replace_ile  | translate_ile |    trim_ile    | btrim_ile | lpad_ile |  initcap_ile  
---------------+---------------+----------------+-----------+----------+---------------
 ürün adı yeni | Cagri Isik    | boşluklu metin | abc       | 000042   | Ali Veli Kaya
(1 row)

lpad metin hedef uzunluktan uzunsa sağdan keser; bu sessiz kırpma beklenmedik veri kaybına yol açabilir. initcap kelimeleri harf ve rakam olmayan karakterlere göre ayırır, yani "o'neil" gibi bir adda kesme işaretinden sonraki harfi de büyütür.

Türkçe I ve i: upper ve lower

upper ve lower veritabanının yerel ayar kurallarına göre çalışır. Türkçede "ı" harfinin büyüğü "I", "i" harfinin büyüğü "İ"dir; birçok yerel ayar ise İngilizce kuralı uygular. Bu veritabanının varsayılanı olan builtin sağlayıcılı C.UTF-8 ile ICU'nun Türkçe collation'ını karşılaştıralım:

sql
SELECT upper('ılık istanbul') AS builtin_upper,
       upper('ılık istanbul' COLLATE "tr-TR-x-icu") AS icu_tr_upper,
       lower('IŞIK İZMİR' COLLATE "tr-TR-x-icu") AS icu_tr_lower;
text
 builtin_upper | icu_tr_upper  | icu_tr_lower 
---------------+---------------+--------------
 ILIK ISTANBUL | ILIK İSTANBUL | ışık izmir
(1 row)

Varsayılan kurallarla "istanbul" kelimesi "ISTANBUL" oldu; Türkçe collation ile doğru biçimde "İSTANBUL" elde edildi. Türkçe metinlerde büyük-küçük harf dönüşümüne dayanan her karşılaştırmada hangi collation'ın kullanıldığını bilmeniz gerekir. Konunun tamamı, sütun ve veritabanı düzeyinde collation seçimi collation ve Türkçe sıralama yazısında anlatılıyor.

PostgreSQL 18 sürüm notları büyük-küçük harfe duyarsız eşleştirme için casefold() fonksiyonunu ekledi. Belgelere göre bu fonksiyon çoğu durumda küçük harfe çevirme gibi davranır ama collation'a göre istisnalar olabilir ve sonuç metninin uzunluğu değişebilir; libc sağlayıcısında lower ile aynıdır. Bu kümede casefold('İSTANBUL') sonucu İstanbul çıktı, yani noktalı büyük İ bu collation'da katlanmadı; lower('İSTANBUL') ise istanbul döndürdü. Türkçe veride kullanmadan önce kendi collation'ınızla deneyin.

format(): biçimli metin ve güvenli SQL üretimi

format(sablon, arg1, ...) C dilindeki sprintf fonksiyonuna benzer. Üç biçim türü vardır: %s değeri düz metin olarak yazar (NULL boş metin olur), %I değeri SQL tanımlayıcısı olarak gerekiyorsa çift tırnaklar, %L değeri SQL sabiti olarak tek tırnaklar ve içindeki tırnakları ikiler (NULL değeri tırnaksız NULL yazar). Genişlik ve - bayrağıyla hizalama, %1$s biçiminde konumla argüman seçme de desteklenir.

sql
SELECT format('Merhaba %s, bakiyeniz %s TL', 'Ayşe', 125.50) AS s_ile,
       format('|%-10s|%5s|', 'ürün', 7) AS genislik,
       format('SELECT * FROM %I WHERE sehir = %L', 'Müşteri Listesi', 'İstanbul') AS sql_metni,
       format('%L', NULL) AS null_literal;
text
               s_ile               |      genislik      |                        sql_metni                         | null_literal 
-----------------------------------+--------------------+----------------------------------------------------------+--------------
 Merhaba Ayşe, bakiyeniz 125.50 TL | |ürün      |    7| | SELECT * FROM "Müşteri Listesi" WHERE sehir = 'İstanbul' | NULL
(1 row)

PL/pgSQL içinde dinamik SQL kurarken tablo ve sütun adlarını %I, değerleri %L ile eklemek, metni || ile elle birleştirmekten çok daha güvenlidir. Değerler için daha da iyi yol, EXECUTE ... USING ile parametre geçmektir; bu PL/pgSQL temelleri yazısında, saldırı türünün kendisi ise SQL injection önleme yazısında anlatılıyor.

LIKE ve ILIKE ile desen arama

LIKE iki joker karakter tanır: % sıfır ya da daha fazla karakterle, _ tam bir karakterle eşleşir. Desen her zaman metnin tamamına uygulanır; metnin içinde bir yerde geçen bir parçayı arıyorsanız desenin iki ucuna % koymalısınız. ILIKE aynı işi büyük-küçük harfe duyarsız yapar ve PostgreSQL'e özgü bir eklentidir:

sql
SELECT id, ad FROM musteri WHERE eposta LIKE '%ornek%' ORDER BY id;
SELECT id, ad FROM musteri WHERE eposta ILIKE '%ornek%' ORDER BY id;
text
 id |  ad  
----+------
  1 | Ayşe
(1 row)

 id |   ad   
----+--------
  1 | Ayşe
  2 | Mehmet
(2 rows)

Sık yapılan bir hata, e-posta ya da kod gibi alt çizgi içeren değerleri ararken _ karakterinin joker olduğunu unutmaktır. Desende gerçek bir alt çizgi ya da yüzde işareti aramak için önüne kaçış karakteri koyun. Varsayılan kaçış karakteri ters bölü işaretidir; ESCAPE ile başka bir karakter seçebilirsiniz:

sql
SELECT 'zeynep_d@posta.net' LIKE 'zeynep_d%' AS alt_cizgi_joker,
       'zeynepxd@posta.net' LIKE 'zeynep_d%' AS bu_da_eslesir,
       'zeynepxd@posta.net' LIKE 'zeynep\_d%' AS kacisli,
       '%50 indirim' LIKE '!%50%' ESCAPE '!' AS escape_ile;
text
 alt_cizgi_joker | bu_da_eslesir | kacisli | escape_ile 
-----------------+---------------+---------+------------
 t               | t             | f       | t
(1 row)

Kullanıcının yazdığı arama metnini doğrudan desene koyuyorsanız içindeki % ve _ karakterlerini kaçışlamanız gerekir; aksi hâlde "50%" arayan biri beklenmedik sonuçlar görür. Belgeler ayrıca LIKE sözdiziminin içeride ~~ operatörüne, ILIKE'ın ~~* operatörüne çevrildiğini, bu adları EXPLAIN çıktısında görebileceğinizi belirtir. Yalnızca baş kısmı kontrol edecekseniz starts_with(metin, onek) fonksiyonu ya da ^@ operatörü daha açık bir yazımdır.

Performans açısından: belgelere göre B-tree indeksi LIKE ve ~ sorgularında yalnızca desen sabitse ve metnin başına bağlıysa kullanılabilir (col LIKE 'foo%' olur, col LIKE '%bar' olmaz). Veritabanı C yerel ayarında değilse indeksi text_pattern_ops operatör sınıfıyla oluşturmanız gerekir. Ayrıntılar PostgreSQL indeks türleri yazısında. Kelime bazlı arama ihtiyacınız varsa LIKE '%...%' yerine full text search daha uygun bir araçtır.

SIMILAR TO

SIMILAR TO, SQL standardının düzenli ifade tanımıdır ve LIKE ile POSIX düzenli ifadeleri arasında bir yerde durur. LIKE gibi metnin tamamına uygulanır ve %, _ jokerlerini kullanır; buna ek olarak | ile seçenek, *, +, ?, {m,n} ile tekrar, parantezle gruplama ve köşeli parantezle karakter sınıfı destekler. Nokta ise burada özel karakter değildir:

sql
SELECT 'ankara' SIMILAR TO '(ankara|izmir)' AS similar1,
       'ankara' SIMILAR TO 'ank' AS similar2,
       'a.b' SIMILAR TO 'a.b' AS nokta_meta_degil;
text
 similar1 | similar2 | nokta_meta_degil 
----------+----------+------------------
 t        | f        | t
(1 row)

Pratikte çoğu geliştirici ya basit işler için LIKE, ya da güçlü bir dil gerektiğinde POSIX düzenli ifadelerini tercih eder; SIMILAR TO daha çok standart uyumu gereken yerlerde görülür.

POSIX düzenli ifadeler: ~, ~* ve regexp_* fonksiyonları

POSIX düzenli ifadeler: ~, ~* ve regexp_* fonksiyonları: Eşleşen parçaları almak, Bölme

Dört operatör vardır: ~ (eşleşir, harfe duyarlı), ~* (eşleşir, harfe duyarsız), !~ ve !~* (eşleşmez). LIKE'tan en önemli farkı, düzenli ifadenin ^ ve $ ile bağlanmadıkça metnin herhangi bir yerinde eşleşebilmesidir:

sql
SELECT 'abc' LIKE 'b' AS like_tamami, 'abc' ~ 'b' AS regex_parcasi;
text
 like_tamami | regex_parcasi 
-------------+---------------
 f           | t
(1 row)

Telefon numaralarını tek biçime getirmek tipik bir temizlik işidir. regexp_replace varsayılan olarak yalnızca ilk eşleşmeyi değiştirir; hepsini değiştirmek için 'g' bayrağını vermelisiniz. Bu bayrağı unutmak en sık yapılan regex hatasıdır:

sql
SELECT telefon,
       regexp_replace(telefon, '[^0-9]', '', 'g') AS sadece_rakam,
       telefon ~ '^\+90' AS ulke_kodlu
FROM musteri ORDER BY id;
text
      telefon      | sadece_rakam | ulke_kodlu 
-------------------+--------------+------------
 0532 111 22 33    | 05321112233  | f
 (0505) 444-55-66  | 05054445566  | f
 +90 555 777 88 99 | 905557778899 | t
(3 rows)

Değiştirme metninde \1 ile \9 arası parantezli grupları, \& bütün eşleşmeyi geri çağırır. Örneğin ISO tarih metnini gün.ay.yıl sırasına çevirmek:

sql
SELECT regexp_replace('2026-03-15', '(\d{4})-(\d{2})-(\d{2})', '\3.\2.\1') AS ters;
text
    ters    
------------
 15.03.2026
(1 row)

Gerçek tarih biçimlendirmesi için metin işlemek yerine to_char kullanmak daha doğrudur; tarih ve saat fonksiyonları yazısına bakın.

Eşleşen parçaları almak

regexp_match ilk eşleşmedeki grupları bir metin dizisi olarak döndürür, eşleşme yoksa NULL. regexp_substr eşleşen metnin kendisini verir; belgeler yalnızca bütün eşleşmeyi istediğiniz durumda bu fonksiyonu önerir. regexp_count eşleşme sayısını sayar. Bütün eşleşmeleri satır satır almak için regexp_matches fonksiyonunu 'g' bayrağıyla kullanın:

sql
SELECT regexp_match('Fatura TR-2026-00981 tarihli', '([A-Z]{2})-(\d{4})-(\d+)') AS parcalar,
       (regexp_match('Fatura TR-2026-00981 tarihli', '([A-Z]{2})-(\d{4})-(\d+)'))[3] AS sira_no,
       regexp_substr('Fatura TR-2026-00981 tarihli', '\d{5}') AS substr_ile,
       regexp_count('a1b22c333', '\d+') AS sayi_grubu;

SELECT regexp_matches('#postgresql #sql ve #veritabanı', '#(\w+)', 'g') AS etiket;
text
    parcalar     | sira_no | substr_ile | sayi_grubu 
-----------------+---------+------------+------------
 {TR,2026,00981} | 00981   | 00981      |          3
(1 row)

    etiket    
--------------
 {postgresql}
 {sql}
 {veritabanı}
(3 rows)

Belgelerdeki bir uyarıyı not edin: regexp_matches bir küme döndüren fonksiyondur ve eşleşme olmayan satır için hiç satır üretmez. SELECT listesinde bir tablo sütunuyla kullanırsanız eşleşmeyen satırlar sonuçtan düşer. Satır başına tek sonuç istiyorsanız regexp_match kullanın. \w sınıfının "veritabanı" kelimesindeki ı harfini de yakaladığına dikkat edin.

Bölme

regexp_split_to_array ve regexp_split_to_table metni bir düzenli ifadeyi ayırıcı kabul ederek böler. Virgüllerin çevresindeki düzensiz boşluklar ya da birden çok boşluk gibi durumlarda split_part'tan daha esnektir:

sql
SELECT regexp_split_to_array('elma, armut ,  kiraz', '\s*,\s*') AS dizi;
SELECT kelime FROM regexp_split_to_table('bir  iki   üç', '\s+') AS kelime;
text
        dizi        
--------------------
 {elma,armut,kiraz}
(1 row)

 kelime 
--------
 bir
 iki
 üç
(3 rows)

PostgreSQL 18 sürüm notları bütün regexp_* fonksiyonlarının adlandırılmış argümanlarla çağrılabilmesini sağladı; örneğin belgelerde regexp_replace(string=>'hello world', pattern=>'l', replacement=>'XX', start=>1, "N"=>2) biçiminde bir çağrı gösterilir.

Karşılaştırma tablosu

YöntemMetnin tamamı mı?JokerlerHarf duyarlılığı
LIKEevet% ve _duyarlı
ILIKEevet% ve _duyarsız
SIMILAR TOevet% ve _ ile birlikte | * + ? {m,n}duyarlı
~ ve ~*hayır, bağlanmadıkça herhangi bir yerPOSIX: . * + ? [] () {}~ duyarlı, ~* duyarsız
starts_with ve ^@yalnızca baş kısımyokduyarlı

Sık yapılan hatalar

  • || ile birleştirirken NULL bir sütunun bütün sonucu NULL yapması. concat_ws ya da coalesce kullanın.
  • LIKE 'ali' yazıp metnin içinde aranacağını sanmak: jokersiz desen eşitlik gibi davranır.
  • Desendeki _ karakterinin joker olduğunu unutmak.
  • regexp_replace çağrısında 'g' bayrağını unutup yalnızca ilk eşleşmeyi değiştirmek.
  • lower(sehir) = 'istanbul' gibi karşılaştırmalarda Türkçe İ/ı harflerini hesaba katmamak.
  • length ile bayt sayısını ölçtüğünüzü sanmak; bayt için octet_length.
  • Dinamik SQL'de kullanıcı girdisini || ile birleştirmek; format ile %I/%L ya da USING parametreleri kullanın.

Sık Sorulan Sorular

PostgreSQL'de iki metni nasıl birleştiririm?

'a' || 'b' ya da concat('a', 'b'). Değerlerden biri NULL olabiliyorsa concat veya ayırıcılı concat_ws tercih edin, çünkü || NULL ile karşılaşınca bütün sonucu NULL yapar.

Büyük-küçük harfe duyarsız arama nasıl yapılır?

Desen araması için ILIKE ya da ~*. Eşitlik için iki tarafı lower() ile karşılaştırabilirsiniz; Türkçe veride doğru sonuç için uygun bir collation kullanın. Büyük-küçük harfe duyarsız (nondeterministic) collation tanımlama seçeneği collation yazısında anlatılıyor; belgeler ILIKE'ın bu tür collation'ları desteklemediğini, LIKE'ın ise desteklediğini belirtir.

Bir metindeki rakam dışındaki her şeyi nasıl silerim?

regexp_replace(metin, '[^0-9]', '', 'g'). Sondaki g bayrağı bütün eşleşmelerin değiştirilmesini sağlar.

substring ile regexp_substr arasında ne fark var?

substring(metin FROM baslangic FOR adet) konuma göre keser. substring(metin FROM desen) biçimi ise POSIX deseniyle eşleşen ilk parçayı (desende parantez varsa ilk grubu) döndürür. regexp_substr başlangıç konumu, kaçıncı eşleşme ve hangi grup gibi ek parametreler alır.

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 · collation ve Türkçe sıralama · full text search · array (dizi) tipi · tarih ve saat fonksiyonları · PL/pgSQL temelleri · SQL injection önleme