سال‌ها پیش، در اولین پروژه‌ی جدی که با MySQL کار می‌کردم، یک جدول طراحی کرده بودم که همه‌ی اطلاعات مشتری، سفارش، آدرس و محصول را در خودش داشت. آن روز فکر می‌کردم کارم حرفه‌ای است — همه‌چیز در یک جا و بدون پیچیدگی JOIN. سه ماه بعد که گزارش فروش ماهانه خواستند، فهمیدم آن طراحی، شکست خورده است: نمی‌توانستم بفهمم یک مشتری چند سفارش داده، چون نام مشتری در چند ردیف تکرار شده بود و هیچ شناسه‌ی یکتایی برای تفکیک نداشت. آن روز برایم روشن شد که آموزش MySQL از صفر فقط یاد گرفتن دستورات نیست؛ یاد گرفتن تفکر داده‌ای است: چطور داده را به شکل درست مدل کنیم، چطور روابط را ببینیم، و چطور از همان اول، پرسش‌های آینده را پیش‌بینی کنیم. در این مقاله، همان مسیری را می‌روم که با تازه‌کارها طی می‌کنم: از نصب و اولین جدول تا JOIN، ایندکس، تراکنش، و بهینه‌سازی کوئری‌های واقعی.

چرا MySQL و چه زمانی انتخاب درستی است؟

اگر تازه با دنیای وب آشنا می‌شوید، بدانید که MySQL یکی از پرکاربردترین دیتابیس‌های رابطه‌ای (Relational Database) در دنیاست. وردپرس، بسیاری از فروشگاه‌های آنلاین و بخش بزرگی از پلتفرم‌های وب روی MySQL یا معادل آزاد آن، MariaDB اجرا می‌شوند. اگر با مفاهیم پایه‌ی وب آشنا نیستید، وردپرس چیست نقطه‌ی شروع خوبی است چون نشان می‌دهد چرا دیتابیس در یک CMS نقش حیاتی دارد.

سه دلیل که MySQL را در پروژه‌های واقعی انتخاب می‌کنم:

  • سادگی نسبی: سینتکس SQL آن خواناتر از بسیاری از گزینه‌هاست و جامعه‌ی بزرگی دارد. برای کسی که تازه با دیتابیس آشنا می‌شود، این مزیت بزرگی است.
  • اکوسیستم: هر زبانی — PHP، Python، Node.js، Java — کتابخانه‌ی بالغ و تست‌شده‌ای برای اتصال به MySQL دارد.
  • مقیاس‌پذیری: از یک وبلاگ شخصی تا یک فروشگاه با میلیون‌ها رکورد، MySQL در هر دو سایز جواب می‌دهد اگر درست طراحی شود.

نکته‌ی مهمی که در پروژه‌های واقعی به آن رسیده‌ام: MySQL برای داده‌های ساختاریافته (جدولی) بی‌نظیر است، ولی برای داده‌های غیرساختاریافته یا گراف، ابزارهای دیگری مثل MongoDB یا Neo4j مناسب‌ترند. اگر این تفکیک را در ذهن داشته باشید، انتخاب‌هایتان در پروژه‌های آینده شفاف‌تر می‌شود. اگر به دیتابیس‌های NoSQL علاقه دارید، NoSQL برای چه پروژه‌هایی مناسب است تفاوت‌ها را روشن می‌کند.

دیتابیس، پایه‌ی نرم‌افزار است؛ پایه‌ای که اگر درست گذاشته نشود، هر طبقه‌ی بالای آن، سست‌تر خواهد بود.

نصب MySQL و اولین اتصال

نصب MySQL به سیستم عامل شما بستگی دارد. در سیستم‌های لینوکسی، معمولاً از پکیج‌منیجر استفاده می‌کنم:

# روی Ubuntu/Debian
sudo apt update
sudo apt install mysql-server

# روی macOS با Homebrew
brew install mysql

# بررسی نسخه
mysql --version

روی ویندوز، نصب‌کننده‌ی رسمی از سایت MySQL بهترین گزینه است. یک نکته‌ی مهم که در پروژه‌های واقعی به آن برخورده‌ام: اگر محیطی برای توسعه‌ی وب دارید، احتمالاً MySQL از قبل نصب است — چون بخش بزرگی از ابزارها مثل XAMPP و WAMP آن را همراه دارند.

پس از نصب، وارد محیط MySQL می‌شوید:

mysql -u root -p

