حل مشکل (MySQL Error 1118 (Row size too large

خطای ERROR 1118: Row size too large (> 8126) یعنی مجموع فضایی که ستون‌های یک جدول InnoDB داخل هر سطر اشغال می‌کنند از سقف مجاز عبور کرده است. در بیشتر موارد تغییر قالب سطرِ آن جدول به ROW_FORMAT=DYNAMIC مشکل را همان‌جا حل می‌کند و راه‌حل ریشه‌ای، کم‌کردن تعداد ستون‌های عریض و اصلاح ساختار جدول است.

متن کامل خطا معمولاً به این شکل ظاهر می‌شود:

ERROR 1118 (42000) at line 1292: Row size too large (> 8126). Changing some columns to TEXT or BLOB or using ROW_FORMAT=DYNAMIC or ROW_FORMAT=COMPRESSED may help. In current row format, BLOB prefix of 768 bytes is stored inline.

این خطا دقیقاً از کجا می‌آید

InnoDB داده‌ها را در صفحه‌هایی ذخیره می‌کند که اندازهٔ پیش‌فرضشان ۱۶ کیلوبایت است و هر سطر باید در حدود نیمی از یک صفحه جا شود؛ همین محدودیت است که به عدد ۸۱۲۶ بایت می‌رسد. تفاوت قالب‌های سطر دقیقاً همین‌جا خودش را نشان می‌دهد:

  • در قالب‌های قدیمی COMPACT و REDUNDANT، از هر ستون بلند (یک VARCHAR طولانی یا TEXT) ۷۶۸ بایت اول داخل خودِ سطر ذخیره می‌شود. با چند ده ستون از این جنس، سطر پر می‌شود و خطا می‌گیرید.
  • در قالب DYNAMIC فقط یک اشاره‌گر ۲۰ بایتی داخل سطر می‌ماند و بدنهٔ داده به صفحه‌های سرریز منتقل می‌شود. به همین دلیل یک دستور ALTER ساده اغلب کافی است.

یک محدودیت دوم هم وجود دارد که پیام شبیهی می‌دهد: سقف ۶۵۵۳۵ بایت برای مجموع ستون‌ها بدون احتساب TEXT و BLOB. این یکی با تغییر قالب سطر حل نمی‌شود و حتماً باید نوع ستون‌ها را عوض کنید. هر جدول InnoDB هم حداکثر ۱۰۱۷ ستون می‌پذیرد.

چرا سایت‌های وردپرسی بیشتر درگیر می‌شوند

جدول‌های اصلی وردپرس مثل wp_options و wp_postmeta از نوع LONGTEXT هستند و عملاً هیچ‌وقت باعث این خطا نمی‌شوند؛ یک نصب تازهٔ وردپرس روی هاست وردپرس ایران یا هر سرور دیگری هم با این خطا روبه‌رو نمی‌شود. گرفتاری تقریباً همیشه از جدول‌های افزونه‌ها می‌آید: جدول‌هایی با ده‌ها ستون VARCHAR(255) که هر کدام فقط یک تنظیم کوچک را نگه می‌دارند.

عامل دومی که خیلی‌ها از قلم می‌اندازند، تغییر کاراکترست از utf8 به utf8mb4 است. utf8mb4 برای هر کاراکتر تا ۴ بایت رزرو می‌کند، بنابراین VARCHAR(255) از ۷۶۵ بایت به ۱۰۲۰ بایت می‌رسد. جدولی که سال‌ها بی‌سروصدا کار می‌کرد، درست هنگام انتقال به یک هاست یا سرور تازه، ناگهان با ۱۱۱۸ متوقف می‌شود؛ داده تغییر نکرده، فقط عرض بایتیِ ستون‌ها بیشتر شده است.

کوئری‌های تشخیص

قبل از هر تغییری، وضعیت فعلی سرور را ببینید:

SELECT @@innodb_page_size, @@innodb_default_row_format, @@innodb_file_per_table, @@innodb_strict_mode;

حالا جدول‌هایی را پیدا کنید که هنوز با قالب قدیمی ساخته شده‌اند (به جای mydb نام دیتابیس خودتان را بگذارید):

SELECT TABLE_NAME, ENGINE, ROW_FORMAT FROM information_schema.TABLES WHERE TABLE_SCHEMA='mydb' AND ROW_FORMAT NOT IN ('Dynamic','Compressed');

و برای اینکه بفهمید کدام جدول‌ها واقعاً عریض‌اند:

SELECT TABLE_NAME, COUNT(*) AS cols, SUM(CHARACTER_OCTET_LENGTH) AS bytes FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='mydb' GROUP BY TABLE_NAME ORDER BY bytes DESC LIMIT 10;

وقتی جدول مقصر مشخص شد، ستون‌هایش را به ترتیب اندازه ببینید:

SELECT COLUMN_NAME, COLUMN_TYPE, CHARACTER_OCTET_LENGTH FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='mydb' AND TABLE_NAME='wp_plugin_data' ORDER BY CHARACTER_OCTET_LENGTH DESC;

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

راه‌حل فوری: تغییر قالب سطر به DYNAMIC

برای یک جدول مشخص:

ALTER TABLE wp_plugin_data ROW_FORMAT=DYNAMIC;

اگر جدول‌ها زیادند، دستورها را با خود MySQL بسازید و خروجی را اجرا کنید:

SELECT CONCAT('ALTER TABLE `', TABLE_NAME, '` ROW_FORMAT=DYNAMIC;') FROM information_schema.TABLES WHERE TABLE_SCHEMA='mydb' AND ENGINE='InnoDB' AND ROW_FORMAT<>'Dynamic';

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

دو پیش‌نیاز را هم چک کنید. اول innodb_file_per_table که باید روشن باشد (از نسخه‌های مدرن MySQL پیش‌فرض روشن است). دوم مقدار innodb_default_row_format که بهتر است روی DYNAMIC باشد تا جدول‌های جدید هم درست ساخته شوند. مسیر این فایل در هر توزیع فرق می‌کند و فهرست کامل مسیرها را در آموزش تغییر پورت mysql در centos آورده‌ایم. این تنظیم در /etc/my.cnf یا /etc/mysql/my.cnf قرار می‌گیرد:

  • [mysqld]
  • innodb_file_per_table = 1
  • innodb_default_row_format = DYNAMIC

سپس سرویس را ری‌استارت کنید: systemctl restart mysqld (یا systemctl restart mariadb). توجه کنید که این تنظیم فقط روی جدول‌های تازه اثر دارد؛ جدول‌های موجود همچنان به ALTER نیاز دارند. اگر سرویس بعد از این ری‌استارت اصلا بالا نیامد، دلایل start نشدن سرویس Mysql را بررسی کنید. دسترسی به فایل کانفیگ فقط روی سروری ممکن است که کنترل کاملش دست خودتان باشد، مثل یک سرور مجازی ایران با دسترسی کامل مدیریتی.

وقتی خطا وسط import رخ می‌دهد

اگر خطا هنگام بازگرداندن یک فایل دامپ می‌آید، جدول هنوز وجود ندارد که بتوانید ALTER بزنید؛ باید خود فایل SQL را اصلاح کنید. اول ببینید دامپ اصلاً قالب سطر را صریح تعیین کرده یا نه:

grep -c 'ROW_FORMAT' dump.sql

اگر نتیجه صفر بود، قالب را به تعریف جدول‌ها اضافه کنید (یک نسخهٔ پشتیبان هم با .bak ساخته می‌شود):

sed -i.bak 's/ENGINE=InnoDB/ENGINE=InnoDB ROW_FORMAT=DYNAMIC/g' dump.sql

و اگر دامپ صراحتاً ROW_FORMAT=COMPACT دارد، اول آن را بردارید تا تعریف تکراری نشود و بعد دستور بالا را بزنید:

sed -i.bak 's/ROW_FORMAT=COMPACT//g' dump.sql

روی هاست اشتراکی که دسترسی SSH محدود است، ساده‌ترین راه این است که فایل دامپ را پیش از آپلود روی سیستم خودتان اصلاح کنید و بعد از طریق phpMyAdmin واردش کنید.

راه‌حل موقتی دیگری هم هست: SET SESSION innodb_strict_mode=OFF; که خطا را به یک هشدار تبدیل می‌کند. اما این فقط ساخت جدول را عبور می‌دهد؛ اگر واقعاً سطری بزرگ‌تر از حد مجاز درج شود، همان خطا موقع INSERT برمی‌گردد. از آن برای رد شدن از یک import اضطراری استفاده کنید، نه به عنوان راه‌حل دائمی.

توصیه‌ای که دیگر معتبر نیست

در راهنماهای قدیمی زیاد می‌بینید که برای خطای ۱۱۱۸ پیشنهاد می‌شود innodb_log_file_size را در my.cnf بزرگ کنید. این مربوط به خطای دیگری است (وقتی حجم یک تراکنش از ظرفیت لاگ ردو بیشتر می‌شود) و ربطی به محدودیت اندازهٔ سطر ندارد؛ اجرایش این خطا را برطرف نمی‌کند. ضمن اینکه خودِ innodb_log_file_size از MySQL 8.0.30 منسوخ شده و جای آن را innodb_redo_log_capacity گرفته است. دستور service mysqld restart هم به سبک قدیمی SysVinit است و روی توزیع‌های امروزی باید از systemctl استفاده کنید.

گزینهٔ بزرگ‌کردن صفحه با innodb_page_size = 32K نیز روی کاغذ سقف را بالاتر می‌برد، ولی فقط هنگام مقداردهی اولیهٔ دیتادایرکتوری قابل تنظیم است و روی یک سرور در حال کار عملاً به معنی راه‌اندازی مجدد از صفر و بازگرداندن کل داده‌هاست.

راه‌حل ریشه‌ای: اصلاح ساختار جدول

اگر بعد از DYNAMIC باز هم خطا می‌گیرید، جدول واقعاً بدطراحی شده است. سه کار مؤثر:

  • ستون‌هایی که متن بلند نگه می‌دارند را از VARCHAR به TEXT تغییر دهید.
  • ستون‌هایی که فقط چند مقدار ثابت یا بله/خیر دارند اما VARCHAR(255) تعریف شده‌اند را به TINYINT یا ENUM ببرید. این بیشترین صرفه‌جویی را دارد.
  • ستون‌های کم‌استفاده را به یک جدول جانبی با رابطهٔ یک‌به‌یک منتقل کنید و با کلید خارجی به جدول اصلی وصلشان کنید.

در پایان، صحت کار را با همان کوئری اول تأیید کنید: خروجی ROW_FORMAT برای جدول‌ها باید Dynamic باشد و import بدون خطا کامل شود. اگر جدول متعلق به یک افزونه است، تغییر ساختار را با سازندهٔ افزونه هماهنگ کنید تا بروزرسانی بعدی آن را برنگرداند؛ و اگر دیتابیس تولیدی و پرترافیک است، بازسازی جدول‌های بزرگ کاری است که بهتر است با پشتیبانی و مدیریت سرور پیش ببرید تا بکاپ و بازگردانی هم پیش از شروع تست شده باشد.