FiveM MySQL Upsert คืออะไร? วิธีใช้ INSERT ON DUPLICATE KEY UPDATE กับ oxmysql ให้เพิ่มหรืออัปเดตข้อมูลใน Query เดียว

 FiveM MySQL Upsert คือรูปแบบ SQL ที่รวมแนวคิด INSERT + UPDATE ไว้ในคำสั่งเดียว โดยถ้ายังไม่มี Row ที่ชนกับ PRIMARY KEY หรือ UNIQUE KEY ระบบจะ Insert Record ใหม่ แต่ถ้ามี Duplicate Key อยู่แล้ว Database จะ Update Row เดิมแทนผ่านคำสั่ง INSERT ... ON DUPLICATE KEY UPDATE.

oxmysql แนะนำ Upsert โดยตรงสำหรับกรณีที่ Resource ไม่แน่ใจว่า Row นั้นมีอยู่แล้วหรือยัง และระบุว่าแนวทางนี้ดีกว่าการ Query เช็กก่อนว่ามี Row หรือไม่ แล้วจึงเลือกว่าจะ INSERT หรือ UPDATE.

จำง่ายๆ:

ไม่มีข้อมูล
→ INSERT

มีข้อมูลที่ชน UNIQUE/PRIMARY KEY
→ UPDATE

แทนรูปแบบเดิม:

SELECT
↓
มีไหม?
↓
YES → UPDATE
NO  → INSERT

① Upsert คืออะไร

คำว่า Upsert มาจากแนวคิด:

UPDATE
+
INSERT
=
UPSERT

MariaDB รองรับรูปแบบ:

INSERT INTO table_name (...)
VALUES (...)
ON DUPLICATE KEY UPDATE
    column = value;

เมื่อเกิด Duplicate ของ Primary Key หรือ Unique Key จะเปลี่ยนจากการ Insert เป็นการ Update Row ที่ตรงกับ Key นั้น.

② ตัวอย่าง Upsert พื้นฐาน

สมมติมี Table:

CREATE TABLE `player_settings` (
    `identifier` VARCHAR(64) NOT NULL,
    `theme` VARCHAR(30) NOT NULL,
    PRIMARY KEY (`identifier`)
);

ต้องการบันทึก Theme:

INSERT INTO `player_settings`
    (`identifier`, `theme`)
VALUES
    (?, ?)
ON DUPLICATE KEY UPDATE
    `theme` = VALUES(`theme`);

ถ้า Identifier ยังไม่มี:

INSERT Row ใหม่

ถ้ามีแล้ว:

UPDATE theme ของ Row เดิม

MariaDB ระบุว่า VALUES(column) ภายในส่วน ON DUPLICATE KEY UPDATE ใช้อ้างถึงค่าที่กำลังจะถูก Insert.

③ ทำไม Upsert มีประโยชน์กับ FiveM

FiveM Resource มีข้อมูลหลายประเภทที่มักใช้รูปแบบ:

ถ้าไม่มี
→ สร้าง

ถ้ามีแล้ว
→ อัปเดต

เช่น:

  • Player Settings

  • UI Preferences

  • Character Metadata

  • Resource Configuration ต่อผู้เล่น

  • Persistent State บางประเภท

  • Last-known Settings

  • Profile Options

Upsert ช่วยลด Logic แบบ Check-before-write ให้เหลือ SQL Statement เดียว

④ oxmysql แนะนำ Upsert หรือไม่

แนะนำ

เอกสารหลักของ oxmysql มีหัวข้อ Upserting โดยตรง และแสดงตัวอย่าง:

MySQL.prepare(
    [[
        INSERT INTO ox_inventory
            (owner, name, data)
        VALUES (?, ?, ?)
        ON DUPLICATE KEY UPDATE
            data = VALUES(data)
    ]],
    {
        owner,
        dbId,
        inventory
    }
)

พร้อมระบุว่าแนวทางนี้ควรใช้แทนการตรวจว่า Row มีอยู่ก่อนแล้วค่อยเลือก Insert หรือ Update.

⑤ วิธีเดิม SELECT → INSERT/UPDATE มีปัญหาอะไร

ตัวอย่าง:

local exists =
    MySQL.scalar.await(
        'SELECT 1 FROM `player_settings` WHERE `identifier` = ? LIMIT 1',
        {
            identifier
        }
    )

if exists then

    MySQL.update.await(
        'UPDATE `player_settings` SET `theme` = ? WHERE `identifier` = ?',
        {
            theme,
            identifier
        }
    )

else

    MySQL.insert.await(
        'INSERT INTO `player_settings` (`identifier`, `theme`) VALUES (?, ?)',
        {
            identifier,
            theme
        }
    )

end

Flow นี้ต้อง:

Query 1
→ SELECT

แล้ว

Query 2
→ INSERT หรือ UPDATE

Upsert สามารถลดเหลือ:

Query เดียว

ซึ่งเป็น Pattern ที่ oxmysql แนะนำ.

⑥ เขียน Upsert ด้วย oxmysql อย่างไร

ตัวอย่าง:

local function saveTheme(
    identifier,
    theme
)
    return MySQL.prepare.await(
        [[
            INSERT INTO `player_settings`
                (`identifier`, `theme`)
            VALUES (?, ?)
            ON DUPLICATE KEY UPDATE
                `theme` = VALUES(`theme`)
        ]],
        {
            identifier,
            theme
        }
    )
end

MySQL.prepare เหมาะกับ Query Structure เดิมที่ถูกเรียกบ่อย และรองรับ ? Value Placeholders.

⑦ ต้องใช้ MySQL.prepare เท่านั้นไหม

ไม่

Upsert เป็น SQL Syntax ของ Database

ส่วน:

MySQL.prepare
MySQL.query
MySQL.update

เป็น API ของ oxmysql

ดังนั้นแนวคิด Upsert ไม่ได้ผูกอยู่กับ Prepare เพียงตัวเดียว แต่ oxmysql ใช้ MySQL.prepare ในตัวอย่าง Upsert อย่างเป็นทางการ.

⑧ Upsert ต้องมี PRIMARY KEY หรือ UNIQUE KEY ไหม

ต้องมี Key ที่สามารถเกิด Duplicate Conflict ได้จึงจะเข้าส่วน:

ON DUPLICATE KEY UPDATE