و برای اولین بار، یک کاربر و یک دیتابیس بسازید:

CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE USER "db_user"@"localhost" IDENTIFIED BY "strong_password";
GRANT ALL PRIVILEGES ON mydb.* TO "db_user"@"localhost";
FLUSH PRIVILEGES;

USE mydb;

سه نکته‌ی مهم در همین چند خط که در پروژه‌های واقعی حیاتی است:

  • utf8mb4: برای پشتیبانی کامل از یونیکد، از جمله متن فارسی و ایموجی. اگر utf8 بگذارید، فقط کاراکترهای ۳ بایتی ذخیره می‌شوند و بعضی کاراکترهای نادر خطا می‌دهند. اگر با تفاوت این دو درگیر شده‌اید، رفع خطای Incorrect string value در MySQL مسیر رفع را نشان می‌دهد.
  • کاربر جداگانه: هرگز از کاربر root برای پروژه‌های واقعی استفاده نکنید. یک کاربر با دسترسی محدود به همان دیتابیس بسازید. این اصل، همان قائده‌ای است که در امنیت در MySQL روی آن تأکید کرده‌ام.
  • رمز قوی: رمز عبور ضعیف، مستقیماً دعوت به حمله‌ی Brute Force است. از یک رمز حداقل ۱۶ کاراکتری با ترکیب حروف، اعداد و نمادها استفاده کنید.

اگر در فرآیند نصب با خطای دسترسی موبرو شدید، رفع خطای Access denied برای کاربر MySQL و رفع خطای Can not connect to MySQL server مسیر تشخیص را گام‌به‌گام نشان می‌دهند. تجربه‌ی من: در ۸۰٪ موارد، خطای اتصال به دو دلیل است — رمز عبور اشتباه، یا اینکه سرور MySQL فقط از localhost اتصال می‌پذیرد.

دیتابیس و جدول: اولین ساختار داده

در MySQL، داده در جدول ذخیره می‌شود و جدول‌ها در دیتابیس قرار می‌گیرند. ساختار جدول با دستور CREATE TABLE تعریف می‌شود:

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(150) NOT NULL UNIQUE,
    age INT DEFAULT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

پنج تصمیم مهم در همین ساختار که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • id به‌عنوان PRIMARY KEY: هر جدول باید یک کلید اصلی داشته باشد که هر رکورد را یکتا مشخص کند. AUTO_INCREMENT کار ساخت شناسه‌ی خودکار را انجام می‌دهد.
  • NOT NULL: ستون‌هایی که هرگز نباید خالی باشند. این یک سخت‌گیری اخلاقی است: اگر نام کاربر همیشه باید پر باشد، اجازه ندهید کسی آن را خالی بگذارد.
  • UNIQUE: برای ستون‌هایی که نباید تکراری باشند (مثل ایمیل، شماره تلفن). این قید، جلوی داده‌های تکراری را می‌گیرد.
  • created_at و updated_at: در پروژه‌های واقعی، بدون این دو ستون، هیچ‌وقت نمی‌دانید چه اتفاقی چه‌وقت افتاده. CURRENT_TIMESTAMP مقدار زمان فعلی را می‌گذارد و ON UPDATE CURRENT_TIMESTAMP خودکار هنگام تغییر رکورد، تاریخ را به‌روز می‌کند.
  • ENGINE=InnoDB: موتور ذخیره‌سازی. InnoDB از تراکنش و روابط (Foreign Key) پشتیبانی می‌کند و استاندارد امروز است. MyISAM دیگر انتخاب پیش‌فرض نیست — تفاوت InnoDB و MyISAM توضیح می‌دهد چرا.

یک نکته‌ی مهم که در پروژه‌های واقعی زیاد دیده‌ام: طراحی جدول، تصمیمی است که بعداً گران عوض می‌شود. قبل از ساخت، دو سؤال از خودتان بپرسید: «چه پرسش‌هایی قرار است از این داده بپرسم؟» و «آیا فیلدهایی که امروز اضافه می‌کنم، فردا معنای جدیدی پیدا می‌کنند؟». این تفکر، جلوی بسیاری از بازطراحی‌های پرهزینه را می‌گیرد.

جدول، آینه‌ی ذهنیِ سازنده‌ی آن است؛ اگر نگاه سازنده به داده ساختارمند نباشد، جدول هم بی‌نظم می‌شود.

انواع داده: انتخاب درست، نیمی از موفقیت

