PostgreSQL Antipatterns: Dictionary Hit Heavy JOIN

Kami melanjutkan rangkaian artikel yang ditujukan untuk mempelajari cara-cara yang kurang diketahui untuk meningkatkan kinerja kueri PostgreSQL yang “tampaknya sederhana”:

Jangan berpikir bahwa saya tidak terlalu suka BERGABUNG... :)

Namun seringkali tanpanya, permintaan tersebut ternyata jauh lebih produktif dibandingkan dengan permintaan tersebut. Jadi hari ini kami akan mencoba singkirkan GABUNG yang intensif sumber daya - menggunakan kamus.

PostgreSQL Antipatterns: Dictionary Hit Heavy JOIN

Dimulai dengan PostgreSQL 12, beberapa situasi yang dijelaskan di bawah ini mungkin direproduksi sedikit berbeda karena CTE non-materialisasi default. Perilaku ini dapat dikembalikan dengan menentukan kuncinya MATERIALIZED.

Banyak “fakta” ​​dalam kosakata yang terbatas

Mari kita ambil tugas aplikasi yang sangat nyata - kita perlu menampilkan daftar pesan masuk atau tugas aktif dengan pengirim:

25.01 | Иванов И.И. | Подготовить описание нового алгоритма.
22.01 | Иванов И.И. | Написать статью на Хабр: жизнь без JOIN.
20.01 | Петров П.П. | Помочь оптимизировать запрос.
18.01 | Иванов И.И. | Написать статью на Хабр: JOIN с учетом распределения данных.
16.01 | Петров П.П. | Помочь оптимизировать запрос.

Di dunia abstrak, penulis tugas harus didistribusikan secara merata di antara seluruh karyawan organisasi kita, tetapi dalam kenyataannya tugas biasanya datang dari sejumlah orang yang terbatas - “dari manajemen” ke atas hierarki atau “dari subkontraktor” dari departemen tetangga (analis, desainer, pemasaran, ...).

Mari kita asumsikan bahwa dalam organisasi kita yang beranggotakan 1000 orang, hanya 20 penulis (biasanya bahkan lebih sedikit) yang menetapkan tugas untuk setiap pelaku tertentu dan Mari kita gunakan pengetahuan mata pelajaran iniuntuk mempercepat kueri "tradisional".

Pembuat skrip

-- сотрудники
CREATE TABLE person AS
SELECT
  id
, repeat(chr(ascii('a') + (id % 26)), (id % 32) + 1) "name"
, '2000-01-01'::date - (random() * 1e4)::integer birth_date
FROM
  generate_series(1, 1000) id;

ALTER TABLE person ADD PRIMARY KEY(id);

-- задачи с указанным распределением
CREATE TABLE task AS
WITH aid AS (
  SELECT
    id
  , array_agg((random() * 999)::integer + 1) aids
  FROM
    generate_series(1, 1000) id
  , generate_series(1, 20)
  GROUP BY
    1
)
SELECT
  *
FROM
  (
    SELECT
      id
    , '2020-01-01'::date - (random() * 1e3)::integer task_date
    , (random() * 999)::integer + 1 owner_id
    FROM
      generate_series(1, 100000) id
  ) T
, LATERAL(
    SELECT
      aids[(random() * (array_length(aids, 1) - 1))::integer + 1] author_id
    FROM
      aid
    WHERE
      id = T.owner_id
    LIMIT 1
  ) a;

ALTER TABLE task ADD PRIMARY KEY(id);
CREATE INDEX ON task(owner_id, task_date);
CREATE INDEX ON task(author_id);

Mari tunjukkan 100 tugas terakhir untuk pelaksana tertentu:

SELECT
  task.*
, person.name
FROM
  task
LEFT JOIN
  person
    ON person.id = task.author_id
WHERE
  owner_id = 777
ORDER BY
  task_date DESC
LIMIT 100;

PostgreSQL Antipatterns: Dictionary Hit Heavy JOIN
[lihat penjelasan.tensor.ru]

Ternyata bahwa 1/3 total waktu dan 3/4 pembacaan halaman data dibuat hanya untuk mencari penulis 100 kali - untuk setiap tugas keluaran. Tapi kita tahu itu di antara ratusan ini hanya 20 yang berbeda - Apakah mungkin menggunakan pengetahuan ini?