MariaDB ระบุว่า Behavior นี้เกิดเมื่อ Insert พบ Duplicate บน PRIMARY KEY หรือ UNIQUE KEY.

ถ้า Column:

identifier

ไม่ได้ Unique และ Table อนุญาตข้อมูลซ้ำ Upsert อาจ Insert Row ใหม่แทนที่จะ Update Row ที่ Developerคาดหวัง

⑨ ตัวอย่าง UNIQUE KEY

CREATE TABLE `player_settings` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
    `identifier` VARCHAR(64) NOT NULL,
    `theme` VARCHAR(30) NOT NULL,

    PRIMARY KEY (`id`),
    UNIQUE KEY `uq_player_settings_identifier`
        (`identifier`)
);

ตอนนี้:

identifier

ไม่สามารถซ้ำได้

ดังนั้น Upsert ที่ Insert Identifier เดิมจะเข้าส่วน Update

⑩ ถ้าไม่มี UNIQUE KEY จะเกิดอะไร

สมมติ:

CREATE TABLE `player_settings` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
    `identifier` VARCHAR(64) NOT NULL,
    `theme` VARCHAR(30) NOT NULL,

    PRIMARY KEY (`id`)
);

แล้ว Resource เรียก:

INSERT INTO `player_settings`
    (`identifier`, `theme`)
VALUES
    ('license:abc', 'dark')
ON DUPLICATE KEY UPDATE
    `theme` = VALUES(`theme`);

ถ้า identifier ไม่มี Unique Constraint และ id เป็น Auto Increment ใหม่ทุกครั้ง Database อาจ Insert Record ใหม่เพราะไม่มี Duplicate Key ที่เกี่ยวข้อง

ผลอาจกลายเป็น:

license:abc | dark
license:abc | light
license:abc | blue

ดังนั้น Schema สำคัญมาก

⑪ Upsert ไม่ได้หา Duplicate จากทุก Column

ON DUPLICATE KEY UPDATE ไม่ได้หมายถึง:

ถ้ามีข้อมูลเหมือนกันตรง Column ไหนก็ได้ให้ Update

มันพิจารณา Conflict กับ Key ที่มี Constraint เช่น Primary/Unique.

ดังนั้นต้องกำหนด Business Key ให้ชัด

⑫ Business Key คืออะไร

คือข้อมูลที่ Application ใช้กำหนดว่า Record ใดควรมีเพียงหนึ่งชุด

ตัวอย่าง Player Settings:

identifier

อาจเป็น Business Key

Vehicle Setting:

vehicle_id
+
setting_name

อาจต้องใช้ Composite Unique Key

ตัวอย่าง:

UNIQUE KEY `uq_vehicle_setting`
    (`vehicle_id`, `setting_name`)

จากนั้น Upsert สามารถอ้าง Conflict ของคู่ค่านี้ได้

⑬ Composite UNIQUE KEY ใช้ Upsert ได้ไหม

ได้

ตัวอย่าง:

CREATE TABLE `character_settings` (
    `character_id` INT UNSIGNED NOT NULL,
    `setting_name` VARCHAR(50) NOT NULL,
    `setting_value` TEXT NULL,

    UNIQUE KEY `uq_character_setting`
        (`character_id`, `setting_name`)
);

Query:

INSERT INTO `character_settings`
    (
        `character_id`,
        `setting_name`,
        `setting_value`
    )
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE
    `setting_value` =
        VALUES(`setting_value`);

ถ้า:

character_id + setting_name

ซ้ำ Database จะ Update

⑭ VALUES(column) คืออะไร

ใน:

ON DUPLICATE KEY UPDATE
    `theme` = VALUES(`theme`)

VALUES(theme) หมายถึงค่าของ theme ที่อยู่ในส่วน INSERT

MariaDB ระบุว่า VALUES(col_name) ใช้ใน ON DUPLICATE KEY UPDATE เพื่ออ้างค่าที่จะถูก Insert หากไม่เกิด Duplicate Conflict.

⑮ ตัวอย่าง VALUES()

Query:

INSERT INTO `player_settings`
    (`identifier`, `theme`)
VALUES
    (?, ?)
ON DUPLICATE KEY UPDATE
    `theme` = VALUES(`theme`);

Parameters:

{
    'license:abc',
    'dark'
}

ถ้ามี Record เดิม:

identifier = license:abc
theme = light

หลัง Upsert:

identifier = license:abc
theme = dark

เพราะ VALUES(theme) คือ dark

⑯ Upsert หลาย Columns ได้ไหม

ได้

ตัวอย่าง:

INSERT INTO `player_settings`
    (
        `identifier`,
        `theme`,
        `language`,
        `updated_at`
    )