MySQL طیف گسترده‌ای از انواع داده دارد. انتخاب درست، هم حافظه را صرفه‌جویی می‌کند و هم کوئری‌ها را سریع‌تر می‌سازد. جدول زیر، انواع کلیدی که در پروژه‌های واقعی استفاده می‌کنم:

دستهنوعکاربرد
عددیINT، BIGINT، SMALLINTشناسه‌ها، شماره، سن
عددی اعشاریDECIMAL، FLOAT، DOUBLEمبالغ مالی (DECIMAL)، محاسبات علمی
رشتهVARCHAR، TEXT، CHARنام، توضیحات، کدهای ثابت
تاریخ و زمانDATE، DATETIME، TIMESTAMPثبت تاریخ ایجاد، به‌روزرسانی
دودوییBLOB، VARBINARYفایل‌های باینری (کمتر توصیه می‌شود)
شمارشیENUM، SETمقادیر محدود و مشخص

سه نکته‌ی مهم در انتخاب انواع داده که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

۱) برای مبالغ مالی، DECIMAL نه FLOAT

این مهم‌ترین قاعده در طراحی مالی است. FLOAT و DOUBLE اعداد را با نمایش باینری ذخیره می‌کنند و در محاسبات اعشاری، خطاهای کوچک می‌سازند. یک مبلغ ۱۰.۲۵ ممکن است به‌عنوان ۱۰.۲۴۹۹۹۹ ذخیره شود. DECIMAL(10,2) به‌طور دقیق، ۱۰ رقم با ۲ رقم اعشار ذخیره می‌کند — همان چیزی که برای پول نیاز دارید.

price DECIMAL(10, 2) NOT NULL  -- حداکثر 99,999,999.99

۲) VARCHAR با طول درست

VARCHAR(255) برای همه‌چیز انتخاب راحتی است، ولی در پروژه‌های حجیم، تفاوت VARCHAR(50) و VARCHAR(255) محسوس است. طول واقعی مورد نیازتان را انتخاب کنید. برای نام، VARCHAR(100) کافی است. برای آدرس ایمیل، VARCHAR(150). برای توضیحات طولانی، TEXT.

۳) TIMESTAMP در برابر DATETIME

TIMESTAMP در منطقه‌ی زمانی UTC ذخیره می‌شود و هنگام خواندن، به منطقه‌ی زمانی جاری تبدیل می‌شود. DATETIME بدون منطقه‌ی زمانی ذخیره می‌شود. انتخاب من: DATETIME برای داده‌های تاریخی (مثل تاریخ تولد) و TIMESTAMP برای زمان رویدادها (زمان ثبت سفارش). این تفکیک در پروژه‌های چندمنطقه‌ای، جلوی اشتباهات عجیب را می‌گیرد.

اگر با انواع داده در پروژه‌های پایتونی هم سر و کار دارید، اتصال پایتون به MySQL نشان می‌دهد چطور این انواع بین دو سیستم ترجمه می‌شوند.

CRUD: چهار عملیات بنیادین

CRUD سرنام چهار عملیات پایه است: Create (ایجاد)، Read (خواندن)، Update (به‌روزرسانی)، Delete (حذف). این چهار، پایه‌ی هر تعاملی با دیتابیس هستند.

Create: درج داده

INSERT INTO users (name, email, age)
VALUES ("Ali", "ali@example.com", 30);

-- درج چند رکورد همزمان
INSERT INTO users (name, email, age) VALUES
    ("Sara", "sara@example.com", 25),
    ("Reza", "reza@example.com", 35);

Read: خواندن داده

SELECT id, name, email FROM users;

-- خواندن یک رکورد خاص
SELECT * FROM users WHERE id = 1;

Update: به‌روزرسانی

UPDATE users
SET age = 31, updated_at = NOW()
WHERE id = 1;

Delete: حذف

DELETE FROM users WHERE id = 1;

سه نکته‌ی حیاتی در CRUD که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • همیشه WHERE در UPDATE و DELETE: اگر WHERE را فراموش کنید، همه‌ی رکوردهای جدول تغییر می‌کنند یا حذف می‌شوند. در پروژه‌های واقعی، این اتفاق در محیط توسعه رخ می‌دهد و شما یاد می‌گیرید همیشه اول در یک تراکنش تست کنید.
  • قبل از DELETE، یک SELECT بزنید: با همان شرط. ببینید کدام رکوردها هدف قرار می‌گیرند. اگر تعداد غیرمنتظره بود، شرط را بازبینی کنید.
  • NOW() در UPDATE دستی: اگر ستون updated_at را با ON UPDATE CURRENT_TIMESTAMP تعریف کرده‌اید، خودکار به‌روز می‌شود. اگر نه، خودتان باید در کوئری بنویسید.

