Profesional data tidak selalu dapat menggunakan database yang memiliki desain yang optimal. Terkadang hal-hal yang membuat Anda menangis adalah hal-hal yang telah kita lakukan pada diri kita sendiri, karena itu tampak seperti ide yang bagus pada saat itu. Terkadang mereka karena aplikasi pihak ketiga. Terkadang mereka mendahului Anda.
Yang saya pikirkan dalam posting ini adalah ketika kolom datetime Anda (atau datetime2, atau lebih baik lagi, datetimeoffset) sebenarnya adalah dua kolom – satu untuk tanggal, dan satu untuk waktu. (Jika Anda memiliki kolom terpisah lagi untuk offset, maka saya akan memeluk Anda lain kali saya melihat Anda, karena Anda mungkin harus berurusan dengan semua jenis luka.)
Saya melakukan survei di Twitter, dan menemukan bahwa ini adalah masalah yang sangat nyata bahwa sekitar setengah dari Anda harus berurusan dengan tanggal dan waktu dari waktu ke waktu.
AdventureWorks hampir melakukan ini – jika Anda melihat tabel Sales.SalesOrderHeader, Anda akan melihat kolom tanggal waktu yang disebut OrderDate, yang selalu memiliki tanggal pasti di dalamnya. Saya yakin jika Anda adalah pengembang laporan di AdventureWorks, Anda mungkin pernah menulis kueri yang mencari jumlah pesanan pada hari tertentu, menggunakan GROUP BY OrderDate, atau semacamnya. Bahkan jika Anda tahu bahwa ini adalah kolom datetime dan ada potensi untuk itu juga menyimpan waktu non-tengah malam, Anda masih akan mengatakan GROUP BY OrderDate hanya demi menggunakan indeks dengan benar. GROUP BY CAST (OrderDate AS DATE) tidak memotongnya.
Saya memiliki indeks pada OrderDate, seperti yang Anda lakukan jika Anda secara teratur menanyakan kolom itu, dan saya dapat melihat bahwa pengelompokan berdasarkan CAST(OrderDate AS DATE) sekitar empat kali lebih buruk dari perspektif CPU.
Jadi saya mengerti mengapa Anda akan senang untuk menanyakan kolom Anda seolah-olah itu adalah tanggal, hanya mengetahui bahwa Anda akan memiliki dunia kesakitan jika penggunaan kolom itu berubah. Mungkin Anda memecahkan ini dengan memiliki kendala di atas meja. Mungkin Anda hanya meletakkan kepala Anda di pasir.
Dan ketika seseorang datang dan berkata "Anda tahu, kita harus menyimpan waktu pesanan terjadi juga", Anda memikirkan semua kode yang mengasumsikan OrderDate hanyalah sebuah tanggal, dan gambar yang memiliki kolom terpisah yang disebut OrderTime (tipe data waktu, tolong) akan menjadi pilihan yang paling masuk akal. Saya mengerti. Ini tidak ideal, tetapi berfungsi tanpa merusak terlalu banyak barang.
Pada titik ini, saya sarankan Anda juga membuat OrderDateTime, yang akan menjadi kolom terhitung yang menggabungkan keduanya (yang harus Anda lakukan dengan menambahkan jumlah hari sejak hari 0 ke CAST(OrderDate as datetime2), daripada mencoba menambahkan waktu ke tanggal, yang umumnya jauh lebih berantakan). Dan kemudian indeks OrderDateTime, karena itu masuk akal.
Tetapi cukup sering, Anda akan menemukan diri Anda dengan tanggal dan waktu sebagai kolom terpisah, pada dasarnya tidak ada yang dapat Anda lakukan untuk itu. Anda tidak dapat menambahkan kolom yang dihitung, karena ini adalah aplikasi pihak ketiga dan Anda tidak tahu apa yang mungkin rusak. Apakah Anda yakin mereka tidak pernah melakukan SELECT *? Suatu hari saya berharap mereka mengizinkan kami menambahkan kolom dan menyembunyikannya, tetapi untuk saat ini, Anda pasti berisiko merusak barang.
Dan, Anda tahu, bahkan msdb melakukan ini. Keduanya bilangan bulat. Dan itu karena kompatibilitas ke belakang, saya berasumsi. Tapi saya ragu Anda mempertimbangkan untuk menambahkan kolom yang dihitung ke tabel di msdb.
Jadi bagaimana kita menanyakan ini? Misalkan kita ingin menemukan entri yang berada dalam rentang waktu tertentu?
Mari kita lakukan beberapa percobaan.
Pertama, mari buat tabel dengan 3 juta baris, dan indeks kolom yang penting bagi kita.
select identity(int,1,1) as ID, OrderDate, dateadd(minute, abs(checksum(newid())) % (60 * 24), cast('00:00' as time)) as OrderTime into dbo.Sales3M from Sales.SalesOrderHeader cross apply (select top 100 * from master..spt_values) v; create index ixDateTime on dbo.Sales3M (OrderDate, OrderTime) include (ID);
(Saya bisa saja menjadikannya indeks berkerumun, tetapi menurut saya indeks yang tidak berkerumun lebih umum untuk lingkungan Anda.)
Data kami terlihat seperti ini, dan saya ingin mencari baris antara, katakanlah, 2 Agustus 2011 pukul 8:30, dan 5 Agustus 2011 pukul 21:30.
Dengan melihat melalui data, saya dapat melihat bahwa saya ingin semua baris antara 48221 dan 50171. Itu 50171-48221+1=1951 baris (+1 karena ini adalah rentang inklusif). Ini membantu saya yakin bahwa hasil saya benar. Anda mungkin akan memiliki yang serupa di mesin Anda, tetapi tidak tepat, karena saya menggunakan nilai acak saat membuat tabel saya.
Saya tahu bahwa saya tidak bisa melakukan sesuatu seperti ini:
select * from dbo.Sales3M where OrderDate between '20110802' and '20110805' and OrderTime between '8:30' and '21:30';
…karena ini tidak termasuk sesuatu yang terjadi dalam semalam pada tanggal 4. Ini memberi saya 1268 baris – jelas tidak benar.
Salah satu opsi adalah menggabungkan kolom:
select * from dbo.Sales3M where dateadd(day,datediff(day,0,OrderDate),cast(OrderTime as datetime2)) between '20110802 8:30' and '20110805 21:30';
Ini memberikan hasil yang benar. Itu tidak. Hanya saja ini benar-benar tidak dapat dimaklumi, dan memberi kami Pemindaian di semua baris di tabel kami. Pada 3 juta baris kami, mungkin perlu beberapa detik untuk menjalankan ini.
Masalah kami adalah bahwa kami memiliki kasus biasa, dan dua kasus khusus. Kita tahu bahwa setiap baris yang memenuhi OrderDate> '20110802' AND OrderDate <'20110805' adalah yang kita inginkan. Tapi kita juga membutuhkan setiap baris yang pada atau setelah 8:30 pada 20110802, dan pada atau sebelum 21:30 pada 20110805. Dan itu membawa kita ke:
select * from dbo.Sales3M where (OrderDate > '20110802' and OrderDate < '20110805') or (OrderDate = '20110802' and OrderTime >= '8:30') or (OrderDate = '20110805' and OrderTime <= '21:30');
ATAU mengerikan, saya tahu. Itu juga dapat menyebabkan Pemindaian, meskipun tidak harus. Di sini saya melihat tiga Pencarian Indeks, digabungkan dan kemudian diperiksa keunikannya. Pengoptimal Kueri jelas menyadari bahwa seharusnya tidak mengembalikan baris yang sama dua kali, tetapi tidak menyadari bahwa ketiga kondisi tersebut saling eksklusif. Dan sebenarnya, jika Anda melakukan ini dalam rentang waktu satu hari, Anda akan mendapatkan hasil yang salah.
Kita bisa menggunakan UNION ALL dalam hal ini, yang berarti QO tidak akan peduli apakah kondisinya saling eksklusif. Ini memberi kita tiga Seek yang digabungkan – itu cukup bagus.
select * from dbo.Sales3M where (OrderDate > '20110802' and OrderDate < '20110805') union all select * from dbo.Sales3M where (OrderDate = '20110802' and OrderTime >= '8:30') union all select * from dbo.Sales3M where (OrderDate = '20110805' and OrderTime <= '21:30');
Tapi itu masih tiga pencarian. Statistics IO memberi tahu saya bahwa ada 20 pembacaan di mesin saya.
Sekarang, ketika saya berpikir tentang sargability, saya tidak hanya berpikir untuk menghindari menempatkan kolom indeks di dalam ekspresi, saya juga memikirkan apa yang mungkin membantu sesuatu tampak sargable.
Ambil WHERE LastName LIKE 'Far%' misalnya. Ketika saya melihat rencana untuk ini, saya melihat Seek, dengan Predikat Seek mencari nama apa pun dari Jauh hingga (tetapi tidak termasuk) FaS. Dan kemudian ada Predikat Residual yang memeriksa kondisi LIKE. Ini bukan karena QO menganggap LIKE itu sargable. Jika ya, itu akan dapat menggunakan LIKE di Seek Predicate. Karena ia tahu bahwa segala sesuatu yang dipenuhi oleh kondisi LIKE itu harus berada dalam kisaran itu.
Ambil WHERE CAST(OrderDate AS DATE) ='20110805'
Di sini kita melihat Seek Predicate yang mencari nilai OrderDate antara dua nilai yang telah dikerjakan di tempat lain dalam rencana, tetapi membuat rentang di mana nilai yang benar harus ada. Ini bukan>=20110805 00:00 dan <20110806 00:00 (yang akan saya lakukan), ini sesuatu yang lain. Nilai awal rentang ini harus lebih kecil dari 20110805 00:00, karena>, bukan>=. Yang benar-benar dapat kami katakan adalah bahwa ketika seseorang di Microsoft menerapkan bagaimana QO harus menanggapi predikat semacam ini, mereka memberikan informasi yang cukup untuk menghasilkan apa yang saya sebut "predikat pembantu."
Sekarang, saya ingin Microsoft membuat lebih banyak fungsi yang dapat dimaklumi, tetapi permintaan khusus itu telah Ditutup jauh sebelum mereka menghentikan Connect.
Tapi mungkin yang saya maksud adalah agar mereka membuat lebih banyak predikat pembantu.
Masalah dengan predikat pembantu adalah bahwa mereka hampir pasti membaca lebih banyak baris daripada yang Anda inginkan. Tapi itu masih jauh lebih baik daripada melihat seluruh indeks.
Saya tahu bahwa semua baris yang ingin saya kembalikan akan memiliki OrderDate antara 20110802 dan 20110805. Hanya saja ada beberapa yang tidak saya inginkan.
Saya bisa saja menghapusnya, dan ini akan valid:
select * from dbo.Sales3M where OrderDate between '20110802' and '20110805' and not (OrderDate = '20110802' and OrderTime < '8:30') and not (OrderDate = '20110805' and OrderTime > '21:30');
Tapi saya merasa ini adalah solusi yang membutuhkan upaya pemikiran untuk menghasilkan. Sedikit usaha di pihak pengembang adalah dengan hanya memberikan predikat pembantu untuk versi kami yang benar-tapi-lambat.
select * from dbo.Sales3M where dateadd(day,datediff(day,0,OrderDate),cast(OrderTime as datetime2)) between '20110802 8:30' and '20110805 21:30' and OrderDate between '20110802' and '20110805';
Kedua kueri ini menemukan 2300 baris yang berada pada hari yang tepat, dan kemudian perlu memeriksa semua baris tersebut terhadap predikat lainnya. Seseorang harus memeriksa dua kondisi NOT, yang lain harus melakukan beberapa konversi tipe dan matematika. Tetapi keduanya jauh lebih cepat daripada yang kami miliki sebelumnya, dan melakukan satu Pencarian (13 bacaan). Tentu, saya mendapat peringatan tentang RangeScan yang tidak efisien, tetapi ini adalah preferensi saya daripada melakukan tiga yang efisien.
Dalam beberapa hal, masalah terbesar dengan contoh terakhir ini adalah bahwa beberapa orang yang bermaksud baik akan melihat bahwa predikat helper berlebihan dan mungkin menghapusnya. Ini adalah kasus dengan semua predikat pembantu. Jadi beri komentar.
select * from dbo.Sales3M where dateadd(day,datediff(day,0,OrderDate),cast(OrderTime as datetime2)) between '20110802 8:30' and '20110805 21:30' /* This next predicate is just a helper to improve performance */ and OrderDate between '20110802' and '20110805';
Jika Anda memiliki sesuatu yang tidak cocok dengan predikat sargable yang bagus, kerjakan salah satunya, dan kemudian cari tahu apa yang perlu Anda kecualikan darinya. Anda mungkin akan menemukan solusi yang lebih baik.
@rob_farley