# Oracle’da Güvenilir Benzersiz Anahtar Olmadan Güvenli Satır Güncelleme

> Benzersizliği veri tabanı tarafından güvence altına alınmamış eski Oracle tablolarında tek satır güncellemenin neden güvenli olmadığı; iş anahtarı ile fiziksel satır kimliğinin ayrılması, deterministik aday seçimi, ROWID adreslemesi, transaction sınırı ve ORM kimlik varsayımları.

- Author: Muhammet Ali Köker
- Language: tr
- Canonical: https://alikoker.com.tr/oracle-benzersiz-anahtar-olmadan-guvenli-satir-guncelleme
- Translation: https://alikoker.com.tr/en/safe-row-updates-in-oracle-without-reliable-unique-keys
- Published: 2020-06-18T12:00:00+03:00
- Modified: 2026-09-02T00:00:00+03:00
- Verified: 2026-09-02T00:00:00+03:00
- Type: article

Eski bir veri tabanında en tehlikeli tablo, uygulamanın kayıt kimliği gibi kullandığı sütunun veri tabanı tarafından benzersiz kılınmadığı tablodur. Kayıt numarası, belge numarası veya işlem zamanı gibi bir sütun kodda anahtar gibi geçer; arkasında `PRIMARY KEY` veya doğrulanmış bir `UNIQUE` kısıtı yoksa aynı değer birden çok fiziksel satırda bulunabilir. Böyle bir tabloda `UPDATE t SET ... WHERE kayit_no = :no` deyimi tek satır güncelleme niyetini ifade etmez; eşleşen bütün satırları değiştirir. Aynı sütunu skaler alt sorguda kullanan kod `ORA-01427: single-row subquery returns more than one row` ile durur, JPA tarafında `getSingleResult()` çağrısı `NonUniqueResultException` üretir. Sorun SQL bilgisinden değil, iş anahtarı ile fiziksel satır kimliğinin aynı şey sanılmasından doğar.

## Yinelemelerin nereden geldiği

Kısıtsız bir tabloda yineleme oluşmasının birkaç tipik yolu vardır ve düzeltme stratejisi hangisinin geçerli olduğuna göre değişir. En yaygın durum kısıtın hiç tanımlanmamış olmasıdır: benzersizlik uygulama katmanında denetleniyordu, ikinci bir uygulama aynı tabloya yazmaya başlayınca denetim delindi. İkinci durum kısıtın toplu yükleme öncesinde devre dışı bırakılıp yeniden etkinleştirilmemesidir; `DISABLE` edilmiş bir kısıt yükleme sırasında hiçbir şeyi engellemez. Üçüncü durum iki ayrı sistemin verisinin aynı tabloya taşınmasıdır; her kaynak kendi içinde benzersizdi, birleşik küme değildir.

Dördüncü durum daha sinsidir: satırlar fiziksel olarak farklı olsa da uygulamanın normalizasyon kurallarına göre aynı iş kaydını temsil edebilir. Büyük/küçük harf, sondaki boşluklar, karakter normalizasyonu ve bileşik anahtarlardaki `NULL` değerleri ayrı ayrı ele alınmalıdır. Oracle sıfır uzunluklu karakter dizisini `NULL` olarak değerlendirir; buna karşılık `NULL` içeren benzersiz veya bileşik anahtarların davranışı, anahtarın diğer bileşenlerine ve tanımlanan kısıta bağlıdır. Bu nedenle yineleme ölçümü uygulamanın gerçek iş anahtarı normalizasyonuyla yapılmalıdır.

Teşhis her zaman güncellemeden önce gelir. İlk sorgu yalnızca hangi iş anahtarlarının birden fazla fiziksel satıra karşılık geldiğini güvenilir biçimde göstermelidir; adayların içerik açısından eşdeğer olup olmadığı bundan sonra, iş açısından anlamlı sütunlar üzerinden ayrıca incelenmelidir. Birkaç sütunu ayraçlarla birleştirerek `COUNT(DISTINCT ...)` üretmek `NULL`, NLS dönüşümü ve ayraç çakışmaları nedeniyle yanıltıcı olabilir.