فیلتر داده با WHERE

قدرت واقعی SQL در فیلتر کردن داده است. WHERE شرط‌های شما را می‌سنجد:

-- شرط ساده
SELECT * FROM users WHERE age > 18;

-- ترکیب شرط‌ها
SELECT * FROM users
WHERE age >= 18 AND age <= 65 AND email LIKE "%@gmail.com";

-- شرط‌های چندگانه
SELECT * FROM users
WHERE city IN ("Tehran", "Isfahan", "Shiraz") OR vip = 1;

-- بازه
SELECT * FROM users WHERE created_at BETWEEN "2026-01-01" AND "2026-12-31";

عملگرهای اصلی که در پروژه‌های واقعی دائماً استفاده می‌کنم:

عملگرمعنیمثال
=برابریWHERE status = "active"
!= یا <>نابرابریWHERE status != "deleted"
>، <بزرگ‌تر، کوچک‌ترWHERE price > 100000
BETWEENبازهWHERE date BETWEEN "2026-01-01" AND "2026-12-31"
INعضویت در مجموعهWHERE city IN ("Tehran", "Shiraz")
LIKEالگوWHERE name LIKE "Ali%"
IS NULLخالیWHERE deleted_at IS NULL

دو نکته‌ی ظریف در WHERE که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • NULL با = مقایسه نمی‌شود: WHERE deleted_at = NULL هیچ رکوردی برنمی‌گرداند؛ باید WHERE deleted_at IS NULL استفاده کنید. این یک دام کلاسیک برای تازه‌کار است.
  • LIKE با درصد: LIKE "Ali%" یعنی «شروع با Ali»، LIKE "%Ali" یعنی «پایان با Ali» و LIKE "%Ali%" یعنی «حاوی Ali». ولی توجه کنید که LIKE "%...%" نمی‌تواند از ایندکس استفاده کند — بخش ایندکس‌گذاری در MySQL این محدودیت را توضیح می‌دهد.

ORDER BY و LIMIT: مرتب‌سازی و صفحه‌بندی

برای ارائه‌ی داده به کاربر، ترتیب و مقدار اهمیت دارد:

-- مرتب‌سازی نزولی بر اساس تاریخ
SELECT * FROM posts ORDER BY created_at DESC;

-- مرتب‌سازی چندسطحی
SELECT * FROM posts
ORDER BY status ASC, created_at DESC;

-- صفحه‌بندی
SELECT * FROM posts
ORDER BY created_at DESC
LIMIT 10 OFFSET 20;  -- 10 رکورد، از رکورد 21 به بعد

دو نکته‌ی مهم در صفحه‌بندی که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • OFFSET بزرگ کند است: اگر LIMIT 10 OFFSET 10000 بزنید، MySQL باید اول ۱۰۰۰۰ رکورد را رد کند و بعد ۱۰ بخواند. برای صفحات پایانی این کند می‌شود. راه‌حل: pagination بر اساس id (cursor-based):
-- صفحه‌بندی سریع با cursor
SELECT * FROM posts
WHERE id < 10000
ORDER BY id DESC
LIMIT 10;
  • ORDER BY روی ستون بدون ایندکس: اگر ستون مرتب‌سازی ایندکس نداشته باشد، MySQL کل داده را در حافظه مرتب می‌کند. برای جداول بزرگ، این یک گلوگاه جدی است. همیشه ستون‌های مرتب‌سازی مکرر را ایندکس کنید.

توابع تجمیعی و GROUP BY

توابع تجمیعی، محاسباتی روی گروهی از رکوردها انجام می‌دهند:

SELECT
    COUNT(*) AS total_users,
    AVG(age) AS average_age,
    MIN(age) AS min_age,
    MAX(age) AS max_age,
    SUM(orders_count) AS total_orders
FROM users
WHERE active = 1;

و GROUP BY، این محاسبات را روی گروه‌ها انجام می‌دهد:

SELECT
    city,
    COUNT(*) AS user_count,
    AVG(age) AS avg_age
FROM users
GROUP BY city
HAVING COUNT(*) > 10
ORDER BY user_count DESC;

