ایمپورت SQL بزرگ بدون تایم‌اوت و خطا

دامپ چند صد مگابایتی را نمی‌توانید از phpMyAdmin وارد کنید؟ راهکارهای عملی ایمپورت SQL بزرگ، از تقسیم فایل تا اجرای مستقیم روی سرور را ببینید.

۷ دقیقه به‌روزرسانی ۲۸ شهریور ۱۴۰۵

فایل dump.sql را انتخاب کرده‌اید، دکمه Import را زده‌اید، نوار پیشرفت تا نیمه رفته و بعد صفحه سفید شده. یا بدتر: پیام 504 Gateway Timeout آمده و وقتی لاگ MySQL را باز می‌کنید، می‌بینید جدول نصفه‌ونیمه ساخته شده و ایمپورت بعدی روی Table already exists گیر می‌کند. این دقیقاً همان جایی است که اکثر مدیرهای سایت چند ساعت را از دست می‌دهند. مشکل نه سرعت اینترنت شماست و نه خرابی دامپ؛ مسئله این است که ابزار وب برای فایل‌های بزرگ ساخته نشده.

چرا ایمپورت SQL بزرگ از phpMyAdmin شکست می‌خورد

وقتی از پنل وب ایمپورت می‌کنید، فایل اول باید از مرورگر به PHP آپلود شود، بعد PHP آن را به MySQL پاس بدهد. سه سقف جدا سر راه شماست و هر کدام پیام خطای متفاوتی می‌دهد:

  • upload_max_filesize و post_max_size در php.ini — اگر فایل بزرگ‌تر باشد، $_FILES خالی برمی‌گردد و phpMyAdmin می‌گوید «No file was uploaded».
  • max_execution_time — معمولاً ۳۰ یا ۶۰ ثانیه. فایل ۲۰۰ مگابایتی خیلی بیشتر از این طول می‌کشد و اسکریپت وسط کار کشته می‌شود.
  • max_allowed_packet در MySQL — پیش‌فرض در بسیاری از نسخه‌ها ۴ مگابایت است. اگر یک INSERT تک‌خطی بزرگ‌تر از این باشد، خطای MySQL server has gone away می‌گیرید، حتی اگر دو سقف قبلی را رد کرده باشید.

نکته‌ای که کمتر گفته می‌شود: بالا بردن این مقادیر روی هاست اشتراکی همیشه جواب نمی‌دهد، چون وب‌سرور جلوی PHP یک لایه پروکسی دارد که خودش تایم‌اوت مستقل دارد. حتی اگر max_execution_time را ۶۰۰ کنید، ممکن است پروکسی در ثانیه ۱۲۰ اتصال را ببندد. برای اینکه بدانید کدام سقف واقعاً به شما خورده، مرجع کامل محدودیت‌های منابع هاست را ببینید؛ هر عدد آنجا مشخص می‌کند کدام پارامتر چه چیزی را می‌شمارد.

راه اول: ایمپورت تکه‌ای دامپ با split

اگر دسترسی SSH دارید، این تمیزترین راه است. فایل را به قطعات کوچک بشکنید و هر قطعه را جدا وارد کنید. ابزار split روی لینوکس این کار را در چند ثانیه انجام می‌دهد، ولی باید مراقب باشید که وسط یک دستور INSERT برش نخورد.

split -l 5000 dump.sql chunk_ --additional-suffix=.sql
for f in chunk_*.sql; do
  mysql -u dbuser -p dbname < "$f" || echo "FAILED: $f"
done

عدد ۵۰۰۰ خط یک نقطه شروع معقول است؛ اگر جدول‌هایتان رکوردهای خیلی بزرگ دارند، کمترش کنید. مزیت این روش این است که وقتی قطعه هفتم شکست خورد، فقط همان را دوباره اجرا می‌کنید، نه کل دامپ را. عیبش هم واضح است: اگر دامپ شما تراکنش‌محور باشد و جدول‌ها به هم وابستگی کلید خارجی داشته باشند، اجرای ترتیبی قطعات می‌تواند موقتاً خطای constraint بدهد. در آن حالت باید SET FOREIGN_KEY_CHECKS=0; را ابتدای هر قطعه بگذارید و در انتهای آخرین قطعه روشنش کنید.

