Apa overhead dari memperbarui semua kolom, bahkan yang belum berubah [ditutup]

17

Ketika datang untuk memperbarui baris, banyak alat ORM mengeluarkan pernyataan UPDATE yang mengatur setiap kolom yang terkait dengan entitas tertentu .

Keuntungannya adalah Anda dapat dengan mudah mengelompokkan pernyataan pembaruan karena UPDATEpernyataan itu sama, apa pun atribut entitas yang Anda ubah. Selain itu, Anda bahkan dapat menggunakan caching pernyataan sisi-server dan sisi klien.

Jadi, jika saya memuat entitas dan hanya menetapkan satu properti:

Post post = entityManager.find(Post.class, 1L);
post.setScore(12);

Semua kolom akan diubah:

UPDATE post
SET    score = 12,
       title = 'High-Performance Java Persistence'
WHERE  id = 1

Sekarang, dengan asumsi bahwa kita memiliki indeks pada titleproperti juga, bukankah seharusnya DB menyadari bahwa nilainya tidak berubah?

Dalam artikel ini , Markus Winand mengatakan:

Pembaruan pada semua kolom menunjukkan pola yang sama yang telah kita amati di bagian sebelumnya: waktu respons tumbuh dengan setiap indeks tambahan.

Saya bertanya-tanya mengapa overhead ini karena database memuat halaman data terkait dari disk ke memori dan sehingga dapat mengetahui apakah nilai kolom perlu diubah atau tidak.

Bahkan untuk indeks, itu tidak untuk menyeimbangkan kembali apa pun karena nilai indeks tidak berubah untuk kolom yang belum berubah, namun mereka dimasukkan dalam UPDATE.

Apakah itu karena indeks B + Tree yang terkait dengan kolom tidak berubah yang mubazir perlu dinavigasi juga, hanya agar database menyadari bahwa nilai daunnya masih sama?

Tentu saja, beberapa alat ORM memungkinkan Anda untuk UPDATE hanya properti yang diubah:

UPDATE post
SET    score = 12,
WHERE  id = 1

Tetapi jenis UPDATE ini mungkin tidak selalu mendapat manfaat dari pembaruan batch atau caching pernyataan ketika properti yang berbeda diubah untuk baris yang berbeda.

Vlad Mihalcea
sumber
1
Jika database yang PostgreSQL (atau beberapa orang lain bahwa penggunaan MVCC ), seorang UPDATEpraktis setara dengan DELETE+ INSERT(karena Anda benar-benar membuat yang baru V ersion baris). Overheadnya tinggi, dan tumbuh dengan jumlah indeks , khususnya jika banyak kolom yang menyusunnya benar-benar diperbarui, dan pohon (atau apa pun) yang digunakan untuk mewakili indeks memerlukan perubahan yang signifikan. Itu bukan jumlah kolom yang diperbarui yang relevan, tetapi apakah Anda memperbarui bagian kolom dari indeks.
joanolo
@joanolo Ini hanya perlu benar untuk implementasi postgres MVCC. MySQL, Oracle (dan lainnya) melakukan pembaruan di tempat dan memindahkan kolom yang diubah ke ruang UNDO.
Morgan Tocker
2
Saya harus menunjukkan bahwa ORM yang baik harus melacak kolom mana yang perlu diperbarui, dan mengoptimalkan pernyataan yang dikirim ke database. Ini relevan, jika hanya untuk jumlah data yang dikirim ke DB, khususnya jika beberapa kolom adalah teks panjang atau gumpalan .
joanolo
1
Pertanyaan membahas hal ini untuk SQL Server dba.stackexchange.com/q/114360/3690
Martin Smith
2
DBMS mana yang Anda gunakan?
a_horse_with_no_name

Jawaban:

12