سه نکته‌ی مهم در GROUP BY که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • تفاوت WHERE و HAVING: WHERE قبل از گروه‌بندی فیلتر می‌کند و HAVING بعد از گروه‌بندی. یعنی HAVING می‌تواند روی توابع تجمیعی شرط بگذارد ولی WHERE نمی‌تواند.
  • ONLY_FULL_GROUP_BY: در نسخه‌های جدید MySQL، همه‌ی ستون‌های SELECT که در GROUP BY نیستند، باید داخل توابع تجمیعی باشند. این محدودیت، جلوی کوئری‌های مبهم را می‌گیرد.
  • عملکرد: GROUP BY روی ستون‌های بدون ایندکس، کند است. اگر این کار را زیاد انجام می‌دهید، ایندکس روی آن ستون‌ها اضافه کنید. اصول بهینه‌سازی این نوع کوئری‌ها در بهینه‌سازی کوئری‌های MySQL با جزئیات آمده است.
توابع تجمیعی، جایی هستند که دیتابیس از یک دفتر یادداشت به یک ابزار تحلیلی تبدیل می‌شود.

JOIN: قلب دیتابیس رابطه‌ای

دیتابیس رابطه‌ای، قدرت واقعی خود را در JOIN نشان می‌دهد. اگر مفهوم کلید خارجی (Foreign Key) برایتان روشن نیست، اول بخش بعدی این مقاله را بخوانید و بعد به اینجا برگردید. چهار نوع JOIN که در پروژه‌های واقعی دائماً استفاده می‌کنم:

INNER JOIN: رکوردهای مشترک

SELECT
    u.name AS customer_name,
    o.id AS order_id,
    o.total AS order_total
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.created_at >= "2026-01-01";

INNER JOIN فقط رکوردهایی را برمی‌گرداند که در هر دو جدول تطبیق داشته باشند. اگر کاربری سفارش نداشته باشد، در نتیجه نیست.

LEFT JOIN: همه‌ی رکوردهای جدول سمت چپ

SELECT
    u.name,
    COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;

LEFT JOIN همه‌ی کاربران را برمی‌گرداند، حتی اگر سفارشی نداشته باشند. برای گزارش «هر کاربر چند سفارش داده» این نوع join انتخاب درست است.

RIGHT JOIN: معادل آینه‌ای LEFT

RIGHT JOIN نادر است ولی گاهی برای خوانایی بهتر به‌کار می‌آید. در عمل، معمولاً می‌توانید جای جداول را عوض کنید و از LEFT استفاده کنید.

Self JOIN: جدول با خودش

-- پیدا کردن کارمند و مدیرش
SELECT
    e.name AS employee,
    m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

پنج نکته‌ی مهم در JOIN که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • همیشه از alias استفاده کنید: وقتی چند جدول دارید، u.name خواناتر از users.name است — مخصوصاً در JOINهای چندگانه.
  • ترتیب JOIN مهم است: MySQL از چپ به راست، JOINها را اجرا می‌کند. جدول کوچک‌تر (با رکوردهای کمتر) را اول بیاورید.
  • ستون‌های JOIN را ایندکس کنید: در ستون‌هایی که در شرط ON استفاده می‌شوند (مثل user_id در جدول orders)، همیشه ایندکس بگذارید. این یک‌خط، سرعت JOIN را چند برابر می‌کند.
  • مراقب JOINهای انفجاری باشید: اگر شرط JOIN شما یکتا نباشد، تعداد رکوردهای خروجی می‌تواند از هر دو جدول بیشتر شود. این پدیده، «fan-out» نام دارد و در پروژه‌های واقعی، به گزارش‌های اشتباه منجر می‌شود.
  • EXPLAIN را جدی بگیرید: قبل از اجرای JOIN روی جدول بزرگ، EXPLAIN SELECT ... بزنید و خروجی را بررسی کنید. اصول خواندن EXPLAIN در بهینه‌سازی کوئری‌های MySQL آمده است.

ایندکس: تفاوت بین کوئری سریع و کند

ایندکس، ابزاری است که سرعت جستجو را از O(n) (پیمایش کل جدول) به O(log n) (جستجوی دودویی) می‌رساند. اگر این تفاوت برایتان شفاف نیست، فکر کنید کتابی را بدون فهرست الفبایی جستجو کنید — ایندکس، همان فهرست است.

-- ایندکس ساده
CREATE INDEX idx_users_email ON users (email);

-- ایندکس یکتا
CREATE UNIQUE INDEX idx_users_email ON users (email);

