{"id":12836,"date":"2025-05-02T09:16:32","date_gmt":"2025-05-02T09:16:32","guid":{"rendered":"https:\/\/binus.ac.id\/binus-digital\/?p=12836"},"modified":"2025-12-02T09:17:17","modified_gmt":"2025-12-02T09:17:17","slug":"biaya-tersembunyi-default-mysql-di-aplikasi-laravel-stabilitas-kinerja-dan-data-yang-akurat","status":"publish","type":"post","link":"https:\/\/binus.ac.id\/binus-digital\/2025\/05\/02\/biaya-tersembunyi-default-mysql-di-aplikasi-laravel-stabilitas-kinerja-dan-data-yang-akurat\/","title":{"rendered":"Biaya Tersembunyi Default MySQL di Aplikasi Laravel: Stabilitas, Kinerja, dan Data yang Akurat"},"content":{"rendered":"<article class=\"wp-article mysql-defaults-laravel\" lang=\"id\">\n<header>\n<p class=\"lead\">\n      Banyak proyek Laravel berjalan mulus di lokal, lalu mendapati anomali saat ke staging\/produksi: urutan sort yang berbeda,<br \/>\n      indeks mendadak meledak, data yang \u201cdiam-diam\u201d terpotong, atau jam yang tidak sinkron. Seringkali penyebabnya sederhana:<br \/>\n      kita membiarkan default MySQL apa adanya. Artikel ini membedah risiko-risiko tersebut dan memberikan pola aman yang<br \/>\n      pragmatis\u2014tanpa mengorbankan kenyamanan pengembangan sehari-hari.\n    <\/p>\n<\/header>\n<section id=\"ringkasan\">\n<h2>Ringkasan Cepat<\/h2>\n<ul>\n<li><strong>Samakan charset\/collation:<\/strong> Pastikan skema, tabel, dan kolom konsisten (idealnya <code>utf8mb4<\/code> + collation yang tepat).<\/li>\n<li><strong>Kendalikan panjang indeks:<\/strong> Hindari workaround global <code>defaultStringLength(191)<\/code> bila Anda pakai MySQL 5.7+\/8\u2014benahi di akar.<\/li>\n<li><strong>Gunakan SQL strict mode:<\/strong> Biar kegagalan terlihat cepat, bukan menyimpan utang teknis.<\/li>\n<li><strong>Selaraskan timezone:<\/strong> Simpan UTC di DB, konversi di aplikasi\/UI.<\/li>\n<li><strong>Pilih tipe data dengan sadar:<\/strong> <code>decimal<\/code> untuk uang, <code>json<\/code> untuk semi-terstruktur, <code>bigIncrements<\/code> untuk tabel besar.<\/li>\n<\/ul>\n<\/section>\n<section id=\"charset-collation\">\n<h2>1) Charset &amp; Collation: Konsistensi yang Menentukan<\/h2>\n<p>\n      Laravel cenderung menset <code>utf8mb4<\/code> dan kolasi berbasis Unicode. Namun banyak server MySQL lama\/host bersama masih<br \/>\n      menyalakan default lain (mis. <code>latin1<\/code>), lalu tabel\/kolom baru warisi default yang berbeda. Dampaknya:\n    <\/p>\n<ul>\n<li><strong>Sort\/compare berbeda:<\/strong> ORDER BY berubah antara lingkungan.<\/li>\n<li><strong>Emoji\/karakter non-BMP rusak:<\/strong> Selain data korup, indeks bisa gagal bila panjang byte tak cukup.<\/li>\n<li><strong>Join error:<\/strong> \u201cIllegal mix of collations\u201d saat menggabungkan kolom dengan kolasi tak seragam.<\/li>\n<\/ul>\n<pre><code>\/\/ config\/database.php (cuplikan)\r\n'mysql' =&gt; [\r\n  'driver' =&gt; 'mysql',\r\n  'charset' =&gt; 'utf8mb4',\r\n  'collation' =&gt; 'utf8mb4_unicode_ci', \/\/ Pertimbangkan 'utf8mb4_0900_ai_ci' di MySQL 8+\r\n  'engine' =&gt; null,\r\n  'strict' =&gt; true,\r\n  \/\/ 'options' =&gt; [PDO::MYSQL_ATTR_INIT_COMMAND =&gt; \"SET time_zone = '+00:00'\"],\r\n],<\/code><\/pre>\n<pre><code>-- Audit cepat kolasi di server\r\nSHOW VARIABLES LIKE 'character_set_%';\r\nSHOW VARIABLES LIKE 'collation%';\r\n\r\n-- Tabel\/kolom yang kolasinya berbeda\r\nSELECT t.TABLE_SCHEMA, t.TABLE_NAME, t.TABLE_COLLATION\r\nFROM information_schema.TABLES t\r\nWHERE t.TABLE_SCHEMA = DATABASE()\r\nORDER BY t.TABLE_COLLATION;<\/code><\/pre>\n<p><strong>Rekomendasi:<\/strong> Standardisasi pada level database dan override per tabel bila perlu:\n    <\/p>\n<pre><code>\/\/ Migration contoh\r\nSchema::create('posts', function (Blueprint $table) {\r\n  $table-&gt;id();\r\n  $table-&gt;string('title')\r\n        -&gt;charset('utf8mb4')\r\n        -&gt;collation('utf8mb4_unicode_ci');\r\n  $table-&gt;text('body');\r\n  $table-&gt;timestampsTz(); \/\/ simpan UTC, tampilkan sesuai zona di UI\r\n});<\/code><\/pre>\n<\/section>\n<section id=\"panjang-index\">\n<h2>2) Panjang String &amp; Batas Indeks: 191 vs 255<\/h2>\n<p>\n      Di MySQL lama (batas indeks 767 byte), <code>utf8mb4<\/code> membuat indeks pada <code>VARCHAR(255)<\/code> gagal. Solusi historis: <code>Schema::defaultStringLength(191)<\/code>.<br \/>\n      Ini bekerja, tetapi menyembunyikan masalah lingkungan dan menurunkan ketelitian indeks.\n    <\/p>\n<ul>\n<li><strong>MySQL 5.7+\/8:<\/strong> Dengan InnoDB modern (row format DYNAMIC), batas indeks efektif 3072 byte\u2014hindari workaround global.<\/li>\n<li><strong>Prefer refactor:<\/strong> Upgrade server\/engine\/row format daripada memendekkan semua kolom secara buta.<\/li>\n<li><strong>Kasus unik:<\/strong> Untuk kolom unik email\/slug, batasi panjang kolom secara semantik (mis. 200) bila Anda benar-benar butuh.<\/li>\n<\/ul>\n<pre><code>\/\/ Hindari: di AppServiceProvider men-set defaultStringLength(191)\r\n\/\/ Gunakan design yang jelas: batasi panjang kolom yang memang perlu\r\n$table-&gt;string('slug', 200)-&gt;unique();<\/code><\/pre>\n<\/section>\n<section id=\"sql-modes\">\n<h2>3) SQL Modes &amp; Strict Mode: Gagal Cepat Lebih Murah<\/h2>\n<p>\n      Tanpa strict mode, MySQL bisa memotong nilai diam-diam (truncation) atau mengkonversi nilai tak valid. Ini berbahaya karena<br \/>\n      bug terlihat sangat terlambat. Laravel defaultnya <code>strict: true<\/code>\u2014pertahankan demikian dan sinkronkan daftar mode di server.\n    <\/p>\n<ul>\n<li><strong>ONLY_FULL_GROUP_BY:<\/strong> Paksa agregasi benar. Perbaiki query, jangan matikan.<\/li>\n<li><strong>NO_ZERO_DATE\/NO_ZERO_IN_DATE:<\/strong> Cegah tanggal semu <code>0000-00-00<\/code>.<\/li>\n<li><strong>ERROR_FOR_DIVISION_BY_ZERO:<\/strong> Lebih baik meledak daripada hasil kacau.<\/li>\n<\/ul>\n<pre><code>-- Lihat SQL_MODE\r\nSELECT @@GLOBAL.sql_mode, @@SESSION.sql_mode;<\/code><\/pre>\n<\/section>\n<section id=\"null-empties\">\n<h2>4) NULL vs String Kosong: Selaraskan dengan Validasi<\/h2>\n<p>\n      Default kolom sering <em>nullable<\/em> tanpa alasan. Padahal pada domain tertentu, \u201ctidak diisi\u201d berbeda makna dengan \u201cdiisi tapi kosong\u201d.<br \/>\n      Pastikan migrasi dan rules Laravel konsisten agar analitik tidak bias.\n    <\/p>\n<pre><code>\/\/ Migration: eksplisitkan nullability\r\n$table-&gt;string('middle_name')-&gt;nullable(); \/\/ benar-benar boleh kosong\r\n$table-&gt;string('phone')-&gt;nullable(false);  \/\/ wajib diisi\r\n\r\n\/\/ Form request\r\n'phone' =&gt; ['required','string','max:32']<\/code><\/pre>\n<\/section>\n<section id=\"timestamps-tz\">\n<h2>5) Timestamps &amp; Timezone: Simpan UTC, Tampilkan Lokal<\/h2>\n<p>\n      Default <code>timestamps()<\/code> tidak menyertakan zona. Simpan UTC di DB dan konversikan di aplikasi\/klien.<br \/>\n      Hindari campuran timezone antar layanan.\n    <\/p>\n<pre><code>\/\/ Migration: gunakan tipe bertzona bila tersedia\r\n$table-&gt;timestampsTz();       \/\/ created_at\/updated_at dengan zona\r\n$table-&gt;softDeletesTz();      \/\/ deleted_at dengan zona\r\n\r\n\/\/ AppServiceProvider (opsional): set session time_zone\r\nuse Illuminate\\Support\\Facades\\DB;\r\n\r\npublic function boot(): void\r\n{\r\n  DB::listen(function () {}); \/\/ agar koneksi terinisiasi\r\n  DB::unprepared(\"SET time_zone = '+00:00'\"); \/\/ simpan UTC\r\n}<\/code><\/pre>\n<p><strong>Catatan:<\/strong> Pastikan server MySQL punya tabel zona waktu lengkap jika menggunakan nama zona.<\/p>\n<\/section>\n<section id=\"engine-rowformat\">\n<h2>6) Engine, Row Format, dan Stabilitas Indeks<\/h2>\n<p>\n      InnoDB adalah pilihan aman. Di server lama, pastikan row format DYNAMIC agar indeks panjang (utf8mb4) aman.\n    <\/p>\n<pre><code>-- Cek engine\/row format\r\nSELECT TABLE_NAME, ENGINE, ROW_FORMAT\r\nFROM information_schema.TABLES\r\nWHERE TABLE_SCHEMA = DATABASE();<\/code><\/pre>\n<pre><code>\/\/ Migration: set per tabel bila perlu\r\nSchema::create('users', function (Blueprint $table) {\r\n  $table-&gt;engine = 'InnoDB';\r\n  $table-&gt;id();\r\n  $table-&gt;string('email')-&gt;unique();\r\n  $table-&gt;timestamps();\r\n});<\/code><\/pre>\n<\/section>\n<section id=\"numeric-money\">\n<h2>7) Tipe Numerik &amp; Uang: Hindari Float untuk Nilai Finansial<\/h2>\n<ul>\n<li><strong>Gunakan DECIMAL:<\/strong> <code>decimal(18,2)<\/code> atau sesuai kebutuhan agar akurat.<\/li>\n<li><strong>Unsigned untuk ID\/Hitung ke atas:<\/strong> Hemat rentang dan konsisten dengan relasi.<\/li>\n<li><strong>Skala besar:<\/strong> Gunakan <code>bigIncrements<\/code>\/<code>unsignedBigInteger<\/code> sejak awal untuk tabel yang akan tumbuh cepat.<\/li>\n<\/ul>\n<pre><code>\/\/ Migration\r\n$table-&gt;decimal('amount', 18, 2)-&gt;default(0);\r\n$table-&gt;unsignedBigInteger('user_id');<\/code><\/pre>\n<\/section>\n<section id=\"json\">\n<h2>8) Kolom JSON: Kekuatan &amp; Perbedaan MySQL vs MariaDB<\/h2>\n<p>\n      Di MySQL 5.7+, <code>JSON<\/code> punya validasi dan operator bawaan. Di MariaDB, tipe <code>JSON<\/code> hanyalah alias <code>LONGTEXT<\/code>.<br \/>\n      Ketahui platform Anda sebelum mengandalkan fungsi lanjutan.\n    <\/p>\n<pre><code>\/\/ Query builder\r\nUser::whereJsonContains('meta-&gt;roles', 'admin')-&gt;get();\r\n\r\n\/\/ Index pada path JSON (MySQL): gunakan kolom terhitung\r\nALTER TABLE users\r\n  ADD COLUMN role_first VARCHAR(32)\r\n    GENERATED ALWAYS AS (json_unquote(json_extract(meta, '$.roles[0]'))) STORED,\r\n  ADD INDEX idx_role_first (role_first);<\/code><\/pre>\n<\/section>\n<section id=\"boolean-enum\">\n<h2>9) Boolean &amp; Enum: Tegaskan Intent<\/h2>\n<ul>\n<li><strong>Boolean = TINYINT(1):<\/strong> Tetap angka. Dokumentasikan maknanya dan gunakan cast Eloquent.<\/li>\n<li><strong>Enum:<\/strong> Di MySQL 8+ bisa kombinasikan dengan CHECK untuk validasi tambahan, atau gunakan tabel referensi.<\/li>\n<\/ul>\n<pre><code>\/\/ Model cast\r\nprotected $casts = [\r\n  'is_active' =&gt; 'boolean',\r\n];<\/code><\/pre>\n<\/section>\n<section id=\"ordering-collation\">\n<h2>10) Ordering, Collation-Sensitive LIKE, dan Pencarian<\/h2>\n<p>\n      Kolasi memengaruhi perbandingan\/penyortiran. Case-insensitive mungkin bagus untuk pencarian, tetapi bisa<br \/>\n      menabrak kebutuhan case-sensitive tertentu. Terapkan kolasi pada level kolom atau query saat diperlukan.\n    <\/p>\n<pre><code>\/\/ Case-sensitive exact match\r\nUser::whereRaw(\"BINARY username = ?\", [$input])-&gt;first();\r\n\r\n\/\/ Pakai kolasi berbeda di query\r\nPost::orderByRaw(\"title COLLATE utf8mb4_0900_as_cs ASC\")-&gt;get();<\/code><\/pre>\n<\/section>\n<section id=\"migrations-konvensi\">\n<h2>11) Migrations &amp; Konvensi: Lebih Eksplisit, Lebih Sedikit Kejutan<\/h2>\n<ul>\n<li><strong>Eksplisitkan panjang dan nullability:<\/strong> Hindari default yang kabur.<\/li>\n<li><strong>Gunakan <code>timestampsTz<\/code>\/<code>softDeletesTz<\/code>:<\/strong> Seragamkan waktu di UTC.<\/li>\n<li><strong>Index yang bermakna:<\/strong> Nama indeks, urutan kolom, dan panjang kolom yang realistis.<\/li>\n<li><strong>Hindari hack global:<\/strong> Seperti <code>defaultStringLength(191)<\/code> kecuali benar-benar diperlukan untuk kompatibilitas sementara.<\/li>\n<\/ul>\n<pre><code>\/\/ Contoh migration terstandar\r\nSchema::create('orders', function (Blueprint $table) {\r\n  $table-&gt;bigIncrements('id');\r\n  $table-&gt;foreignId('user_id')-&gt;constrained()-&gt;cascadeOnDelete();\r\n  $table-&gt;string('number', 40)-&gt;unique();\r\n  $table-&gt;decimal('total', 18, 2)-&gt;default(0);\r\n  $table-&gt;json('meta')-&gt;nullable();\r\n  $table-&gt;timestampsTz();\r\n  $table-&gt;index(['user_id', 'created_at']);\r\n});<\/code><\/pre>\n<\/section>\n<section id=\"observability\">\n<h2>12) Observability &amp; Audit: Kenali Lingkungan Anda<\/h2>\n<pre><code>-- Temukan kolom dengan collation berbeda dari default DB\r\nSELECT c.TABLE_NAME, c.COLUMN_NAME, c.COLLATION_NAME\r\nFROM information_schema.COLUMNS c\r\nWHERE c.TABLE_SCHEMA = DATABASE()\r\n  AND (c.COLLATION_NAME IS NOT NULL)\r\nORDER BY c.COLLATION_NAME;\r\n\r\n-- Cari indeks yang berisiko (panjang besar pada utf8mb4)\r\nSELECT TABLE_NAME, INDEX_NAME, SUM(CARDINALITY) AS card\r\nFROM information_schema.STATISTICS\r\nWHERE TABLE_SCHEMA = DATABASE()\r\nGROUP BY TABLE_NAME, INDEX_NAME\r\nORDER BY card ASC;<\/code><\/pre>\n<pre><code>\/\/ Cek nilai runtime di Laravel (Tinker)\r\nDB::select('select @@version as v, @@sql_mode as m, @@time_zone as tz');<\/code><\/pre>\n<\/section>\n<section id=\"checklist\">\n<h2>13) Checklist Produksi<\/h2>\n<ul class=\"checklist\">\n<li>[ ] DB, tabel, dan kolom memakai <code>utf8mb4<\/code> + kolasi seragam.<\/li>\n<li>[ ] Strict mode aktif; <code>ONLY_FULL_GROUP_BY<\/code> tidak dinonaktifkan demi kenyamanan.<\/li>\n<li>[ ] Timestamps disimpan UTC; UI mengonversi ke zona pengguna.<\/li>\n<li>[ ] Kolom finansial memakai <code>decimal<\/code>; tidak ada <code>float<\/code> untuk uang.<\/li>\n<li>[ ] Panjang kolom dan indeks dievaluasi semantik; tidak mengandalkan hack global.<\/li>\n<li>[ ] Perbedaan MySQL vs MariaDB diakomodasi (terutama JSON &amp; kolasi).<\/li>\n<li>[ ] Migrations eksplisit; tidak ada kolom nullable tanpa alasan.<\/li>\n<li>[ ] Audit Dev\/Stage\/Prod memiliki konfigurasi yang konsisten.<\/li>\n<\/ul>\n<\/section>\n<section id=\"penutup\">\n<h2>Penutup<\/h2>\n<p>\n      Default nyaman di awal, tetapi mahal di akhir. Dengan menstandardisasi charset\/collation, mengaktifkan strict mode,<br \/>\n      menyelaraskan timezone, memilih tipe data yang tepat, dan menulis migrasi eksplisit, Anda mengurangi kelas problem<br \/>\n      \u201caneh di produksi\u201d. Perlakukan database sebagai bagian dari desain sistem\u2014bukan sekadar tempat menyimpan data.\n    <\/p>\n<\/section>\n<footer>\n<hr>\n<p>Artikel ini ditulis secara orisinal dalam bahasa Indonesia, mengembangkan praktik aman MySQL untuk aplikasi Laravel modern.<\/p>\n<\/footer>\n<\/article>\n","protected":false},"excerpt":{"rendered":"<p>Banyak proyek Laravel berjalan mulus di lokal, lalu mendapati anomali saat ke staging\/produksi: urutan sort yang berbeda, indeks mendadak meledak, data yang \u201cdiam-diam\u201d terpotong, atau jam yang tidak sinkron. Seringkali penyebabnya sederhana: kita membiarkan default MySQL apa adanya. Artikel ini membedah risiko-risiko tersebut dan memberikan pola aman yang pragmatis\u2014tanpa mengorbankan kenyamanan pengembangan sehari-hari. Ringkasan Cepat [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":12837,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[77],"tags":[],"class_list":["post-12836","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-articles"],"_links":{"self":[{"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/posts\/12836","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/comments?post=12836"}],"version-history":[{"count":1,"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/posts\/12836\/revisions"}],"predecessor-version":[{"id":12838,"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/posts\/12836\/revisions\/12838"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/media\/12837"}],"wp:attachment":[{"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/media?parent=12836"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/categories?post=12836"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/tags?post=12836"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}