VALUES
    (?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
    `theme` = VALUES(`theme`),
    `language` = VALUES(`language`),
    `updated_at` = NOW();

เมื่อ Duplicate จะ Update Columns ที่ระบุในส่วน Update

ไม่จำเป็นต้อง Update ทุก Column ของ Row

⑰ Column ไหนไม่ใส่ใน UPDATE จะเป็นอย่างไร

สมมติ:

ON DUPLICATE KEY UPDATE
    `theme` = VALUES(`theme`)

แต่ไม่ได้ใส่:

language
created_at

เมื่อ Duplicate เกิด Columns เหล่านั้นจะไม่ถูกแก้โดยส่วน Update นี้

จึงสามารถออกแบบให้:

created_at

คงค่าเดิม

แต่:

updated_at

เปลี่ยนทุกครั้ง

⑱ created_at กับ updated_at ควรใช้ยังไง

ตัวอย่าง:

INSERT INTO `player_settings`
    (
        `identifier`,
        `theme`,
        `created_at`,
        `updated_at`
    )
VALUES
    (?, ?, NOW(), NOW())
ON DUPLICATE KEY UPDATE
    `theme` = VALUES(`theme`),
    `updated_at` = NOW();

ผล:

Insert ใหม่

created_at = เวลาสร้าง
updated_at = เวลาสร้าง

Update เดิม

created_at = ไม่เปลี่ยน
updated_at = เวลาปัจจุบัน

เป็น Pattern ที่ใช้ได้กับข้อมูล Persistent ทั่วไป

⑲ Upsert กับ MySQL.insert ต่างกันอย่างไร

MySQL.insert มีหน้าที่หลักคือ Insert Record และคืน Insert ID หากใช้ได้.

ตัวอย่าง:

local id =
    MySQL.insert.await(
        [[
            INSERT INTO `profiles`
                (`identifier`)
            VALUES (?)
        ]],
        {
            identifier
        }
    )

Upsert มี Requirement เพิ่มคือ:

ถ้ามี Duplicate Key
→ Update

ดังนั้นจุดประสงค์ต่างกัน

⑳ Upsert กับ MySQL.update ต่างกันอย่างไร

MySQL.update เหมาะเมื่อรู้ว่า Record มีอยู่แล้วและต้องการ Update

oxmysql ระบุว่า MySQL.update คืนจำนวน Rows ที่ได้รับผล.

ตัวอย่าง:

local affected =
    MySQL.update.await(
        [[
            UPDATE `player_settings`
            SET `theme` = ?
            WHERE `identifier` = ?
        ]],
        {
            theme,
            identifier
        }
    )

Upsert เหมาะเมื่อ:

ไม่แน่ใจว่า Record มีหรือไม่

㉑ Upsert กับ Transaction ต่างกันอย่างไร

Upsert

หนึ่ง SQL Statement
→ Insert หรือ Update

Transaction

หลาย Queries
→ Commit เมื่อทั้งหมดสำเร็จ

oxmysql ระบุว่า Transaction จะ Commit ก็ต่อเมื่อ Queries ทั้งหมดสำเร็จ และถ้าหนึ่ง Query ล้มเหลวจะไม่ Commit ทั้งชุด.

จึงเป็นเครื่องมือคนละระดับ

㉒ Upsert ใช้ใน Transaction ได้ไหม

ได้ หาก Business Operation ต้องมีหลาย Database Operations ที่ต้องสำเร็จพร้อมกัน

ตัวอย่างเชิงแนวคิด:

Transaction
├── Upsert profile
├── Upsert settings
└── Insert related record

ถ้าทุก Query ต้อง Commit พร้อมกัน Transaction ยังมีประโยชน์

Upsert ไม่ได้แทน Atomicity ระหว่างหลาย Tables

㉓ Upsert กับ REPLACE INTO ต่างกันอย่างไร

นี่เป็นข้อแตกต่างสำคัญ

oxmysql ระบุว่า Upsert แบบ ON DUPLICATE KEY UPDATE ต่างจาก REPLACE INTO เพราะ Row เดิม ไม่ได้ถูกลบแล้ว Insert ใหม่.

MariaDB ระบุว่า REPLACE ทำงานคล้าย INSERT แต่ถ้ามี Duplicate Key จะ Delete Row ที่ชนก่อนแล้วจึง Insert Row ใหม่.

จำ:

ON DUPLICATE KEY UPDATE
→ UPDATE Row เดิม

REPLACE
→ DELETE Row เดิม
→ INSERT ใหม่

㉔ ทำไม REPLACE อาจอันตรายกว่าในบาง Schema

เพราะ Delete + Insert อาจมีผลต่างจาก Update เช่น:

Auto Increment
Foreign Key
Delete Trigger
Insert Trigger
Related Data

ดังนั้น oxmysql จึงแนะนำ Upsert และระบุข้อดีชัดเจนว่า Row ไม่ถูกลบและ Insert ใหม่เหมือน REPLACE INTO.

㉕ INSERT IGNORE ต่างจาก Upsert อย่างไร

MariaDB รองรับ INSERT IGNORE ซึ่งเมื่อพบ Duplicate/Error บางประเภทจะเปลี่ยน Error เป็น Warning และไม่ทำงานแบบ Update ค่าเดิมเหมือน ON DUPLICATE KEY UPDATE.

จำ:

INSERT IGNORE
→ เจอ Duplicate
→ ไม่ Update ค่าเดิมแบบ Upsert

ON DUPLICATE KEY UPDATE
→ เจอ Duplicate
→ Update ตามที่กำหนด

㉖ Upsert เหมาะกับ Player Settings มาก

ตัวอย่าง Table:

CREATE TABLE `player_settings` (
    `identifier` VARCHAR(64) NOT NULL,
    `theme` VARCHAR(30) NOT NULL DEFAULT 'default',
    `language` VARCHAR(10) NOT NULL DEFAULT 'th',

    PRIMARY KEY (`identifier`)
);

บันทึก:

local function saveSettings(
    identifier,
    theme,
    language
)
    MySQL.prepare.await(
        [[
            INSERT INTO `player_settings`
                (
                    `identifier`,
                    `theme`,
                    `language`
                )
            VALUES (?, ?, ?)
            ON DUPLICATE KEY UPDATE
                `theme` = VALUES(`theme`),
                `language` = VALUES(`language`)
        ]],
        {
            identifier,
            theme,
            language
        }
    )
end

ไม่ต้อง SELECT ก่อนว่าผู้เล่นมี Settings Row แล้วหรือยัง

㉗ Upsert เหมาะกับ Character Metadata

ตัวอย่าง:

CREATE TABLE `character_meta` (
    `character_id` INT UNSIGNED NOT NULL,
    `meta_key` VARCHAR(50) NOT NULL,
    `meta_value` TEXT NULL,

    UNIQUE KEY `uq_character_meta`
        (`character_id`, `meta_key`)
);

Upsert:

MySQL.prepare.await(
    [[
        INSERT INTO `character_meta`
            (
                `character_id`,
                `meta_key`,
                `meta_value`
            )
        VALUES (?, ?, ?)
        ON DUPLICATE KEY UPDATE
            `meta_value` =
                VALUES(`meta_value`)
    ]],
    {
        characterId,
        key,
        value
    }
)

เหมาะเมื่อ Metadata หนึ่ง Key ควรมีเพียงหนึ่ง Row ต่อ Character

㉘ Upsert เหมาะกับ Last Seen หรือไม่

ได้ในบาง Architecture

ตัวอย่าง:

INSERT INTO `player_activity`
    (
        `identifier`,
        `last_seen`
    )
VALUES
    (?, NOW())
ON DUPLICATE KEY UPDATE
    `last_seen` = NOW();

ถ้ายังไม่มี Player:

สร้าง Row

ถ้ามี:

อัปเดต last_seen

㉙ Upsert ใช้แทน UPDATE ทุกอย่างไหม

ไม่

ถ้าระบบรู้แน่นอนว่า Record ต้องมีอยู่และถ้าไม่มีถือเป็น Error:

Profile ID ต้องมี
แต่หาไม่เจอ
→ ควรถือว่าผิดปกติ

UPDATE ปกติอาจเหมาะกว่า Upsert เพราะการสร้าง Record ใหม่อัตโนมัติอาจซ่อน Bug

㉚ ตัวอย่างที่ไม่ควรใช้ Upsert

สมมติ Admin ต้องแก้ Character:

character_id = 500

ถ้า Character 500 ไม่มีจริง สิ่งที่ต้องการอาจเป็น:

ERROR

ไม่ใช่:

สร้าง Character 500 ใหม่

กรณีนี้ใช้ UPDATE ... WHERE id = ? และตรวจ affectedRows ชัดกว่า

㉛ ใช้ Upsert เมื่อ Business Rule อนุญาตให้สร้างได้

ถามว่า:

ถ้า Record ยังไม่มี ระบบควรสร้างมันอัตโนมัติจริงหรือไม่?

ถ้าคำตอบ:

YES

Upsert เป็น Candidate

ถ้า:

NO

ใช้ Update/Validation ตามปกติ

㉜ Upsert ช่วยลด Race Condition แบบ Check-before-write หรือไม่

Upsert ช่วยหลีกเลี่ยง Application Pattern:

SELECT ว่ามีไหม
↓
ค่อย INSERT/UPDATE

โดยให้ Database ตัดสินจาก Unique/Primary Key ใน SQL Statement เดียว ซึ่งเป็นเหตุผลหนึ่งที่ oxmysql แนะนำ Upsert เหนือการตรวจ Record ก่อน.

แต่ Resource ยังต้องออกแบบ Constraints และ Concurrency ของ Business Logic ให้ถูกต้อง

㉝ Unique Constraint สำคัญกับ Concurrency อย่างไร

ถ้าสอง Requests พยายามสร้าง:

identifier = license:abc

ใกล้กัน

Unique Key เป็น Rule ที่ Database ใช้ป้องกันไม่ให้มีค่าซ้ำ

Upsert จึงทำงานบน Constraint จริง แทนการอาศัยว่า Application เคย SELECT แล้วพบว่าไม่มี

㉞ อย่าใช้ SELECT EXISTS ก่อน Upsert

หากสุดท้าย Query ต้องทำ:

Insert if missing
Update if exists

oxmysql ระบุชัดว่า Upsert ควรถูกใช้แทนการตรวจว่ามี Row แล้วค่อย Insert หรือ Update.

ดังนั้น Pattern นี้:

SELECT EXISTS
↓
Upsert

มักเป็น Query ที่ซ้ำซ้อนหากไม่มี Logic อื่นต้องใช้ผลจาก SELECT

㉟ Upsert ช่วยลด Query Count

เดิม:

SELECT
+
INSERT/UPDATE
=
อย่างน้อย 2 Database Operations

Upsert:

INSERT ... ON DUPLICATE KEY UPDATE
=
1 SQL Statement

สำหรับ Hot Path ที่ถูกเรียกบ่อย การลด Round Trips ที่ไม่จำเป็นเป็นเรื่องที่ควรพิจารณา

แต่ต้อง Benchmark Workload จริง

㊱ Upsert ทำให้ Server เร็วขึ้นแน่นอนไหม

ไม่สามารถรับประกัน

oxmysql ระบุว่า Query Performance ขึ้นกับ:

  • Hardware

  • Database Settings

  • Database Version

  • Current Workload

และแนะนำดู Real Query Speeds ผ่าน Debug UI หรือ mysql_debug.

Upsert ลด Query Flow ได้ แต่ SQL และ Index ยังต้องเหมาะสม

㊲ MySQL.prepare ช่วย Upsert ได้อย่างไร

oxmysql ระบุว่า MySQL.prepare เหมาะกับ Frequently-called Queries และรองรับ Query เดียวกับ Parameter Sets ที่เปลี่ยนไป.

ดังนั้น Upsert ที่เรียกบ่อย เช่น:

Save Setting
Save Metadata
Save Preference

เป็น Candidate ที่เหมาะกับ Prepare

㊳ อย่าใช้ Prepare แทน UNIQUE KEY

Prepare ไม่ได้ทำให้ Column Unique

ถ้า Table ไม่มี:

PRIMARY KEY
หรือ
UNIQUE KEY

ที่สัมพันธ์กับข้อมูล Upsert ก็ไม่มี Duplicate Key Conflict ที่ต้องการให้ Update

ดังนั้น:

Prepare
≠ Constraint

㊴ Index กับ Upsert เกี่ยวกันไหม

เกี่ยว

UNIQUE KEY เป็นทั้ง Constraint และ Index Structure ของ Database

แต่ Regular Index อย่าง:

INDEX (`identifier`)

ไม่ได้ทำให้ค่าห้ามซ้ำ

ถ้าต้องการ Duplicate Detection ต้องใช้ Constraint ที่เหมาะสม เช่น:

UNIQUE KEY (`identifier`)

㊵ Regular Index ไม่เท่ากับ UNIQUE Index

จำ:

INDEX(identifier)
→ ค่าซ้ำได้

UNIQUE INDEX(identifier)
→ ค่าซ้ำไม่ได้

Upsert ต้องเกิด Conflict กับ Unique/Primary Key จึงเข้าส่วน Update.

㊶ ตัวอย่าง Schema ที่ถูก

CREATE TABLE `resource_preferences` (
    `identifier` VARCHAR(64) NOT NULL,
    `preference_key` VARCHAR(50) NOT NULL,
    `preference_value` TEXT NULL,

    UNIQUE KEY `uq_resource_preference`
        (
            `identifier`,
            `preference_key`
        )
);

Upsert:

INSERT INTO `resource_preferences`
    (
        `identifier`,
        `preference_key`,
        `preference_value`
    )
VALUES
    (?, ?, ?)
ON DUPLICATE KEY UPDATE
    `preference_value` =
        VALUES(`preference_value`);

㊷ UNIQUE หลายตัวใน Table ต้องระวัง

MariaDB Documentation เตือนว่า INSERT ... ON DUPLICATE KEY UPDATE ไม่แนะนำสำหรับ Table ที่มี Unique Index มากกว่าหนึ่งชุดที่สามารถ Match ได้ เพราะหากมี Conflict หลาย Unique Index ระบบจะ Update ตาม Key ที่ Match ก่อนเท่านั้น.

ดังนั้น Schema ที่ซับซ้อนควรทดสอบ Conflict Cases ให้ครบ

㊸ ตัวอย่างปัญหา Unique หลายชุด

สมมติ:

UNIQUE(identifier)
UNIQUE(email)

แล้ว Insert Row ใหม่ที่:

identifier

ชนกับ Row A

แต่:

email

ชนกับ Row B

นี่เป็น Conflict ที่ซับซ้อนและไม่ควรพึ่ง Upsert แบบไม่วิเคราะห์

ควรออกแบบ Business Key ให้ชัด

㊹ Upsert ควร Validate Input ไหม

ต้อง

Database Upsert ไม่ได้ทำให้ Client Request ถูกต้องหรือมีสิทธิ์โดยอัตโนมัติ

Flow ที่เหมาะ:

Client Request
↓
Server ตรวจ Input
↓
Server ตรวจ Player
↓
Server ตรวจสิทธิ์
↓
Upsert

อย่าใช้ Upsert เป็น Permission System

㊺ อย่าให้ Client ส่ง Identifier แล้วเชื่อทันที

ถ้า Server หา Identifier จาก source ได้เอง ควรทำฝั่ง Server

ตัวอย่าง:

local identifier =
    GetPlayerIdentifierByType(
        source,
        'license'
    )

แล้วใช้ Identifier ที่ Server Resolve เองในการ Query

ไม่จำเป็นต้องให้ Client บอกว่า:

ฉันคือ license:xxx

㊻ ตัวอย่างบันทึก Setting ฝั่ง Server

RegisterNetEvent(
    'settings:saveTheme',
    function(theme)

        local src = source

        if type(theme) ~= 'string' then
            return
        end

        local allowedThemes = {
            dark = true,
            light = true
        }

        if not allowedThemes[theme] then
            return
        end

        local identifier =
            GetPlayerIdentifierByType(
                src,
                'license'
            )

        if not identifier then
            return
        end

        MySQL.prepare.await(
            [[
                INSERT INTO `player_settings`
                    (`identifier`, `theme`)
                VALUES (?, ?)
                ON DUPLICATE KEY UPDATE
                    `theme` = VALUES(`theme`)
            ]],
            {
                identifier,
                theme
            }
        )

    end
)

ตรงนี้ Client เลือกได้เฉพาะ Theme ที่ Server Allowlist ไว้

㊼ Parameterized Query ยังสำคัญไหม

สำคัญ

ใช้:

VALUES (?, ?)

และ Parameters:

{
    identifier,
    theme
}

อย่าต่อ Input เข้า SQL String โดยตรง

oxmysql prepare รองรับ ? เป็น Value Placeholder และไม่รองรับ Named Placeholder ใน API นี้.

㊽ Upsert JSON Data ได้ไหม

ได้ถ้า Column/Schema รองรับและ Resource ต้องการเก็บ Structured Data

ตัวอย่าง:

local encoded =
    json.encode(settings)

MySQL.prepare.await(
    [[
        INSERT INTO `player_data`
            (`identifier`, `data`)
        VALUES (?, ?)
        ON DUPLICATE KEY UPDATE
            `data` = VALUES(`data`)
    ]],
    {
        identifier,
        encoded
    }
)

แต่ถ้า Resource ต้อง Search/Filter ข้อมูลย่อยภายใน Data บ่อย อาจต้องพิจารณา Schema ที่แยก Columns มากกว่าเก็บทุกอย่างเป็น Blob เดียว

㊾ Upsert Data ขนาดใหญ่ทุก Frame ดีไหม

ไม่

แม้ใช้ Upsert Query เดียวก็ไม่ควร Write Database ทุก Frame

ตัวอย่างที่ควรหลีกเลี่ยง:

CreateThread(function()

    while true do

        savePlayerData()

        Wait(0)

    end

end)

Database เป็น Persistent Layer ไม่ใช่ Runtime State Storage

㊿ ใช้ Dirty Flag กับ Upsert ได้ไหม

ได้ และเหมาะมาก

ตัวอย่าง:

PlayerState[id] = {
    settings = settings,
    dirty = false
}

เมื่อข้อมูลเปลี่ยน:

PlayerState[id].dirty = true

ตอน Save:

dirty = false
→ ไม่ Query

dirty = true
→ Upsert

ช่วยลด Write ที่ไม่จำเป็น

51. Upsert + Cache เป็น Pattern ที่ดี

ตัวอย่าง Architecture:

Player Login
↓
Load Database
↓
Cache ใน Memory
↓
Gameplay ใช้ Cache
↓
State เปลี่ยน
↓
Dirty = true
↓
Autosave / Important Event
↓
Upsert Database

ดีกว่าการ Query Database ทุกครั้งที่ Resource ต้องอ่าน Setting

52. Upsert + Autosave ต้องระวังอะไร

ถ้า Player จำนวนมาก Save พร้อมกัน:

200 Players
↓
200 Upserts

ในช่วงเวลาเดียว

Database Workload สามารถ Spike ได้

ควรพิจารณา:

Dirty Flag
Staggered Save
Save เฉพาะข้อมูลเปลี่ยน

และวัดด้วย oxmysql Debug Tools

53. เปิด mysql_debug ตรวจ Upsert ยังไง

oxmysql รองรับ:

set mysql_debug true

เพื่อ Print Queries ใน Server Console และยังสามารถ Debug เฉพาะ Resources ได้.

ใช้ตรวจว่า:

Upsert ถูกเรียกบ่อยแค่ไหน
Query ใช้เวลากี่ ms
Resource ไหนเรียก

54. Slow Upsert ต้องตรวจอะไร

ตรวจ:

Table Size
Unique Index
Columns ที่ Update
Query Frequency
Write Volume
Database CPU
Disk
Concurrent Writes
Server Hitch

และ Benchmark ด้วย Workload จริง เพราะ oxmysql ระบุว่า Query Speed เปลี่ยนตาม Hardware, Database Settings, Version และ Workload.

55. Upsert กับ Slow Query Warning

Upsert ก็สามารถกลายเป็น Slow Query ได้เหมือน SQL อื่น

อย่าคิดว่า:

Query เดียว
=
เร็วเสมอ

ถ้า Table ใหญ่, Write หนัก หรือ Constraints/Indexes ซับซ้อน ก็ต้องวัด Performance จริง

56. Upsert หลาย Columns ทุกครั้งอาจไม่จำเป็น

ถ้าผู้เล่นเปลี่ยนแค่:

theme

แต่ Query Update:

theme
language
volume
display_mode
notification
...

ทุกครั้ง อาจเขียนข้อมูลมากเกิน Requirement

ออกแบบ Save Strategy ให้เหมาะกับ Resource จริง

57. Upsert แยก Settings เป็น Key/Value ดีไหม

ขึ้นกับระบบ

แบบ:

identifier
setting_name
setting_value

มีความยืดหยุ่น

แต่ Query หลาย Settings อาจกลายเป็นหลาย Rows

อีกแบบ:

identifier
theme
language
volume

มี Structure ชัดเจน

ไม่มีแบบใดดีที่สุดสำหรับทุก Resource

58. Upsert Batch Data ได้ไหม

MariaDB รองรับ Multi-row Insert และ ON DUPLICATE KEY UPDATE ใน Syntax ของ INSERT.

ตัวอย่างเชิง SQL:

INSERT INTO `player_settings`
    (`identifier`, `theme`)
VALUES
    (?, ?),
    (?, ?)
ON DUPLICATE KEY UPDATE
    `theme` = VALUES(`theme`);

แต่ Parameter จำนวนมากและ Batch Size ต้องทดสอบกับ Database Workload จริง

59. Batch Upsert ดีกว่า Upsert ทีละ Row เสมอไหม

ไม่ควรสรุปแบบนั้น

ขึ้นกับ:

จำนวน Rows
Query Size
Database
Workload
Frequency
Failure Handling

ถ้ามีเพียง 2–3 Records บางครั้งความเรียบง่ายสำคัญกว่า Micro-optimization

60. Upsert กับ Transaction เมื่อ Batch สำคัญ

ถ้ามีหลาย Upserts ที่ต้อง:

สำเร็จทั้งหมด
หรือ
ไม่สำเร็จเลย

Transaction ยังเหมาะสม

oxmysql Transaction ถูกออกแบบเพื่อ Commit หลาย Queries เฉพาะเมื่อทุก Query สำเร็จ.

61. Upsert Result ควรตรวจอย่างไร

ขึ้นกับ oxmysql API ที่ใช้

ถ้าต้องการ:

Insert ID

MySQL.insert มี Return Contract สำหรับ Insert ID.

ถ้าต้องการ:

affectedRows

MySQL.update มี Return Contract สำหรับจำนวน Rows ที่ได้รับผล.

ส่วน prepare มี Result Behavior ตาม SQL ที่ Execute จึงควรทดสอบ Return Value ของ Upsert ที่ Resource ใช้จริง

62. อย่าผูก Business Logic กับ affectedRows แบบไม่ทดสอบ

Database อาจรายงานผลของ Insert และ Duplicate Update ต่างกันตาม Statement/Client Behavior

ดังนั้นหาก Resource ต้องแยกว่า:

สร้างใหม่
หรือ
อัปเดต

แบบมีผลต่อ Gameplay สำคัญ ควรออกแบบ Flow ให้ชัดและทดสอบกับ Database Version ที่ใช้อยู่จริง

อย่าเดาจากตัวเลขเพียงอย่างเดียว

63. Upsert ใช้กับ Auto Increment ได้ไหม

ได้หาก Table มี Auto Increment Primary Key และมี Unique Key อื่นที่ใช้ตรวจ Duplicate

ตัวอย่าง:

CREATE TABLE `profiles` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
    `identifier` VARCHAR(64) NOT NULL,
    `display_name` VARCHAR(50) NOT NULL,

    PRIMARY KEY (`id`),
    UNIQUE KEY `uq_profiles_identifier`
        (`identifier`)
);

