Beberapa waktu yang lalu, saya menjawab pertanyaan tentang NULL di Stack Exchange berjudul, “Mengapa kita tidak mengizinkan NULL?” Saya memiliki bagian dari kekesalan dan gairah hewan peliharaan, dan ketakutan akan NULL cukup tinggi dalam daftar saya. Seorang rekan baru-baru ini berkata kepada saya, setelah menyatakan preferensi untuk memaksa string kosong daripada mengizinkan NULL:
"Saya tidak suka berurusan dengan nol dalam kode."
Maaf, tapi itu bukan alasan yang bagus. Bagaimana lapisan presentasi menangani string kosong atau NULL seharusnya tidak menjadi driver untuk desain tabel dan model data Anda. Dan jika Anda mengizinkan "kurangnya nilai" di beberapa kolom, apakah penting bagi Anda dari sudut pandang logis apakah "kurangnya nilai" diwakili oleh string panjang nol atau NULL? Atau lebih buruk lagi, nilai token seperti 0 atau -1 untuk bilangan bulat, atau 1900-01-01 untuk tanggal?
Itzik Ben-Gan baru-baru ini menulis seluruh seri tentang NULL, dan saya sangat menyarankan untuk membaca semuanya:
- Kompleksitas NULL – Bagian 1
- Kompleksitas NULL – Bagian 2
- Kompleksitas NULL – Bagian 3, Fitur standar yang hilang dan alternatif T-SQL
- Kompleksitas NULL – Bagian 4, Batasan unik standar tidak ada
Tetapi tujuan saya di sini sedikit lebih rumit dari itu, setelah topik muncul dalam pertanyaan Stack Exchange yang berbeda:“Tambahkan bidang sekarang otomatis ke tabel yang ada.” Di sana, pengguna menambahkan kolom baru ke tabel yang ada, dengan tujuan mengisinya secara otomatis dengan tanggal/waktu saat ini. Mereka bertanya-tanya apakah mereka harus meninggalkan NULL di kolom itu untuk semua baris yang ada atau menetapkan nilai default (seperti 1900-01-01, mungkin, meskipun tidak eksplisit).
Mungkin mudah bagi seseorang yang tahu untuk memfilter baris lama berdasarkan nilai token—bagaimanapun juga, bagaimana orang bisa percaya bahwa semacam Bluetooth doodad diproduksi atau dibeli pada 1900-01-01? Yah, saya telah melihat ini dalam sistem saat ini di mana mereka menggunakan beberapa tanggal yang terdengar sewenang-wenang dalam tampilan untuk bertindak sebagai filter ajaib, hanya menampilkan baris di mana nilainya dapat dipercaya. Faktanya, dalam setiap kasus yang saya lihat sejauh ini, tanggal dalam klausa WHERE adalah tanggal/waktu ketika kolom (atau batasan defaultnya) ditambahkan. Yang semuanya baik-baik saja; itu mungkin bukan cara terbaik untuk menyelesaikan masalah, tapi ini a cara.
Namun, jika Anda tidak mengakses tabel melalui tampilan, implikasi dari diketahui . ini nilai masih dapat menyebabkan masalah yang berhubungan dengan logika dan hasil. Masalah logisnya adalah bahwa seseorang yang berinteraksi dengan tabel harus tahu 1900-01-01 adalah nilai token palsu yang mewakili "tidak diketahui" atau "tidak relevan." Untuk contoh dunia nyata, berapa kecepatan pelepasan rata-rata, dalam hitungan detik, untuk quarterback yang bermain di tahun 1970-an, sebelum kami mengukur atau melacak hal seperti itu? Apakah 0 nilai token yang bagus untuk "tidak diketahui"? Bagaimana dengan -1? Atau 100? Kembali ke tanggal, jika pasien tanpa ID masuk rumah sakit dan tidak sadarkan diri, apa yang harus mereka masukkan sebagai tanggal lahir? Saya tidak berpikir 1900-01-01 adalah ide yang bagus, dan itu jelas bukan ide yang baik ketika itu lebih mungkin menjadi tanggal lahir yang sebenarnya.
Implikasi Kinerja Nilai Token
Dari perspektif kinerja, nilai palsu atau "token" seperti 1900-01-01 atau 9999-21-31 dapat menimbulkan masalah. Mari kita lihat beberapa di antaranya dengan contoh yang didasarkan secara longgar pada pertanyaan terbaru yang disebutkan di atas. Kami memiliki tabel Widget dan, setelah pengembalian garansi, kami memutuskan untuk menambahkan kolom EnteredService tempat kami akan memasukkan tanggal/waktu saat ini untuk baris baru. Dalam satu kasus kami akan meninggalkan semua baris yang ada sebagai NULL, dan di sisi lain kami akan memperbarui nilai ke tanggal 1900-01-01 ajaib kami. (Untuk saat ini, kami akan mengabaikan segala jenis kompresi dari percakapan.)
CREATE TABLE dbo.Widgets_NULL ( WidgetID int IDENTITY(1,1) NOT NULL, SerialNumber uniqueidentifier NOT NULL DEFAULT NEWID(), Description nvarchar(500), CONSTRAINT PK_WNULL PRIMARY KEY (WidgetID) ); CREATE TABLE dbo.Widgets_Token ( WidgetID int IDENTITY(1,1) NOT NULL, SerialNumber uniqueidentifier NOT NULL DEFAULT NEWID(), Description nvarchar(500), CONSTRAINT PK_WToken PRIMARY KEY (WidgetID) );
Sekarang kita akan memasukkan 100.000 baris yang sama ke dalam setiap tabel:
INSERT dbo.Widgets_NULL(Description) OUTPUT inserted.Description INTO dbo.Widgets_Token(Description) SELECT TOP (100000) LEFT(OBJECT_DEFINITION(o.object_id), 250) FROM master.sys.all_objects AS o CROSS JOIN (SELECT TOP (50) * FROM master.sys.all_objects) AS o2 WHERE o.[type] IN (N'P',N'FN',N'V') AND OBJECT_DEFINITION(o.object_id) IS NOT NULL;
Kemudian kita dapat menambahkan kolom baru dan memperbarui 10% dari nilai yang ada dengan distribusi tanggal aktual, dan 90% lainnya ke tanggal token kita hanya di salah satu tabel:
ALTER TABLE dbo.Widgets_NULL ADD EnteredService datetime; ALTER TABLE dbo.Widgets_Token ADD EnteredService datetime; GO UPDATE dbo.Widgets_NULL SET EnteredService = DATEADD(DAY, WidgetID/250, '20200101') WHERE WidgetID > 90000; UPDATE dbo.Widgets_Token SET EnteredService = DATEADD(DAY, WidgetID/250, '20200101') WHERE WidgetID > 90000; UPDATE dbo.Widgets_Token SET EnteredService = '19000101' WHERE WidgetID <= 90000;
Terakhir, kita dapat menambahkan indeks:
CREATE INDEX IX_EnteredService ON dbo.Widgets_NULL (EnteredService); CREATE INDEX IX_EnteredService ON dbo.Widgets_Token(EnteredService);
Ruang Digunakan
Saya selalu mendengar "ruang disk murah" ketika kita berbicara tentang pilihan tipe data, fragmentasi, dan nilai token vs. NULL. Kekhawatiran saya bukan pada ruang disk yang digunakan oleh nilai-nilai ekstra yang tidak berarti ini. Lebih dari itu, ketika tabel ditanyakan, itu membuang-buang memori. Di sini kita bisa mendapatkan gambaran singkat tentang berapa banyak ruang yang digunakan nilai token kita sebelum dan sesudah kolom dan indeks ditambahkan:
Menyimpan ruang tabel setelah menambahkan kolom dan menambahkan indeks. Spasi hampir dua kali lipat dengan nilai token.
Eksekusi Kueri
Tak pelak lagi, seseorang akan membuat asumsi tentang data dalam tabel dan kueri terhadap kolom EnteredService seolah-olah semua nilai di sana adalah sah. Misalnya:
SELECT COUNT(*) FROM dbo.Widgets_Token WHERE EnteredService <= '20210101'; SELECT COUNT(*) FROM dbo.Widgets_NULL WHERE EnteredService <= '20210101';
Nilai token dapat mengacaukan perkiraan dalam beberapa kasus, tetapi yang lebih penting, mereka akan menghasilkan hasil yang salah (atau setidaknya tidak terduga). Berikut adalah rencana eksekusi untuk kueri terhadap tabel dengan nilai token:
Rencana eksekusi untuk tabel token; perhatikan biayanya yang tinggi.
Dan inilah rencana eksekusi untuk kueri terhadap tabel dengan NULL:
Rencana eksekusi untuk tabel NULL; perkiraan yang salah, tetapi biayanya jauh lebih rendah.
Hal yang sama akan terjadi sebaliknya jika kueri meminta>={beberapa tanggal} dan 9999-12-31 digunakan sebagai nilai ajaib yang mewakili tidak diketahui.
Sekali lagi, bagi orang-orang yang kebetulan mengetahui hasilnya salah khususnya karena Anda telah menggunakan nilai token, ini bukan masalah. Namun semua orang yang tidak mengetahuinya—termasuk calon kolega, pewaris dan pengelola kode lainnya, dan bahkan Anda yang akan menghadapi tantangan memori—mungkin akan tersandung.
Kesimpulan
Pilihan untuk mengizinkan NULL dalam kolom (atau untuk menghindari NULL sepenuhnya) tidak boleh direduksi menjadi keputusan ideologis atau berbasis rasa takut. Ada kerugian nyata dan nyata untuk merancang model data Anda untuk memastikan tidak ada nilai yang bisa NULL, atau menggunakan nilai yang tidak berarti untuk mewakili sesuatu yang bisa dengan mudah tidak disimpan sama sekali. Saya tidak menyarankan setiap kolom dalam model Anda harus mengizinkan NULL; hanya saja Anda tidak menentang gagasan dari NULL.