รู้หรือไม่ ว่าการเรียง column ใน DB ก็สำคัญนะจร๊ะส์
เวลาสร้างตารางในฐานข้อมูลเคยสังเกตไหมครับว่า ทำไมเราต้องมานั่งเลือกขนาดของข้อมูล เลยจะมาลองดูกันว่ามันสำคัญยังไงไอ้การเรียง column เนี้ย โดยตัวอย่างที่ยกมาจะเป็น PGSQL เด้อ
การจัดเรียงข้อมูลบน PGSQL
อันดับแรกเรามาดูใน document กันว่ามีอะไรที่เกี่ยวข้องกับประเภทของข้อมูลบน DB กันบ้าง
typalign
char
typalign
is the alignment required when storing a value of this type. It applies to storage on disk as well as most representations of the value inside PostgreSQL. When multiple values are stored consecutively, such as in the representation of a complete row on disk, padding is inserted before a datum of this type so that it begins on the specified boundary. The alignment reference is the beginning of the first datum in the sequence. Possible values are:
- c = char alignment, i.e., no alignment needed.
- s = short alignment (2 bytes on most machines).
- i = int alignment (4 bytes on most machines).
- d = double alignment (8 bytes on many machines, but by no means all).
จะเห็นว่าแต่ละประเภทข้อมูลจะใช้พื้นที่ไม่เท่ากันตามแต่ละประเภท ซึ่งขนาดบน storage ที่ใช้งานจริงจะขึ้นอยู่กับการเรียงลำดับของ column ด้วย
pg_attribute
. The key values needed to identify field locations are attlen
and attalign
. There is no way to directly get a particular attribute
, except when there are only fixed width fields and no null values. All this trickery is wrapped up in the functions heap_getattr
, fastgetattr
and heap_getsysattr
.จากข้อความที่เน้นไว้จะพบว่าการเรียง column ให้ดี ตอนที่สร้างตารางไม่ได้มีผลแค่ storage เท่านั้นยังมีผลต่อ memory กับ cpu ที่ใช้งานตารางนั้นด้วย
ว่าด้วยเรื่อง Padding
หลังจากอ่าน Docs แล้วยัง งง กันอยู่เรามาลองดูว่าพอใช้งานจริง ๆ มันจะเป็นยังไงนะ
เริ่มจากสร้างตารางมา 2 อัน
CREATE TABLE tb_fragment (col_1 smallint, col_2 bigint, col_3 int, col_4 bigint);
CREATE TABLE tb_continuous (col_1 bigint, col_2 bigint, col_3 int, col_4 smallint);
แล้วก็มาส่องดูว่าหน้าตาบน storage เป็นยังไง
SELECT a.attname, t.typname, t.typalign, t.typlen
FROM pg_class c
JOIN pg_attribute a ON (a.attrelid = c.oid)
JOIN pg_type t ON (t.oid = a.atttypid)
WHERE c.relname = 'tb_fragment' AND a.attnum >= 0;
attname | typname | typalign | typlen
---------+---------+----------+--------
col_1 | int2 | s | 2
col_2 | int8 | d | 8
col_3 | int4 | i | 4
col_4 | int8 | d | 8
(4 rows)
SELECT a.attname, t.typname, t.typalign, t.typlen
FROM pg_class c
JOIN pg_attribute a ON (a.attrelid = c.oid)
JOIN pg_type t ON (t.oid = a.atttypid)
WHERE c.relname = 'tb_continuous' AND a.attnum >= 0;
attname | typname | typalign | typlen
---------+---------+----------+--------
col_1 | int8 | d | 8
col_2 | int8 | d | 8
col_3 | int4 | i | 4
col_4 | int2 | s | 2
(4 rows)
จากที่เห็นก็คือ
BIGINT
ใช้ 8 bytesINT
ใช้ 4 bytesSMALLINT
ใช้ 2 bytes
เราคำนวณออกมาได้ว่าทั้ง 2 ตารางใช้ก็น่าจะใช้ 22 bytes ต่อหนึ่ง row แต่แล้วเรามาดูความเป็นจริง
select pg_column_size(tb_fragment.*) - 24 as row_size from tb_fragment limit 1;
row_size
----------
32
(1 row)
select pg_column_size(tb_continuous.*) - 24 as row_size from tb_continuous limit 1;
row_size
----------
22
(1 row)
อ้าวเห้ย ทำไมไม่เท่ากันละ ถถถ
มาถึงตรงนี้ถ้าสังเกตจาก typlen
คนที่เคยอ่าน (ออกแบบ Go struct ด้วยความรู้วิชา Computer Architecture และ Data Structure) ก็จะพบว่ามันคล้าย ๆ กันเลยนะ ใช่ครับมันคือสิ่งเดียวกันเลย สิ่งที่เกิดขึ้นคือ padding ระหว่าง filed ที่ออกมานั่นเองครับ
ซึ่งเราสามารถใช้ extension pageinspect
เพื่ออธิบายสิ่งที่เกิดขึ้นได้ตามนี้
SELECT t_data FROM heap_page_items(get_raw_page('tb_fragment','main',0))gx
-[ RECORD 1 ]--------------------------------------------------------------
t_data | xff7f000000000000ffffffffffffff7fffffff7f00000000ffffffffffffff7f
SELECT t_data FROM heap_page_items(get_raw_page('tb_continuous','main',0))gx
-[ RECORD 1 ]------------------------------------------
t_data | xffffffffffffff7fffffffffffffff7fffffff7fff7f
จะเห็นว่าในตาราง tb_fragment
เวลาดึงข้อมูลออกมานั้นจะมี zero padding จนเต็ม 8 bytes แล้วค่อยเป็น column ถัดไปเพราะ col_1(2 bytes) + col_2(8 bytes) เกิน 8 bytes ทำให้โดนแยกออกจากกัน ซึ่งจะต่างจาก tb_continuous
ที่ col_3(4 bytes) + col_4(2 bytes) ไม่เกิน 8 bytes ทำให้รวมกันออกมาด้วย word เดียวกันได้เลย
สรุป
การวางแผนออกแบบโครงสร้างโดยพิจารณาสิ่งที่จะเกิดขึ้นระดับ low-level นอกจากจะช่วยให้เราไม่สูญเสีย storage ไปเปล่า ๆ แล้วยังช่วยเพิ่มประสิทธิภาพในการใช้งาน memory กับ cpu ได้อีกด้วย เนื่องจากไม่ต้องเสียไปกับการ load data ที่ไม่จำเป็น แต่ว่าก็ต้องแลกมากับลำดับความสัมพันของ column ที่จะดูไม่รู้เรื่อง เราก็เลยอาจจะเว้นไว้สำหรับ PKEY หรือ index อื่น ๆ ที่จะทำให้เราอ่านตารางได้ง่าย ให้มาอยู่ column แรก ๆ แบบที่ชาวโลกปกติทำกัน โดยไม่ต้องไปฝืนเรียงเพื่อเอา perfomance เพียงอย่างเดียว