اولین بار که خطای Duplicate entry در MySQL یک پروژه‌ی فروشگاهی را متوقف کرد، وسط یک کمپین تبلیغاتی بود. هر تلاش کاربر برای ثبت سفارش، با پیام ساده‌ای شکست می‌خورد: Duplicate entry '12345' for key 'order_number'. تا آن روز، این خطا را به‌عنوان یک مشکل ساده می‌شناختم که با حذف رکورد تکراری حل می‌شود. آن روز فهمیدم که خطای Duplicate entry در MySQL، در ظاهر یک خطای ساده به‌نظر می‌رسد ولی در باطن، پنجره‌ای است به سمت معماری صحیح داده، تعریف constraintها، تفکر همزمانی (concurrency)، و طراحی درست جداول در سطح مهندسی داده.

خطای Duplicate entry در MySQL دقیقاً چیست؟

MySQL برای حفظ یکپارچگی داده، از محدودیت‌هایی به نام constraint استفاده می‌کند. یکی از رایج‌ترین این محدودیت‌ها، UNIQUE است که اجازه نمی‌دهد دو رکورد با مقدار یکسان در یک یا چند ستون مشخص وجود داشته باشند. علاوه بر این، هر جدول یک PRIMARY KEY دارد که به‌طور پیش‌فرض UNIQUE است. وقتی اپلیکیشن شما تلاش می‌کند یک رکورد جدید درج کند که مقدار آن با یک رکورد موجود در ستون UNIQUE یا PRIMARY KEY تلاقی دارد، MySQL خطای زیر را مطرح می‌کند:

ERROR 1062 (23000): Duplicate entry '12345' for key 'order_number'

# یا در PDO:

SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'user@example.com' for key 'users.email'

# یا در Django:

IntegrityError: (1062, "Duplicate entry 'admin' for key 'username'")

پیام خطا چند داده‌ی مهم دارد: کد خطا (1062)، SQLSTATE (23000)، مقدار تکراری، و نام index یا constraint که نقض شده است. ترکیب این داده‌ها، جهت تشخیص را از ابتدا روشن می‌کند.

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

اگر با مبانی MySQL آشنایی ندارید، ابتدا یادگیری MySQL از صفر را بخوانید تا مدل ذهنی درستی از جدول، کلید، و constraint شکل بگیرد. درک خطای Duplicate entry بدون فهم مکانیزم UNIQUE و PRIMARY KEY ممکن نیست.

خطای Duplicate entry یک شکایت از یکپارچگی است، نه از کد. MySQL می‌گوید این رکورد با یک قانونی که خودت وضع کردی در تناقض است. راه‌حل، بررسی قانون است نه سرکوب خطا.

کد خطا و پیام‌های مختلف

خطای Duplicate entry در نسخه‌های مختلف MySQL و در لایه‌های مختلف، پیام‌های متفاوتی دارد. شناخت این تفاوت‌ها، در تشخیص سریع کمک‌کننده است:

کد خطا SQLSTATE پیام
1062 23000 Duplicate entry for key
1586 23000 Duplicate entry for key (with multiple constraints)
1169 23000 Can't write; duplicate key in table
1022 23000 Can't write; duplicate key in table (MyISAM)

در MySQL 8.x، پیام خطا معمولاً اطلاعات دقیق‌تری می‌دهد و نشان می‌دهد کدام constraint نقض شده. در MariaDB، رفتار مشابه است. تفاوت‌های جزئی در پیام وجود دارد ولی مفهوم یکسان است.

جزئیات کامل سایر خطاهای MySQL در خطاهای رایج MySQL آمده است.

انواع constraint و مکانیزم تشخیص تکراری

در MySQL، چهار نوع اصلی constraint وجود دارد که می‌توانند به خطای Duplicate entry منجر شوند:

یک: PRIMARY KEY. هر جدول فقط یک PRIMARY KEY دارد که به‌طور پیش‌فرض UNIQUE و NOT NULL است. نقش اصلی آن، شناسه‌ی یکتای هر رکورد است.

دو: UNIQUE. می‌توان چند UNIQUE constraint در یک جدول داشت. هر UNIQUE، می‌تواند روی یک ستون یا ترکیبی از چند ستون تعریف شود.

سه: Composite UNIQUE. ترکیب چند ستون که با هم باید یکتا باشند. مثال: ترکیب (user_id, product_id) در جدول امتیازات.

چهار: Foreign Key با ON DUPLICATE. در بعضی سناریوهای پیشرفته، Foreign Key با تنظیم ON DUPLICATE KEY UPDATE می‌تواند رفتار متفاوتی داشته باشد.

مکانیزم تشخیص تکراری در MySQL با استفاده از B-Tree Index انجام می‌شود. هر UNIQUE index، یک درخت B-Tree است که جستجو در آن O(log n) است. وقتی رکورد جدیدی درج می‌شود، MySQL در B-Tree جستجو می‌کند که آیا مقدار مشابهی وجود دارد یا نه. اگر وجود داشته باشد، خطا مطرح می‌شود.

نکته‌ی ظریف: در MySQL، NULL در UNIQUE index رفتار متفاوتی دارد. برخلاف SQL استاندارد، MySQL چند NULL را به‌عنوان مقادیر یکتا قبول می‌کند. یعنی می‌توانید چند رکورد با مقدار NULL در ستون UNIQUE داشته باشید:

CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    email VARCHAR(255) UNIQUE,
    name VARCHAR(100)
);

-- این سه رکورد با هم conflict ندارند
INSERT INTO users (email, name) VALUES (NULL, "Ali");
INSERT INTO users (email, name) VALUES (NULL, "Sara");
INSERT INTO users (email, name) VALUES (NULL, "Reza");