Conflict สามารถเกิดจาก:

identifier

แม้ id จะ Auto Increment

64. อย่าใช้ Runtime Server ID เป็น Unique Persistent Key

FiveM source หรือ Server ID เปลี่ยนได้ตาม Connection Session

Persistent Table ควรใช้ Application/Persistent Identifier ที่เหมาะสม

เช่น:

character_id
profile_id
identifier

ตามระบบที่ออกแบบ

ไม่ควรใช้:

source = 27

เป็น Identity ถาวร

65. Upsert ใช้กับ NetID ได้ไหม

ไม่ควรใช้ NetID เป็น Persistent Identity ของ Entity

NetID เป็น Runtime Network Reference

ถ้าต้องเก็บ Persistent Vehicle Setting ควรอิง:

vehicle_id

หรือ Internal Persistent ID

จาก Database

66. Upsert กับ State Bag ต่างกันอย่างไร

Upsert
= Persistent Database Write

State Bag
= Runtime Network State

ตัวอย่าง:

Player เปลี่ยน Setting
↓
Server Validate
↓
Update Cache
↓
Update State Bag ถ้าต้อง Sync
↓
Upsert Database ตาม Save Strategy

ไม่จำเป็นต้อง Upsert ทุกครั้งที่ Network State เปลี่ยนเล็กน้อย