```sql
SELECT kayit_no,
       COUNT(*) AS adet
FROM   islem_tablosu
GROUP  BY kayit_no
HAVING COUNT(*) > 1;
```

Aynı iş anahtarı altında bulunan satırların gerçekten eşdeğer sayılıp sayılmayacağı SQL sözdiziminin değil iş kuralının konusudur. Karşılaştırılacak sütun kümesi açıkça tanımlanmadan "tam kopya" kararı verilmemelidir.

## İş anahtarı ile fiziksel satır kimliği

İş anahtarı, uygulamanın kaydı nasıl adlandırdığını söyler. Fiziksel satır kimliği daha dar bir soruya yanıt verir: bu işlem hangi tablo satırını değiştirecek. Kısıtın bulunduğu sağlıklı bir şemada iki kavram çakışır ve ayrım gereksiz görünür. Kısıt yoksa çakışma ortadan kalkar ve her yazma işleminin iki adımı ayrı ayrı yanıtlaması gerekir: hangi satır seçildi ve o satır yazma anına kadar aynı satır olarak kaldı mı.

Bu ayrımı yapmadan yazılan kod iki yolla bozulur. Birincisi kapsam hatasıdır: tek satır güncellemek isteyen deyim n satır günceller ve `SQL%ROWCOUNT` beklenen 1 yerine n döner. İkincisi belirsizlik hatasıdır: kod n satırdan birini okur, karar verir, sonra "aynı" anahtarla yazar; okunan satır ile yazılan satır aynı olmayabilir. Birinci hata gürültülüdür ve testte görünür; ikincisi sessizdir ve üretimde veri tutarsızlığı olarak birikir.

## Deterministik seçim kuralı

"En yeni kayıt geçerlidir" kuralı yalnız başına yeterli değildir ve her zaman doğru da değildir. Bu kural ancak iş alanı gerçekten böyle tanımlanmışsa, yani aynı iş anahtarı için sonradan yazılan satırın öncekini geçersiz kıldığı bir düzeltme akışı varsa uygulanır. Kaynak sistemlerin bağımsız kayıt ürettiği bir birleştirmede en yeni satır en doğru satır olmayabilir; orada kural kaynak önceliği veya doluluk olabilir.

Kural hangisi olursa olsun üç şeyi açıkça söylemelidir: hangi sütun otorite kabul edilir, `NULL` bu sıralamada nereye düşer ve tam eşitlikte hangi satır kazanır. Saniye çözünürlüğündeki bir `DATE` sütununda eşitlik nadir değildir; toplu yükleme ile gelen satırlarda aynı damga onlarca satırda tekrarlanabilir.

```sql
SELECT rid
FROM (
    SELECT ROWID AS rid,
           ROW_NUMBER() OVER (
               ORDER BY islem_tarihi DESC NULLS LAST,
                        surum_no      DESC NULLS LAST,
                        ROWID         DESC
           ) AS rn
    FROM   islem_tablosu
    WHERE  kayit_no = :kayitNo
)
WHERE rn = 1;
```

Sıralamanın sonundaki `ROWID` iş anlamı taşımaz; aynı tarih ve aynı sürüm numarasına sahip mevcut adaylar arasında toplam bir sıra oluşturmak için kullanılır. Aday kümesi ve fiziksel satır yerleşimi değişmediği sürece eşitliği deterministik biçimde kırar. Bu özellik önemlidir: seçim ölçütleri eşitlikleri kırmıyorsa iki uygulama sunucusu aynı iş anahtarı için farklı fiziksel satırları hedefleyebilir. Veri modelinde anlamlı ve benzersiz bir ikincil ölçüt varsa (sürüm numarası, kaynak sistem kodu, doldurulmuş alan sayısı) o ölçüt `ROWID`'den önce gelmelidir; `ROWID` yalnız son çare eşitlik bozucudur.

Aynı sonuca analitik fonksiyon yerine `MAX(...) KEEP (DENSE_RANK FIRST ORDER BY ...)` toplulaştırmasıyla da ulaşılabilir; iki biçim de tek geçişte çalışır. Önemli olan sözdizimi değil, sıralamanın eşitlikleri tamamen kıran bir toplam sıra tanımlamasıdır.

