ค่าใช้จ่ายในการอัปเดตคอลัมน์ทั้งหมดคืออะไรแม้แต่คนที่ไม่ได้เปลี่ยนแปลง [ปิด]


17

เมื่อพูดถึงการอัพเดตแถวเครื่องมือ ORM จำนวนมากออกคำสั่ง UPDATE ที่ตั้งค่าทุกคอลัมน์ที่เกี่ยวข้องกับเอนทิตีนั้น

ข้อดีคือคุณสามารถแบทช์ข้อความสั่งการอัพเดทได้อย่างง่ายดายเนื่องจากUPDATEข้อความนั้นเหมือนกันไม่ว่าคุณจะเปลี่ยนเอนทิตีแอตทริบิวต์ใด ยิ่งไปกว่านั้นคุณยังสามารถใช้การแคชคำสั่งฝั่งเซิร์ฟเวอร์และไคลเอนต์ได้เช่นกัน

ดังนั้นถ้าฉันโหลดเอนทิตีและตั้งค่าคุณสมบัติเดียวเท่านั้น:

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

คอลัมน์ทั้งหมดจะมีการเปลี่ยนแปลง:

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

ทีนี้สมมติว่าเรามีดัชนีในtitleคุณสมบัติเช่นกันฐานข้อมูลไม่ควรตระหนักว่ามูลค่าไม่เปลี่ยนแปลง

ในบทความนี้ Markus Winand พูดว่า:

การอัปเดตในคอลัมน์ทั้งหมดแสดงรูปแบบเดียวกับที่เราสังเกตเห็นแล้วในส่วนก่อนหน้า: เวลาตอบสนองจะเพิ่มขึ้นพร้อมกับดัชนีเพิ่มเติมแต่ละรายการ

ฉันสงสัยว่าทำไมโอเวอร์เฮดนี้เนื่องจากฐานข้อมูลโหลดหน้าข้อมูลที่เกี่ยวข้องจากดิสก์ไปยังหน่วยความจำและเพื่อให้สามารถทราบได้ว่าค่าคอลัมน์จำเป็นต้องเปลี่ยนหรือไม่

แม้สำหรับดัชนีก็ไม่ต้องปรับสมดุลอะไรเลยเนื่องจากค่าดัชนีไม่เปลี่ยนแปลงสำหรับคอลัมน์ที่ไม่ได้เปลี่ยนแปลง แต่รวมอยู่ในการอัพเดท

เป็นดัชนี B + Tree ที่เชื่อมโยงกับคอลัมน์ที่ไม่มีการเปลี่ยนแปลงซ้ำซ้อนหรือไม่จำเป็นต้องสำรวจด้วยเช่นกันเฉพาะฐานข้อมูลเพื่อรับรู้ว่าค่า leaf ยังคงเหมือนเดิมหรือไม่

แน่นอนว่าเครื่องมือ ORM บางตัวช่วยให้คุณสามารถอัปเดตคุณสมบัติที่เปลี่ยนแปลง:

UPDATE post
SET    score = 12,
WHERE  id = 1

แต่การอัปเดตประเภทนี้อาจไม่ได้รับประโยชน์จากการอัพเดตแบบแบตช์หรือการแคชคำสั่งเสมอเมื่อมีการเปลี่ยนแปลงคุณสมบัติที่แตกต่างกันสำหรับแถวที่ต่างกัน


1
หากฐานข้อมูลเป็น PostgreSQL (หรืออื่น ๆ ที่ใช้MVCC ) UPDATEจะมีค่าเท่ากับDELETE+ INSERT(เพราะคุณสร้างV ersion ใหม่ของแถว) ค่าใช้จ่ายสูงและเติบโตตามจำนวนดัชนีโดยเฉพาะอย่างยิ่งหากมีคอลัมน์จำนวนมากที่ประกอบด้วยพวกเขาได้รับการปรับปรุงจริง ๆ และต้นไม้ (หรืออะไรก็ตาม) ที่ใช้แทนดัชนีจำเป็นต้องมีการเปลี่ยนแปลงที่สำคัญ ไม่ใช่จำนวนคอลัมน์ที่อัปเดตสิ่งที่เกี่ยวข้อง แต่ไม่ว่าคุณจะอัปเดตส่วนคอลัมน์ของดัชนีหรือไม่
joanolo