67. Upsert กับ Resource Restart

ข้อมูลที่ Upsert และ Commit แล้วจะอยู่ใน Database

แต่:

Lua Cache
Runtime Table
State Bag

อาจถูกสร้างใหม่เมื่อ Resource Restart

Resource จึงต้อง Reload Persistent State ตาม Architecture

68. Upsert Error Duplicate ยังเกิดได้ไหม

ถ้า Duplicate Conflict ไม่สามารถถูกจัดการตาม Statement/Schema ที่ออกแบบ หรือมี Constraint อื่นที่ไม่ตรงกับ Upsert Logic ก็ยังสามารถเกิด Database Error ได้

MariaDB ระบุว่า Duplicate Value ปกติจะทำให้ INSERT Error หากชน Unique Index เว้นแต่ใช้วิธีจัดการอย่าง IGNORE, ON DUPLICATE KEY UPDATE หรือ REPLACE.

ดังนั้นต้องอ่าน Error จริง ไม่ควรคิดว่า Upsert ลบ Duplicate Errors ทุกประเภท

69. Resource Update แล้ว Upsert พังควรตรวจอะไร

ตรวจ:

Schema เปลี่ยนหรือไม่
Unique Key ยังอยู่หรือไม่
Column ถูก Rename หรือไม่
Reserved Keyword หรือไม่
Database Version
Resource SQL Migration