kamus toko-h

Mari manfaatkan tipe toko untuk menghasilkan nilai kunci "kamus":

CREATE EXTENSION hstore

Kita hanya perlu memasukkan ID penulis dan namanya ke dalam kamus sehingga kita dapat mengekstraknya menggunakan kunci ini:

-- формируем целевую выборку
WITH T AS (
  SELECT
    *
  FROM
    task
  WHERE
    owner_id = 777
  ORDER BY
    task_date DESC
  LIMIT 100
)
-- формируем словарь для уникальных значений
, dict AS (
  SELECT
    hstore( -- hstore(keys::text[], values::text[])
      array_agg(id)::text[]
    , array_agg(name)::text[]
    )
  FROM
    person
  WHERE
    id = ANY(ARRAY(
      SELECT DISTINCT
        author_id
      FROM
        T
    ))
)
-- получаем связанные значения словаря
SELECT
  *
, (TABLE dict) -> author_id::text -- hstore -> key
FROM
  T;

PostgreSQL Antipatterns: Dictionary Hit Heavy JOIN
[lihat penjelasan.tensor.ru]

Dihabiskan untuk memperoleh informasi tentang orang 2 kali lebih sedikit waktu dan 7 kali lebih sedikit data yang dibaca! Selain “kosa kata”, yang juga membantu kami mencapai hasil tersebut adalah pengambilan catatan massal dari tabel dalam sekali jalan menggunakan = ANY(ARRAY(...)).

Entri Tabel: Serialisasi dan Deserialisasi

Namun bagaimana jika kita perlu menyimpan tidak hanya satu kolom teks, namun seluruh entri dalam kamus? Dalam hal ini, kemampuan PostgreSQL akan membantu kita memperlakukan entri tabel sebagai nilai tunggal:

...
, dict AS (
  SELECT
    hstore(
      array_agg(id)::text[]
    , array_agg(p)::text[] -- магия #1
    )
  FROM
    person p
  WHERE
    ...
)
SELECT
  *
, (((TABLE dict) -> author_id::text)::person).* -- магия #2
FROM
  T;

Mari kita lihat apa yang terjadi di sini:

  1. Kami mengambil p sebagai alias untuk entri tabel orang lengkap dan mengumpulkan serangkaian dari mereka.
  2. ini susunan rekaman disusun kembali ke array string teks (orang[]::teks[]) untuk menempatkannya di kamus hstore sebagai array nilai.
  3. Ketika kami menerima catatan terkait, kami diambil dari kamus dengan kunci sebagai string teks.
  4. Kami membutuhkan teks berubah menjadi nilai tipe tabel orang (untuk setiap tabel, tipe dengan nama yang sama dibuat secara otomatis).
  5. “Perluas” rekaman yang diketik ke dalam kolom menggunakan (...).*.

kamus json

Namun trik seperti yang kami terapkan di atas tidak akan berhasil jika tidak ada tipe tabel yang sesuai untuk melakukan “casting”. Situasi yang persis sama akan muncul, dan jika kita mencoba menggunakannya baris CTE, bukan tabel "nyata"..

Dalam hal ini mereka akan membantu kita fungsi untuk bekerja dengan json:

...
, p AS ( -- это уже CTE
  SELECT
    *
  FROM
    person
  WHERE
    ...
)
, dict AS (
  SELECT
    json_object( -- теперь это уже json
      array_agg(id)::text[]
    , array_agg(row_to_json(p))::text[] -- и внутри json для каждой строки
    )
  FROM
    p
)
SELECT
  *
FROM
  T
, LATERAL(
    SELECT
      *
    FROM
      json_to_record(
        ((TABLE dict) ->> author_id::text)::json -- извлекли из словаря как json
      ) AS j(name text, birth_date date) -- заполнили нужную нам структуру
  ) j;

Perlu dicatat bahwa ketika menjelaskan struktur target, kita tidak dapat membuat daftar semua bidang dari string sumber, tetapi hanya bidang yang benar-benar kita perlukan. Jika kita memiliki tabel “asli”, lebih baik menggunakan fungsinya json_populate_record.

