Sayaçtan gelen veri eksik mi, bir okuma iki kez mi yüklendi, endeks geri mi gitti? Bu üçü, rapor yanlış çıkana kadar fark edilmez. Aşağıdaki altı sorgu PostgreSQL 14 ve üstünde çalışır; eksik, yinelenen ve geri giden okumayı, Türkiye saatindeki gün sınırı kaymasını bulur ve sonuçları saklar. Fatura ve mevzuat kuralları konu dışı; yalnız verinin kendi içinde tutarlı olup olmadığına bakılır. OSOS için OSOS sayaç verisi rehberine, EDAŞ veri çerçevesi için EDAŞ veri yönetimi rehberine bakın.
Örnek okuma tablosu ve zaman damgası tipi
Okumalar 15 dakikalık aralıkla, kümülatif endeks (kWh) olarak geliyor. Hiçbir kontrol ham tabloya yazmaz.
CREATE TABLE sayac_okuma (
kayit_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sayac_id text NOT NULL,
olcum_zamani timestamptz NOT NULL,
endeks_kwh numeric(12,3) NOT NULL,
seri_no text
);Sütunun timestamptz olmasının nedeni beşinci bölümde. Varsayalım ki A1 sayacının 1 Mart 2026 gecesinden şu 11 satırı var (örnek veri, gerçek bir sayaca ait değil):
INSERT INTO sayac_okuma (sayac_id, olcum_zamani, endeks_kwh, seri_no) VALUES
('A1', '2026-03-01 00:00+03', 100.000, 'SN-1001'),
('A1', '2026-03-01 00:15+03', 100.400, 'SN-1001'),
('A1', '2026-03-01 00:30+03', 100.800, 'SN-1001'),
('A1', '2026-03-01 01:15+03', 102.000, 'SN-1001'),
('A1', '2026-03-01 01:30+03', 102.400, 'SN-1001'),
('A1', '2026-03-01 01:30+03', 102.400, 'SN-1001'),
('A1', '2026-03-01 01:45+03', 102.900, 'SN-1001'),
('A1', '2026-03-01 02:00+03', 102.700, 'SN-1001'),
('A1', '2026-03-01 02:15+03', 103.100, 'SN-1001'),
('A1', '2026-03-01 02:15+03', 103.250, 'SN-1001'),
('A1', '2026-03-01 02:30+03', 103.600, 'SN-1001');Bu iki buçuk saatlik pencerede 15 dakikalık 11 beklenen zaman var. Çıktılar SET timezone = 'Europe/Istanbul'; oturumunda alındı. PostgreSQL timestamptz değerlerini oturumun saat dilimiyle gösterir; varsayılan dilim sunucu yapılandırmasından gelir, kendi sunucunuzdakini SHOW timezone; ile görün. Verideki sorunlar: 00:45 ve 01:00 yok, 01:30 aynı değerle iki kez var, 02:15 farklı iki değerle var, 02:00'de endeks 102.900'den 102.700'e düşmüş.
1. Beklenen ızgarayı kurup eksik aralıkları listelemek
Her sayaç için beklenen zamanları üretir, okuması olmayanları süzersiniz:
SELECT s.sayac_id, g.beklenen
FROM (SELECT DISTINCT sayac_id FROM sayac_okuma) s
CROSS JOIN generate_series(timestamptz '2026-03-01 00:00+03',
timestamptz '2026-03-01 02:30+03',
interval '15 minutes') AS g(beklenen)
LEFT JOIN sayac_okuma o
ON o.sayac_id = s.sayac_id AND o.olcum_zamani = g.beklenen
WHERE o.kayit_id IS NULL
ORDER BY s.sayac_id, g.beklenen;Örnek veride iki satır beklenir: A1 için 00:45 ve 01:00. Sorgu boş dönerse veri tamdır ya da kontrol çalışmıyordur; ayırt etmek için bir okumayı bilerek silip deneyin.
PostgreSQL 16 ile generate_series'e timestamptz için saat dilimi parametresi geldi (16 sürüm notları). Günlük ya da aylık ızgarada işe yarar (PostgreSQL 16+); 14 ve 15'te yukarıdaki sürümü kullanın.
2. Eksik aralıkları tek satırlık boşluğa indirmek
İki eksik aralık aslında tek boşluktur. "00:45–01:15 arası, 2 aralık" diye göstermek için art arda gelen eksikleri gruplarsınız: zamandan sıra numarası çarpı aralığı çıkarırsanız ardışık satırlar aynı değeri alır (yönteme "gaps and islands" denir).
WITH eksik AS (
SELECT s.sayac_id, g.beklenen
FROM (SELECT DISTINCT sayac_id FROM sayac_okuma) s
CROSS JOIN generate_series(timestamptz '2026-03-01 00:00+03',
timestamptz '2026-03-01 02:30+03',
interval '15 minutes') AS g(beklenen)
LEFT JOIN sayac_okuma o
ON o.sayac_id = s.sayac_id AND o.olcum_zamani = g.beklenen
WHERE o.kayit_id IS NULL
), numarali AS (
SELECT sayac_id, beklenen,
beklenen - row_number() OVER (PARTITION BY sayac_id ORDER BY beklenen)
* interval '15 minutes' AS grup
FROM eksik
)
SELECT sayac_id,
min(beklenen) AS bosluk_basi,
max(beklenen) + interval '15 minutes' AS bosluk_sonu,
count(*) AS eksik_aralik
FROM numarali
GROUP BY sayac_id, grup
ORDER BY sayac_id, bosluk_basi;Beklenen satır: A1, 00:45, 01:15, 2. bosluk_sonu boşluktan sonra gelen ilk okumanın zamanıdır; süre 30 dakika. Sayaç başına boşluk sayısı ve en uzun boşluğun süresi, tek tek eksik satırdan daha işe yarar iki sayıdır.
3. Aynı sayaç, aynı an, iki kayıt
SELECT sayac_id, olcum_zamani,
count(*) AS kayit_sayisi,
count(DISTINCT endeks_kwh) AS farkli_deger
FROM sayac_okuma
GROUP BY sayac_id, olcum_zamani
HAVING count(*) > 1;Örnekte iki satır döner: 01:30 (2 kayıt, 1 değer) ve 02:15 (2 kayıt, 2 değer). Bu ikisi aynı sorun değil:
- Değerler aynıysa (01:30) dosya iki kez yüklenmiştir; biri ayıklanır, hangisi olduğu önemsizdir.
- Değerler farklıysa (02:15) hangisinin doğru olduğunu veriden bilemezsiniz. İkisini de saklayın, o zamanı kaynağa sorulacaklar listesine alın.
Ham tablodan silmeyin; tekilleştirme kuralını bir görünüme yazın:
CREATE VIEW temiz_okuma AS
SELECT sayac_id, olcum_zamani, endeks_kwh, seri_no
FROM (
SELECT o.*,
row_number() OVER (PARTITION BY sayac_id, olcum_zamani, endeks_kwh
ORDER BY kayit_id) AS sira,
min(endeks_kwh) OVER (PARTITION BY sayac_id, olcum_zamani)
<> max(endeks_kwh) OVER (PARTITION BY sayac_id, olcum_zamani) AS celisiyor
FROM sayac_okuma o
) t
WHERE sira = 1 AND NOT celisiyor;Görünüm birebir aynı satırlardan ilkini tutar, çelişen zamanları dışarıda bırakır: örnekte 11 satırdan 8'i kalır. row_number() ve lag() için PostgreSQL pencere işlevleri belgesine bakın.
4. Endeks geri gittiyse, ya da hiç kıpırdamadıysa
Kümülatif endeks azalmamalıdır. lag() bir önceki okumayı yanına getirir:
SELECT sayac_id, onceki_zaman, olcum_zamani, onceki_endeks, endeks_kwh,
seri_no IS DISTINCT FROM onceki_seri AS sayac_degisti
FROM (
SELECT t.*,
lag(olcum_zamani) OVER w AS onceki_zaman,
lag(endeks_kwh) OVER w AS onceki_endeks,
lag(seri_no) OVER w AS onceki_seri
FROM temiz_okuma t
WINDOW w AS (PARTITION BY sayac_id ORDER BY olcum_zamani)
) x
WHERE endeks_kwh < onceki_endeks;Örnekte tek satır döner: 01:45 → 02:00, 102.900 → 102.700, sayac_degisti yanlış (false). Karar buna göre verilir:
| Gözlem | Veriden ne anlaşılır | Yapılacak iş |
|---|---|---|
| Endeks düştü, seri no değişti | Sayaç değişmiş olabilir | Değişim kaydı var mı bak; yoksa saha kaydını iste |
| Endeks düştü, seri no aynı | Hatalı okuma ya da düzeltme | Kaydı hatalı say, düzeltme isteği aç |
| Endeks değişmedi, satırlar geliyor | Sıfır tüketim ya da değer üretmeyen sayaç; veriden ayrılmaz | Komşu sayaçlara bak; hepsi artıyorsa işaretle |
| Okuma satırı hiç yok | İletişim kesintisi olabilir (varsayım, saha doğrulaması gerekir) | 1. ve 2. sorgudaki boşluk kaydı |
Kıpırdamayan endeks, aynı değerin art arda sürdüğü okuma dizisidir. Eşik 8 okuma (15 dakikalık veride 2 saat) örnektir:
SELECT sayac_id, endeks_kwh,
min(olcum_zamani) AS baslangic, max(olcum_zamani) AS bitis, count(*) AS okuma_sayisi
FROM (
SELECT sayac_id, olcum_zamani, endeks_kwh,
row_number() OVER (PARTITION BY sayac_id ORDER BY olcum_zamani)
- row_number() OVER (PARTITION BY sayac_id, endeks_kwh ORDER BY olcum_zamani) AS grup
FROM temiz_okuma
) t
GROUP BY sayac_id, endeks_kwh, grup
HAVING count(*) >= 8;Örnek veride dizi yok, sorgu boş döner.
5. Üç saatlik kayma: gün sınırı
Gün toplamında kaymanın kaynağı, saatin hangi dilimde yazıldığının bilinmemesidir. timestamp (dilimsiz) sütununa +03 ile yazılmış bir değer girerseniz PostgreSQL ofseti sessizce atar; timestamptz ise ofseti kullanıp değeri UTC olarak saklar (PostgreSQL tarih/saat belgesi). Fark iki satırla görülür:
SELECT timestamp '2026-03-01 00:30:00+03'; -- 2026-03-01 00:30:00 (+03 atıldı)
SELECT timestamptz '2026-03-01 00:30:00+03' AT TIME ZONE 'UTC'; -- 2026-02-28 21:30:00Türkiye'de yaz saati uygulamasını düzenleyen 2016/9154 sayılı karar, 8 Eylül 2016 tarihli Resmî Gazete'de yayımlandı; metni Resmî Gazete'nin PDF dosyasında. Yerel saatin UTC'den farkı üç saattir; bunu aşağıdaki saat dilimi denemesiyle kendi sunucunuzda doğrulayın. UTC'ye göre gün alan bir rapor, yerel 00:00–02:59 arasındaki okumaları bir önceki güne yazar. 15 dakikalık tam bir günde 96 okumanın 12'si (yüzde 12,5) yanlış güne düşer.
Gün sınırını denetleyen sorgu, her yerel gün için iki hesabı yan yana koyar:
SELECT sayac_id,
(olcum_zamani AT TIME ZONE 'Europe/Istanbul')::date AS yerel_gun,
(olcum_zamani AT TIME ZONE 'UTC')::date AS utc_gunu,
count(*) AS okuma
FROM temiz_okuma
GROUP BY 1, 2, 3
ORDER BY 1, 2, 3;Tam bir günde yerel gün 96 okuma gösterir, UTC günü ise 84 ve 12 diye ikiye bölünür. Rapor sorgularında günü hep AT TIME ZONE 'Europe/Istanbul' ile alın; date_trunc('day', olcum_zamani) oturum diliminin keyfine kalmasın.
Üç saatlik farkı sunucuda bir kez deneyin: SELECT timestamptz '2026-01-15 12:00+00' AT TIME ZONE 'Europe/Istanbul'; ocak ayında 15:00 dönmeli. 14:00 dönüyorsa sunucunun saat dilimi verisi eskidir ve güncellenmelidir.
Dilimsiz timestamp ile gelen eski veride dilimi tahmin etmeyin: yerel günün ilk okuması dosyada 21:00 görünüyorsa kaynak büyük ihtimalle UTC yazmıştır. Bu ipucudur, kanıt değil; kaynak sisteme sorun.
6. Sonuçları ham tabloya dokunmadan saklamak
Her kontrol, sonraki yüklemede kıyaslanacak bir sayı bırakmalı:
CREATE TABLE okuma_kontrol_sonuc (
kontrol_zamani timestamptz NOT NULL DEFAULT now(),
kontrol_adi text NOT NULL,
sayac_id text NOT NULL,
baslangic timestamptz,
bitis timestamptz,
adet integer,
detay text
);
INSERT INTO okuma_kontrol_sonuc (kontrol_adi, sayac_id, baslangic, adet, detay)
SELECT 'yinelenen', sayac_id, olcum_zamani, count(*)::int,
CASE WHEN count(DISTINCT endeks_kwh) = 1 THEN 'ayni_deger' ELSE 'celisen_deger' END
FROM sayac_okuma
GROUP BY sayac_id, olcum_zamani
HAVING count(*) > 1;Boşluk (2. sorgu) ve geri giden endeks (4. sorgu) aynı sütunlara INSERT INTO … SELECT ile eklenir; GROUP BY kontrol_adi ile "bu yüklemede kaç boşluk, kaç yineleme, kaç geri giden endeks" sorusu tek tablodan cevaplanır. Okumaların toplandığı ambar için enerji veri ambarı çözümüne, dağıtım şirketlerine yönelik çözümler için enerji çözümleri sayfasına bakabilirsiniz.
Boşluk bulundu, şimdi ne yapılır
Eşikler 15 dakikalık veri için örnektir; hangi boşluğun kabul edileceğini kurumun kendi kuralı belirler. Tablo mevzuat değil, veri kalitesi pratiğidir.
| Boşluk (örnek eşik) | Tekrar (örnek eşik) | Yapılacak iş |
|---|---|---|
| 1–2 aralık (30 dakikaya kadar) | Sayaç başına ayda 3'e kadar | Satırı işaretle, raporda "tahmini" olarak göster |
| 3–8 aralık (2 saate kadar) | Sayaç başına ayda 1'e kadar | Kaynaktan yeniden okuma iste; gelene kadar işaretli kal |
| 8 aralıktan uzun | Sayısı fark etmez | Yeniden okuma iste, gelmezse o gün sayacı kalite raporunda ayrı say |
| Uzunluğu fark etmez | Aynı sayaçta günde 3'ten fazla boşluk | Sayaç listesini saha tarafına ver; iletişim sorunu olabilir (varsayım) |
| Çelişen yinelenen değer | Sayısı fark etmez | İkisini sakla, kaynağa sor; tahmin yazma |



