BIAnalytics yükleniyor

Sayaç verisinde eksik ve yinelenen okumayı bulma

Eksik, yinelenen ve geri giden sayaç okumasını PostgreSQL'de altı sorguyla bulun; Türkiye saatindeki üç saatlik gün kaymasını da denetleyin.

BIAnalytics BIAnalytics

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özlemVeriden ne anlaşılırYapılacak iş
Endeks düştü, seri no değiştiSayaç değişmiş olabilirDeğişim kaydı var mı bak; yoksa saha kaydını iste
Endeks düştü, seri no aynıHatalı okuma ya da düzeltmeKaydı hatalı say, düzeltme isteği aç
Endeks değişmedi, satırlar geliyorSıfır tüketim ya da değer üretmeyen sayaç; veriden ayrılmazKomş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:00

Tü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 kadarSatırı işaretle, raporda "tahmini" olarak göster
3–8 aralık (2 saate kadar)Sayaç başına ayda 1'e kadarKaynaktan yeniden okuma iste; gelene kadar işaretli kal
8 aralıktan uzunSayısı fark etmezYeniden okuma iste, gelmezse o gün sayacı kalite raporunda ayrı say
Uzunluğu fark etmezAynı sayaçta günde 3'ten fazla boşlukSayaç listesini saha tarafına ver; iletişim sorunu olabilir (varsayım)
Çelişen yinelenen değerSayısı fark etmezİkisini sakla, kaynağa sor; tahmin yazma
Bu yazıyı paylaşın LinkedIn'de paylaş

Sıradaki yazılar

Burada anlatılana benzer bir işiniz mi var?

Yazıların arkasındaki ekibe doğrudan yazabilirsiniz.

Bizimle iletişime geçin

Demo Talebi

Çözümlerimizi yakından tanımak için formu doldurun.

person
mail
phone
notes