Saya tahu Anda sebagian besar mengkhawatirkan UPDATEdan sebagian besar tentang kinerja, tetapi sebagai sesama pengelola "ORM", izinkan saya memberi Anda perspektif lain tentang masalah membedakan antara nilai "berubah" , "nol" , dan "default" , yang merupakan tiga hal berbeda dalam SQL, tetapi mungkin hanya satu hal di Java dan di sebagian besar ORM:

Menerjemahkan alasan Anda ke INSERTpernyataan

Argumen Anda yang mendukung batchability dan pernyataan cacheability berlaku dengan cara yang sama untuk INSERTpernyataan seperti yang mereka lakukan untuk UPDATEpernyataan. Tetapi dalam kasus INSERTpernyataan, menghilangkan kolom dari pernyataan memiliki semantik yang berbeda dari pada UPDATE. Itu artinya melamar DEFAULT. Dua berikut ini setara secara semantik:

INSERT INTO t (a, b)    VALUES (1, 2);
INSERT INTO t (a, b, c) VALUES (1, 2, DEFAULT);

Ini tidak benar untuk UPDATE, di mana dua yang pertama secara semantik setara, dan yang ketiga memiliki makna yang sama sekali berbeda:

-- These are the same
UPDATE t SET a = 1, b = 2;
UPDATE t SET a = 1, b = 2, c = c;

-- This is different!
UPDATE t SET a = 1, b = 2, c = DEFAULT;

Sebagian besar API klien basis data, termasuk JDBC, dan akibatnya, JPA, tidak memungkinkan untuk mengikat DEFAULTekspresi ke variabel terikat - sebagian besar karena server juga tidak mengizinkan ini. Jika Anda ingin menggunakan kembali pernyataan SQL yang sama untuk alasan batchability dan cacheability pernyataan tersebut di atas, Anda akan menggunakan pernyataan berikut dalam kedua kasus (dengan asumsi (a, b, c)semua kolom di t):

INSERT INTO t (a, b, c) VALUES (?, ?, ?);

Dan karena ctidak disetel, Anda mungkin akan mengikat Java nullke variabel mengikat ketiga, karena banyak ORM juga tidak dapat membedakan antara NULLdan DEFAULT( jOOQ , misalnya menjadi pengecualian di sini). Mereka hanya melihat Java nulldan tidak tahu apakah ini artinyaNULL (seperti dalam nilai yang tidak diketahui) atau DEFAULT(seperti dalam nilai yang tidak diinisialisasi).

Dalam banyak kasus, perbedaan ini tidak masalah, tetapi dalam kasus kolom Anda c menggunakan salah satu dari fitur berikut, pernyataan itu salah :

  • Ada DEFAULTklausa
  • Mungkin dihasilkan oleh pemicu

Kembali ke UPDATEpernyataan

Sementara hal di atas berlaku untuk semua database, saya dapat meyakinkan Anda bahwa masalah pemicunya juga berlaku untuk database Oracle. Pertimbangkan SQL berikut:

CREATE TABLE x (a INT PRIMARY KEY, b INT, c INT, d INT);

INSERT INTO x VALUES (1, 1, 1, 1);

CREATE OR REPLACE TRIGGER t
  BEFORE UPDATE OF c, d
  ON x
BEGIN
  IF updating('c') THEN
    dbms_output.put_line('Updating c');
  END IF;
  IF updating('d') THEN
    dbms_output.put_line('Updating d');
  END IF;
END;
/

SET SERVEROUTPUT ON
UPDATE x SET b = 1 WHERE a = 1;
UPDATE x SET c = 1 WHERE a = 1;
UPDATE x SET d = 1 WHERE a = 1;
UPDATE x SET b = 1, c = 1, d = 1 WHERE a = 1;

Ketika Anda menjalankan di atas, Anda akan melihat output berikut:

table X created.
1 rows inserted.
TRIGGER T compiled
1 rows updated.
1 rows updated.
Updating c

1 rows updated.
Updating d

1 rows updated.
Updating c
Updating d