## ROWID neyi adresler, neyi adreslemez

Oracle `ROWID` sözde sütunu heap tablodaki satırın konumunu kodlar: veri nesnesi numarası, göreli dosya numarası, blok numarası ve blok içindeki satır yuvası. Satır yerinde durduğu sürece bu adres Oracle'ın doğrudan tek satır erişim yollarından biridir; yürütme planında `TABLE ACCESS BY USER ROWID` olarak görünebilir ve ayrı bir iş anahtarı indeks taramasına gerek bırakmaz. Yinelenen iş anahtarları arasında karar verildikten sonra seçilen satırı hedeflemek için tam olarak bu özellik gerekir.

Buna karşılık `ROWID` kalıcı kimlik değildir. `ALTER TABLE ... MOVE`, `SHRINK SPACE`, satır taşımaya izin verilmiş bölümlü tabloda bölüm anahtarının güncellenmesi, export/import ile yeniden yükleme ve Flashback Table işlemleri adresi değiştirebilir. Index-organized tablolarda fiziksel adres yerine birincil anahtara bağlı mantıksal `UROWID` döner. Silinmiş bir satırın adresi başka bir satıra yeniden verilebilir. Bu nedenle `ROWID`, uygulama belleğinde, HTTP oturumunda veya başka bir tabloda saklanacak bir kimlik değil, aynı işlem sınırı içinde seçim ile güncelleme arasında taşınan geçici bir adrestir. Adres saklanıp sonra kullanılırsa `ORA-08006: specified row no longer exists` alınabilir; daha kötü olasılık, aynı adrese yerleşmiş başka bir satırın sessizce güncellenmesidir.

## Seçim ile güncelleme arasındaki yarış

Adayı seçip adresini uygulamaya döndürmek, sonra ikinci bir istekle güncellemek iki işlem arasında bir pencere açar. Bu pencerede başka bir oturum aynı iş anahtarı için yeni satır ekleyebilir, seçilen satırı silebilir veya aynı satırı başka bir değerle güncelleyebilir. Güncellemenin gerçekten seçilen satıra uygulanması isteniyorsa seçim, kilitleme ve güncelleme aynı transaction içinde kalmalıdır.

En yalın biçim tek deyimdir:

```sql
UPDATE islem_tablosu
SET    durum = :yeniDurum,
       guncelleme_zamani = SYSTIMESTAMP
WHERE  ROWID = (
    SELECT rid FROM (
        SELECT ROWID AS rid,
               ROW_NUMBER() OVER (
                   ORDER BY islem_tarihi DESC NULLS LAST, ROWID DESC
               ) AS rn
        FROM   islem_tablosu
        WHERE  kayit_no = :kayitNo
    )
    WHERE rn = 1
);
```

Tek deyim Oracle'ın deyim düzeyi read-consistency ve write-consistency kuralları altında değerlendirilir; aday seçimi ile DML aynı SQL deyiminin parçasıdır ve `ROWID = (scalar_subquery)` biçimi, alt sorgu sıfır veya bir adres döndürdüğü sürece en fazla bir fiziksel satırı hedefler. Uygulama satırı okuyup karar verdikten sonra yazmak zorundaysa seçim ve kilitleme aynı transaction içinde tutulmalıdır. Analitik fonksiyon içeren inline view üzerinde doğrudan `FOR UPDATE` kullanmak view merging koşullarına bağlıdır ve `ORA-02014` gibi hatalara yol açabilir. Daha açık yaklaşım, önce adresi belirlemek ve ardından temel tablo satırını kilitlemektir:

```sql
SELECT rid
FROM (
    SELECT ROWID AS rid,
           ROW_NUMBER() OVER (
               ORDER BY islem_tarihi DESC NULLS LAST, ROWID DESC
           ) AS rn
    FROM   islem_tablosu
    WHERE  kayit_no = :kayitNo
)
WHERE rn = 1;

SELECT durum, tutar
FROM   islem_tablosu
WHERE  ROWID = :rid
FOR UPDATE WAIT 3;
```