چرا split همیشه کافی نیست

بعضی دامپ‌ها یک رکورد واحد دارند که خودش ۵۰ مگابایت است (مثلاً یک فیلد LONGTEXT با محتوای base64). اینجا split خطی هیچ کمکی نمی‌کند، چون آن یک خط از سقف max_allowed_packet بزرگ‌تر است. باید یا مقدار پکت را بالا ببرید یا از mysqldump با گزینه --skip-extended-insert دامپ بگیرید تا هر رکورد یک دستور جدا باشد.

راه دوم: اجرای مستقیم روی سرور بدون آپلود از مرورگر

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

mysql -u dbuser -p --max_allowed_packet=256M dbname < /home/user/dump.sql

این دستور نه تایم‌اوت مرورگر دارد، نه سقف آپلود PHP. تنها محدودیتی که می‌ماند زمان اجرای خود کوئری‌هاست که با SET SESSION wait_timeout=0; قابل مدیریت است. اگر فایل روی سرور دیگری است، اول با scp منتقلش کنید، بعد ایمپورت. انتقال ۵۰۰ مگابایت روی شبکه داخلی معمولاً زیر یک دقیقه تمام می‌شود، در حالی که همان فایل از طریق مرورگر ممکن است ده دقیقه طول بکشد و آخرش هم شکست بخورد.

روی سرور اختصاصی این روش تقریباً همیشه بهترین انتخاب است، چون منابع را در اختیار خودتان دارید و می‌توانید موقتاً innodb_buffer_pool_size را بالا ببرید تا ایمپورت سریع‌تر شود. روی هاست اشتراکی این پارامترها قابل تغییر نیستند و باید با همان پیش‌فرض کار کنید.

راه سوم: وقتی فقط phpMyAdmin در دسترس است

بعضی هاست‌ها SSH نمی‌دهند و شما فقط پنل وب دارید. در این حالت دو کار می‌توانید بکنید. اول، فایل را با gzip فشرده کنید؛ phpMyAdmin فایل .sql.gz را خودش باز می‌کند و حجم انتقال را تا ۸۰ درصد کم می‌کند. دوم، از گزینه partial import خودش استفاده کنید: در تب Import، بخش «Partial import» را باز کنید و تعداد خطوط هر بار را مثلاً ۲۰۰۰ بگذارید. phpMyAdmin خودش فایل را از خط مشخص‌شده ادامه می‌دهد.

این روش کند است و برای دامپ‌های بالای ۵۰۰ مگابایت عملاً غیرقابل استفاده می‌شود. اگر مرتب با دامپ‌های بزرگ سر و کار دارید، وقتش رسیده که به هاست لینوکس با دسترسی SSH مهاجرت کنید؛ تفاوتش در همین لحظه‌ها معلوم می‌شود.

این‌جا اشتباه می‌کنند

رایج‌ترین اشتباهی که می‌بینم این است: کاربر ایمپورت را نصفه رها می‌کند، بعد دوباره از اول اجرا می‌کند و می‌خورد به Table 'x' already exists. بعد شروع می‌کند به دستی حذف کردن جدول‌ها، و چون جدول‌ها به هم کلید خارجی دارند، حذف هم شکست می‌خورد. علامتش این است که در phpMyAdmin لیست جدول‌ها را می‌بینید ولی تعداد رکوردها صفر است یا نصفه. راه درست این است که قبل از هر تلاش مجدد، دیتابیس را کامل خالی کنید:

DROP DATABASE dbname;
CREATE DATABASE dbname CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

اشتباه دوم: کالیشن را نادیده گرفتن. اگر دامپ با utf8mb4_unicode_ci ساخته شده و دیتابیس مقصد latin1 باشد، ایمپورت موفق می‌شود ولی متن‌های فارسی بعداً به شکل سلام درمی‌آیند. این را بعد از ایمپورت نمی‌شود درست کرد؛ باید از اول دیتابیس را با کالیشن درست بسازید.