این رفتار، در بعضی سناریوها مزیت است (مثلاً فیلد اختیاری) و در بعضی موارد منبع سردرگمی. مبانی کامل در طراحی دیتابیس در MySQL آمده است.

چرا MySQL این خطا را مطرح می‌کند؟

MySQL به‌طور طراحی‌شده، خطای Duplicate entry را در شرایطی مطرح می‌کند که اپلیکیشن تلاش می‌کند رکوردی درج کند که با یکی از constraintهای جدول در تناقض است. این تصمیم، از چند اصل بنیادین می‌آید:

یک: یکپارچگی داده. اصلی‌ترین هدف constraint، حفظ یکپارچگی داده است. اگر MySQL اجازه دهد رکوردهای تکراری درج شوند، ممکن است سیستم‌های وابسته به شناسه‌ی یکتا (مثل Foreign Key) به مشکل بخورند.

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

سه: عملکرد بهینه. UNIQUE index، B-Tree است که جستجو در آن بسیار سریع است. اجازه‌دادن به درج رکوردهای تکراری، این ساختار را بی‌اثر می‌کرد و عملکرد کوئری‌ها را کاهش می‌داد.

در چارچوب کلی MySQL، خطای Duplicate entry بخشی از ساختار دفاعی این دیتابیس است. این خطا، شما را وادار می‌کند درباره‌ی constraintها، یکپارچگی داده، و معماری درست جداول صریح باشید. همین فلسفه در سایر خطاهای MySQL مثل خطای Unknown database در MySQL هم دیده می‌شود.

نُه علت رایج خطای Duplicate entry

در تجربه‌ی من روی صدها پروژه‌ی MySQL، خطای Duplicate entry از نُه علت مشخص می‌آید. شناخت این علت‌ها، تشخیص را در چند ثانیه ممکن می‌کند.

  1. درج رکورد تکراری در PRIMARY KEY: شایع‌ترین علت در جداول بدون AUTO_INCREMENT.
  2. درج مقدار تکراری در UNIQUE: مثل ایمیل، نام کاربری، یا کد ملی.
  3. تکرار در Composite UNIQUE: ترکیب چند ستون که قبلاً یکتا تعریف شده.
  4. خطای همزمانی (Concurrency): دو درخواست همزمان که هر دو یک رکورد را درج می‌کنند.
  5. Migration یا Import تکراری: اجرای دوباره migration یا import داده.
  6. مشکل در AUTO_INCREMENT: ترتیب AUTO_INCREMENT با داده موجود تلاقی دارد.
  7. UNIQUE روی ستون nullable: سردرگمی درباره‌ی رفتار NULL در UNIQUE.
  8. کد اپلیکیشن بدون بررسی: عدم استفاده از INSERT IGNORE یا ON DUPLICATE KEY UPDATE.
  9. مشکل در Foreign Key و CASCADE: درج رکورد در جدول فرزند با شناسه‌ی تکراری.

هر علت، نشانه‌های مخصوص به خود و راه‌حل اختصاصی دارد. در بخش‌های بعدی، هر علت را جداگانه باز می‌کنم.

تکرار در PRIMARY KEY

شایع‌ترین منبع خطای Duplicate entry، تکرار در PRIMARY KEY است. این حالت در جداولی رخ می‌دهد که PRIMARY KEY روی ستون‌هایی غیر از AUTO_INCREMENT تعریف شده:

CREATE TABLE orders (
    order_number VARCHAR(20) PRIMARY KEY,
    customer_id INT,
    total DECIMAL(10, 2)
);

INSERT INTO orders VALUES ("ORD-12345", 1, 100.00);
INSERT INTO orders VALUES ("ORD-12345", 2, 200.00);
-- ERROR 1062: Duplicate entry 'ORD-12345' for key 'PRIMARY'

در این مثال، order_number به‌عنوان PRIMARY KEY تعریف شده. وقتی دوباره با همان شماره درج می‌کنیم، خطا می‌دهیم.

دلایل رایج:

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

راه‌حل:

راه اول: استفاده از AUTO_INCREMENT

CREATE TABLE orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_number VARCHAR(20) UNIQUE,
    customer_id INT,
    total DECIMAL(10, 2)
);

این رویکرد، از تلاقی شناسه‌ها جلوگیری می‌کند. ولی order_number هنوز UNIQUE است، پس همچنان باید الگوریتم یکتا داشته باشد.

راه دوم: تولید شناسه‌ی یکتا با ترکیب زمان و entropy

-- در PHP
$order_number = "ORD-" . time() . "-" . bin2hex(random_bytes(4));

راه سوم: بررسی قبل از درج

INSERT INTO orders (order_number, customer_id, total)
SELECT "ORD-12345", 1, 100.00
WHERE NOT EXISTS (
    SELECT 1 FROM orders WHERE order_number = "ORD-12345"
);

مبانی طراحی جداول در طراحی دیتابیس در MySQL آمده است.

تکرار در UNIQUE Index

دومین منبع شایع، تکرار در UNIQUE index است. الگوی کلاسیک، فیلد email در جدول کاربران:

CREATE TABLE users (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    email VARCHAR(255) UNIQUE,
    username VARCHAR(50) UNIQUE,
    created_at TIMESTAMP
);

INSERT INTO users (email, username) VALUES ("ali@example.com", "ali");
INSERT INTO users (email, username) VALUES ("ali@example.com", "ali2");
-- ERROR 1062: Duplicate entry 'ali@example.com' for key 'email'