Seperti yang Anda lihat, pernyataan yang selalu memperbarui semua kolom akan selalu memicu pemicu untuk semua kolom, sedangkan pernyataan yang memperbarui hanya kolom yang berubah akan memecat hanya pemicu yang mendengarkan perubahan spesifik tersebut.

Dengan kata lain:

Perilaku Hibernate saat ini yang Anda gambarkan tidak lengkap dan bahkan dapat dianggap salah di hadapan pemicu (dan mungkin alat lain).

Saya pribadi berpikir bahwa argumen optimisasi cache kueri Anda dinilai terlalu tinggi dalam kasus SQL dinamis. Tentu, akan ada beberapa pertanyaan lagi di cache seperti itu, dan sedikit pekerjaan parsing yang harus dilakukan, tetapi ini biasanya bukan masalah untuk UPDATEpernyataan dinamis , apalagi untuk SELECT.

Batching tentu saja merupakan masalah, tetapi menurut pendapat saya, satu pembaruan tidak seharusnya dinormalisasi untuk memperbarui semua kolom hanya karena ada sedikit kemungkinan pernyataan tersebut dapat dikelompokkan. Kemungkinannya adalah, ORM dapat mengumpulkan sub-batch dari pernyataan identik yang berurutan dan mengelompokkannya sebagai ganti dari "seluruh batch" (jika ORM bahkan mampu melacak perbedaan antara "berubah" , "null" , dan "default"

Lukas Eder
sumber
Kasus DEFAULTpenggunaan dapat diatasi oleh @DynamicInsert. Situasi TRIGGER juga dapat diatasi menggunakan cek suka WHEN (NEW.b <> OLD.b)atau hanya beralih ke @DynamicUpdate.
Vlad Mihalcea
Ya, beberapa hal dapat diatasi, tetapi Anda awalnya bertanya tentang kinerja dan solusi Anda menambah biaya tambahan.
Lukas Eder
Saya pikir Morgan mengatakan yang terbaik: itu rumit .
Vlad Mihalcea
Saya pikir itu agak sederhana. Dari perspektif kerangka kerja, ada lebih banyak argumen yang mendukung default ke SQL dinamis. Dari perspektif pengguna, ya, ini rumit.
Lukas Eder
9

Saya kira jawabannya - rumit . Saya mencoba menulis bukti cepat menggunakan longtextkolom di MySQL, tetapi jawabannya sedikit tidak meyakinkan. Bukti pertama:

# in advance:
set global max_allowed_packet=1024*1024*1024;

CREATE TABLE `t2` (
  `a` int(11) NOT NULL AUTO_INCREMENT,
  `b` char(255) NOT NULL,
  `c` LONGTEXT,
  PRIMARY KEY (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

mysql> insert into t2 (a, b, c) values (null, 'b', REPEAT('c', 1024*1024*1024));
Query OK, 1 row affected (38.81 sec)

mysql> UPDATE t2 SET b='new'; # fast
Query OK, 1 row affected (6.73 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql>  UPDATE t2 SET b='new'; # fast
Query OK, 0 rows affected (2.87 sec)
Rows matched: 1  Changed: 0  Warnings: 0

mysql> UPDATE t2 SET b='new'; # fast
Query OK, 0 rows affected (2.61 sec)
Rows matched: 1  Changed: 0  Warnings: 0

mysql> UPDATE t2 SET c= REPEAT('d', 1024*1024*1024); # slow (changed value)
Query OK, 1 row affected (22.38 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> UPDATE t2 SET c= REPEAT('d', 1024*1024*1024); # still slow (no change)
Query OK, 0 rows affected (14.06 sec)
Rows matched: 1  Changed: 0  Warnings: 0

Jadi ada perbedaan waktu yang kecil antara nilai lambat + yang diubah, dan lambat + tidak ada nilai yang berubah. Jadi saya memutuskan untuk melihat metrik lain, yaitu halaman yang ditulis:

mysql> show global status like 'innodb_pages_written';
+----------------------+--------+
| Variable_name        | Value  |
+----------------------+--------+
| Innodb_pages_written | 198656 |
+----------------------+--------+
1 row in set (0.00 sec)

mysql> show global status like 'innodb_pages_written';
+----------------------+--------+
| Variable_name        | Value  |
+----------------------+--------+
| Innodb_pages_written | 198775 | <-- 119 pages changed in a "no change"
+----------------------+--------+
1 row in set (0.01 sec)

mysql> show global status like 'innodb_pages_written';
+----------------------+--------+
| Variable_name        | Value  |
+----------------------+--------+
| Innodb_pages_written | 322494 | <-- 123719 pages changed in a "change"!
+----------------------+--------+
1 row in set (0.00 sec)

Jadi sepertinya waktu meningkat karena harus ada perbandingan untuk mengkonfirmasi bahwa nilai itu sendiri belum dimodifikasi, yang dalam kasus longtext 1G membutuhkan waktu (karena terbagi di banyak halaman). Tetapi modifikasi itu sendiri tampaknya tidak berputar melalui redo log.

Saya menduga bahwa jika nilai adalah kolom reguler yang ada di halaman perbandingan hanya menambahkan sedikit overhead. Dan dengan asumsi optimasi yang sama berlaku, ini adalah no-ops ketika datang ke pembaruan.

Jawaban yang lebih panjang

Saya benar-benar berpikir ORM seharusnya tidak menghilangkan kolom yang telah dimodifikasi ( tetapi tidak diubah ), karena optimasi ini memiliki efek samping yang aneh.

Pertimbangkan hal berikut dalam kode pseudo:

# Initial Data does not make sense
# should be either "Harvey Dent" or "Two Face"

id: 1, firstname: "Two Face", lastname: "Dent"

session1.start
session2.start

session1.firstname = "Two"
session1.lastname = "Face"
session1.save

session2.firstname = "Harvey"
session2.lastname = "Dent"
session2.save

Hasilnya jika ORM ingin "Optimalkan keluar" modifikasi tanpa perubahan:

id: 1, firstname: "Harvey", lastname: "Face"

Hasilnya jika ORM mengirim semua modifikasi ke server:

id: 1, firstname: "Harvey", lastname: "Dent"

Kasing uji coba di sini bergantung pada repeatable-readisolasi (default MySQL), tetapi ada time-window untuk read-committedisolasi di mana session2 membaca terjadi sebelum session1 komit.

Dengan kata lain: optimasi hanya aman jika Anda mengeluarkan SELECT .. FOR UPDATEuntuk membaca baris diikuti oleh UPDATE. SELECT .. FOR UPDATEtidak menggunakan MVCC dan selalu membaca versi baris terbaru.


Sunting: Memastikan set data kasus uji memiliki memori 100%. Hasil pengaturan waktu yang disesuaikan.

Morgan Tocker
sumber
Terima kasih untuk penjelasannya. Itu juga intuisi saya. Saya pikir DB akan memeriksa baris di halaman data dan semua indeks yang terkait. Jika kolomnya sangat besar atau ada banyak indeks yang terlibat, overhead bisa menjadi nyata. Tetapi untuk sebagian besar situasi, ketika menggunakan jenis kolom kompak dan hanya indeks sebanyak yang diperlukan, saya kira overhead mungkin kurang dari tidak mendapat manfaat dari caching pernyataan atau memiliki peluang yang lebih rendah untuk menyusun pernyataan.
Vlad Mihalcea
1
@VladMihalcea berhati-hatilah bahwa jawabannya adalah tentang MySQL. Kesimpulannya mungkin tidak sama di DBMS berbeda.
ypercubeᵀᴹ
@ ypercube Saya tahu itu. Itu semua tergantung pada RDBMS.
Vlad Mihalcea