oxmysql เองระบุว่า Resources รุ่นเก่าบางตัวอาจมี Compatibility Differences กับ Database Versions โดยเฉพาะ MySQL 8.

70. Upsert กับ MariaDB เหมาะไหม

MariaDB รองรับ INSERT ... ON DUPLICATE KEY UPDATE โดยตรง และ oxmysql Documentation แนะนำ MariaDB สำหรับ Compatibility กับ FiveM Resources.

แต่ Server ที่ใช้งาน Production อยู่แล้วไม่ควรย้าย Database Engine เพียงเพื่อใช้ Upsert เพราะต้องตรวจ Compatibility ของระบบทั้งหมดก่อน

Checklist FiveM MySQL Upsert

ก่อนใช้ตรวจว่า:

  1. ต้องการ Insert หากไม่มีจริงหรือไม่

  2. ต้องการ Update หากมีจริงหรือไม่

  3. มี Primary/Unique Key ที่ถูกต้อง

  4. Business Key ถูกออกแบบชัดเจน

  5. ไม่มี Duplicate Records เดิมที่ผิด Schema

  6. ตรวจ Existing Indexes

  7. ไม่ใช้ Regular Index แทน Unique Constraint

  8. ใช้ ON DUPLICATE KEY UPDATE

  9. ใช้ VALUES(column) ตาม Use Case

  10. Update เฉพาะ Columns ที่จำเป็น

  11. ไม่เปลี่ยน created_at โดยไม่ตั้งใจ

  12. Update updated_at เมื่อเหมาะสม

  13. ใช้ Parameterized Query

  14. ไม่ต่อ Client Input เข้า SQL

  15. Validate Network Event

  16. Server Resolve Identifier เมื่อทำได้

  17. ตรวจ Permission

  18. ตรวจ Record Ownership

  19. ไม่ Upsert ทุก Frame

  20. ใช้ Cache เมื่อเหมาะสม

  21. ใช้ Dirty Flag

  22. ระวัง Autosave Burst

  23. Benchmark Query Frequency

  24. เปิด mysql_debug เมื่อต้องวิเคราะห์

  25. ตรวจ Slow Query

  26. ไม่ใช้ Prepare แทน Unique Constraint

  27. ไม่ใช้ Upsert แทน Transaction

  28. ไม่ใช้ Upsert แทน Cache

  29. ระวัง Table ที่มีหลาย Unique Keys

  30. ทดสอบ Duplicate Case

  31. ทดสอบ Insert Case

  32. ทดสอบ Update Case

  33. ทดสอบ Invalid Input

  34. Backup ก่อนแก้ Schema

  35. Test Schema Change บน Development Database

