توضیح دوات: ORMها معمولا به صورت اتوماتیک از selectهای دقیق و صریح استفاده میکنند. بنابراین در صورتی که شما از ORM استفاده میکنید از این مشکل در امان هستید، با این همه برای درک مشکل ایجاد شده ادامه را بخوانید.
از استفاده از SELECT *
حتی در جداول تکستونه اجتناب کنید. این نکته را به خاطر بسپارید حتی اگر مخالف باشید. شاید تا پایان، این مقاله نظر شما را تغییر دهد.
داستانی از سال ۲۰۱۲
این یک داستان واقعی است که حدود ۱۲ سال پیش (تقریباً در سالهای ۲۰۱۲ تا ۲۰۱۳) در یک اپلیکیشن بکاند مشتری با آن مواجه شدم.
یک روز کاربران از کندی و تجربه کاربری نامطلوبی شکایت کردند که توسط یک API بکاند پایدار و در زمان میلیثانیههای تکرقمی اجرا میشد.
کدهای ثبتشده را بررسی کردیم و هیچ مورد مشکوکی به چشم نمیخورد؛ بیشتر تغییرات ساده و بیاهمیت بودند. با این حال، برای اطمینان، تمام تغییرات ثبتشده را برگرداندیم (شاید برخی از شما با این موقعیت آشنا باشید؛ وقتی اوضاع وخیم میشود حتی به دلایل غیرمنطقی شک میکنید).
اما اپ همچنان کند بود. با بررسی دقیقتر، متوجه شدیم زمان پاسخ API از ۵۰۰ میلیثانیه تا گاهی ۲ ثانیه افزایش یافته است، درحالیکه قبلاً در حد میلیثانیههای تکرقمی بود. میدانستیم هیچ تغییری در بکاند ایجاد نشده که باعث این کندی شود، اما شروع به بررسی کوئریهای دیتابیس کردیم.
SELECT *
روی یک جدول که ۳ فیلد blob دارد به بکاند برگردانده میشد؛ این فیلدهای blob شامل اسناد بزرگی بودند. مشخص شد که این جدول در ابتدا فقط ۲ ستون عددی داشت و API برای برگرداندن این دو ستون از SELECT *
استفاده میکرد. اما بعدها، ادمین ۳ ستون blob به جدول اضافه کرده بود که توسط اپلیکیشنی دیگر استفاده و مقداردهی میشدند.
با این که این فیلدهای blob به کلاینت برگردانده نمیشدند، API بکاند مجبور بود این فیلدهای اضافی را که توسط برنامههای دیگر مقداردهی شده بودند، بخواند و این باعث سربار اضافی برای دیتابیس، شبکه و پروتکلهای سریالسازی شد.
نحوه عملکرد خوانش دیتابیس
در یک موتور دیتابیس با ذخیرهسازی ردیفی (row-store)، ردیفها در واحدهایی به نام صفحه (page) ذخیره میشوند. هر صفحه یک هدر ثابت دارد و شامل چندین ردیف است که هر ردیف دارای یک هدر رکورد و ستونهای مربوطه است. به عنوان مثال، به مثال زیر در PostgreSQL توجه کنید:
وقتی دیتابیس یک صفحه را خوانده و در حافظه مشترک (shared buffer pool) قرار میدهد، به تمام ردیفها و ستونهای آن صفحه دسترسی داریم. بنابراین این سوال پیش میآید: اگر تمام ستونها در حافظه در دسترس هستند، چرا SELECT *
کند و پرهزینه است؟ آیا واقعاً به اندازهای که میگویند کند است؟ و اگر چنین است، چرا؟ در این مقاله به این سوالات و موارد دیگر خواهیم پرداخت.
خداحافظی با اسکنهایی که تنها از ایندکس استفاده میکنند (Index-Only Scans)
استفاده از SELECT *
به این معناست که بهینهساز دیتابیس نمیتواند اسکنهای ایندکس-تنها را انتخاب کند. به عنوان مثال، فرض کنید نیاز دارید شناسههای دانشآموزانی که نمره بالای ۹۰ گرفتهاند را استخراج کنید و ایندکسی روی ستون نمرات دارید که شامل شناسه دانشآموز به عنوان یک غیرکلید (non-key) است؛ این ایندکس برای این کوئری ایدهآل است.
اما چون درخواست تمامی فیلدها را دادهاید، دیتابیس مجبور است به صفحه داده اصلی (heap data page) دسترسی پیدا کند تا فیلدهای باقیمانده را استخراج کند، که منجر به افزایش خوانشهای تصادفی و در نتیجه I/O بسیار بیشتر میشود. در مقابل، اگر از SELECT *
استفاده نمیکردید، دیتابیس فقط ایندکس نمرات را اسکن میکرد و شناسهها را برمیگرداند.
هزینههای deserialization
رمزگشایی (deserialization) فرآیند تبدیل بایتهای خام به انواع داده است. این فرآیند شامل گرفتن دنبالهای از بایتها (معمولاً از یک فایل، ارتباط شبکهای یا منبع دیگر) و تبدیل آن به یک قالب داده ساختارمندتر، مانند اشیا یا متغیرها در یک زبان برنامهنویسی است.
وقتی یک کوئری SELECT *
اجرا میکنید، دیتابیس مجبور است تمام ستونها را deserialize کند، حتی ستونهایی که ممکن است در کاربرد خاص شما نیازی به آنها نداشته باشید. این امر میتواند باعث افزایش سربار محاسباتی و کندی عملکرد کوئری شود. با انتخاب فقط ستونهای مورد نیاز، میتوانید هزینه deserialization را کاهش داده و کارایی کوئریها را بهبود بخشید.
همه ستونها به صورت درونخطی ذخیره نمیشوند
یکی از مشکلات مهم با کوئریهای SELECT *
این است که همه ستونها بهصورت درونخطی در یک صفحه ذخیره نمیشوند. ستونهای بزرگ، مانند متن یا blobها، ممکن است در جداول خارجی ذخیره شوند و فقط در صورت درخواست بازیابی شوند (جداول TOAST در PostgreSQL نمونهای از این موارد هستند). این ستونها معمولاً فشرده میشوند؛ بنابراین وقتی یک کوئری SELECT *
با بسیاری از فیلدهای متنی، دادههای هندسی یا blob اجرا میکنید، سربار اضافی روی دیتابیس برای بازیابی مقادیر از جداول خارجی، خارجسازی از حالت فشرده (decompress)، و برگرداندن نتایج به کلاینت تحمیل میشود.
هزینههای شبکه
قبل از اینکه نتیجه کوئری به کلاینت ارسال شود، باید طبق پروتکل ارتباطی دیتابیس سریالسازی شود. هرچه داده بیشتری نیاز به سریالسازی داشته باشد، کار بیشتری از CPU لازم است. پس از سریالسازی بایتها، آنها از طریق TCP/IP منتقل میشوند. هرچه تعداد سگمنتهای ارسالشده بیشتر باشد، هزینه انتقال بیشتر شده و در نهایت باعث افزایش تأخیر شبکه میشود.
برگرداندن همه ستونها ممکن است نیازمند سریالسازی ستونهای بزرگی مانند رشتهها یا blobها باشد که کلاینت شاید هرگز از آنها استفاده نکند.
deserialization در کلاینت
وقتی کلاینت بایتهای خام را دریافت میکند، اپلیکیشن کلاینت باید دادهها را به زبان برنامهنویسی خود deserialize کند که به زمان پردازش کلی اضافه میکند. هرچه داده بیشتری در جریان باشد، این فرآیند کندتر خواهد بود.
پیشبینیناپذیری
استفاده از SELECT *
در سمت کلاینت، حتی اگر تنها به یک فیلد نیاز داشته باشید، میتواند عدم پیشبینیپذیری را ایجاد کند. به این مثال توجه کنید: شما جدولی دارید با یک یا دو فیلد و اپلیکیشن شما یک SELECT *
انجام میدهد و با سرعت زیاد دو فیلد عددی را بازیابی میکند.
اما بعداً ادمین تصمیم میگیرد یک فیلد XML، JSON، blob و سایر فیلدهایی که توسط اپلیکیشنهای دیگر مقداردهی و استفاده میشوند، اضافه کند. با وجود اینکه کد شما اصلاً تغییری نکرده است، عملکرد ناگهان کند میشود زیرا اکنون تمام فیلدهای اضافی که اپلیکیشن شما از ابتدا نیازی به آنها نداشته است، بازیابی میشوند.
جستجوی کد (Code Grep)
یکی دیگر از مزایای SELECT صریح (explicit) این است که میتوانید در کدبیس به دنبال ستونهایی که استفاده میشوند جستجو کنید (grep). بنابراین، اگر بخواهید یک ستون را تغییر نام دهید یا حذف کنید، این کار سادهتر خواهد بود. این امر تغییرات DDL (تعریف داده) در طرح پایگاه داده را قابل مدیریتتر میکند.
خلاصه
در نتیجه، یک کوئری SELECT *
فرآیندهای پیچیده متعددی را در بر میگیرد؛ بنابراین بهتر است فقط ستونهایی که نیاز دارید را انتخاب کنید تا از سربارهای غیرضروری جلوگیری کنید. به خاطر داشته باشید که اگر جدول شما شامل چند ستون با انواع داده ساده باشد، سربار یک کوئری SELECT *
ممکن است ناچیز باشد. با این حال، به طور کلی بهترین روش این است که در کوئریهای خود درباره ستونهایی که بازیابی میکنید، گزینشی عمل کنید.