Bu iki sorgu aynı transaction içinde çalıştırılmalıdır. İkinci sorgunun sıfır satır döndürmesi, seçilen satırın arada artık mevcut olmadığını gösteren ayrı bir hata yoludur. `FOR UPDATE` varsayılan olarak kilit serbest kalana kadar bekler; `NOWAIT` hemen döner, `WAIT n` beklemeyi sınırlar. `SKIP LOCKED` ise kilitli satırı atlayabildiği için belirli bir iş kaydının tek satırını güncelleme semantiğine uygun değildir. Kilit alındıktan sonra güncelleme `WHERE ROWID = :rid` ile yapılır ve etkilenen satır sayısı yine doğrulanır.

Oracle DML sırasında read consistency ile birlikte write consistency uygular; eşzamanlı değişiklikler nedeniyle hedef satırın görünümü değiştiğinde deyimin iç yürütümü yeniden değerlendirme veya restart davranışı gösterebilir. Bu mekanizma tek başına genel anlamda "lost update" problemini çözmez; özellikle uygulama düzeyindeki ayrı oku-karar-ver-yaz adımları ayrıca kilitleme veya sürüm denetimi gerektirir. "Hangi satır en yeni" kararı da işlem izolasyonu ve deyimin gördüğü tutarlı veri görünümü bağlamında değerlendirilmelidir. Aynı iş anahtarı için sürekli yeni satır ekleyen bir kaynak varsa, seçim ile ekleme arasındaki yarış veri modeli düzeltilmeden tamamen kapanmaz; kazanılan şey, hedefin her zaman tek ve belirli bir satır olmasıdır.

Kilit tutulacak süre de bir tasarım kararıdır. Kullanıcı ekranda düşünürken satırın kilitli kalması kabul edilemez; böyle akışlarda kötümser kilit yerine iyimser sürüm denetimi uygundur. Ancak sürüm sütunu güncellemenin hedefini belirlemez, yalnız araya girmeyi saptar; hedefi belirleyen yine deterministik seçim ve `ROWID` adreslemesidir.

## View üzerinden güncelleme ve key-preserved kuralı

Eski uygulamalar çoğu zaman tabloya değil view'a bağlıdır. Birleştirme içeren bir view üzerinde `UPDATE`, Oracle'ın key-preserved tablo kuralına tabidir: view'daki her satırın temel tablodaki tek bir satıra karşılık gelmesi gerekir ve bu ancak birleştirme anahtarı karşı tarafta benzersizse sağlanır. Benzersiz kısıt bulunmayan bir tablo tanım gereği key-preserved olamaz; güncelleme denemesi `ORA-01779: cannot modify a column which maps to a non key-preserved table` ile sonuçlanır. `DISTINCT`, `GROUP BY`, toplulaştırma, `UNION` ve diğer küme işleçlerini içeren view'lar ise doğrudan güncellenebilir değildir ve bu yapılarda `FOR UPDATE` de reddedilir (`ORA-02014`).

Kilidi view üzerinden zorlamak yerine iki adım daha açıktır: view yalnız okuma modeli olarak kullanılır, güncellenecek temel tablonun fiziksel satırı yukarıdaki kuralla belirlenir ve yazma doğrudan tabloya yapılır. `INSTEAD OF` trigger ile view'ı güncellenebilir kılmak mümkündür; ancak trigger içine gömülen seçim kuralı çağıran koddan görünmez hale gelir ve aynı belirsizliği bir katman aşağı taşır.

## JPA ve Hibernate ile çalışırken

JPA entity modeli kararlı bir kimlik varsayar: `@Id` sütunu birinci düzey önbelleğin anahtarıdır, dirty checking bu anahtara göre çalışır, `flush` sırasında üretilen deyim `UPDATE ... WHERE id = ?` biçimindedir. Benzersiz olmayan bir sütunu `@Id` olarak eşlemek veriyi benzersiz kılmaz; aynı kimlik altında iki farklı fiziksel satır önbellekte tek nesneye indirgenir, hangi satırın yazıldığı belirsizleşir ve üretilen `UPDATE` iki satırı birden değiştirir.