ตาราง Upsert เทียบกับคำสั่งอื่น

วิธีเมื่อไม่มี Rowเมื่อชน Duplicate
INSERTInsertError ตาม Constraint
INSERT IGNOREInsertIgnore/Warning ตามกรณี
INSERT ... ON DUPLICATE KEY UPDATEInsertUpdate Row เดิม
REPLACEInsertDelete Row เดิมแล้ว Insert ใหม่
UPDATEไม่สร้าง Row ใหม่Update เฉพาะ Row ที่ Match

MariaDB รองรับกลไกเหล่านี้แยกกัน และ oxmysql แนะนำ ON DUPLICATE KEY UPDATE เหนือ Check-then-insert/update และเหนือ REPLACE INTO สำหรับ Upsert Use Case.

FiveM Upsert ใช้ยังไง

ตัวอย่างที่ควรจำ:

MySQL.prepare.await(
    [[
        INSERT INTO `player_settings`
            (`identifier`, `theme`)
        VALUES (?, ?)
        ON DUPLICATE KEY UPDATE
            `theme` = VALUES(`theme`)
    ]],
    {
        identifier,
        theme
    }
)

เป็น Pattern เดียวกับแนวทาง Upsert ที่ oxmysql Documentation แนะนำ.

FiveM INSERT ON DUPLICATE KEY UPDATE คืออะไร

คือ SQL ที่:

Insert ถ้า Key ยังไม่มี
Update ถ้า Primary/Unique Key ซ้ำ

MariaDB รองรับ Syntax นี้โดยตรง.

FiveM Upsert ต้องมี Unique Key ไหม

ต้องมี Key Conflict ที่ Database สามารถตรวจได้ เช่น Primary Key หรือ Unique Key จึงจะเข้าส่วน Update.

หาก Column ที่คิดว่าเป็น Identity ไม่มี Unique Constraint ระบบอาจสร้าง Rows ซ้ำ

FiveM Upsert ต่างจาก REPLACE ยังไง

Upsert:

Duplicate
→ UPDATE Row เดิม

REPLACE:

Duplicate
→ DELETE Row เดิม
→ INSERT ใหม่

oxmysql ระบุความแตกต่างนี้โดยตรง และแนะนำ Upsert มากกว่าการใช้ REPLACE INTO สำหรับ Use Case ดังกล่าว.

FiveM Upsert ดีกว่า SELECT ก่อนหรือไม่

ถ้าเป้าหมายคือเพียง:

มี → Update
ไม่มี → Insert

oxmysql แนะนำ Upsert แทนการตรวจว่ามี Row ก่อนแล้วค่อย Insert/Update.

แต่ถ้า SELECT นั้นจำเป็นสำหรับ Business Validation อื่น ก็ยังอาจต้อง Query เพิ่ม

FiveM Upsert ทำให้ Query น้อยลงไหม

โดยทั่วไป Pattern:

SELECT
+
INSERT/UPDATE

สามารถเปลี่ยนเป็น SQL Upsert Statement เดียวได้

แต่ Performance จริงต้องวัด เพราะ Query Speed ยังขึ้นกับ Database Environment และ Workload.

FiveM Upsert ใช้กับ MySQL.prepare ได้ไหม