در این مثال، email به‌عنوان UNIQUE تعریف شده. درج ایمیل تکراری، خطا می‌دهد.

دلایل رایج:

  • ثبت‌نام کاربر با ایمیل تکراری. اپلیکیشن قبل از درج، وجود ایمیل را بررسی نکرده.
  • دو درخواست همزمان. کاربر با کلیک دوباره، دو درخواست ارسال کرده.
  • مهاجرت داده. داده‌های قبلی، ایمیل‌های تکراری دارند.
  • حساسیت به بزرگی و کوچکی. ALI@example.com و ali@example.com دو مقدار متفاوت هستند، ولی در بعضی تنظیمات collation، یکسان در نظر گرفته می‌شوند.

راه‌حل:

راه اول: بررسی قبل از درج

INSERT INTO users (email, username)
SELECT "ali@example.com", "ali"
WHERE NOT EXISTS (
    SELECT 1 FROM users WHERE email = "ali@example.com"
);

راه دوم: INSERT IGNORE

INSERT IGNORE INTO users (email, username)
VALUES ("ali@example.com", "ali");

این رویکرد، رکورد تکراری را درج نمی‌کند و خطا نمی‌دهد. نکته: ممکن است در بعضی موارد، دلیل گمراه‌کننده‌ای در warning ثبت شود.

راه سوم: ON DUPLICATE KEY UPDATE

INSERT INTO users (email, username)
VALUES ("ali@example.com", "ali")
ON DUPLICATE KEY UPDATE username = VALUES(username);

این رویکرد، در صورت تکراری بودن، به‌جای خطا، رکورد موجود را به‌روز می‌کند. مناسب برای upsert. جزئیات کامل در بخش‌های بعدی آمده است.

نکته‌ی ظریف: در MySQL، collation پیش‌فرض utf8mb4_0900_ai_ci (در MySQL 8) یا utf8mb4_unicode_ci (در MySQL 5.7) است که به بزرگی و کوچکی حروف حساس نیست. یعنی ALI@example.com و ali@example.com در UNIQUE index یکسان در نظر گرفته می‌شوند. اگر می‌خواهید حساس باشد، از collation با پسوند _bin یا _cs استفاده کنید. مبانی کامل در یادگیری MySQL از صفر آمده است.

تکرار در Composite Key

سومین منبع، تکرار در Composite UNIQUE است. این حالت در جداولی رخ می‌دهد که یکتایی بر اساس ترکیب چند ستون تعریف شده:

CREATE TABLE ratings (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT,
    product_id BIGINT,
    rating TINYINT,
    UNIQUE KEY unique_user_product (user_id, product_id)
);

INSERT INTO ratings (user_id, product_id, rating) VALUES (1, 100, 5);
INSERT INTO ratings (user_id, product_id, rating) VALUES (1, 100, 4);
-- ERROR 1062: Duplicate entry '1-100' for key 'unique_user_product'

در این مثال، ترکیب (user_id, product_id) یکتا است. یعنی یک کاربر نمی‌تواند دو بار به یک محصول امتیاز بدهد.

دلایل رایج:

  • خرید یا امتیاز تکراری. کاربر دوباره همان محصول را امتیاز داده.
  • مشکل در منطق اپلیکیشن. کد، وضعیت کاربر را چک نکرده.
  • مهاجرت داده. داده‌های قبلی، ترکیب‌های تکراری دارند.

راه‌حل:

-- در اپلیکیشن، قبل از درج بررسی کنید
INSERT INTO ratings (user_id, product_id, rating)
VALUES (1, 100, 5)
ON DUPLICATE KEY UPDATE rating = VALUES(rating);

-- این رویکرد، در صورت تکراری بودن، امتیاز را به‌روز می‌کند

نکته‌ی مهم در Composite Key: ترتیب ستون‌ها مهم است. UNIQUE (user_id, product_id) و UNIQUE (product_id, user_id) از نظر یکتایی معادل هستند، ولی از نظر performance در جستجو تفاوت دارند. برای کوئری‌های سریع‌تر، ستونی که در WHERE بیشتر استفاده می‌شود را اول بگذارید. مبانی ایندکس در ایندکس‌گذاری در MySQL آمده است.

خطای Duplicate در محیط‌های همزمان

چهارمین منبع، خطای همزمانی (concurrency) است. این حالت در برنامه‌های پرمعامله شایع است و تشخیص آن دشوارتر است:

-- سناریو: دو درخواست همزمان برای ثبت‌نام با یک ایمیل

-- درخواست اول (Thread 1)
SELECT COUNT(*) FROM users WHERE email = "ali@example.com";
-- خروجی: 0

-- درخواست دوم (Thread 2)
SELECT COUNT(*) FROM users WHERE email = "ali@example.com";
-- خروجی: 0

-- درخواست اول (Thread 1)
INSERT INTO users (email, username) VALUES ("ali@example.com", "ali");
-- موفق

-- درخواست دوم (Thread 2)
INSERT INTO users (email, username) VALUES ("ali@example.com", "ali");
-- ERROR 1062: Duplicate entry 'ali@example.com' for key 'email'

در این مثال، هر دو درخواست، ابتدا وجود ایمیل را بررسی کرده‌اند (خروجی 0)، سپس هر دو تلاش کرده‌اند درج کنند. اولی موفق شده، دومی خطا داده.

راه‌حل‌های موثر:

راه اول: تکیه بر constraint

try {
    INSERT INTO users (email, username) VALUES (?, ?);
} catch (DuplicateEntryException $e) {
    return "Email already exists";
}