-- ایندکس ترکیبی
CREATE INDEX idx_orders_user_date ON orders (user_id, created_at);

-- مشاهده ایندکس‌های یک جدول
SHOW INDEX FROM users;

چهار نکته‌ی مهم در ایندکس که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • ستون‌های WHERE و JOIN و ORDER BY: این سه، کاندیدای اصلی ایندکس هستند. اگر کوئری شما در WHERE ستون خاصی دارد و کند است، اولین کار اضافه‌کردن ایندکس است.
  • ایندکس ترکیبی از چپ به راست فعال است: اگر ایندکس روی (a, b) باشد، کوئری‌هایی که فقط a را فیلتر می‌کنند از ایندکس استفاده می‌کنند، ولی کوئری‌هایی که فقط b را فیلتر می‌کنند، نه. ترتیب ستون‌ها در ایندکس مهم است.
  • ایندکس روی LIKE "%..." فایده ندارد: وقتی الگو با % شروع شود، MySQL نمی‌تواند از ایندکس استفاده کند. اگر جستجوی متنی نیاز دارید، FULLTEXT ایندکس یا ابزارهای تخصصی مثل Elasticsearch را در نظر بگیرید.
  • ایندکس‌های اضافی، هزینه دارند: هر ایندکس، سرعت INSERT و UPDATE را کم می‌کند چون باید جداگانه به‌روز شود. جدول‌هایی با ایندکس‌های زیاد، کند نوشته می‌شوند. اصول کامل این تعادل در ایندکس‌گذاری در MySQL آمده است.

یک نکته‌ی عملی از تجربه: قبل از اضافه‌کردن ایندکس روی یک جدول حجیم، همیشه در محیط staging تست کنید. ساخت ایندکس روی یک جدول با میلیون‌ها رکورد، می‌تواند دقایق طول بکشد و در محیط تولید، جدول را قفل کند.

تراکنش: همه یا هیچ

تراکنش، مجموعه‌ای از عملیات است که یا همه با موفقیت انجام می‌شوند یا هیچ‌کدام. مثال کلاسیک، انتقال پول بین دو حساب است:

START TRANSACTION;

UPDATE accounts SET balance = balance - 100000 WHERE id = 1;
UPDATE accounts SET balance = balance + 100000 WHERE id = 2;

COMMIT;

و اگر خطایی رخ داد:

START TRANSACTION;

UPDATE accounts SET balance = balance - 100000 WHERE id = 1;
-- خطایی رخ داد
ROLLBACK;  -- تغییرات لغو می‌شوند

چهار نکته‌ی مهم در تراکنش که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • InnoDB لازم است: موتور MyISAM از تراکنش پشتیبانی نمی‌کند. اگر جدول شما روی MyISAM باشد، تراکنش هیچ اثری ندارد.
  • تراکنش را کوتاه نگه دارید: تراکنش طولانی، ردیف‌ها را قفل می‌کند و در پروژه‌های پربازدید به خطای Lock wait timeout می‌انجامد.
  • سطوح Isolation: MySQL چهار سطح ایزوله دارد (READ UNCOMMITTED، READ COMMITTED، REPEATABLE READ، SERIALIZABLE). پیش‌فرض REPEATABLE READ برای اکثر پروژه‌ها مناسب است، ولی در سناریوهای خاص، درک تفاوت‌ها حیاتی است. تراکنش‌ها در MySQL این تفاوت‌ها را با مثال توضیح می‌دهد.
  • Deadlock را جدی بگیرید: وقتی دو تراکنش، هرکدام منتظر قفل دیگری باشند، Deadlock رخ می‌دهد. MySQL یکی از دو تراکنش را انتخاب و لغو می‌کند. برنامه‌ی شما باید این حالت را مدیریت کند — معمولاً با retry. اگر با خطای Deadlock در MySQL روبرو شده‌اید، مقاله‌ی مخصوص همین موضوع مسیر تشخیص را نشان می‌دهد.
تراکنش، قولِ صادقانه‌ی دیتابیس است که یا همه‌چیز اعمال می‌شود یا هیچ؛ اگر این قول را جدی نگیرید، داده‌ی شما در روز بحران، نیمه‌کاره می‌ماند.

charset و درد متن فارسی

یکی از بزرگ‌ترین دام‌های متن فارسی در MySQL، charset است. اگر با این مشکل درگیر نشده‌اید، احتمالاً هنوز به آن برنخورده‌اید — ولی دیر یا زود، برخواهید خورد. سه قاعده‌ی طلایی:

۱) از utf8mb4 استفاده کنید، نه utf8

در MySQL، utf8 فقط کاراکترهای ۳ بایتی را پشتیبانی می‌کند. یعنی همه‌ی کاراکترهای پایه، ولی کاراکترهای نادر و بعضی ایموجی‌ها را نه. utf8mb4 نسخه‌ی کامل است و از ۴ بایت پشتیبانی می‌کند.

CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

۲) Collation مناسب انتخاب کنید

برای متن فارسی، utf8mb4_persian_ci یا utf8mb4_unicode_ci. تفاوت در مرتب‌سازی و مقایسه‌ی رشته‌هاست. persian_ci برای زبان فارسی بهینه‌تر است، ولی unicode_ci عمومی‌تر و سازگارتر با داده‌های چندزبانه.

۳) در هر اتصال، charset را تنظیم کنید

اگر از یک زبان برنامه‌نویسی مثل PHP یا Python به MySQL متصل می‌شوید، charset اتصال هم باید utf8mb4 باشد:

-- در PHP PDO
$pdo = new PDO(
    "mysql:host=localhost;dbname=mydb;charset=utf8mb4",
    "user",
    "pass"
);

برای پایتون، اتصال پایتون به MySQL نمونه‌های کامل دارد. برای PHP، اتصال PHP به MySQL همین تنظیمات را با جزئیات توضیح می‌دهد.

اگر با خطای Incorrect string value روبرو شده‌اید، رفع خطای Incorrect string value در MySQL مسیر تشخیص را گام‌به‌گام نشان می‌دهد. تجربه‌ی من: در نود درصد موارد، علت این خطا، charset اشتباه در یکی از سه سطح (دیتابیس، جدول، اتصال) است.

امنیت: SQL Injection و Prepared Statements

امنیت MySQL، فقط درباره‌ی رمز عبور قوی نیست. مهم‌ترین خطر، SQL Injection است: مهاجم می‌تواند از طریق ورودی کاربر، کوئری دلخواه خودش را اجرا کند. مثال کلاسیک:

-- کد خطرناک
$query = "SELECT * FROM users WHERE email = " . $_POST["email"];

اگر کاربر ورودی " OR 1=1 -- را وارد کند، کوئری به SELECT * FROM users WHERE email = "" OR 1=1 --" تبدیل می‌شود که تمام رکوردها را برمی‌گرداند. حتی بدتر: با ورودی مناسب، می‌توان DROP TABLE اجرا کرد.

راه‌حل: Prepared Statements

-- در PHP PDO
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$_POST["email"]]);
$user = $stmt->fetch();

در Prepared Statement، پارامترها از کوئری جدا می‌شوند. MySQL ابتدا کوئری را پارس می‌کند، بعد مقادیر را جایگزین می‌کند — و همین باعث می‌شود ورودی مخرب، به‌عنوان بخشی از کوئری اجرا نشود.

سه نکته‌ی مهم در امنیت MySQL:

  • همیشه Prepared Statements: نه فقط برای فرم‌های کاربر. حتی اگر داده از سیستم خودتان می‌آید، پارامتریک کنید — چون یک روز داده از جای دیگری می‌آید.
  • کاربر با حداقل دسترسی: برای هر برنامه، یک کاربر با دسترسی محدود به همان دیتابیس بسازید. کاربر نباید دسترسی DROP یا CREATE USER داشته باشد.
  • حذف کاربران و دیتابیس‌های پیش‌فرض: دیتابیس test و کاربران نمونه در MySQL پیش‌فرض هستند. در محیط تولید، حذفشان کنید.

اگر با امنیت MySQL جدی درگیرید، بهترین روش‌های امنیت MySQL و امنیت دیتابیس وردپرس نکات تکمیلی دارند. برای درک تفاوت رمزنگاری در سطح دیتابیس، رمزنگاری دیتابیس نیز موضوع مکملی است.

در دیتابیس، یک پارامترِ غیرامن، می‌تواند کل داده‌ی شما را به گروگان بگیرد؛ Prepared Statement، تنها راه استانداردِ بستنِ این در است.

اشتباهاتی که در پروژه‌های واقعی دیده‌ام