قبل از شروع، سه چیز را چک کنید

  1. حجم فایل و تعداد خطوطش را بدانید: wc -l dump.sql و du -h dump.sql.
  2. نسخه MySQL مبدأ و مقصد را مقایسه کنید. دامپ گرفته‌شده از MySQL 8 روی سرور MySQL 5.7 اغلب خطای syntax می‌دهد، مخصوصاً در تعریف کالیشن‌ها.
  3. مطمئن شوید فضای دیسک کافی است. یک دامپ ۲۰۰ مگابایتی معمولاً بعد از ایمپورت دو تا سه برابر حجم می‌گیرد، چون ایندکس‌ها اضافه می‌شوند.

برای بررسی سریع وضعیت سرور مقصد و مطمئن شدن از اینکه به درستی resolve می‌شود، ابزار بررسی DNS و شبکه کارتان را راه می‌اندازد. و اگر می‌خواهید قبل از هر تغییری روی دیتابیس، یک نسخه پشتیبان سالم بگیرید، ابزارهای رایگان وب‌مستر نقطه شروع خوبی است.

خلاصه‌اش این است: اگر SSH دارید، هیچ‌وقت از مرورگر ایمپورت نکنید. اگر SSH ندارید، فایل را فشرده کنید و partial import را با تعداد خطوط کم اجرا کنید. و قبل از هر تلاش مجدد، دیتابیس را کامل پاک کنید تا با خطای جدول تکراری درگیر نشوید.

پرسش‌های پرتکرار

چرا ایمپورت SQL بزرگ با خطای MySQL server has gone away متوقف می‌شود؟

این خطا تقریباً همیشه به max_allowed_packet مربوط است. وقتی یک دستور INSERT از این مقدار بزرگ‌تر باشد، سرور MySQL اتصال را می‌بندد. مقدار پیش‌فرض در بسیاری از نصب‌ها ۴ مگابایت است. آن را به ۶۴ یا ۲۵۶ مگابایت افزایش دهید یا دامپ را با --skip-extended-insert بگیرید تا هر رکورد یک دستور جدا باشد.

آیا می‌توانم فایل SQL را با gzip فشرده کنم و مستقیم ایمپورت کنم؟

بله، هم phpMyAdmin و هم کلاینت خط فرمان MySQL فایل .sql.gz را می‌خوانند. در خط فرمان کافی است از zcat dump.sql.gz | mysql -u user -p dbname استفاده کنید. فشرده‌سازی حجم انتقال را به‌شدت کم می‌کند و روی دامپ‌های متنی معمولاً بین ۷۰ تا ۸۵ درصد صرفه‌جویی دارد.

چرا بعد از ایمپورت، متن‌های فارسی به شکل علامت سؤال یا کاراکترهای عجیب دیده می‌شوند؟

مشکل کالیشن است، نه ایمپورت. دیتابیس مقصد باید با CHARACTER SET utf8mb4 و COLLATE utf8mb4_unicode_ci ساخته شده باشد. اگر دامپ با این تنظیمات ساخته شده ولی مقصد latin1 است، بایت‌ها اشتباه تفسیر می‌شوند. این را بعد از ایمپورت نمی‌شود ترمیم کرد؛ باید دیتابیس را از نو با کالیشن درست بسازید و دامپ را دوباره وارد کنید.

برای دامپ ۲ گیگابایتی چه روشی را پیشنهاد می‌کنید؟

در این حجم، phpMyAdmin را کنار بگذارید. بهترین گزینه انتقال فایل به سرور با scp و اجرای مستقیم mysql < dump.sql است. اگر SSH ندارید، دامپ را به قطعات ۵۰ مگابایتی تقسیم کنید و هر قطعه را جدا وارد کنید. روی هاست اشتراکی، دامپ‌های بالای یک گیگابایت معمولاً به محدودیت منابع می‌خورند و بهتر است با پشتیبانی درباره ارتقا به پلن بالاتر صحبت کنید.

آیا این مطلب برایتان مفید بود؟