این رویکرد، ساده‌ترین و موثرترین است. SELECT را حذف کنید و به INSERT تکیه کنید. اگر خطا داد، ایمیل تکراری است. این روش، شرایط race condition را حل می‌کند چون خود MySQL، اتمی است.

راه دوم: استفاده از transaction با lock

START TRANSACTION;

SELECT * FROM users WHERE email = "ali@example.com" FOR UPDATE;
-- اگر خروجی نداشت:

INSERT INTO users (email, username) VALUES ("ali@example.com", "ali");

COMMIT;

این رویکرد، از race condition جلوگیری می‌کند ولی در محیط‌های پرمعامله، می‌تواند به deadlock منجر شود.

راه سوم: استفاده از INSERT ... ON DUPLICATE KEY UPDATE

INSERT INTO users (email, username)
VALUES ("ali@example.com", "ali")
ON DUPLICATE KEY UPDATE
    username = username,
    updated_at = NOW();

این رویکرد، در سناریوهای upsert (به‌روزرسانی یا درج) بسیار مفید است.

مبانی تراکنش‌ها در تراکنش‌ها در MySQL و مبانی همزمانی در مباحث مربوط به دیتابیس آمده است.

INSERT IGNORE و ON DUPLICATE KEY UPDATE

دو دستور مهم در MySQL که برای مدیریت خطای Duplicate entry استفاده می‌شوند:

INSERT IGNORE

INSERT IGNORE INTO users (email, username)
VALUES ("ali@example.com", "ali");

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

ON DUPLICATE KEY UPDATE

INSERT INTO users (email, username, updated_at)
VALUES ("ali@example.com", "ali", NOW())
ON DUPLICATE KEY UPDATE
    username = VALUES(username),
    updated_at = VALUES(updated_at);

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

REPLACE INTO

REPLACE INTO users (email, username)
VALUES ("ali@example.com", "ali");

این دستور، در صورت تکراری بودن، ابتدا رکورد قدیمی را حذف و رکورد جدید را درج می‌کند. معایب: در بعضی موارد، id تغییر می‌کند و Foreign Keyهای وابسته از کار می‌افتند. توصیه نمی‌شود در جداول با Foreign Key.

مقایسه‌ی سه رویکرد

دستور رفتار در تکرار مناسب برای
INSERT IGNORE نادیده می‌گیرد درج‌های بی‌اثر
ON DUPLICATE KEY UPDATE به‌روز می‌کند upsert، counters
REPLACE INTO حذف + درج جداول بدون Foreign Key

مبانی کامل دستورات MySQL در دستورات پرکاربرد MySQL آمده است.

خطای Duplicate entry در وردپرس

در وردپرس، خطای Duplicate entry معمولاً در دو سناریو ظاهر می‌شود:

سناریو اول: درج ایمیل تکراری کاربر

WordPress database error Duplicate entry 'ali@example.com' for key 'user_email'
for query INSERT INTO wp_users (...) VALUES (...)

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

if (email_exists($email)) {
    wp_die("Email already registered");
}

wp_insert_user([...]);

سناریو دوم: خطای option_name تکراری

WordPress database error Duplicate entry 'my_plugin_setting' for key 'option_name'
for query INSERT INTO wp_options (option_name, option_value) VALUES (...)

در این حالت، افزونه‌ای با add_option() سعی کرده گزینه‌ای تکراری اضافه کند. راه‌حل: استفاده از update_option() به‌جای add_option():

// اشتباه
add_option("my_plugin_setting", $value);

// درست
update_option("my_plugin_setting", $value);

نکته‌ی ظریف: در وردپرس، option_name در جدول wp_options معمولاً UNIQUE است. برای افزونه‌هایی که درج‌های مکرر دارند، استفاده از update_option با پارامتر خودکار درست است. مبانی وردپرس در وردپرس چیست و چگونه شروع کنیم آمده است.

در PHP، PDO و mysqli

در PHP، خطای Duplicate entry از دو مسیر می‌آید:

در mysqli

$conn = new mysqli("localhost", "user", "pass", "mydb");
$stmt = $conn->prepare("INSERT INTO users (email, username) VALUES (?, ?)");
$stmt->bind_param("ss", $email, $username);

if (!$stmt->execute()) {
    if ($conn->errno === 1062) {
        echo "Email already exists";
    } else {
        echo "Error: " . $conn->error;
    }
}

در PDO

try {
    $stmt = $pdo->prepare("INSERT INTO users (email, username) VALUES (?, ?)");
    $stmt->execute([$email, $username]);
} catch (PDOException $e) {
    // SQLSTATE[23000]: Integrity constraint violation
    if ($e->getCode() === "23000") {
        // بررسی وجود Duplicate entry
        if (strpos($e->getMessage(), "1062") !== false) {
            echo "Email already exists";
        }
    }
    error_log($e->getMessage());
}

نکته‌ی مهم: در PDO، خطای Duplicate entry با SQLSTATE 23000 مشخص می‌شود. این کد SQL استاندارد برای Integrity Constraint Violation است. برای تشخیص دقیق‌تر نوع خطا، از errorInfo استفاده کنید:

$errorInfo = $e->errorInfo;
// $errorInfo[0] = SQLSTATE (23000)
// $errorInfo[1] = MySQL error code (1062)
// $errorInfo[2] = Error message

مبانی کامل در آموزش PDO در PHP و اتصال PHP به MySQL آمده است.

در Django، Flask و ORMها

در فریم‌ورک‌ها، خطای Duplicate entry با exceptionهای خاص مدیریت می‌شود:

در Django

from django.db import IntegrityError

try:
    user = User.objects.create(email="ali@example.com", username="ali")
except IntegrityError as e:
    if "Duplicate entry" in str(e):
        return HttpResponse("Email already exists", status=400)
    raise