@joanolo สิ่งนี้จะต้องเป็นจริงสำหรับการใช้ MVCC ของ postgres เท่านั้น MySQL, Oracle (และอื่น ๆ ) ทำการอัปเดตและย้ายตำแหน่งคอลัมน์ที่ถูกเปลี่ยนไปยังพื้นที่ UNDO
Morgan Tocker

2
ฉันควรชี้ให้เห็นว่า ORM ที่ดีควรติดตามว่าคอลัมน์ใดที่ต้องมีการอัปเดตและเพิ่มประสิทธิภาพคำสั่งที่ส่งไปยังฐานข้อมูล มันเป็นเรื่องที่เกี่ยวข้องถ้าเพียงจำนวนของข้อมูลที่ส่งไปยังฐานข้อมูลเป็นพิเศษถ้าบางคอลัมน์ที่มีข้อความยาวหรือBLOBs
joanolo

1
คำถามพูดคุยเรื่องนี้สำหรับ SQL Server dba.stackexchange.com/q/114360/3690
Martin Smith

2
คุณใช้ DBMS รุ่นใดอยู่
a_horse_with_no_name

คำตอบ:


12

ฉันรู้ว่าคุณส่วนใหญ่กังวลUPDATEและเกี่ยวกับประสิทธิภาพเป็นส่วนใหญ่ แต่ในฐานะผู้ดูแล "ORM" ฉันขอให้คุณอีกมุมมองเกี่ยวกับปัญหาการแยกแยะระหว่างค่า"เปลี่ยน" , "โมฆะ"และ"ค่าเริ่มต้น"ซึ่งเป็น สามสิ่งที่แตกต่างกันใน SQL แต่อาจเป็นเพียงสิ่งเดียวใน Java และใน ORMs ส่วนใหญ่:

การแปลเหตุผลของคุณเป็นINSERTข้อความ

ข้อโต้แย้งของคุณในแง่ของความสามารถในการแบทช์และความสามารถในการแคชคำสั่งถือเป็นจริงในแบบเดียวกับที่ใช้สำหรับINSERTข้อความUPDATEสั่ง แต่ในกรณีของงบถนัดคอลัมน์จากคำสั่งที่มีความหมายที่แตกต่างกันกว่าในINSERT มันหมายถึงการที่จะใช้UPDATE DEFAULTสองสิ่งต่อไปนี้เทียบเท่ากันทางความหมาย:

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

สิ่งนี้ไม่เป็นความจริงเพราะUPDATEที่สองคนแรกมีความหมายเทียบเท่ากันและคนที่สามมีความหมายที่แตกต่างกันโดยสิ้นเชิง:

-- 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;

API ฐานข้อมูลไคลเอ็นต์ส่วนใหญ่รวมถึง JDBC และด้วยเหตุนี้ JPA จึงไม่อนุญาตให้ผูกDEFAULTนิพจน์กับตัวแปรผูก - ส่วนใหญ่เป็นเพราะเซิร์ฟเวอร์ไม่อนุญาตสิ่งนี้ หากคุณต้องการใช้คำสั่ง SQL เดียวกันอีกครั้งสำหรับเหตุผลด้านความสามารถในการแบทช์และคำสั่งแคชดังกล่าวข้างต้นคุณจะใช้คำสั่งต่อไปนี้ในทั้งสองกรณี (สมมติว่า(a, b, c)เป็นคอลัมน์ทั้งหมดในt):

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

และเนื่องจากcไม่ได้ตั้งค่าไว้คุณอาจผูก Java nullกับตัวแปรผูกที่สามเนื่องจาก ORM จำนวนมากยังไม่สามารถแยกแยะความแตกต่างระหว่างNULLและDEFAULT( jOOQเช่นเป็นข้อยกเว้นที่นี่) พวกเขาเห็นเฉพาะ Java nullและไม่ทราบว่าสิ่งนี้หมายถึงNULL(ตามค่าที่ไม่รู้จัก) หรือDEFAULT(ตามค่าเริ่มต้น)