Şema değiştirilemiyorsa okuma modeli ile yazma modeli ayrılmalıdır. Yinelenen iş anahtarlarını benzersizmiş gibi bir JPA entity `@Id` alanına eşlemek doğru değildir; DTO/projection veya kontrollü native okuma modeli, fiziksel satır hedefini açıkça taşıyan ayrı bir yazma yolu daha güvenlidir. Hibernate `@RowId`, desteklenen diyalektlerde bir rowid-benzeri locator'ın CRUD işlemlerinde kullanılmasını sağlayabilir; ancak entity kimliği gereksinimini ortadan kaldırmaz ve benzersiz olmayan sahte bir `@Id` eşlemesini güvenli hale getirmez. Bu nedenle legacy tabloda gerçek bir kararlı entity kimliği yoksa yazmanın açık `ROWID` hedefli SQL/prosedür katmanında tutulması daha öngörülebilirdir. `LockModeType.PESSIMISTIC_WRITE`, lock timeout hint'leri ve `@RowId` davranışı Hibernate/diyalekt sürümüne göre doğrulanmalı; üretilen SQL gözlemlenmeden belirli bir `NOWAIT` veya `ROWID` biçimi varsayılmamalıdır.

Aynı sorun toplu işlemlerde daha görünür hale gelir. ORM katmanı beklediği satır sayısıyla gerçek etkilenen satır sayısı uyuşmadığında optimistic-lock veya stale-state sınıfındaki hatalar üretebilir. Yinelenen iş anahtarları bulunan bir tabloda bu tür bir hatayı otomatik retry ile bastırmadan önce üretilen `UPDATE` koşulu ve etkilenen fiziksel satır sayısı incelenmelidir; aksi halde veri modeli problemi eşzamanlılık problemi sanılabilir.

## Toplu temizlik ve kısıt eklemenin mümkün olduğu durum

Şema üzerinde değişiklik yapılabiliyorsa asıl çözüm yinelemeleri temizleyip kısıt eklemektir. Klasik `DELETE ... WHERE ROWID NOT IN (SELECT MIN(ROWID) ... GROUP BY kayit_no)` deyimi çalışır, fakat `MIN(ROWID)` "en eski" ya da "en doğru" satır anlamına gelmez; yalnız fiziksel olarak en küçük adresi seçer. Hangi satırın kalacağı yukarıdaki iş kuralıyla belirlenmeli ve silme, aynı `ROW_NUMBER()` sıralamasında `rn > 1` olan adresleri hedeflemelidir.

Temizlik tek seferde yapılamıyorsa `ENABLE NOVALIDATE` bir geçiş aracı olabilir: mevcut satırlar yeniden doğrulanmadan sonraki DML için kısıt uygulanır. Ancak `PRIMARY KEY`/`UNIQUE` kısıtlarında kullanılacak indeksin yapısı ve mevcut yinelemeler nedeniyle kısıtın hangi DDL sırasıyla etkinleştirileceği önceden planlanmalıdır; tek bir `ADD CONSTRAINT ... ENABLE NOVALIDATE` deyiminin her mevcut şemada sorunsuz çalışacağı varsayılmamalıdır. Gerekirse uygun non-unique indeks önceden hazırlanır ve ihlaller `EXCEPTIONS INTO` ile ayrı olarak belirlenir. Amaç, eski veri temizlenirken yeni ihlallerin girişini kontrollü biçimde durdurmaktır.

## Karar kuralları

Yukarıdaki tartışma pratikte birkaç kısa kurala indirgenebilir.

```text
Kısıt eklenebiliyorsa                → önce temizle, sonra UNIQUE ekle; gerisi geçici çözümdür
Kısıt eklenemiyorsa                  → iş anahtarını seçim, ROWID'i hedefleme için kullan
Seçim kuralı                         → iş alanı tanımlar; "en yeni" varsayılan değildir
Eşitlik                              → sıralama toplam sıra üretene kadar ölçüt ekle
Oku-karar-ver-yaz akışı              → tek transaction, FOR UPDATE ile sınırlı bekleme
Tek adımlık güncelleme               → tek UPDATE deyimi, alt sorguda ROW_NUMBER
ROWID'in ömrü                        → seçim ile yazma arasında; hiçbir yerde saklanmaz
View karmaşıksa                      → temel tabloya yaz, INSTEAD OF ile gizleme
ORM                                  → okuma modeli ile yazma hedefi ayrı tutulur
Etkilenen satır sayısı               → 1 beklenir; 0 ve n ayrı hata yollarıdır
```