نکته‌ی ظریف: در Django، بهترین رویکرد استفاده از get_or_create یا update_or_create است:

user, created = User.objects.get_or_create(
    email="ali@example.com",
    defaults={"username": "ali"}
)

if not created:
    # کاربر قبلاً وجود داشته
    pass

مبانی کامل در آموزش جنگو برای مبتدیان و مباحث امنیتی در بهترین روش‌های امنیت Django آمده است.

در Flask با SQLAlchemy

from sqlalchemy.exc import IntegrityError

try:
    db.session.add(user)
    db.session.commit()
except IntegrityError as e:
    db.session.rollback()
    if "Duplicate entry" in str(e.orig):
        return "Email already exists", 400
    raise

نکته‌ی مهم: در SQLAlchemy، پس از IntegrityError، باید rollback کنید تا session قابل استفاده شود.

مبانی Flask در آموزش فلسک در پایتون آمده است.

روش تشخیص اصولی در پنج گام

در تجربه‌ی من، تشخیص خطای Duplicate entry در چند دقیقه انجام می‌شود، اگر روش سیستماتیک داشته باشید:

گام اول: خواندن دقیق پیام خطا. پیام MySQL دقیقاً می‌گوید چه مقدار تکراری و کدام index نقض شده. این دو داده، جهت جستجو را تعیین می‌کند.

گام دوم: بررسی ساختار جدول. ساختار جدول را بررسی کنید تا indexها و constraintها را ببینید:

SHOW CREATE TABLE users;

-- خروجی شامل:
-- PRIMARY KEY (`id`),
-- UNIQUE KEY `email` (`email`),
-- UNIQUE KEY `username` (`username`)

گام سوم: بررسی داده موجود. ببینید چه رکوردهایی با مقدار مشابه وجود دارند:

SELECT * FROM users WHERE email = "ali@example.com";

-- یا برای Composite
SELECT * FROM ratings WHERE user_id = 1 AND product_id = 100;

گام چهارم: بررسی منطق اپلیکیشن. کد اپلیکیشن را بررسی کنید:

// در PHP
$existing = $pdo->query("SELECT id FROM users WHERE email = " . $pdo->quote($email))->fetch();
if ($existing) {
    // ایمیل تکراری است
}

گام پنجم: بررسی لاگ MySQL. در بعضی موارد، لاگ MySQL اطلاعات بیشتری دارد:

tail -100 /var/log/mysql/error.log | grep "Duplicate entry"

ابزارهای تشخیص:

  • SHOW CREATE TABLE tablename: بررسی ساختار جدول.
  • SHOW INDEX FROM tablename: لیست indexها.
  • SELECT ... WHERE column = value: بررسی وجود رکورد.
  • لاگ MySQL: بررسی خطاها با جزئیات.
  • Query Monitor (در وردپرس): بررسی کوئری‌های مشکل‌دار.

مبانی کامل دستورات MySQL در دستورات پرکاربرد MySQL آمده است.

راهبردهای رفع اصولی

بعد از تشخیص، نوبت به رفع می‌رسد. راهبردهای رفع، بر اساس نوع خطا متفاوت است:

راهبرد اول: استفاده از ON DUPLICATE KEY UPDATE

ساده‌ترین و موثرترین راه‌حل:

INSERT INTO users (email, username, updated_at)
VALUES ("ali@example.com", "ali", NOW())
ON DUPLICATE KEY UPDATE
    username = VALUES(username),
    updated_at = VALUES(updated_at);

این رویکرد، هم خطا را حل می‌کند و هم در صورت تکراری بودن، رکورد را به‌روز می‌کند.

راهبرد دوم: استفاده از INSERT IGNORE

INSERT IGNORE INTO users (email, username)
VALUES ("ali@example.com", "ali");

این رویکرد، مناسب سناریوهایی است که نادیده گرفتن تکراری، رفتار مطلوب است.

راهبرد سوم: بررسی قبل از درج

SELECT COUNT(*) FROM users WHERE email = "ali@example.com";

-- اگر 0 بود:
INSERT INTO users (email, username) VALUES ("ali@example.com", "ali");

این رویکرد، در محیط‌های non-concurrent مناسب است ولی در محیط‌های همزمان، به race condition منجر می‌شود.

راهبرد چهارم: مدیریت exception در اپلیکیشن

try {
    INSERT INTO users (email, username) VALUES (?, ?);
} catch (IntegrityError $e) {
    if ($e->getCode() === "23000") {
        return "Email already exists";
    }
    throw $e;
}

این رویکرد، در پروژه‌های production، استاندارد است.

راهبرد پنجم: بازنویسی constraint

در بعضی موارد، constraint فعلی با نیاز کسب‌وکار همخوان نیست. مثال: UNIQUE روی email با collation حساس به بزرگی:

ALTER TABLE users
    DROP INDEX email,
    ADD UNIQUE KEY email (email COLLATE utf8mb4_bin);

راهبرد ششم: طراحی درست جداول

در بعضی موارد، بهترین راه‌حل، بازطراحی جداول است. مثال: استفاده از AUTO_INCREMENT برای PRIMARY KEY و UNIQUE برای مقادیر کسب‌وکار:

CREATE TABLE orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_number VARCHAR(20) UNIQUE,
    customer_id INT,
    total DECIMAL(10, 2),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

مبانی طراحی جداول در طراحی دیتابیس در MySQL آمده است.

UNIQUE در برابر AUTO_INCREMENT

یکی از مفاهیمی که در طراحی جداول باید بشناسید، تفاوت UNIQUE و AUTO_INCREMENT است:

ویژگی UNIQUE AUTO_INCREMENT
هدف جلوگیری از تکراری تولید خودکار شناسه
تعداد در جدول چندین فقط یک
اعمال روی PRIMARY KEY خیر (به‌طور پیش‌فرض) بله (معمولاً)
مقدار صریح بله معمولاً خودکار
NULL پذیر بله (چند NULL مجاز) خیر

نکته‌ی مهم: ترکیب این دو می‌تواند بهترین رویکرد برای جداول کسب‌وکار باشد:

CREATE TABLE products (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,  -- شناسه داخلی
    sku VARCHAR(50) UNIQUE NOT NULL,       -- کد کسب‌وکار
    name VARCHAR(255),
    price DECIMAL(10, 2)
);

در این الگو، id برای Foreign Key و ارتباط داخلی استفاده می‌شود، و sku برای شناسایی کسب‌وکار. این جداسازی، امکان تغییر sku را بدون تأثیر روی ارتباط‌های داخلی فراهم می‌کند.

این خطا در محیط production

در محیط production، خطای Duplicate entry ابعاد جدی‌تری دارد:

قطع سرویس

اگر خطای Duplicate entry در مسیر بحرانی (مثل ثبت‌نام یا ثبت سفارش) رخ دهد، کاربر با خطای 500 مواجه می‌شود و فرآیند کسب‌وکار متوقف می‌شود. در فروشگاه‌های آنلاین، این مسئله به از‌دست‌رفتن سفارش‌ها منجر می‌شود.

خطاهای آبشاری

در بعضی موارد، خطای Duplicate entry در یک جدول، به خطاهای بعدی منجر می‌شود. مثلاً در ثبت سفارش: اگر order_number تکراری باشد، ثبت سفارش شکست می‌خورد و رکوردهای وابسته (order_items, payments) هم درج نمی‌شوند.

نشت اطلاعات

پیام خطا، مقدار تکراری و نام index را افشا می‌کند. اگر این پیام به کاربر نمایش داده شود، اطلاعات حساس افشا می‌شود. راه‌حل: در production، خطاها را در سطح اپلیکیشن به پیام عمومی تبدیل کنید:

try {
    $stmt->execute();
} catch (PDOException $e) {
    if ($e->getCode() === "23000") {
        error_log($e->getMessage());
        return "This email is already registered";
    }
    throw $e;
}

پایش و آلارم‌دهی

در production، خطای Duplicate entry باید به‌طور مناسب پایش شود. ابزارهایی مثل Sentry و Rollbar این خطاها را جمع‌بندی می‌کنند. نکته: بخش بزرگی از این خطاها از race condition می‌آید. راه‌حل: پایش مداوم کوئری‌های خطادار و بررسی الگوهای زمانی.

پیشگیری با تست

  • Unit tests: تست درج تکراری.
  • Concurrency tests: تست با درخواست‌های همزمان.
  • Integration tests: تست فرآیندهای کامل.
  • Load tests: تست با بار واقعی.

مبانی بهینه‌سازی در بهینه‌سازی کوئری‌های MySQL آمده است.

اشتباهات رایج در برخورد با این خطا

در طول سال‌ها، الگوهای تکراری از اشتباهات دیده‌ام که هر کدام می‌تواند پروژه را به چالش بکشد:

اشتباه اول: سرکوب خطا با @ یا error suppression

سرکوب خطا با @ در PHP یا try/catch بدون مدیریت، مشکل را پنهان می‌کند. راه‌حل: خطا را صریح مدیریت کنید و بازخورد مناسب به کاربر بدهید.

اشتباه دوم: استفاده بی‌محابای INSERT IGNORE

INSERT IGNORE خطاهای دیگر (مثل Foreign Key violation) را هم نادیده می‌گیرد. راه‌حل: از INSERT ... ON DUPLICATE KEY UPDATE استفاده کنید که فقط روی Duplicate key عمل می‌کند.

اشتباه سوم: استفاده از REPLACE INTO در جداول با Foreign Key

REPLACE INTO ابتدا رکورد قدیمی را حذف می‌کند و این می‌تواند Foreign Keyهای وابسته را بی‌اعتبار کند. راه‌حل: از ON DUPLICATE KEY UPDATE استفاده کنید.

اشتباه چهارم: نادیده گرفتن race condition

استفاده از SELECT برای بررسی وجود رکورد قبل از INSERT، در محیط‌های همزمان به race condition منجر می‌شود. راه‌حل: به UNIQUE constraint و INSERT تکیه کنید، نه به SELECT.

اشتباه پنجم: عدم مستندسازی constraintها

اگر constraintهای جدول مستند نشوند، توسعه‌دهندگان جدید ممکن است از وجود آن‌ها بی‌خبر باشند. راه‌حل: مستندسازی دقیق ساختار جداول در ویکی تیم.

اشتباه ششم: استفاده از AUTO_INCREMENT بدون مدیریت ظرفیت

وقتی AUTO_INCREMENT به سقف BIGINT یا INT برسد، خطای متفاوتی می‌دهد. راه‌حل: استفاده از BIGINT برای جداول پرترافیک و پایش مداوم.

اشتباه هفتم: عدم استفاده از collation مناسب

collation پیش‌فرض، به بزرگی و کوچکی حساس نیست. این مسئله ممکن است در بعضی سناریوها منبع سردرگمی باشد. راه‌حل: انتخاب آگاهانه collation بر اساس نیاز کسب‌وکار.

خطای Duplicate entry یک پیام از یکپارچگی است، نه از کد. MySQL می‌گوید این رکورد با یک قانونی که خودت وضع کردی در تناقض است. راه‌حل، بررسی قانون است نه سرکوب خطا.