ในหลายกรณีความแตกต่างนี้ไม่สำคัญ แต่ในกรณีที่คอลัมน์ของคุณใช้คุณสมบัติใด ๆ ต่อไปนี้ข้อความสั่งนั้นเรียบง่าย ผิดปกติ :

  • มันมี DEFAULTประโยค
  • มันอาจจะถูกสร้างขึ้นโดยทริกเกอร์

กลับไปที่UPDATEข้อความ

ในขณะที่ข้างต้นเป็นจริงสำหรับฐานข้อมูลทั้งหมดฉันสามารถมั่นใจได้ว่าปัญหาทริกเกอร์เป็นจริงสำหรับฐานข้อมูล Oracle เช่นกัน พิจารณา SQL ต่อไปนี้:

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;

เมื่อคุณเรียกใช้ข้างต้นคุณจะเห็นผลลัพธ์ต่อไปนี้:

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

อย่างที่คุณเห็นคำสั่งที่อัปเดตคอลัมน์ทั้งหมดเสมอจะเรียกทริกเกอร์สำหรับคอลัมน์ทั้งหมดเสมอในขณะที่คำสั่งที่อัปเดตเฉพาะคอลัมน์ที่มีการเปลี่ยนแปลงจะเริ่มทำงานเฉพาะทริกเกอร์ที่กำลังฟังการเปลี่ยนแปลงเฉพาะดังกล่าว

ในคำอื่น ๆ :

พฤติกรรมปัจจุบันของ Hibernate ที่คุณกำลังอธิบายไม่สมบูรณ์และอาจถือได้ว่าผิดในสถานะที่มีทริกเกอร์ (และอาจเป็นเครื่องมืออื่น ๆ )

ฉันเองคิดว่าอาร์กิวเมนต์การเพิ่มประสิทธิภาพแคชแบบสอบถามของคุณมีการใช้มากเกินไปในกรณีของ SQL แบบไดนามิก แน่นอนว่าจะมีคำสั่งอีกไม่กี่ในแคชและอีกเล็กน้อยแยกงานที่จะทำ แต่นี้มักจะไม่เป็นปัญหาสำหรับแบบไดนามิกงบมากน้อยกว่าUPDATESELECT

การแบตช์นั้นเป็นปัญหา แต่ในความเห็นของฉันการอัพเดทครั้งเดียวไม่ควรทำให้เป็นมาตรฐานเพื่ออัพเดทคอลัมน์ทั้งหมดเพียงเพราะมีความเป็นไปได้เล็กน้อยที่คำสั่งนั้นจะสามารถทำการแบตช์ได้ โอกาสคือ ORM สามารถรวบรวมแบทช์ย่อยของชุดคำสั่งที่เหมือนกันติดต่อกันและแบทช์แทนที่จะเป็น "ทั้งชุด" (ในกรณีที่ ORM สามารถติดตามความแตกต่างระหว่าง"เปลี่ยน" , "โมฆะ"และ"ค่าเริ่มต้น" ได้)


กรณีใช้สามารถแก้ไขโดยDEFAULT @DynamicInsertสถานการณ์ TRIGGER ยังสามารถได้รับการแก้ไขโดยใช้การตรวจสอบเช่นหรือเพียงแค่สลับไปWHEN (NEW.b <> OLD.b) @DynamicUpdate
Vlad Mihalcea

ใช่สามารถแก้ไขสิ่งต่าง ๆ ได้ แต่เดิมคุณถูกถามเกี่ยวกับประสิทธิภาพและวิธีแก้ไขปัญหาของคุณเพิ่มค่าใช้จ่ายมากขึ้น
ลูคัสเอเดอร์

ผมคิดว่ามอร์แกนกล่าวว่ามันที่ดีที่สุด: มันซับซ้อน
Vlad Mihalcea

ฉันคิดว่ามันค่อนข้างง่าย จากมุมมองเฟรมเวิร์กมีอาร์กิวเมนต์เพิ่มเติมที่สนับสนุนการกำหนดค่าเริ่มต้นเป็น SQL แบบไดนามิก จากมุมมองของผู้ใช้ใช่มันซับซ้อน
Lukas Eder

9

ผมคิดว่าคำตอบคือ - มันซับซ้อน ฉันพยายามที่จะเขียนหลักฐานอย่างรวดเร็วโดยใช้longtextคอลัมน์ใน MySQL แต่คำตอบนั้นไม่สามารถสรุปได้เล็กน้อย พิสูจน์ก่อน:

# 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

ดังนั้นจึงมีความแตกต่างของเวลาเล็กน้อยระหว่างค่าช้า + ที่เปลี่ยนแปลงและช้า + ไม่มีค่าที่เปลี่ยนแปลง ดังนั้นฉันตัดสินใจที่จะดูตัวชี้วัดอื่นซึ่งเป็นหน้าที่เขียน:

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)

ดังนั้นดูเหมือนว่าเวลาที่เพิ่มขึ้นเพราะจะต้องมีการเปรียบเทียบเพื่อยืนยันว่าค่าตัวเองยังไม่ได้รับการแก้ไขซึ่งในกรณีของ 1G longtext ต้องใช้เวลา (เพราะมันถูกแบ่งออกเป็นหลายหน้า) แต่ดูเหมือนว่าการดัดแปลงเองจะไม่ทำซ้ำผ่านบันทึกการทำซ้ำ

ฉันสงสัยว่าถ้าค่าเป็นคอลัมน์ปกติที่อยู่ในหน้าการเปรียบเทียบจะเพิ่มค่าใช้จ่ายเพียงเล็กน้อย และสมมติว่ามีการใช้การเพิ่มประสิทธิภาพเดียวกันนี้จะไม่มีการใช้งานเมื่อมาถึงการอัปเดต

คำตอบอีกต่อไป

ฉันคิดว่า ORM ไม่ควรกำจัดคอลัมน์ที่มีการแก้ไข ( แต่ไม่เปลี่ยนแปลง ) เนื่องจากการเพิ่มประสิทธิภาพนี้มีผลข้างเคียงที่แปลก

พิจารณาสิ่งต่อไปนี้ในรหัสหลอก:

# 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

ผลลัพธ์ถ้าการเปลี่ยนแปลง ORM เป็นการ "ปรับให้เหมาะสม" โดยไม่มีการเปลี่ยนแปลง:

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

ผลลัพธ์ถ้า ORM ส่งการแก้ไขทั้งหมดไปยังเซิร์ฟเวอร์:

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

กรณีทดสอบที่นี่ขึ้นอยู่กับrepeatable-readการแยก (ค่าเริ่มต้นของ MySQL) แต่จะมีหน้าต่างเวลาสำหรับread-committedการแยกที่การอ่านเซสชั่น 2 เกิดขึ้นก่อนที่เซสชั่น 1 จะกระทำ

ที่จะนำมันวิธีอื่น: การเพิ่มประสิทธิภาพที่มีความปลอดภัยเท่านั้นถ้าคุณออกจะอ่านแถวตามด้วย SELECT .. FOR UPDATE ไม่ใช้ MVCC และอ่านแถวเวอร์ชันล่าสุดเสมอUPDATESELECT .. FOR UPDATE


แก้ไข:ตรวจสอบให้แน่ใจว่าชุดข้อมูลกรณีทดสอบอยู่ในหน่วยความจำ 100% ปรับผลการจับเวลา


ขอบคุณสำหรับคำอธิบาย นั่นคือสัญชาตญาณของฉันเช่นกัน ฉันคิดว่า DB จะตรวจสอบทั้งแถวในหน้าข้อมูลและดัชนีที่เกี่ยวข้องทั้งหมด หากคอลัมน์มีขนาดใหญ่มากหรือมีดัชนีจำนวนมากที่เกี่ยวข้องค่าโสหุ้ยอาจสังเกตเห็นได้ แต่สำหรับสถานการณ์ส่วนใหญ่เมื่อใช้ชนิดคอลัมน์แบบกระชับและดัชนีมากเท่าที่ต้องการฉันคิดว่าค่าโสหุ้ยอาจน้อยกว่าไม่ได้รับประโยชน์จากการแคชคำสั่งหรือมีโอกาสน้อยในการแบตช์คำสั่ง
Vlad Mihalcea

1
@VladMihalcea ระวังว่าคำตอบนั้นเกี่ยวกับ MySQL ข้อสรุปอาจไม่เหมือนกันใน DBMS ที่แตกต่างกัน
ypercubeᵀᴹ

@ypercube ฉันรู้ว่า ทุกอย่างขึ้นอยู่กับ RDBMS
Vlad Mihalcea
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.