ได้ และ oxmysql ใช้ MySQL.prepare ในตัวอย่าง Upsert อย่างเป็นทางการ.

Prepare เหมาะกับ Query ที่ถูกเรียกบ่อยและรองรับ ? Value Placeholders.

FiveM Upsert ใช้กับ Player Settings ดีไหม

เหมาะมากเมื่อ Requirement คือ:

Player ใหม่
→ สร้าง Settings

Player เดิม
→ Update Settings

และ Table มี Unique Key ที่บอกว่า Settings Row ใดเป็นของ Player ใด

FiveM Upsert ทำให้ Duplicate Data หายไหม

ไม่จำเป็น

Upsert ป้องกัน Duplicate ตาม Keys ที่ Database กำหนดเท่านั้น

ถ้า Database มี Rows ซ้ำอยู่แล้วและ Column ไม่มี Unique Constraint การเพิ่ม Upsert ใน Resource ไม่ได้ลบข้อมูลซ้ำเก่าให้อัตโนมัติ

ต้อง Cleanup Data และแก้ Schemaแยก

FiveM Upsert ช้าแก้อย่างไร

ตรวจ:

Query Count
Unique Index
Table Size
Columns Updated
Database Workload
Concurrent Writes
Server Hitch

แล้วใช้ mysql_debug/Debug UI เพื่อวัด Query จริง เพราะ oxmysql ระบุว่า Performance ขึ้นกับ Hardware, Settings, Database Version และ Current Workload.

FAQ FiveM MySQL Upsert

FiveM Upsert คืออะไร

คือการ Insert Record ถ้ายังไม่มี และ Update Row เดิมเมื่อเกิด Duplicate Primary/Unique Key ผ่าน INSERT ... ON DUPLICATE KEY UPDATE.

oxmysql รองรับ Upsert ไหม

รองรับ และ Documentation ของ oxmysql แนะนำ Upsert โดยใช้ MySQL.prepare.

Upsert ต้อง SELECT ก่อนหรือไม่

ไม่ หากเป้าหมายคือแค่ตัดสินว่าจะ Insert หรือ Update oxmysql แนะนำให้ใช้ Duplicate Key Upsert แทน Check-before-write.

Upsert ต้องใช้ Unique Key หรือไม่

ต้องมี Primary/Unique Key Conflict ที่ใช้ตัดสิน Duplicate.

VALUES(theme) หมายถึงอะไร

หมายถึงค่า theme จากส่วน INSERT ที่กำลังจะถูกใช้ใน Statement นั้น.

Upsert ต่างจาก UPDATE อย่างไร

UPDATE แก้เฉพาะ Row ที่มีอยู่ ส่วน Upsert สามารถ Insert หากยังไม่มีและ Update เมื่อมี Duplicate Key

Upsert ต่างจาก INSERT IGNORE อย่างไร

INSERT IGNORE ไม่ได้ Update Row เดิมแบบ ON DUPLICATE KEY UPDATE; มันจัดการ Error/Warnings ในลักษณะ Ignore ตามกฎของ MariaDB.

Upsert ต่างจาก REPLACE INTO อย่างไร

Upsert Update Row เดิม ส่วน REPLACE จะ Delete Row ที่ชนแล้ว Insert ใหม่.

Upsert แทน Transaction ได้ไหม

ไม่ได้ Transaction ใช้เมื่อหลาย Queries ต้อง Commit พร้อมกันทั้งหมด.

Upsert แทน Cache ได้ไหม

ไม่ได้ Upsert ยังเป็น Database Write ส่วน Cache ใช้ลดการเข้าถึง Database ซ้ำระหว่าง Runtime

Upsert ใช้ทุก Frame ได้ไหม

ไม่ควร Database Persistent Writes ควรเกิดตาม State Change/Save Strategy ไม่ใช่ทุก Render/Game Tick

Upsert ทำ Server เร็วขึ้นไหม

สามารถลด Check-before-write Query Pattern ได้ แต่ Performance จริงขึ้นกับ Database และ Workload จึงต้อง Benchmark.

ประเด็นสำคัญ

FiveM MySQL Upsert เหมาะเมื่อ Resource มี Requirement:

ไม่มี Row
→ INSERT

มี Row เดิม
→ UPDATE

โดยใช้:

INSERT ...
ON DUPLICATE KEY UPDATE ...

MariaDB จะตัดสิน Duplicate จาก PRIMARY KEY หรือ UNIQUE KEY และ oxmysql แนะนำ Pattern นี้โดยตรง แทนการ Query ตรวจว่ามี Row ก่อนแล้วค่อยเลือก Insert หรือ Update.

สำหรับ Resource ที่เรียก Save Pattern นี้บ่อย สามารถใช้ MySQL.prepare พร้อม ? Placeholders ได้ แต่ Prepare ไม่ได้แทน Unique Constraint และ Upsert ก็ไม่ได้แทน Cache หรือ Transaction.

สิ่งที่ต้องระวังมากคือ REPLACE INTO ไม่เหมือน Upsert เพราะ MariaDB จะลบ Row เดิมก่อนแล้ว Insert ใหม่เมื่อชน Key ขณะที่ ON DUPLICATE KEY UPDATE Update Row เดิม และ oxmysql ระบุว่า Upsert เป็นแนวทางที่ต้องการมากกว่าสำหรับ Use Case นี้.

สำหรับผู้อ่าน comsiam ให้จำสูตร “Unique Key → INSERT → Duplicate → UPDATE” และ comsiam แนะนำให้ใช้ Upsert กับข้อมูลประเภท Settings, Metadata หรือ Persistent Preferences ที่ควรสร้างเองเมื่อยังไม่มี แต่ต้องออกแบบ Unique Key ให้ถูกก่อนเสมอ เพราะหัวใจของ Upsert ไม่ใช่เพียง SQL Statement แต่คือ Schema ที่บอก Database ได้อย่างชัดเจนว่า Record ไหนคือข้อมูลชุดเดียวกัน

Comments

Popular posts from this blog

FiveM ยังน่าเล่นไหม? Enhanced เปลี่ยน FiveM แค่ไหน

FiveM คืออะไร เล่นอย่างไร สำหรับมือใหม่ เริ่มต้นตั้งแต่ศูนย์

วิธีตั้ง Admin Permission ด้วย add_ace และ add_principal FiveM แบบละเอียด