PostgreSQL Antipatterns: Evaluasi Kondisi dalam SQL

SQL bukan C++, juga bukan JavaScript. Oleh karena itu, evaluasi ekspresi logis berbeda, dan ini sama sekali bukan hal yang sama:

WHERE fncondX() AND fncondY()

= fncondX() && fncondY()

Sambil mengoptimalkan rencana eksekusi kueri PostgreSQL dapat sewenang-wenang "mengatur ulang" kondisi yang setara, jangan hitung salah satunya untuk catatan individual, lihat kondisi indeks yang diterapkan ... Singkatnya, cara termudah adalah dengan berasumsi bahwa Anda tidak bisa mengatur urutan di mana mereka akan (dan apakah mereka akan dihitung sama sekali) sama kondisi.

Oleh karena itu, jika Anda masih ingin mengelola prioritas, Anda perlu melakukannya secara struktural membuat kondisi ini tidak seimbang dengan kondisional ekspresi ΠΈ operator.

PostgreSQL Antipatterns: Evaluasi Kondisi dalam SQL
Data dan bekerja dengan mereka adalah dasarnya dari kompleks VLSI kami, jadi sangat penting bagi kami bahwa operasi pada mereka dilakukan tidak hanya dengan benar, tetapi juga efisien. Mari kita lihat contoh konkret di mana kesalahan dalam evaluasi ekspresi dapat dibuat, dan di mana efisiensinya perlu ditingkatkan.

#0: RTFM

Mulai contoh dari dokumentasi:

Ketika urutan evaluasi penting, dapat diperbaiki dengan konstruk CASE. Misalnya, cara ini untuk menghindari pembagian dengan nol dalam sebuah kalimat WHERE tidak bisa diandalkan:

SELECT ... WHERE x > 0 AND y/x > 1.5;

Opsi aman:

SELECT ... WHERE CASE WHEN x > 0 THEN y/x > 1.5 ELSE false END;

Konstruksi yang digunakan CASE melindungi ekspresi dari pengoptimalan, sehingga hanya boleh digunakan jika diperlukan.

#1: kondisi pemicu

BEGIN
  IF cond(NEW.fld) AND EXISTS(SELECT ...) THEN
    ...
  END IF;
  RETURN NEW;
END;

Segalanya tampak terlihat bagus, tapi... Tidak ada yang menjanjikan investasi itu SELECT tidak akan dieksekusi jika kondisi pertama salah. Perbaiki dengan bersarang IF:

BEGIN
  IF cond(NEW.fld) THEN
    IF EXISTS(SELECT ...) THEN
      ...
    END IF;
  END IF;
  RETURN NEW;
END;

Sekarang mari kita perhatikan baik-baik - seluruh badan fungsi pemicu ternyata "terbungkus". IF. Dan ini berarti tidak ada yang menghalangi kami untuk menghapus kondisi ini dari penggunaan prosedur WHEN-kondisi:

BEGIN
  IF EXISTS(SELECT ...) THEN
    ...
  END IF;
  RETURN NEW;
END;
...
CREATE TRIGGER ...
  WHEN cond(NEW.fld);

Pendekatan ini memungkinkan Anda menghemat sumber daya server dengan jaminan jika kondisinya salah.

#2: ATAU/DAN rantai

SELECT ... WHERE EXISTS(... A) OR EXISTS(... B)

Jika tidak, dapat diperoleh keduanya EXISTS akan benar, tapi keduanya akan dieksekusi.

Tetapi jika kita tahu pasti bahwa salah satunya "benar" lebih sering (atau "salah" - untuk AND-chains) - apakah mungkin untuk "meningkatkan prioritasnya" sehingga yang kedua tidak dieksekusi sekali lagi?

Ternyata itu mungkin - pendekatan algoritme dekat dengan topik artikel PostgreSQL Antipatterns: Entri langka mencapai tengah GABUNG.

Mari kita "mendorong berdasarkan KASUS" kedua kondisi ini:

SELECT ...
WHERE
  CASE
    WHEN EXISTS(... A) THEN TRUE
    WHEN EXISTS(... B) THEN TRUE
  END

Dalam hal ini, kami tidak mendefinisikan ELSE-nilai, yaitu, jika kedua kondisi salah CASE akan kembali NULL, yang diartikan sebagai FALSE Π² WHERE- kondisi.

Contoh ini dapat digabungkan dengan cara lain - sesuai selera dan warna:

SELECT ...
WHERE
  CASE
    WHEN NOT EXISTS(... A) THEN EXISTS(... B)
    ELSE TRUE
  END

#3: bagaimana [tidak] menulis kondisi

Kami menghabiskan dua hari untuk menganalisis alasan pemicu "aneh" dari pemicu ini - mari kita lihat alasannya.

Sumber:

IF( NEW."Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚_" is null or NEW."Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚_" = (select '"ΠšΠΎΠΌΠΏΠ»Π΅ΠΊΡ‚"'::regclass::oid) or NEW."Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚_" = (select to_regclass('"Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚ΠŸΠΎΠ—Π°Ρ€ΠΏΠ»Π°Ρ‚Π΅"')::oid)
     AND (   OLD."Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚ΠΠ°ΡˆΠ°ΠžΡ€Π³Π°Π½ΠΈΠ·Π°Ρ†ΠΈΡ" <> NEW."Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚ΠΠ°ΡˆΠ°ΠžΡ€Π³Π°Π½ΠΈΠ·Π°Ρ†ΠΈΡ"
          OR OLD."Π£Π΄Π°Π»Π΅Π½" <> NEW."Π£Π΄Π°Π»Π΅Π½"
          OR OLD."Π”Π°Ρ‚Π°" <> NEW."Π”Π°Ρ‚Π°"
          OR OLD."ВрСмя" <> NEW."ВрСмя"
          OR OLD."Π›ΠΈΡ†ΠΎΠ‘ΠΎΠ·Π΄Π°Π»" <> NEW."Π›ΠΈΡ†ΠΎΠ‘ΠΎΠ·Π΄Π°Π»" ) ) THEN ...

Masalah #1: Ketimpangan tidak memperhitungkan NULL

Mari kita asumsikan semuanya OLD-bidang penting NULL. Apa yang akan terjadi?

SELECT NULL <> 1 OR NULL <> 2;
-- NULL

Dan dari sudut pandang mengerjakan kondisi NULL setara FALSE, seperti yang disebutkan di atas.

keputusan: menggunakan operator IS DISTINCT FROM dari ROW-operator, membandingkan seluruh catatan sekaligus:

SELECT (NULL, NULL) IS DISTINCT FROM (1, 2);
-- TRUE

Masalah nomor 2: implementasi berbeda dari fungsi yang sama

Bandingkan:

NEW."Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚_" = (select '"ΠšΠΎΠΌΠΏΠ»Π΅ΠΊΡ‚"'::regclass::oid)
NEW."Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚_" = (select to_regclass('"Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚ΠŸΠΎΠ—Π°Ρ€ΠΏΠ»Π°Ρ‚Π΅"')::oid)

Mengapa ada investasi tambahan SELECT? Sebuah fungsi to_regclass? Kenapa berbeda...

Mari kita perbaiki:

NEW."Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚_" = '"ΠšΠΎΠΌΠΏΠ»Π΅ΠΊΡ‚"'::regclass::oid
NEW."Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚_" = '"Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚ΠŸΠΎΠ—Π°Ρ€ΠΏΠ»Π°Ρ‚Π΅"'::regclass::oid

Masalah #3: bool diutamakan

Mari format sumbernya:

{... IS NULL} OR
{... ΠšΠΎΠΌΠΏΠ»Π΅ΠΊΡ‚} OR
{... Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚ΠŸΠΎΠ—Π°Ρ€ΠΏΠ»Π°Ρ‚Π΅} AND
( {... нСравСнства} )

Ups ... Nyatanya, ternyata dalam kasus kebenaran salah satu dari dua syarat pertama, seluruh syarat berubah menjadi TRUE, dengan mengabaikan ketidaksetaraan. Dan ini sama sekali bukan yang kami inginkan.

Mari kita perbaiki:

(
  {... IS NULL} OR
  {... ΠšΠΎΠΌΠΏΠ»Π΅ΠΊΡ‚} OR
  {... Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚ΠŸΠΎΠ—Π°Ρ€ΠΏΠ»Π°Ρ‚Π΅}
) AND
( {... нСравСнства} )

Masalah #4 (kecil): kondisi OR kompleks untuk satu bidang

Sebenarnya kami bermasalah di No. 3 justru karena ada tiga syarat. Tapi alih-alih mereka, Anda bisa bertahan dengan satu, menggunakan mekanismenya coalesce ... IN:

coalesce(NEW."Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚_"::text, '') IN ('', '"ΠšΠΎΠΌΠΏΠ»Π΅ΠΊΡ‚"', '"Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚ΠŸΠΎΠ—Π°Ρ€ΠΏΠ»Π°Ρ‚Π΅"')

Kita juga NULL "menangkap", dan kompleks OR Anda tidak perlu repot dengan tanda kurung.

Total

Mari kita perbaiki apa yang kita punya:

IF (
  coalesce(NEW."Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚_"::text, '') IN ('', '"ΠšΠΎΠΌΠΏΠ»Π΅ΠΊΡ‚"', '"Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚ΠŸΠΎΠ—Π°Ρ€ΠΏΠ»Π°Ρ‚Π΅"') AND
  (
    OLD."Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚ΠΠ°ΡˆΠ°ΠžΡ€Π³Π°Π½ΠΈΠ·Π°Ρ†ΠΈΡ"
  , OLD."Π£Π΄Π°Π»Π΅Π½"
  , OLD."Π”Π°Ρ‚Π°"
  , OLD."ВрСмя"
  , OLD."Π›ΠΈΡ†ΠΎΠ‘ΠΎΠ·Π΄Π°Π»"
  ) IS DISTINCT FROM (
    NEW."Π”ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚ΠΠ°ΡˆΠ°ΠžΡ€Π³Π°Π½ΠΈΠ·Π°Ρ†ΠΈΡ"
  , NEW."Π£Π΄Π°Π»Π΅Π½"
  , NEW."Π”Π°Ρ‚Π°"
  , NEW."ВрСмя"
  , NEW."Π›ΠΈΡ†ΠΎΠ‘ΠΎΠ·Π΄Π°Π»"
  )
) THEN ...

Dan mengingat bahwa fungsi pemicu ini hanya dapat digunakan di UPDATEpemicu karena keberadaannya OLD/NEW dalam kondisi tingkat atas, maka kondisi ini umumnya dapat dibawa keluar WHEN-kondisi seperti di no 1...

Sumber: www.habr.com

Tambah komentar