در بازبینی پروژه‌های MySQL، این اشتباهات را زیاد دیده‌ام:

  • چسباندن رشته در کوئری: همان دام SQL Injection که در بخش امنیت توضیح دادم. حتی برای کوئری‌های داخلی، پارامتریک کنید.
  • فقدان ایندکس روی Foreign Key: در ستون‌های user_id و مشابه، اغلب ایندکس فراموش می‌شود. این یک‌خط، سرعت JOIN را چند برابر می‌کند.
  • استفاده از SELECT * در پروژه‌های حجیم: خواندن تمام ستون‌ها حتی وقتی لازم نیست، حجم داده‌ی منتقل‌شده را چند برابر می‌کند و شانس استفاده از ایندکس پوششی را از بین می‌برد.
  • نبود WHERE در UPDATE و DELETE: باعث تغییر یا حذف تمام رکوردهای جدول می‌شود. اولین قاعده‌ی کار با دیتابیس تولید: همیشه اول یک SELECT با همان شرط بزنید.
  • charset اشتباه: utf8 به‌جای utf8mb4 برای متن فارسی و ایموجی. یک‌بار تنظیم درست، همیشه راحت.
  • نبود backup: بدون backup، یک کوئری اشتباه می‌تواند ساعت‌ها کار را از بین ببرد. پشتیبان‌گیری از MySQL اصول کار را توضیح می‌دهد.
  • جدول بدون کلید اصلی: هر جدول باید یک PRIMARY KEY داشته باشد. بدون آن، به‌روزرسانی و حذف رکوردهای خاص کند و نادرست می‌شود.
  • استفاده از ENUM برای چیزهایی که تغییر می‌کنند: اگر مقادیر مجاز ممکن است تغییر کنند، ENUM انتخاب بدی است چون تغییرش نیاز به ALTER TABLE دارد. برای این موارد از جدول جداگانه و Foreign Key استفاده کنید.
  • پیچیدن به دیتابیس برای هر چیز: بعضی داده‌ها (تنظیمات، کش موقت) در فایل یا سیستم‌های دیگر جای بهتری دارند. دیتابیس، هر چیزی نیست؛ فقط داده‌ی ساختاریافته‌ی ماندگار است.
  • نبود monitoring: بدون پایش، کند شدن تدریجی کوئری‌ها را نمی‌بینید. ابزارهایی مثل slow_query_log و performance_schema برای همین هستند.

یک توصیه‌ی عملی از تجربه: قبل از هر تغییر ساختاری روی دیتابیس تولید، حتماً در محیط staging تست کنید و از دیتابیس یک backup بگیرید. یک ALTER TABLE اشتباه روی جدول حجیم، می‌تواند ساعت‌ها طول بکشد. اصول کاربردی این نوع تغییرات در بهینه‌سازی جداول MySQL برای سرعت بیشتر با جزئیات آمده است.

از اینجا به کجا؟

MySQL، از یک SELECT ساده شروع می‌شود ولی در پروژه‌های واقعی، به ستون فقرات یک برنامه تبدیل می‌شود. سه نکته‌ی اصلی که در این مقاله به آن‌ها رسیدیم: اول، طراحی درست جدول و انتخاب نوع داده، نیمی از موفقیت است — این تصمیم‌ها، بعداً گران عوض می‌شوند؛ دوم، ایندکس، JOIN و تراکنش سه ابزاری هستند که تفاوت بین یک دیتابیس معمولی و یک دیتابیس حرفه‌ای را می‌سازند؛ سوم، امنیت و charset را از روز اول جدی بگیرید — چون جبرانشان در ماه ششم، به بازنویسی کامل منجر می‌شود.

اگر امروز می‌خواهید شروع کنید، سه کار کوچک پیشنهاد می‌کنم: یک دیتابیس تستی با utf8mb4 بسازید، یک جدول ساده با حداقل پنج ستون طراحی کنید، و در آن چند رکورد با متن فارسی درج کنید. همین پروژه‌ی کوچک، ۹۰٪ مفاهیم پایه‌ی این مقاله را در عمل زنده می‌کند. مسیر طبیعی بعدی، یادگیری عمیق‌تر بهینه‌سازی کوئری‌ها، ایندکس‌گذاری، و تراکنش‌ها است. اگر تجربه‌ای از MySQL در پروژه‌های خودتان دارید — مخصوصاً اگر با چالش charset، کارایی یا امنیت روبرو شده‌اید — در دیدگاه‌ها بنویسید؛ همین نکته‌های میدانی، برای خواننده‌ی بعدی از هر مستند رسمی ارزشمندتر است. 🗄️