Kami masih mengakses kamus sekali, tapi biaya serialisasi json-[de]cukup tinggi, oleh karena itu, masuk akal untuk menggunakan metode ini hanya dalam beberapa kasus ketika CTE Scan yang “jujur” menunjukkan hasil yang lebih buruk.

Menguji kinerja

Jadi, kami punya dua cara untuk membuat serial data ke dalam kamus - hstore/json_object. Selain itu, susunan kunci dan nilai itu sendiri juga dapat dihasilkan dengan dua cara, dengan konversi internal atau eksternal ke teks: array_agg(i::teks) / array_agg(i)::teks[].

Mari kita periksa efektivitas berbagai jenis serialisasi menggunakan contoh sintetik murni - membuat serial sejumlah kunci yang berbeda:

WITH dict AS (
  SELECT
    hstore(
      array_agg(i::text)
    , array_agg(i::text)
    )
  FROM
    generate_series(1, ...) i
)
TABLE dict;

Skrip evaluasi: serialisasi

WITH T AS (
  SELECT
    *
  , (
      SELECT
        regexp_replace(ea[array_length(ea, 1)], '^Execution Time: (d+.d+) ms$', '1')::real et
      FROM
        (
          SELECT
            array_agg(el) ea
          FROM
            dblink('port= ' || current_setting('port') || ' dbname=' || current_database(), $$
              explain analyze
              WITH dict AS (
                SELECT
                  hstore(
                    array_agg(i::text)
                  , array_agg(i::text)
                  )
                FROM
                  generate_series(1, $$ || (1 << v) || $$) i
              )
              TABLE dict
            $$) T(el text)
        ) T
    ) et
  FROM
    generate_series(0, 19) v
  ,   LATERAL generate_series(1, 7) i
  ORDER BY
    1, 2
)
SELECT
  v
, avg(et)::numeric(32,3)
FROM
  T
GROUP BY
  1
ORDER BY
  1;

PostgreSQL Antipatterns: Dictionary Hit Heavy JOIN

Pada PostgreSQL 11, hingga kira-kira ukuran kamus sebanyak 2^12 kunci serialisasi ke json membutuhkan waktu lebih sedikit. Dalam hal ini, yang paling efektif adalah kombinasi konversi tipe json_object dan “internal”. array_agg(i::text).

Sekarang mari kita coba membaca nilai setiap kunci sebanyak 8 kali - lagipula, jika Anda tidak mengakses kamus, mengapa diperlukan?

Skrip evaluasi: membaca dari kamus

WITH T AS (
  SELECT
    *
  , (
      SELECT
        regexp_replace(ea[array_length(ea, 1)], '^Execution Time: (d+.d+) ms$', '1')::real et
      FROM
        (
          SELECT
            array_agg(el) ea
          FROM
            dblink('port= ' || current_setting('port') || ' dbname=' || current_database(), $$
              explain analyze
              WITH dict AS (
                SELECT
                  json_object(
                    array_agg(i::text)
                  , array_agg(i::text)
                  )
                FROM
                  generate_series(1, $$ || (1 << v) || $$) i
              )
              SELECT
                (TABLE dict) -> (i % ($$ || (1 << v) || $$) + 1)::text
              FROM
                generate_series(1, $$ || (1 << (v + 3)) || $$) i
            $$) T(el text)
        ) T
    ) et
  FROM
    generate_series(0, 19) v
  , LATERAL generate_series(1, 7) i
  ORDER BY
    1, 2
)
SELECT
  v
, avg(et)::numeric(32,3)
FROM
  T
GROUP BY
  1
ORDER BY
  1;

PostgreSQL Antipatterns: Dictionary Hit Heavy JOIN

Dan... sudah kira-kira dengan kunci 2^6, membaca dari kamus json mulai hilang beberapa kali membaca dari hstore, untuk jsonb hal yang sama terjadi di 2^9.

Kesimpulan akhir:

  • jika Anda perlu melakukannya GABUNG dengan beberapa catatan berulang — lebih baik menggunakan “kamus” tabel
  • jika kamus Anda diharapkan kecil dan Anda tidak akan membaca banyak darinya - Anda dapat menggunakan json[b]
  • dalam semua kasus lainnya hstore + array_agg(i::teks) akan lebih efektif

Sumber: www.habr.com

Tambah komentar