پرسش‌های پرتکرار درباره Duplicate entry

این پرسش‌ها از دل تجربه‌ی عملی و جلسات مشاوره جمع‌آوری شده‌اند. پاسخ هر کدام بر اساس سناریوهای واقعی است.

تفاوت Duplicate entry و Unique constraint violation چیست؟

هر دو یک چیز هستند. Duplicate entry پیام MySQL است که در صورت نقض UNIQUE یا PRIMARY KEY مطرح می‌شود. Unique constraint violation معادل SQL استاندارد است. جزئیات در مستندات MySQL آمده است.

چرا error 1062 در بعضی مواقع با شماره‌های دیگر می‌آید؟

شماره‌های دیگر مثل 1586 در MariaDB یا 1169 و 1022 در سناریوهای خاص MySQL ظاهر می‌شوند. علت اصلی در همه یکسان است ولی متن پیام تفاوت دارد. برای دیباگ، همیشه به SQLSTATE (23000) توجه کنید.

آیا MySQL به بزرگی و کوچکی حروف در UNIQUE حساس است؟

بستگی به collation دارد. collationهای پیش‌فرض (مثل utf8mb4_0900_ai_ci) حساس نیستند. برای حساسیت، از _bin یا _cs استفاده کنید. مثال:

CREATE TABLE users (
    email VARCHAR(255) UNIQUE COLLATE utf8mb4_bin
);

چگونه بفهمم چه index یا constraint نقض شده؟

پیام خطا، نام index را می‌گوید. اگر نام index مفید نبود، از SHOW CREATE TABLE tablename استفاده کنید تا لیست کامل indexها را ببینید.

چرا NULL در UNIQUE چند بار مجاز است؟

در MySQL (برخلاف SQL استاندارد)، NULL در UNIQUE index، مقدار «ناشناخته» است و با NULL دیگر یکسان در نظر گرفته نمی‌شود. بنابراین چند رکورد با NULL در ستون UNIQUE مجاز هستند. این رفتار در بعضی سناریوها مزیت است، در بعضی موارد منبع سردرگمی. راه‌حل: اگر می‌خواهید NULL هم یکتا باشد، از NOT NULL استفاده کنید.

چگونه در Django، این خطا را به‌طور امن مدیریت کنم؟

در Django، بهترین رویکرد استفاده از get_or_create یا update_or_create است:

user, created = User.objects.get_or_create(
    email=email,
    defaults={"username": username}
)

اگر از create استفاده می‌کنید، IntegrityError را بگیرید و مدیریت کنید. مبانی کامل در آموزش جنگو برای مبتدیان آمده است.

آیا INSERT IGNORE می‌تواند به از‌دست‌رفتن داده منجر شود؟

بله. INSERT IGNORE نه‌فقط Duplicate entry بلکه خطاهای دیگر (مثل Foreign Key violation) را هم نادیده می‌گیرد. بنابراین، ممکن است بعضی رکوردها بی‌صدا نادیده گرفته شوند. راه‌حل: از ON DUPLICATE KEY UPDATE استفاده کنید که هدفمندتر است.

چرا REPLACE INTO توصیه نمی‌شود؟

REPLACE INTO ابتدا رکورد موجود را حذف می‌کند و سپس رکورد جدید را درج می‌کند. اگر جدول، Foreign Key داشته باشد، این حذف می‌تواند به خطای Cannot delete or update a parent row منجر شود. همچنین id رکورد تغییر می‌کند که می‌تواند به مشکلات بعدی منجر شود. راه‌حل: از ON DUPLICATE KEY UPDATE استفاده کنید.

چگونه در وردپرس، این خطا را حل کنم؟

در وردپرس، دو سناریو شایع است: اول، درج ایمیل تکراری کاربر با wp_insert_user. راه‌حل: بررسی email_exists قبل از درج. دوم، درج option_name تکراری با add_option. راه‌حل: استفاده از update_option. مبانی وردپرس در وردپرس چیست و چگونه شروع کنیم آمده است.

آیا Duplicate entry روی performance تأثیر دارد؟

خود خطا در لحظه‌ی وقوع رخ می‌دهد. اگر مدیریت شود، از نظر performance هزینه‌ی ناچیزی دارد. ولی در سیستم‌های پرترافیک، تکرار درخواست‌های خطادار می‌تواند performance را تحت تأثیر قرار دهد. راه‌حل: مدیریت صریح در اپلیکیشن و بازخورد سریع.

چگونه در MySQL، خطای Duplicate را در لاگ ببینم؟

خطاهای Duplicate entry در MySQL معمولاً در لاگ error ثبت نمی‌شوند مگر اینکه log_error_verbosity تنظیم شده باشد. راه‌حل: پایش خطاها در سطح اپلیکیشن با ابزارهایی مثل Sentry یا لاگ‌های ساختارمند.

آیا Duplicate entry می‌تواند ناشی از collation باشد؟

بله. اگر collation به بزرگی و کوچکی حساس نباشد، ALI@example.com و ali@example.com یکسان در نظر گرفته می‌شوند و درج دومی خطا می‌دهد. راه‌حل: انتخاب آگاهانه collation و مستندسازی آن.

چگونه در Laravel، این خطا را مدیریت کنم؟

در Laravel، از updateOrCreate یا firstOrCreate استفاده کنید:

$user = User::updateOrCreate(
    ["email" => $email],
    ["username" => $username]
);

یا QueryException را با کد 23000 مدیریت کنید.

چرا بعد از migration، خطای Duplicate می‌گیرم؟