Son satır çoğu zaman atlanır. `SQL%ROWCOUNT` veya JDBC'nin döndürdüğü güncellenen satır sayısı denetlenmiyorsa, yukarıdaki kuralların hiçbirinin çalıştığı doğrulanmış olmaz.

## Sağlanan ve sağlanmayan garantiler

Toplam sıra üreten seçim ölçütleri, aynı transaction içinde `ROWID` ile hedefleme ve gerektiğinde temel tablo satırında `FOR UPDATE` kullanılması üç önemli özellik sağlar: bir yazma işlemi en fazla bir fiziksel satırı hedefler; aday kümesi ve fiziksel yerleşim değişmediği sürece eşitlikler aynı biçimde kırılır; kilit alındıktan sonra başka bir oturum aynı satırı kilit serbest bırakılana kadar değiştiremez. Sağlanmayanlar da açıktır: yöntem yinelemeleri ortadan kaldırmaz, seçim kuralının iş açısından doğru olduğunu kanıtlamaz, aynı anahtar için yeni satır ekleyen kaynağı durdurmaz ve şema düzeltmesinin yerine geçmez. Kısıtı eklenebilen bir sistemde bu yaklaşım tercih edilecek çözüm değil, geçiş dönemi boyunca veriyi bozmadan çalışmayı sürdürme yöntemidir.

İlişkili içerikler: [Oracle Veritabanı ve PL/SQL: Mimari, SQL ve Performans](/oracle-veritabani-plsql-mimari-sql-performans), [Oracle SQL ile Güvenli Sayı Dönüşümü](/oracle-sql-ile-guvenli-sayi-donusumu), [Java ile Yansıma Tabanlı ORM Geliştirme](/java-ile-reflection-tabanli-orm-gelistirme), [ROWID](/wiki/rowid), [Pessimistic Locking](/wiki/pessimistic-locking), [Optimistic Locking](/wiki/optimistic-locking), [Consistent Read](/wiki/consistent-read), [MVCC](/wiki/mvcc).

## Kaynakça

- Oracle. ROWID Pseudocolumn. Oracle Database SQL Language Reference, 19c. https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/ROWID-Pseudocolumn.html
- Oracle. SELECT: FOR UPDATE Clause. Oracle Database SQL Language Reference, 19c. https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/SELECT.html
- Oracle. Data Concurrency and Consistency. Oracle Database Concepts, 19c. https://docs.oracle.com/en/database/oracle/oracle-database/19/cncpt/data-concurrency-and-consistency.html
- Oracle. Managing Views, Sequences, and Synonyms. Oracle Database Administrator's Guide, 19c. https://docs.oracle.com/en/database/oracle/oracle-database/19/admin/managing-views-sequences-and-synonyms.html
- Oracle. Managing Integrity Constraints. Oracle Database Administrator's Guide, 19c. https://docs.oracle.com/en/database/oracle/oracle-database/19/admin/managing-integrity.html
- Red Hat. Hibernate ORM User Guide: Locking. https://docs.jboss.org/hibernate/orm/6.6/userguide/html_single/Hibernate_User_Guide.html
- Thomas Kyte, Darl Kuhn. Expert Oracle Database Architecture, 3rd Edition. Apress, 2014.

## Bu Çalışmaya Atıf

Köker, M. A. (2020). Oracle’da Güvenilir Benzersiz Anahtar Olmadan Güvenli Satır Güncelleme. alikoker.com.tr. https://alikoker.com.tr/oracle-benzersiz-anahtar-olmadan-guvenli-satir-guncelleme

- BibTeX: https://alikoker.com.tr/oracle-benzersiz-anahtar-olmadan-guvenli-satir-guncelleme.bib
- RIS: https://alikoker.com.tr/oracle-benzersiz-anahtar-olmadan-guvenli-satir-guncelleme.ris
- CSL-JSON: https://alikoker.com.tr/oracle-benzersiz-anahtar-olmadan-guvenli-satir-guncelleme.csl.json
