وقتی پای استانداردهای توسعهٔ نرمافزار به میان میآید مباحثی همچون Design Patterns یا قوانین SOLID که مرتبط با منطق اپلیکیشن هستند بسیار مورد توجه قرار میگیرد اما بخش دیگری نیز حائز اهمیت است تحت عنوان پایگاه داده که رعایت استانداردهای طراحی و مدیریت آن میتواند این تضمین را ایجاد کند که تا حد قابلتوجهی اپلیکیشن استانداردتری داشته باشیم. در عین حال، برخی توسعهدهندگان، که بخش اعظم آنها تازهکار هستند، یا نسبت به استانداردهای طراحی پایگاه داده بیاعتنا هستند و یا نسبت به آنها آگاهی لازم را ندارد و از همین روی به مرور زمان و با بیشتر شدن حجم دادهها به مشکل بر خواهند خورد که در همین راستا در این آموزش قصد داریم با یکسری از اشتباهات رایجی که در طراحی دیتابیس صورت میگیرد آشنا گردیم.
نرمالسازی پایگاه داده
Database Normalization تکنیکی در طراحی دیتابیس است که به موجب آن دادهها در چندین جدول مرتبط با یکدیگر سازماندهی شده تا افزونگی داده به حداقل برسد (Redundancy یا «افزونگی» بدان معنا است که دادهای خاص در چند جدول مختلف نگهداری شود.) وقتی در دیتابیس با افزونگی داده مواجه شویم، مشکلات عدیدهای پیشروی ما قرار خواهد گرفت به طوری که مثلاً حجم دیتابیس به خاطر ذخیرهسازی دادههای تکراری افزایش مییابد مضاف بر اینکه در حین آپدیت یا حذف دادهها ممکن است به مشکل برخوریم.
تجمیع دادههای مختلف در یک جدول
اساساً هدف از بهکارگیری دیتابیسهای رابطهای آن است که دادههای مختلف را داخل جداول مختلفی ذخیره ساخته و در صورت نیاز مابین آنها ارتباط برقرار کرد. با این توضیحات، یکی از اشتباهات رایج در طراحی دیتابیس این است که از یک جدول برای ذخیرهٔ انواع و اقسام دادهها استفاده کرد.
برای درک بهتر این موضوع، فرض کنیم جدولی داریم به اسم users
که حاوی ستونی است به اسم score
که بسته به فعالیت کاربران در سایت، دائم مقدار امتیاز ایشان در حال آپدیت شدن است. همچنین این جدول حاوی فیلدهای دیگری نیز همچون name
یا email
نیز میباشد که کمتر در معرض بهروزرسانی هستند و بیشتر جهت نمایش در سایت مورد استفاده قرار میگیرند. اِسکمای چنین جدولی به صورت زیر است:
+-------+--------------+
| Field | Type |
+-------+--------------+
| id | int(11) |
| name | varchar(255) |
| email | varchar(255) |
| score | int(11) |
+-------+--------------+
در چنین شرایطی، عملیات آپدیت روی فیلد score
منجر به کاهش پرفورمنس خواندن سایر اطلاعات کاربران میگردد که در چنین شرایطی یک سولوشن مناسب این است که جدول فوق را به دو جدول مجزا تقسیمبندی کنیم. به عبارتی، یک جدول خواهیم داشت تحت عنوان users
به صورت زیر:
+-------+--------------+
| Field | Type |
+-------+--------------+
| id | int(11) |
| name | varchar(255) |
| email | varchar(255) |
+-------+--------------+
که معمولاً برای فرایند Read (خواندن) مورد استفاده قرار میگیرد و جدول دیگری تحت عنوان user_score
به طوری که ساختارش به صورت زیر است:
+---------+---------+
| Field | Type |
+---------+---------+
| user_id | int(11) |
| score | int(11) |
+---------+---------+
این جدول دائماً قرار است بهروزرسانی گردد و این در حالی است که عملیات آپدیت منجر به کاهش سرعت فراخوانی دادهها از جدول users
نخواهد شد (البته این در شرایطی صادق است که بار روی چنین دیتابیسی زیاد باشد و چنانچه رکوئستهای ارسالی به سمت چنین جدولی بسیار اندک باشد، در کنار هم قرار گرفتن فیلدهای فوقالذکر اصلاً تداخلی ایجاد نخواهد کرد.)
چنین رویکردی در طراحی دیتابیس برای فروشگاههای آنلاین نیز کاربردی است. فرض کنیم جدولی داریم به اسم purchase
که کلیهٔ خریدها در آن ثبت میگردد. آنچه در یک فروشگاه آنلاین مسلم است اینکه پس از مدتی نیاز است تا خریدهای قبلی مشتریان را فقط به صورت آرشیو داشته باشیم که در چنین شرایطی میتوانیم خریدهای جدید مشتریان را از خریدهای تکمیلشده و قدیمی مجزا سازیم؛ به عبارتی، به جای یک جدول کلی به نام purchase
میتوان از دو جدول مختلف استفاده کرد تحت عناوین purchase
و archived_purchase
بدین صورت که سفارشات تکمیلشده که مشتریان دیگر با آنها کاری ندارند را به جدول آرشیو انتقال داد.
در واقع، سفارشات تازه دائم در حال آپدیت شدن هستند (مثلاً مشتری کالای جدیدی به سبد خرید خود اضافه میکند یا کالایی را حذف میکند.) و این در حالی است که سفارشات تکمیلشده فقط به عنوان آرشیو نگهداری میشوند و هرگز آپدیت نخواهند شد. با این تقسیمبندی دیتا، میتوانیم حجم جدول purchase
که دربرگیرندهٔ خریدهای جدید است را پایین نگاه داریم که مسلماً فراخوانی دادهها و پروسهٔ ایندکسینگ آن سریعتر اتفاق خواهد افتاد اما در عین حال آرشیوی کامل از خریدهای قبلی هم در اختیار خواهیم داشت.
💎 توجه: برای طراحی دیاگرام پایگاه داده، نرم افزار های مختلفی وجود دارد؛ جهت آشنایی با این نرم فزارها به مقالهی 9 نرمافزار برتر برای طراحی دیتابیس مراجعه کنید.
استفاده از دیتا تایپهای نامناسب برای دادهها
گاهی میبینیم که مثلاً برای ذخیرهسازی اعداد یا تاریخ، تایپ char
برای فیلدهای مربوطه انتخاب میشود در حالی که تمامی سیستمهای مدیریت پایگاه داده دارای یکسری قابلیت به اصطلاح Built-in برای ذخیرهسازی انواع و اقسام دادهها هستند که بسته به نوع دادهای که قرار است ذخیره سازیم، بهتر است از دیتا تایپ مناسب استفاده نماییم.
در نظر گرفتن طول ناکافی برای فیلدها
فرض کنیم جدولی داریم به اسم comments
که مسئولیت ذخیرهسازی نظرات کاربران سایت را بر عهده دارد:
+---------+---------------+
| Field | Type |
+---------+---------------+
| user_id | int(11) |
| comment | varchar(1000) |
+---------+---------------+
همانطور که مشاهده میشود، تایپ فیلدی تحت عنوان comment
برابر با varchar
در نظر گرفته شده است که میتواند 1000 کاراکتر را در خود جای دهد. نیاز به توضیح نیست که برخی کاربران نظرات کامل و جامعی دارند و همین باعث میگردد تا بیش از این مقدار به عنوان کامنت درج کنند که در این صورت سیستم ارور خواهد داد چرا که طول آن را محدود در نظر گرفتهایم.
استفاده از اِنکودینگ نامناسب
وقتی قرار باشد دادههایی به غیر از انگلیسی در دیتابیس خود ذخیره سازیم (همچون متون فارسی)، نیاز است تا در انتخاب اِنکودینگ کلی دیتابیس و همچنین اِنکودینگ تکتک جداول دقت به خرج دهیم (در بیشتر مواقع، اِنکودینگی همچون utf8_general_ci
در ذخیرهسازی متون فارسی نیازمان را مرتفع خواهد ساخت.)
استفادهٔ غیراصولی از Primary Key
گاهی اوقات عدم بهکارگیری از Primary Key و یا استفادهٔ غیراصولی از این ویژگی باعث میگردد تا در پروسهٔ کوئری زدن به دیتابیس با مشکل مواجه شویم. به عنوان یک قاعدهٔ پایهای، هر رکورد در دیتابیس باید دارای یک Primary Key منحصربهفرد باشد.
استفاده غیراصولی از Foreign Key
اگر دو جدول نیاز به ارتباط با یکدیگر داشته باشند، در هر دو جدول باید فیلدی وجود داشته باشد که ارتباط معناداری مابین آنها ایجاد کند. با این توضیحات، Foreign Key ستونی است که ارتباط دو جدول با یکدیگر را میسر میسازد بدین صورت که ارتباط مستقیمی با Primary Key در جدول دیگری دارد. برای درک بهتر این موضوع، جدولی به نام users
را مد نظر قرار میدهیم:
+-------+--------------+------+-----+
| Field | Type | Null | Key |
+-------+--------------+------+-----+
| id | int(11) | NO | PRI |
| name | varchar(255) | NO | |
| email | varchar(255) | NO | |
+-------+--------------+------+-----+
حال جدول دیگری به نام orders
به صورت زیر میسازیم:
+---------+---------+------+-----+
| Field | Type | Null | Key |
+---------+---------+------+-----+
| id | int(11) | NO | PRI |
| amount | int(11) | NO | |
| user_id | int(11) | YES | MUL |
+---------+---------+------+-----+
همانطور که ملاحظه میشود، در جدول فوق ستون user_id
همان ستون id
در جدول users
است که اصطلاحاً Foreign Key نامیده میشود؛ به عبارتی، شناسهٔ هر کاربر ارتباطی معنادار مابین دو این جدول برقرار کرده است (نکتهای که در ارتباط با جداول مرتبط با یکدیگر وجود دارد این است که در صورت آپدیت یا حذف دیتا از جدول اصلی، سایر جداول که به آن مرتبط هستند نیز باید آپدیت شوند که در غیر این صورت با دادههایی سروکار خواهیم داشت که اصطلاحاً Orphan نامیده میشوند.)
ارتباطهای غیرضروری مابین جداول
گرچه به سادگی و با استفاده از فیچری به نام Foreign Key میتوانیم ارتباطی معنادار مابین جداول مختلف ایجاد کنیم، اما این در حالی است که گاهی اوقات برخی ارتباطها مابین جداول غیرضروری هستند و اساساً نیازی به آنها نیست.
استفادهٔ نابجا از SQL Constraints
منظور از Constraint یک قانونی است که در زمان ثبت دادهها باید رعایت شود. برای مثال، در زبان اسکیوال برخی از این قوانین عبارتند از:
- NOT NULL: این کانستریت اطمینان حاصل میکند که فیلد نمیتواند حاوی مقدار NULL
باشد.
- UNIQUE: این کانستریت اطمینان حاصل میکند که دادههای یک فیلد نباید تکراری باشند.
- FOREIGN KEY: این کانستریت ارتباطی منحصربهفرد با رکوردی از جدول دیگری را مشخص میسازد.
اطلاع از تمامی کانستریتها در زبانی همچون اسکیوال کمک میکند تا در نهایت به شکل بهینهتری بتوان به مدیریت پایگاه داده پرداخت.
ایندکسگذاری ضعیف
نیاز به توضیح نیست که در حین فراخوانی دادهها از دیتابیس، نیاز داریم تا صرفاً فیلدهای خاصی از یک جدول را به دست آوریم. در شرایط عادی، اگر حجم دادهها کم باشد هیچگونه مشکل خاصی رخ نخواهد داد اما اگر رکوردها به چندین هزار یا چندین میلیون برسد، ریسپانس تایم افزایش مییابد و همین مسئله باعث میگردد تا پرفرومنس کلی وب اپلیکیشن تحتالشعاع قرار گیرد که برای رفع این معضل باید با اصول ایندکسگذاری جداول در سیستمهای مدیریت پایگاه داده آشنا بود. به زبان ساده، با ایندکسگذاری صحیح باید سازوکاری اندیشید تا نتایج دستورات SELECT
فوراً در اختیارمان قرار گیرند. برای درک بهتر این موضوع، فرض کنیم جدولی داریم تحت عنوان users
که حاوی اطلاعات کاربران وب اپلیکشن است:
+-------------+--------------+------+-----+
| Field | Type | Null | Key |
+-------------+--------------+------+-----+
| id | int(11) | NO | PRI |
| username | varchar(255) | NO | |
| age | int(11) | NO | |
| province_id | int(11) | NO | MUL |
+-------------+--------------+------+-----+
در جدول فوق ستونی داریم به اسم province_id
که مسئول نگهداری شناسهٔ استانی است که کاربر در آن سکونت دارد. در شرایط عادی، اگر بخواهیم کاربرانی که در یک استان خاص هستند را انتخاب کنیم، موتور دیتابیس تک به تک شروع به چک کردن تمامی رکوردها میکند تا آنها را بر مبنای province_id
سورت کند و نیاز به توضیح نیست چنانچه دیتای ثبتشده در این جدول به چندین هزار رسیده باشد، این کار زمانبر خواهد بود! حال برای آنکه بتوانیم با سرعت بیشتری کاربرانی که در یک استان بهخصوص سکونت دارند را به دست آوریم، همانطور که ملاحظه میشود، این ستون را ایندکس کردهایم (MUL برگرفته از واژهٔ Multiple است زیرا چندین و چند بار تکرار دادهای یکسان در این فیلد بلامانع است.)
حال ممکن است این پرسش ایجاد شود که «آیا میتوان تمامی فیلدهای یک جدول را ایندکسگذاری کرد تا سرعت فراخوانی دادهها افزایش یابد؟» که در پاسخ به این پرسش باید گفت درست است که پرفورمنس دستورات SELECT
افزایش مییابد اما در عین حال سرعت سایر دستورات CRUD همچون INSERT
یا UPDATE
کاهش مییابد زیرا ایندکسها دائماً باید آپدیت شوند تا معتبر باشند و نیاز به توضیح نیست که هرچه تعداد آنها در یک جدول و به طور کلی در یک دیتابیس بیشتر باشد، ریسورس بیشتری برای آپدیت آنها نیاز خواهد بود (عملیات آپدیت ایندکسها در پشت پرده و در سمت سرور دیتابیس اتفاق میافتد.)
با این تفاسیر، در هر جدول بهتر است یک ایندکس اصولی در نظر گرفت (منظور از اصولی این است که ترکیب درستی از ستونیهایی که در یک ایندکس قرار میگیرند انتخاب گردد.) همچنین ایندکسگذاری روی ستونهایی حاوی دادهٔ int
نسبت به varchar
به مراتب اثربخشی بیشتری دارد.
ستونهای تکراری در یک جدول
بسیاری از سیستمهای مدیریت پایگاه داده (DBMS) اجازهٔ درج فیلدهای تکراری در یک جدول خاص را نمیدهند که این سیاست تا حدی جلوی ذخیرهسازی دیتای تکراری را میگیرد اما در عین حال در شرایط خاصی میبینیم که در یک جدول و یا در جداول مختلف شاهد اصطلاحاً Duplicate Data (دادهٔ تکراری) هستیم که این منجر به حجیم شدن دیتابیس میگردد.
استفاده از اسامی خلاصه
چنانچه در طول رشتهای که به عنوان نام دیتابیس یا جداول داخل آن استفاده میکنیم خساست به خرج دهیم، در نهایت با نامهایی مواجه خواهیم بود که در حین پروسهٔ دیباگینگ مشکلزا خواهند شد. به عبارتی، به جای نامی همچون ui
بهتر است از user_info
استفاده کنیم (در واقع، بهتر است که محدودیت سفت و سختی برای طول نام جداول و ستونها جزو استانداردهای توسعهٔ نرمافزار خود در نظر نگیرید چرا که این قانون در بسیاری از مواقع منجر به این خواهد شد که اسامی کوتاه و در عین حال نامفهوم باشند.)
انتخاب نامهایی که متفاوت اما مترادف هستند نیز اصلاً ایدهٔ خوبی نیست. به طور مثال، اگر در یک جدول دو فیلد تحت عناوین wage
و salary
داشته باشیم، در آینده این تشابه معنایی مسلماً منجر به سردرگی سایر دولوپرها خواهد شد.
به عنوان مثالی دیگر، فرض کنیم جدولی به نام product
داریم که مسئول ذخیرهسازی دیتای مرتبط با محصولات است. در چنین جدولی بهتر است به جای نامهایی همچون product_name
و product_price
از اسامی name
و price
استفاده نمود.
مشکل دیگری که در نامگذاری به چشم میخورد استفاده از کیوردهای پیشفرض اسکیوال است. فرض کنیم جدولی داریم به اسم order
که قرار است سفارشات یک فروشگاه آنلاین را در آن ذخیره سازیم که برای فراخوانی همهٔ دادهها از این جدول خواهیم داشت:
SELECT * FROM order ORDER BY id;
اینجا است که در دیتابیس به مشکل خواهیم خورد چرا که سیستم نمیتواند تمایزی مابین نام جدول که order
است با دستور پیشفرض ORDER
قائل گردد.
به طور کلی، علاوه بر چالش برای انتخاب نام دیتابیس، جداول و ستونها نیاز است تا در ارتباط با انتخاب نامی مناسب برای ایندکسها، کانستریتها و فارین کیها نیز تصمیمگیری کنیم که در چنین شرایطی به نظر میرسد اگر پیش از شروع طراحی دیتابیس یک اصطلاحاً Naming Convention (اصول نامگذاری) داشته باشیم، روند نامگذاری اجزای مختلف دیتابیس هم برای شخصِ خودمان و هم سایر اعضای تیم تسهیل خواهد شد.
عدم لاگگیری
گاهی اوقات به دلایل مختلفی نیاز داریم تا بدانیم چه کسی چه تغییری در دادهها اِعمال کرده است که برای این منظور نیاز است تا علاوه بر دیتایی همچون create_time
و update_time
از user_ip
نیز مطلع باشیم. برای مثال، فرض کنیم کاربری در بخش کامنتینگ سایت محتوایی را به اشتراک میگذارد که بر خلاف قوانین عرف کشور باشد که در چنین شرایطی وبمستر باید پاسخگو باشد اما این در حالی است که اگر آیپی کاربر مذکور را ثبت کرده باشیم، تا حد قابلتوجهی میتوانیم مسئولیت را از خود ساقط کنیم.
اشتباه گرفتن Data Value با Data Element
پیش از پرداختن به این موضوع، نیاز است تا با مفاهیم فوقالذکر آشنا شویم. به طور کلی، منظور از Data Value دادهای است که در یکی از ستونهای جدول ذخیره میشود اما Data Element به نام خودِ ستون اشاره دارد. حال به منظور درک بهتر این موضوع، جدولی به صورت زیر را مد نظر قرار میدهیم که در آن آمار و ارقام فروش یک شرکت ذخیره شده است:
+-------------------+-----------------+-------------------+---------------+-----------+--------------+-----------------+------------+------------+------------+-----------+--------------+--------------+
| sales_person | farvardin_sales | ordibehesht_sales | khordad_sales | tir_sales | mordad_sales | shahrivar_sales | mehr_sales | aban_sales | azar_sales | day_sales | bahman_sales | esfand_sales |
+-------------------+-----------------+-------------------+---------------+-----------+--------------+-----------------+------------+------------+------------+-----------+--------------+--------------+
| behzad moradi | 1024 | 5624 | 6234 | 1000 | 2564 | 9657 | 3524 | 6537 | 1254 | 7852 | 9657 | 7541 |
| sahand iranmanesh | 1452 | 9654 | 2564 | 5624 | 9654 | 8564 | 9654 | 3526 | 6535 | 9654 | 2563 | 8654 |
| mohsen eslami | 8564 | 5325 | 6535 | 4521 | 7854 | 4521 | 6532 | 6666 | 2534 | 8655 | 7854 | 4521 |
+-------------------+-----------------+-------------------+---------------+-----------+--------------+-----------------+------------+------------+------------+-----------+--------------+--------------+
همانطور که میبینیم، «میزان فروش» پرنسل تیم فروش در قالب یکسری ستون در نظر گرفته شدهاند اما این نوع طراحی دیتابیس به دلایل زیر کاربردی نیست:
- برای تکتک فیلدها به جز sales_person
باید کانسترینت تعریف کرد.
- اگر در ماه خاصی فروشی وجود نداشته باشد باید مقدار NULL
در نظر گرفته شود.
- در چنین جدولی کوئریهای سادهای همچون مقایسهٔ میزان فروش فروشندگان مختلف کار چندان سادهای نیست.
سولوشن بهینه این است که جدول فوق را به دو جدول زیر تقسیمبندی کنیم به طوری که جدول اول sales_people
نام دارد:
+----+-------------------+
| id | name |
+----+-------------------+
| 1 | behzad moradi |
| 2 | sahand iranmanesh |
| 3 | mohsen eslami |
+----+-------------------+
سپس جدول sales_data
را به صورت زیر طراحی میکنیم:
+-----------------+-------------+-------+
| sales_person_id | month | sales |
+-----------------+-------------+-------+
| 1 | farvardin | 1024 |
| 2 | farvardin | 1452 |
| 3 | farvardin | 8564 |
| 1 | ordibehesht | 5624 |
+-----------------+-------------+-------+
با این ساختار جدید، بسته به نیازهای مختلفی که ممکن است پیش آید میتوانیم به این جدول کوئری زده و آمار مربوطه را به دست آوریم.
استفاده از ساختار EAV
EAV که مخفف واژگان Entity-Attribute-Value است به ساختار جداولی اشاره دارد که معمولاً سه ستون بیشتر ندارند؛ یک ستون برای اصطلاحاً Entity است (مثلاً شناسهٔ یک کاربر خاص)، ستون دیگر برای Property است (مثلاً نام ستون) و مورد آخر هم مربوط به Value است (مثلاً مقداری که برای پراپرتی در نظر گرفته شده است.) به عنوان جدولی که مبتنی بر EAV است، میتوان employee_info
را مد نظر قرار داد که اسکمایش به صورت زیر است:
+-----------+---------------+------------+
| entity_id | property | value |
+-----------+---------------+------------+
| 1 | first_name | behzad |
| 1 | last_name | moradi |
| 1 | date_of_birth | 1984-03-16 |
| 2 | first_name | sahand |
+-----------+---------------+------------+
نیاز به توضیح نیست که چنین جدول پویایی نیازمان به جداول مختلف را مرتفع میسازد و کسانی که این ایده را مطرح کردهاند بر این باورند که وقتی برخی اِلِمانها نامشخص باشند، این اِسکما به سادگی میتواند کلیهٔ نیازها را مرتفع سازد اما در عین حال و با مرور زمان و حجیمتر شدن دیتابیس با مشکلات زیر مواجهه خواهیم شد:
- به سادگی نمیتوان کانستریتهای الزامی را تعریف کرد چرا که ماهیت دیتا نامشخص است و یک ستون باید هر نوع دادهای را در خود ذخیره سازد.
- با این شرایط، نمیتوان Foreign Key در نظر گرفت.
- مدیریت دادهها کار چندان آسانی نخواهد بود.
- و کوئری زدن به چنین جدولی دشوار است.
اگر بخواهیم جدول فوق را بهبود بخشیم، ساختار به صورت زیر خواهد بود:
+----+------------+------------+---------------+
| id | first_name | last_name | date_of_birth |
+----+------------+------------+---------------+
| 1 | behzad | moradi | 1984-03-16 |
| 2 | sahand | iranmanesh | 2019-01-30 |
+----+------------+------------+---------------+
به عنوان یک قانون کلی در این زمینه، به جای آنکه اِسکمای دیتابیس را به گونهای طراحی کنیم که هر نوع دادهٔ نامشخصی را بتوان داخل آن ثبت کرد، بهتر آن است که در فرآیند آنالیز نرمافزار به دقت نیازها را شناسایی کرده و جداولی تخصصی بسته به نیازهای وب اپلیکیشن خود طراحی نمود.
جمعبندی
آنچه در این مقاله ذکر شد صرفاً بخشی از یکسری اصطلاحاً Best Practice در فرآیند طراحی دیتابیس بود که بهتر است رعایت شوند اما این هرگز بدان معنا نیست که نرمالسازی پایگاه داده به این موارد ختم میگردد بلکه بسته به ماهیت اپلیکیشن و نیازهای مختلف آن، باید سیاستهای مختلفی در پروسهٔ طراحی دیتابیس مورد استفاده قرار گیرد.