چون migration دوباره اجرا شده یا داده‌ی قبلی در جدول وجود دارد. راه‌حل: بررسی migrations table و اطمینان از اینکه migration فقط یک بار اجرا می‌شود. همچنین، بررسی داده‌ی موجود قبل از migration.

آیا می‌توانم UNIQUE را بعد از ساخت جدول اضافه کنم؟

بله:

ALTER TABLE users ADD UNIQUE KEY email (email);

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

چگونه در PHPUnit، این خطا را تست کنم؟

public function testDuplicateEmail()
{
    $this->expectException(PDOException::class);
    $this->expectExceptionCode("23000");
    
    // درج دو رکورد با ایمیل یکسان
    $this->db->exec("INSERT INTO users (email) VALUES ('test@example.com')");
    $this->db->exec("INSERT INTO users (email) VALUES ('test@example.com')");
}

آیا error 1062 با error 1586 تفاوت دارد؟

بله، در MariaDB 10.2+، خطای 1586 برای سناریوهای پیچیده‌تر استفاده می‌شود که در یک درج، چند constraint نقض می‌شوند. در MySQL، معمولاً 1062 استفاده می‌شود. تفاوت در جزئیات پیام است.

چگونه در Flask-SQLAlchemy، این خطا را مدیریت کنم؟

try:
    user = User(email=email, username=username)
    db.session.add(user)
    db.session.commit()
except IntegrityError as e:
    db.session.rollback()
    if "Duplicate entry" in str(e.orig):
        return jsonify({"error": "Email already exists"}), 400
    raise

نکته‌ی مهم: پس از IntegrityError، باید rollback کنید. مبانی در آموزش فلسک در پایتون آمده است.

آیا Duplicate entry روی replication تأثیر دارد؟

در MySQL Replication با تنظیم binlog_format = ROW، خطاهای Duplicate entry روی سرور اصلی، روی slave هم تکرار می‌شوند. راه‌حل: مدیریت خطا در سرور اصلی و اطمینان از هماهنگی slave. مبانی replication در مستندات MySQL آمده است.

چگونه در MariaDB، UNIQUE را بدون خطا حذف کنم؟

ALTER TABLE users DROP INDEX email;
-- اگر نام index متفاوت است:
SHOW INDEX FROM users;
ALTER TABLE users DROP INDEX actual_index_name;

آیا Duplicate entry در Galera Cluster رفتار متفاوتی دارد؟

بله. در Galera Cluster، خطای Deadlock یا WSREP ممکن است با خطای Duplicate همراه شود. راه‌حل: retry منطقی در اپلیکیشن و بررسی retry timeout. مبانی Galera در مستندات مربوطه آمده است.

چگونه از Duplicate entry در INSERT SELECT جلوگیری کنم؟

INSERT INTO target_table (id, name)
SELECT id, name FROM source_table
WHERE id NOT IN (SELECT id FROM target_table);

-- یا:
INSERT IGNORE INTO target_table (id, name)
SELECT id, name FROM source_table;

آنچه از سال‌ها کار با MySQL آموختم

اگر بخواهم چکیده‌ی این سال‌ها را در چند جمله بگویم، سه اصل عملی دارم:

یک: خطای Duplicate entry یک سیگنال از یکپارچگی داده است، نه از کد بد. هر بار که این خطا می‌بینید، به‌جای سرزنش کد، به constraintها، طراحی جداول، و مکانیزم‌های همزمانی نگاه کنید. در 90 درصد موارد، ریشه در طراحی یا منطق اپلیکیشن است.

دو: به UNIQUE constraint تکیه کنید، نه به SELECT قبل از INSERT. در محیط‌های همزمان، استفاده از SELECT برای بررسی وجود رکورد، به race condition منجر می‌شود. راه‌حل مدرن: UNIQUE constraint + INSERT + مدیریت IntegrityError. این رویکرد، اتمی و کارآمد است.

سه: از ON DUPLICATE KEY UPDATE به‌جای INSERT IGNORE یا REPLACE INTO استفاده کنید. INSERT IGNORE خطاهای دیگر را هم پنهان می‌کند و REPLACE INTO با Foreign Key مشکل‌دار است. ON DUPLICATE KEY UPDATE، هدفمندترین راه‌حل است.

در کنار این سه اصل، یک هشدار عملی هم دارم: خطای Duplicate entry در نگاه اول یک مشکل ساده به‌نظر می‌رسد، ولی وقتی در چارچوب کلی معماری داده دیده شود، تبدیل به یک سیگنال می‌شود. این سیگنال می‌گوید که مدل داده، constraintها، و مکانیزم‌های همزمانی نیاز به بازنگری دارند. اگر این سیگنال را جدی بگیرید و ساختار داده را تقویت کنید، پروژه‌ی شما در ماه‌های بعد پایدارتر و قابل نگهداری‌تر خواهد بود.

هدف این مقاله، تمام‌کردن همه‌ی سناریوهای ممکن نبود. هدف، دادن یک چارچوب ذهنی برای تشخیص، پیشگیری و رفع این خطا بود. وقتی این چارچوب را درونی کنید، برخورد با خطای Duplicate entry از یک واکنش اضطراری به یک فرآیند منظم تبدیل می‌شود.

اگر خطای Duplicate entry در پروژه‌ی شما به شکلی ظاهر شده که با الگوهای این مقاله حل نشده، برای من جالب است بدانم کدام سناریو بود. تجربه‌ی خودتان را در دیدگاه‌ها بنویسید؛ به‌ویژه اگر راه‌حلی پیدا کرده‌اید که هنوز در این مقاله نیست. 🔑