SQL چیست؟ آموزش SQL از صفر با MySQL، Python، FastAPI و پروژه هوش مصنوعی

در این آموزش SQL را از صفر و با مثال‌های عملی MySQL یاد می‌گیرید؛ از ساخت جدول و CRUD تا JOIN، Index و Transaction. در پایان نیز یک API واقعی برای ذخیره مکالمات هوش مصنوعی با FastAPI و درواره می‌سازیم.

Share
SQL چیست؟ آموزش SQL از صفر با MySQL، Python، FastAPI و پروژه هوش مصنوعی

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

در این آموزش SQL را فقط در حد تعریف مفاهیم بررسی نمی‌کنیم. ابتدا دستورات اصلی SQL را با MySQL یاد می‌گیریم و سپس یک پروژه واقعی می‌سازیم که مکالمات کاربران با مدل هوش مصنوعی را در پایگاه داده ذخیره می‌کند.

پروژه نهایی شامل اجزای زیر است:

  • MySQL برای ذخیره اطلاعات
  • SQL و SQLModel برای کار با داده‌ها
  • Python و FastAPI برای ساخت API
  • API درواره برای دسترسی به مدل هوش مصنوعی
  • Docker Compose برای اجرای سرویس‌ها
  • ذخیره Conversation و Message
  • بازیابی تاریخچه گفتگو
  • مدیریت Transaction
  • اعتبارسنجی ورودی
  • محافظت از Endpoint با Token
  • جلوگیری از SQL Injection
  • Health Check و مدیریت خطا

SQL چیست؟

SQL مخفف Structured Query Language است و برای تعریف، خواندن، تغییر و مدیریت داده‌های پایگاه‌های داده رابطه‌ای استفاده می‌شود.

با SQL می‌توان عملیات زیر را انجام داد:

  • ساخت پایگاه داده
  • ساخت جدول
  • افزودن اطلاعات
  • جست‌وجو و فیلتر داده
  • ویرایش اطلاعات
  • حذف اطلاعات
  • اتصال چند جدول
  • گروه‌بندی و محاسبه آمار
  • ساخت Index
  • مدیریت Transaction
  • تعریف محدودیت‌های داده
  • مدیریت ارتباط بین رکوردها

یک Query ساده SQL:

SELECT id, name, email
FROM users
WHERE is_active = TRUE
ORDER BY created_at DESC;

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

آیا SQL یک زبان برنامه‌نویسی است؟

SQL یک زبان تخصصی برای کار با پایگاه داده رابطه‌ای است. برخلاف Python یا JavaScript، معمولاً برای ساخت کل برنامه استفاده نمی‌شود.

در یک برنامه واقعی، زبان‌ها در کنار هم قرار می‌گیرند:

Frontend
  |
  v
Python / FastAPI
  |
  v
SQL
  |
  v
MySQL

Python منطق برنامه را اجرا می‌کند، SQL عملیات مربوط به داده را توضیح می‌دهد و MySQL داده‌ها را ذخیره و پردازش می‌کند.

پایگاه داده رابطه‌ای چیست؟

در پایگاه داده رابطه‌ای، اطلاعات داخل Table یا جدول ذخیره می‌شوند. هر جدول دارای ستون و سطر است.

جدول کاربران:

idnameemail
1امیرamir@example.com
2ساراsara@example.com

هر سطر یک Record و هر ستون یک ویژگی از آن Record است.

می‌توان میان جدول‌ها ارتباط ایجاد کرد. برای مثال:

users
  |
  | یک کاربر چند مکالمه دارد
  v
conversations
  |
  | یک مکالمه چند پیام دارد
  v
messages

این ارتباط‌ها با Primary Key و Foreign Key مدیریت می‌شوند.

تفاوت SQL و MySQL چیست؟

SQL یک زبان است، اما MySQL یک سیستم مدیریت پایگاه داده است.

مفهومتوضیح
SQLزبان کار با پایگاه داده رابطه‌ای
MySQLنرم‌افزار مدیریت پایگاه داده
PostgreSQLیک سیستم مدیریت پایگاه داده دیگر
SQLiteپایگاه داده سبک و فایل‌محور
Microsoft SQL Serverمحصول پایگاه داده مایکروسافت
Oracle Databaseپایگاه داده سازمانی Oracle

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

SQL در پروژه‌های هوش مصنوعی چه کاربردی دارد؟

SQL فقط برای فروشگاه اینترنتی یا نرم‌افزار حسابداری نیست. در برنامه‌های هوش مصنوعی نیز کاربردهای فراوانی دارد:

  • ذخیره کاربران
  • ذخیره مکالمات
  • مدیریت دستیارهای هوش مصنوعی
  • ذخیره تنظیمات پرامپت
  • ثبت مدل استفاده‌شده
  • ثبت مصرف توکن
  • ثبت هزینه درخواست
  • مدیریت بازخورد کاربران
  • ذخیره نتیجه ارزیابی مدل
  • نگهداری Jobهای پردازشی
  • ذخیره اسناد و Metadata
  • ثبت Tool Callها
  • گزارش‌گیری از میزان استفاده

برای داده‌های ساختاریافته مانند کاربران، درخواست‌ها، هزینه‌ها و ارتباط میان موجودیت‌ها، پایگاه داده SQL انتخاب مناسبی است.

دسته‌بندی دستورات SQL

دستورات SQL معمولاً در چند گروه قرار می‌گیرند.

DDL

Data Definition Language برای تعریف ساختار پایگاه داده:

CREATE
ALTER
DROP
TRUNCATE

DML

Data Manipulation Language برای تغییر داده:

INSERT
UPDATE
DELETE

DQL

Data Query Language برای خواندن داده:

SELECT

TCL

Transaction Control Language برای مدیریت تراکنش:

START TRANSACTION
COMMIT
ROLLBACK

DCL

Data Control Language برای مدیریت دسترسی:

GRANT
REVOKE

نصب MySQL با Docker

ساده‌ترین راه برای اجرای محیط آزمایشی MySQL استفاده از Docker است.

فایل compose.mysql.yaml را ایجاد کنید:

services:
  mysql:
    image: mysql:8.4
    container_name: sql-tutorial-mysql
    environment:
      MYSQL_ROOT_PASSWORD: root_password
      MYSQL_DATABASE: ai_chat
      MYSQL_USER: ai_user
      MYSQL_PASSWORD: ai_password
    ports:
      - "3306:3306"
    volumes:
      - mysql_tutorial_data:/var/lib/mysql
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
    healthcheck:
      test:
        [
          "CMD",
          "mysqladmin",
          "ping",
          "-h",
          "localhost",
          "-uroot",
          "-proot_password"
        ]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 20s

volumes:
  mysql_tutorial_data:

MySQL را اجرا کنید:

docker compose \
  -f compose.mysql.yaml \
  up -d

وضعیت Container:

docker compose \
  -f compose.mysql.yaml \
  ps

ورود به محیط MySQL:

docker exec -it \
  sql-tutorial-mysql \
  mysql -uai_user -pai_password ai_chat

در محیط Production باید رمزهای قوی و مستقل انتخاب کنید و آن‌ها را مستقیماً داخل فایل عمومی قرار ندهید.

ساخت پایگاه داده

اگر پایگاه داده از قبل ساخته نشده است:

CREATE DATABASE ai_chat
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;

انتخاب پایگاه داده:

USE ai_chat;

مشاهده پایگاه‌های داده:

SHOW DATABASES;

ساخت اولین جدول

جدول کاربران:

CREATE TABLE users (
    id BIGINT UNSIGNED
        AUTO_INCREMENT
        PRIMARY KEY,

    name VARCHAR(100)
        NOT NULL,

    email VARCHAR(255)
        NOT NULL
        UNIQUE,

    is_active BOOLEAN
        NOT NULL
        DEFAULT TRUE,

    created_at DATETIME
        NOT NULL
        DEFAULT CURRENT_TIMESTAMP
);

اجزای مهم:

عبارتکاربرد
BIGINTعدد صحیح بزرگ
UNSIGNEDجلوگیری از مقدار منفی
AUTO_INCREMENTتولید خودکار شناسه
PRIMARY KEYشناسه منحصربه‌فرد
VARCHARرشته با حداکثر طول مشخص
NOT NULLمقدار اجباری
UNIQUEجلوگیری از مقدار تکراری
DEFAULTمقدار پیش‌فرض

مشاهده ساختار جدول:

DESCRIBE users;

مشاهده جدول‌ها:

SHOW TABLES;

Primary Key چیست؟

Primary Key ستونی است که هر رکورد را به‌صورت منحصربه‌فرد مشخص می‌کند:

id BIGINT UNSIGNED
    AUTO_INCREMENT
    PRIMARY KEY

ویژگی‌های Primary Key:

  • تکراری نیست.
  • مقدار NULL نمی‌پذیرد.
  • برای ارتباط میان جدول‌ها استفاده می‌شود.
  • بهتر است در طول عمر رکورد ثابت بماند.

Foreign Key چیست؟

Foreign Key ارتباط میان دو جدول را مشخص می‌کند.

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

CREATE TABLE conversations (
    id BIGINT UNSIGNED
        AUTO_INCREMENT
        PRIMARY KEY,

    user_id BIGINT UNSIGNED
        NOT NULL,

    title VARCHAR(200)
        NOT NULL,

    created_at DATETIME
        NOT NULL
        DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT fk_conversations_user
        FOREIGN KEY (user_id)
        REFERENCES users(id)
        ON DELETE CASCADE
);

هر Conversation به یک User تعلق دارد.

جدول پیام‌ها:

CREATE TABLE messages (
    id BIGINT UNSIGNED
        AUTO_INCREMENT
        PRIMARY KEY,

    conversation_id BIGINT UNSIGNED
        NOT NULL,

    role ENUM(
        'user',
        'assistant',
        'system'
    ) NOT NULL,

    content TEXT
        NOT NULL,

    model_id VARCHAR(255)
        NULL,

    input_tokens INT UNSIGNED
        NULL,

    output_tokens INT UNSIGNED
        NULL,

    created_at DATETIME
        NOT NULL
        DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT fk_messages_conversation
        FOREIGN KEY (conversation_id)
        REFERENCES conversations(id)
        ON DELETE CASCADE
);

با ON DELETE CASCADE اگر یک مکالمه حذف شود، پیام‌های وابسته به آن نیز حذف می‌شوند. استفاده از این رفتار باید آگاهانه و متناسب با نیاز محصول باشد.

افزودن اطلاعات با INSERT

افزودن یک کاربر:

INSERT INTO users (
    name,
    email
)
VALUES (
    'امیر افشار',
    'amir@example.com'
);

افزودن چند کاربر:

INSERT INTO users (
    name,
    email
)
VALUES
    (
        'سارا محمدی',
        'sara@example.com'
    ),
    (
        'علی رضایی',
        'ali@example.com'
    );

افزودن مکالمه:

INSERT INTO conversations (
    user_id,
    title
)
VALUES (
    1,
    'آموزش SQL'
);

افزودن پیام:

INSERT INTO messages (
    conversation_id,
    role,
    content
)
VALUES (
    1,
    'user',
    'SQL چیست؟'
);

خواندن اطلاعات با SELECT

دریافت تمام کاربران:

SELECT *
FROM users;

دریافت ستون‌های مشخص:

SELECT
    id,
    name,
    email
FROM users;

بهتر است در Queryهای برنامه‌ای فقط ستون‌های موردنیاز را انتخاب کنید و بدون دلیل از SELECT * استفاده نکنید.

ساختار کلی SELECT در MySQL شامل بخش‌هایی مانند FROM، WHERE، ORDER BY و LIMIT است. ترتیب Clauseها در Query اهمیت دارد. مستندات SELECT در MySQL

فیلتر کردن با WHERE

کاربر با شناسه مشخص:

SELECT
    id,
    name,
    email
FROM users
WHERE id = 1;

کاربران فعال:

SELECT
    id,
    name
FROM users
WHERE is_active = TRUE;

استفاده از چند شرط:

SELECT
    id,
    name,
    email
FROM users
WHERE is_active = TRUE
  AND created_at >= '2026-08-01';

شرط جایگزین:

SELECT
    id,
    name
FROM users
WHERE id = 1
   OR id = 2;

جست‌وجو با LIKE

یافتن کاربرانی که نام آن‌ها با «امیر» شروع می‌شود:

SELECT
    id,
    name
FROM users
WHERE name LIKE 'امیر%';

الگوهای مهم:

الگومعنی
'امیر%'شروع با امیر
'%امیر'پایان با امیر
'%امیر%'شامل امیر
'A_'A و دقیقاً یک کاراکتر دیگر

در جدول‌های بزرگ، جست‌وجوی LIKE '%word%' ممکن است از Index معمولی به‌خوبی استفاده نکند.

مرتب‌سازی با ORDER BY

مرتب‌سازی از جدید به قدیم:

SELECT
    id,
    title,
    created_at
FROM conversations
ORDER BY created_at DESC;

مرتب‌سازی صعودی:

SELECT
    id,
    name
FROM users
ORDER BY name ASC;

مرتب‌سازی براساس چند ستون:

SELECT
    conversation_id,
    role,
    created_at
FROM messages
ORDER BY
    conversation_id ASC,
    created_at ASC;

محدود کردن نتیجه با LIMIT

دریافت ۱۰ مکالمه جدید:

SELECT
    id,
    title,
    created_at
FROM conversations
ORDER BY created_at DESC
LIMIT 10;

صفحه دوم با ۱۰ نتیجه در هر صفحه:

SELECT
    id,
    title
FROM conversations
ORDER BY id DESC
LIMIT 10
OFFSET 10;

در داده‌های بسیار بزرگ، Pagination مبتنی بر Cursor معمولاً از OFFSETهای بزرگ کارآمدتر است.

ویرایش اطلاعات با UPDATE

غیرفعال کردن یک کاربر:

UPDATE users
SET is_active = FALSE
WHERE id = 1;

تغییر عنوان مکالمه:

UPDATE conversations
SET title = 'آموزش پیشرفته SQL'
WHERE id = 1;

اگر WHERE را حذف کنید، همه رکوردهای جدول تغییر می‌کنند:

UPDATE users
SET is_active = FALSE;

به همین دلیل بهتر است پیش از اجرای UPDATE ابتدا شرط را با SELECT بررسی کنید:

SELECT *
FROM users
WHERE id = 1;

حذف اطلاعات با DELETE

حذف یک پیام:

DELETE FROM messages
WHERE id = 10;

حذف مکالمه:

DELETE FROM conversations
WHERE id = 1;

بدون WHERE تمام رکوردهای جدول حذف می‌شوند:

DELETE FROM messages;

برای عملیات حساس، Backup، Transaction و محدود کردن سطح دسترسی کاربر پایگاه داده اهمیت زیادی دارد.

مقدار NULL در SQL

NULL به معنی نبود مقدار است و با رشته خالی یا عدد صفر تفاوت دارد.

پیام‌هایی که Model ID ندارند:

SELECT
    id,
    content
FROM messages
WHERE model_id IS NULL;

استفاده از این شرط نادرست است:

WHERE model_id = NULL

شرط درست:

WHERE model_id IS NULL

و برای مقدار غیرخالی:

WHERE model_id IS NOT NULL

توابع آماری SQL

تعداد کل پیام‌ها:

SELECT COUNT(*) AS message_count
FROM messages;

مجموع توکن خروجی:

SELECT
    SUM(output_tokens)
        AS total_output_tokens
FROM messages;

میانگین توکن خروجی:

SELECT
    AVG(output_tokens)
        AS average_output_tokens
FROM messages
WHERE output_tokens IS NOT NULL;

کمترین و بیشترین مقدار:

SELECT
    MIN(output_tokens)
        AS minimum_tokens,

    MAX(output_tokens)
        AS maximum_tokens
FROM messages;

GROUP BY چیست؟

تعداد پیام‌ها براساس Role:

SELECT
    role,
    COUNT(*) AS message_count
FROM messages
GROUP BY role;

تعداد پیام‌های هر مکالمه:

SELECT
    conversation_id,
    COUNT(*) AS message_count
FROM messages
GROUP BY conversation_id
ORDER BY message_count DESC;

فیلتر نتیجه گروه‌بندی با HAVING:

SELECT
    conversation_id,
    COUNT(*) AS message_count
FROM messages
GROUP BY conversation_id
HAVING COUNT(*) >= 5;

تفاوت WHERE و HAVING:

  • WHERE رکوردها را قبل از گروه‌بندی فیلتر می‌کند.
  • HAVING نتیجه گروه‌بندی را فیلتر می‌کند.

JOIN چیست؟

JOIN اطلاعات چند جدول مرتبط را در یک Query ترکیب می‌کند.

INNER JOIN

نمایش مکالمه همراه نام کاربر:

SELECT
    conversations.id,
    conversations.title,
    users.name
FROM conversations
INNER JOIN users
    ON users.id =
       conversations.user_id;

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

LEFT JOIN

نمایش همه کاربران حتی اگر مکالمه‌ای نداشته باشند:

SELECT
    users.id,
    users.name,
    conversations.title
FROM users
LEFT JOIN conversations
    ON conversations.user_id =
       users.id;

JOIN سه جدول

نمایش پیام‌ها همراه عنوان مکالمه و نام کاربر:

SELECT
    messages.id,
    users.name AS user_name,
    conversations.title,
    messages.role,
    messages.content,
    messages.created_at
FROM messages
INNER JOIN conversations
    ON conversations.id =
       messages.conversation_id
INNER JOIN users
    ON users.id =
       conversations.user_id
ORDER BY messages.created_at ASC;

ساختار و انواع JOIN در مستندات رسمی MySQL توضیح داده شده است.

Alias در SQL

برای کوتاه‌تر شدن Query می‌توان از Alias استفاده کرد:

SELECT
    m.id,
    u.name,
    c.title,
    m.role,
    m.content
FROM messages AS m
INNER JOIN conversations AS c
    ON c.id = m.conversation_id
INNER JOIN users AS u
    ON u.id = c.user_id;

Alias فقط در همان Query معتبر است و نام اصلی جدول را تغییر نمی‌دهد.

Index چیست؟

Index ساختاری برای سریع‌تر شدن بعضی جست‌وجوهاست.

برای بازیابی پیام‌های یک مکالمه:

CREATE INDEX
    idx_messages_conversation_created
ON messages (
    conversation_id,
    created_at
);

برای جست‌وجوی Model ID:

CREATE INDEX
    idx_messages_model_id
ON messages (
    model_id
);

Index همیشه مفید نیست. هر Index:

  • فضای ذخیره‌سازی مصرف می‌کند.
  • عملیات INSERT را کمی سنگین‌تر می‌کند.
  • عملیات UPDATE و DELETE را تحت تأثیر قرار می‌دهد.
  • باید براساس Queryهای واقعی ساخته شود.

ترتیب ستون‌ها در Composite Index اهمیت دارد:

INDEX (
    conversation_id,
    created_at
)

این Index برای Query زیر مناسب است:

SELECT *
FROM messages
WHERE conversation_id = 10
ORDER BY created_at DESC;

بررسی Query با EXPLAIN

برای مشاهده برنامه اجرای Query:

EXPLAIN
SELECT
    id,
    role,
    content,
    created_at
FROM messages
WHERE conversation_id = 10
ORDER BY created_at DESC
LIMIT 20;

EXPLAIN کمک می‌کند متوجه شوید:

  • چه جدولی خوانده می‌شود.
  • چه Indexی انتخاب شده است.
  • تقریباً چند رکورد بررسی می‌شود.
  • آیا Full Table Scan رخ می‌دهد.
  • ترتیب JOIN چگونه است.

نباید فقط با دیدن یک Query کند، به‌صورت تصادفی Index اضافه کنید. ابتدا الگوی Query و خروجی EXPLAIN را بررسی کنید.

Transaction چیست؟

Transaction چند عملیات وابسته را به‌عنوان یک واحد منطقی اجرا می‌کند.

فرض کنید می‌خواهیم پیام کاربر و پاسخ دستیار را با هم ذخیره کنیم:

START TRANSACTION;

INSERT INTO messages (
    conversation_id,
    role,
    content
)
VALUES (
    1,
    'user',
    'SQL چیست؟'
);

INSERT INTO messages (
    conversation_id,
    role,
    content,
    model_id
)
VALUES (
    1,
    'assistant',
    'SQL زبان کار با پایگاه داده است.',
    'YOUR_MODEL_ID'
);

COMMIT;

اگر عملیات دوم شکست بخورد:

ROLLBACK;

Transaction مانع باقی ماندن وضعیت ناقص می‌شود.

ویژگی‌های شناخته‌شده Transaction با عبارت ACID بیان می‌شوند:

ویژگیمفهوم
Atomicityهمه عملیات انجام شوند یا هیچ‌کدام
Consistencyداده از حالت معتبر به حالت معتبر برود
Isolationتراکنش‌های هم‌زمان کمترین تداخل نامطلوب را داشته باشند
Durabilityپس از Commit داده پایدار بماند

SQL Injection چیست؟

SQL Injection زمانی رخ می‌دهد که ورودی کاربر بدون جداسازی مناسب داخل Query قرار بگیرد.

نمونه نادرست:

email = request.query_params["email"]

query = (
    "SELECT * FROM users "
    f"WHERE email = '{email}'"
)

کاربر می‌تواند بخشی از Query را تغییر دهد.

روش مناسب استفاده از Parameter است:

cursor.execute(
    """
    SELECT id, name, email
    FROM users
    WHERE email = %s
    """,
    (email,),
)

در SQLAlchemy یا SQLModel نیز شرط‌ها به‌صورت جداگانه ساخته می‌شوند:

statement = (
    select(User)
    .where(User.email == email)
)

ورودی کاربر را برای نام جدول، نام ستون یا قطعه SQL آزاد استفاده نکنید. Parameterها معمولاً برای Valueها هستند، نه ساختار Query.

ORM چیست؟

ORM مخفف Object-Relational Mapping است و جدول‌ها را به Classهای زبان برنامه‌نویسی تبدیل می‌کند.

جدول SQL:

CREATE TABLE conversations (
    id BIGINT PRIMARY KEY,
    title VARCHAR(200)
);

مدل Python:

class Conversation(
    SQLModel,
    table=True
):
    id: int | None = Field(
        default=None,
        primary_key=True
    )

    title: str

ORM باعث حذف نیاز به یادگیری SQL نمی‌شود. برای طراحی Schema، بررسی Performance، ساخت Index و تحلیل Query همچنان دانش SQL ضروری است.

FastAPI شما را به یک پایگاه داده یا ORM خاص محدود نمی‌کند. ابزارهایی مانند SQLModel و SQLAlchemy می‌توانند با پایگاه‌های داده مختلف کار کنند. راهنمای پایگاه داده SQL در FastAPI

پروژه عملی: ذخیره مکالمات هوش مصنوعی

اکنون یک API می‌سازیم که:

  1. مکالمه جدید ایجاد می‌کند.
  2. پیام کاربر را دریافت می‌کند.
  3. تاریخچه مکالمه را از MySQL می‌خواند.
  4. درخواست را به API درواره می‌فرستد.
  5. پاسخ مدل را دریافت می‌کند.
  6. پیام کاربر و پاسخ مدل را داخل یک Transaction ذخیره می‌کند.
  7. تاریخچه مکالمه را برمی‌گرداند.

معماری:

Client
  |
  v
FastAPI
  |
  +----> MySQL
  |
  +----> API درواره
             |
             v
       مدل هوش مصنوعی

ساختار پروژه

sql-ai-chat/
├── app/
│   ├── __init__.py
│   ├── database.py
│   ├── main.py
│   ├── models.py
│   └── schemas.py
├── .dockerignore
├── .env
├── .env.example
├── .gitignore
├── compose.yaml
├── Dockerfile
└── requirements.txt

ساخت پوشه‌ها:

mkdir -p sql-ai-chat/app
cd sql-ai-chat
touch app/__init__.py

در Windows می‌توانید پوشه و فایل‌ها را از طریق File Explorer یا VS Code ایجاد کنید.

تعریف وابستگی‌ها

فایل requirements.txt:

fastapi>=0.115,<1.0
uvicorn[standard]>=0.34,<1.0
sqlmodel>=0.0.24,<1.0
sqlalchemy>=2.0,<3.0
pymysql>=1.1,<2.0
cryptography>=44,<46
openai>=1.68,<3.0
python-dotenv>=1.1,<2.0

نصب در محیط مجازی:

python -m venv .venv

فعال‌سازی در Linux یا macOS:

source .venv/bin/activate

فعال‌سازی در Windows PowerShell:

.venv\Scripts\Activate.ps1

نصب Packageها:

python -m pip install \
  --upgrade pip
python -m pip install \
  -r requirements.txt

تنظیم متغیرهای محیطی

فایل .env:

DATABASE_URL=mysql+pymysql://ai_user:ai_password@localhost:3306/ai_chat?charset=utf8mb4

DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL_ID=YOUR_MODEL_ID

APP_TOKEN=YOUR_LONG_RANDOM_APP_TOKEN

فایل .env.example:

DATABASE_URL=mysql+pymysql://USERNAME:PASSWORD@HOST:3306/DATABASE?charset=utf8mb4

DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL_ID=YOUR_MODEL_ID

APP_TOKEN=YOUR_LONG_RANDOM_APP_TOKEN

فایل .gitignore:

.env
.venv/
__pycache__/
*.pyc
.pytest_cache/
.idea/
.vscode/

برای دریافت کلید API در درواره ثبت‌نام کنید. شناسه مدل را نیز از صفحه مدل‌ها و قیمت‌ها بردارید.

اتصال FastAPI به MySQL

فایل app/database.py:

import os
from collections.abc import Generator

from dotenv import load_dotenv
from sqlmodel import Session, create_engine


load_dotenv()


DATABASE_URL = os.environ["DATABASE_URL"]


engine = create_engine(
    DATABASE_URL,
    echo=False,
    pool_pre_ping=True,
    pool_recycle=1800,
)


def get_session() -> Generator[
    Session,
    None,
    None,
]:
    with Session(engine) as session:
        yield session

pool_pre_ping

پیش از استفاده از Connection، معتبر بودن آن را بررسی می‌کند:

pool_pre_ping=True

این تنظیم می‌تواند خطاهای ناشی از Connectionهای قدیمی یا بسته‌شده را کاهش دهد.

pool_recycle

Connectionهای قدیمی را پس از زمان مشخص بازیابی می‌کند:

pool_recycle=1800

مقدار مناسب باید براساس تنظیمات MySQL و زیرساخت انتخاب شود.

ساخت مدل‌های پایگاه داده

فایل app/models.py:

from datetime import UTC, datetime

from sqlalchemy import Index
from sqlmodel import Field, SQLModel


def utc_now() -> datetime:
    return (
        datetime
        .now(UTC)
        .replace(tzinfo=None)
    )


class Conversation(
    SQLModel,
    table=True,
):
    __tablename__ = "conversations"

    id: int | None = Field(
        default=None,
        primary_key=True,
    )

    title: str = Field(
        max_length=200,
    )

    created_at: datetime = Field(
        default_factory=utc_now,
        nullable=False,
    )


class Message(
    SQLModel,
    table=True,
):
    __tablename__ = "messages"

    __table_args__ = (
        Index(
            "idx_messages_conversation_created",
            "conversation_id",
            "created_at",
        ),
    )

    id: int | None = Field(
        default=None,
        primary_key=True,
    )

    conversation_id: int = Field(
        foreign_key="conversations.id",
        index=True,
    )

    role: str = Field(
        max_length=20,
    )

    content: str

    model_id: str | None = Field(
        default=None,
        max_length=255,
    )

    input_tokens: int | None = Field(
        default=None,
    )

    output_tokens: int | None = Field(
        default=None,
    )

    created_at: datetime = Field(
        default_factory=utc_now,
        nullable=False,
    )

در این مدل، Conversation و Message دو جدول مستقل هستند. هر Message با conversation_id به یک مکالمه متصل می‌شود.

ساخت Schemaهای ورودی و خروجی

فایل app/schemas.py:

from datetime import datetime

from pydantic import BaseModel, Field


class ConversationCreate(
    BaseModel,
):
    title: str = Field(
        min_length=1,
        max_length=200,
    )


class ConversationRead(
    BaseModel,
):
    id: int
    title: str
    created_at: datetime


class ChatRequest(
    BaseModel,
):
    message: str = Field(
        min_length=1,
        max_length=8000,
    )

    temperature: float = Field(
        default=0.3,
        ge=0,
        le=2,
    )

    max_tokens: int = Field(
        default=800,
        ge=1,
        le=2000,
    )


class MessageRead(
    BaseModel,
):
    id: int
    conversation_id: int
    role: str
    content: str
    model_id: str | None
    input_tokens: int | None
    output_tokens: int | None
    created_at: datetime


class ChatResponse(
    BaseModel,
):
    conversation_id: int
    user_message: MessageRead
    assistant_message: MessageRead

Schema ورودی از مدل Database جدا شده است تا Client نتواند فیلدهایی مانند id، role یا model_id را آزادانه تعیین کند.

ساخت API اصلی

فایل app/main.py:

import os
import secrets
from contextlib import asynccontextmanager
from typing import Annotated

from dotenv import load_dotenv
from fastapi import (
    Depends,
    FastAPI,
    HTTPException,
    Query,
    status,
)
from fastapi.security import (
    HTTPAuthorizationCredentials,
    HTTPBearer,
)
from openai import OpenAI
from sqlmodel import Session, SQLModel, select

from app.database import engine, get_session
from app.models import Conversation, Message
from app.schemas import (
    ChatRequest,
    ChatResponse,
    ConversationCreate,
    ConversationRead,
    MessageRead,
)


load_dotenv()


DARVAREH_API_KEY = os.environ[
    "DARVAREH_API_KEY"
]

DARVAREH_MODEL_ID = os.environ[
    "DARVAREH_MODEL_ID"
]

APP_TOKEN = os.environ[
    "APP_TOKEN"
]


security = HTTPBearer(
    auto_error=False,
)


ai_client = OpenAI(
    api_key=DARVAREH_API_KEY,
    base_url="https://api.darvareh.ir/v1",
    timeout=90.0,
    max_retries=2,
)


SessionDep = Annotated[
    Session,
    Depends(get_session),
]


def require_app_token(
    credentials: Annotated[
        HTTPAuthorizationCredentials | None,
        Depends(security),
    ],
) -> None:
    if credentials is None:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Authorization header is required",
        )

    valid_scheme = (
        credentials.scheme.lower()
        == "bearer"
    )

    valid_token = (
        secrets.compare_digest(
            credentials.credentials,
            APP_TOKEN,
        )
    )

    if not valid_scheme or not valid_token:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid application token",
        )


AuthDep = Annotated[
    None,
    Depends(require_app_token),
]


@asynccontextmanager
async def lifespan(
    app: FastAPI,
):
    SQLModel.metadata.create_all(
        engine
    )

    yield

    ai_client.close()


app = FastAPI(
    title="SQL AI Chat API",
    version="1.0.0",
    lifespan=lifespan,
)


@app.get("/health")
def health():
    return {
        "status": "ok",
        "service": "sql-ai-chat",
    }


@app.post(
    "/conversations",
    response_model=ConversationRead,
    status_code=status.HTTP_201_CREATED,
)
def create_conversation(
    payload: ConversationCreate,
    session: SessionDep,
    _: AuthDep,
):
    conversation = Conversation(
        title=payload.title.strip(),
    )

    session.add(conversation)
    session.commit()
    session.refresh(conversation)

    return conversation


@app.get(
    "/conversations",
    response_model=list[ConversationRead],
)
def list_conversations(
    session: SessionDep,
    _: AuthDep,
    offset: Annotated[
        int,
        Query(ge=0),
    ] = 0,
    limit: Annotated[
        int,
        Query(ge=1, le=100),
    ] = 20,
):
    statement = (
        select(Conversation)
        .order_by(
            Conversation.id.desc()
        )
        .offset(offset)
        .limit(limit)
    )

    return list(
        session.exec(statement).all()
    )


@app.get(
    "/conversations/{conversation_id}/messages",
    response_model=list[MessageRead],
)
def list_messages(
    conversation_id: int,
    session: SessionDep,
    _: AuthDep,
    limit: Annotated[
        int,
        Query(ge=1, le=100),
    ] = 50,
):
    conversation = session.get(
        Conversation,
        conversation_id,
    )

    if conversation is None:
        raise HTTPException(
            status_code=404,
            detail="Conversation not found",
        )

    statement = (
        select(Message)
        .where(
            Message.conversation_id
            == conversation_id
        )
        .order_by(
            Message.id.desc()
        )
        .limit(limit)
    )

    messages = list(
        session.exec(statement).all()
    )

    messages.reverse()

    return messages


@app.post(
    "/conversations/{conversation_id}/messages",
    response_model=ChatResponse,
)
def send_message(
    conversation_id: int,
    payload: ChatRequest,
    session: SessionDep,
    _: AuthDep,
):
    conversation = session.get(
        Conversation,
        conversation_id,
    )

    if conversation is None:
        raise HTTPException(
            status_code=404,
            detail="Conversation not found",
        )

    history_statement = (
        select(Message)
        .where(
            Message.conversation_id
            == conversation_id
        )
        .order_by(
            Message.id.desc()
        )
        .limit(20)
    )

    history = list(
        session
        .exec(history_statement)
        .all()
    )

    history.reverse()

    model_messages = [
        {
            "role": "system",
            "content": (
                "شما یک دستیار فارسی دقیق و "
                "کاربردی هستید. پاسخ را روشن "
                "و بدون اطلاعات ساختگی ارائه کنید."
            ),
        }
    ]

    for message in history:
        if message.role not in {
            "user",
            "assistant",
        }:
            continue

        model_messages.append(
            {
                "role": message.role,
                "content": message.content,
            }
        )

    model_messages.append(
        {
            "role": "user",
            "content": payload.message,
        }
    )

    try:
        ai_response = (
            ai_client
            .chat
            .completions
            .create(
                model=DARVAREH_MODEL_ID,
                messages=model_messages,
                temperature=(
                    payload.temperature
                ),
                max_tokens=(
                    payload.max_tokens
                ),
            )
        )
    except Exception as exc:
        raise HTTPException(
            status_code=502,
            detail=(
                "AI service request failed"
            ),
        ) from exc

    answer = (
        ai_response
        .choices[0]
        .message
        .content
        or ""
    )

    usage = ai_response.usage

    user_message = Message(
        conversation_id=conversation_id,
        role="user",
        content=payload.message,
    )

    assistant_message = Message(
        conversation_id=conversation_id,
        role="assistant",
        content=answer,
        model_id=DARVAREH_MODEL_ID,
        input_tokens=(
            usage.prompt_tokens
            if usage
            else None
        ),
        output_tokens=(
            usage.completion_tokens
            if usage
            else None
        ),
    )

    try:
        session.add(user_message)
        session.add(assistant_message)

        session.commit()

        session.refresh(
            user_message
        )

        session.refresh(
            assistant_message
        )
    except Exception:
        session.rollback()
        raise

    return ChatResponse(
        conversation_id=conversation_id,
        user_message=user_message,
        assistant_message=(
            assistant_message
        ),
    )

چرا پیام‌ها بعد از پاسخ مدل ذخیره می‌شوند؟

در این پیاده‌سازی ابتدا پاسخ مدل دریافت و سپس پیام کاربر و پاسخ دستیار داخل یک Transaction ذخیره می‌شوند.

اگر درخواست مدل شکست بخورد، پیام ناقص داخل تاریخچه ثبت نمی‌شود.

اگر بخواهید پیام کاربر حتی در صورت شکست مدل ذخیره شود، باید وضعیت پردازش را نیز نگهداری کنید:

pending
completed
failed

این تصمیم به طراحی محصول بستگی دارد.

چرا فقط ۲۰ پیام آخر ارسال می‌شود؟

در کد:

.limit(20)

ارسال تاریخچه نامحدود مشکلات زیر را ایجاد می‌کند:

  • افزایش Token ورودی
  • افزایش هزینه
  • افزایش زمان پاسخ
  • عبور از Context Window
  • ورود اطلاعات قدیمی و نامرتبط
  • بزرگ شدن Request

در محصول واقعی می‌توان پیام‌های قدیمی را خلاصه یا براساس ارتباط انتخاب کرد.

اجرای MySQL پروژه

برای اجرای محلی MySQL می‌توانید از فایل Docker قبلی استفاده کنید یا کل پروژه را با Compose اجرا کنید.

اگر MySQL روی Host اجرا می‌شود، مقدار زیر درست است:

DATABASE_URL=mysql+pymysql://ai_user:ai_password@localhost:3306/ai_chat?charset=utf8mb4

اجرای API:

uvicorn app.main:app \
  --reload \
  --host 0.0.0.0 \
  --port 8000

مستندات Swagger:

http://localhost:8000/docs

Health Check:

curl \
  http://localhost:8000/health

ایجاد Conversation

curl -X POST \
  http://localhost:8000/conversations \
  -H "Authorization: Bearer YOUR_LONG_RANDOM_APP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "آموزش SQL"
  }'

پاسخ:

{
  "id": 1,
  "title": "آموزش SQL",
  "created_at": "2026-08-09T12:00:00"
}

ارسال پیام به مدل

curl -X POST \
  http://localhost:8000/conversations/1/messages \
  -H "Authorization: Bearer YOUR_LONG_RANDOM_APP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "تفاوت INNER JOIN و LEFT JOIN چیست؟",
    "temperature": 0.2,
    "max_tokens": 700
  }'

پاسخ شامل پیام کاربر و دستیار است:

{
  "conversation_id": 1,
  "user_message": {
    "id": 1,
    "conversation_id": 1,
    "role": "user",
    "content": "تفاوت INNER JOIN و LEFT JOIN چیست؟",
    "model_id": null,
    "input_tokens": null,
    "output_tokens": null,
    "created_at": "2026-08-09T12:01:00"
  },
  "assistant_message": {
    "id": 2,
    "conversation_id": 1,
    "role": "assistant",
    "content": "INNER JOIN فقط رکوردهای مشترک را...",
    "model_id": "YOUR_MODEL_ID",
    "input_tokens": 120,
    "output_tokens": 250,
    "created_at": "2026-08-09T12:01:02"
  }
}

دریافت تاریخچه مکالمه

curl \
  http://localhost:8000/conversations/1/messages \
  -H "Authorization: Bearer YOUR_LONG_RANDOM_APP_TOKEN"

محدود کردن تعداد پیام‌ها:

curl \
  "http://localhost:8000/conversations/1/messages?limit=20" \
  -H "Authorization: Bearer YOUR_LONG_RANDOM_APP_TOKEN"

بررسی مستقیم داده در MySQL

ورود به MySQL:

docker exec -it \
  sql-tutorial-mysql \
  mysql -uai_user -pai_password ai_chat

مشاهده مکالمات:

SELECT
    id,
    title,
    created_at
FROM conversations
ORDER BY id DESC;

مشاهده پیام‌ها:

SELECT
    id,
    conversation_id,
    role,
    LEFT(content, 100)
        AS content_preview,
    model_id,
    input_tokens,
    output_tokens,
    created_at
FROM messages
ORDER BY id DESC;

گزارش مصرف هر مکالمه:

SELECT
    conversation_id,
    COUNT(*) AS message_count,
    SUM(input_tokens)
        AS total_input_tokens,
    SUM(output_tokens)
        AS total_output_tokens
FROM messages
WHERE role = 'assistant'
GROUP BY conversation_id
ORDER BY conversation_id DESC;

ساخت Dockerfile

فایل Dockerfile:

FROM python:3.13-slim

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /code

COPY requirements.txt .

RUN pip install \
    --no-cache-dir \
    --upgrade \
    -r requirements.txt

COPY app ./app

RUN useradd \
    --create-home \
    --uid 10001 \
    appuser

USER appuser

EXPOSE 8000

CMD [
    "uvicorn",
    "app.main:app",
    "--host",
    "0.0.0.0",
    "--port",
    "8000"
]

ساخت .dockerignore

.env
.venv
__pycache__
*.pyc
.git
.gitignore
.idea
.vscode

اجرای کامل FastAPI و MySQL با Docker Compose

فایل compose.yaml:

services:
  db:
    image: mysql:8.4
    environment:
      MYSQL_ROOT_PASSWORD: root_password
      MYSQL_DATABASE: ai_chat
      MYSQL_USER: ai_user
      MYSQL_PASSWORD: ai_password
    volumes:
      - mysql_data:/var/lib/mysql
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
    healthcheck:
      test:
        [
          "CMD",
          "mysqladmin",
          "ping",
          "-h",
          "localhost",
          "-uroot",
          "-proot_password"
        ]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 20s
    restart: unless-stopped

  api:
    build:
      context: .
    environment:
      DATABASE_URL: mysql+pymysql://ai_user:ai_password@db:3306/ai_chat?charset=utf8mb4
      DARVAREH_API_KEY: ${DARVAREH_API_KEY}
      DARVAREH_MODEL_ID: ${DARVAREH_MODEL_ID}
      APP_TOKEN: ${APP_TOKEN}
    ports:
      - "8000:8000"
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test:
        [
          "CMD",
          "python",
          "-c",
          "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
        ]
      interval: 15s
      timeout: 5s
      retries: 5
      start_period: 15s
    restart: unless-stopped

volumes:
  mysql_data:

در .env فقط Secretهای موردنیاز Compose را قرار دهید:

DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL_ID=YOUR_MODEL_ID
APP_TOKEN=YOUR_LONG_RANDOM_APP_TOKEN

اجرا:

docker compose up \
  --build \
  --detach

مشاهده وضعیت:

docker compose ps

مشاهده لاگ API:

docker compose logs \
  -f api

مشاهده لاگ MySQL:

docker compose logs \
  -f db

توقف:

docker compose down

حذف Containerها همراه داده MySQL:

docker compose down -v

گزینه -v داده‌های Volume را حذف می‌کند؛ بنابراین فقط زمانی از آن استفاده کنید که واقعاً قصد حذف داده آزمایشی را دارید.

آیا create_all برای Production مناسب است؟

در پروژه آموزشی از دستور زیر استفاده کردیم:

SQLModel.metadata.create_all(
    engine
)

این دستور جدول‌های موجود را ایجاد می‌کند، اما جایگزین کامل Database Migration نیست.

برای تغییرات واقعی Schema بهتر است از Migration استفاده شود:

نسخه ۱:
ساخت conversations

نسخه ۲:
افزودن model_id

نسخه ۳:
افزودن token usage

نسخه ۴:
ساخت index جدید

ابزاری مانند Alembic می‌تواند این تغییرات را نسخه‌بندی و اجرا کند. مستندات FastAPI نیز برای محیط Production استفاده از Migration را توصیه می‌کند.

بهینه‌سازی Schema مکالمات هوش مصنوعی

برای بزرگ‌تر شدن پروژه می‌توانید ستون‌های زیر را اضافه کنید.

جدول Conversation:

ALTER TABLE conversations
ADD COLUMN user_id BIGINT UNSIGNED NULL,
ADD COLUMN updated_at DATETIME NULL,
ADD COLUMN archived_at DATETIME NULL;

جدول Message:

ALTER TABLE messages
ADD COLUMN request_id VARCHAR(100) NULL,
ADD COLUMN finish_reason VARCHAR(50) NULL,
ADD COLUMN latency_ms INT UNSIGNED NULL,
ADD COLUMN error_code VARCHAR(100) NULL;

Indexهای احتمالی:

CREATE INDEX
    idx_conversations_created
ON conversations (
    created_at
);
CREATE INDEX
    idx_messages_model_created
ON messages (
    model_id,
    created_at
);

Index را فقط براساس Query و گزارش واقعی اضافه کنید.

مدیریت هم‌زمانی مکالمات

اگر دو درخواست هم‌زمان برای یک Conversation ارسال شوند، ممکن است هر دو یک تاریخچه یکسان بخوانند و پاسخ‌ها با ترتیب غیرمنتظره ذخیره شوند.

راهکارها براساس نیاز پروژه:

  • جلوگیری از چند درخواست هم‌زمان برای یک Conversation
  • استفاده از Queue
  • ثبت وضعیت پردازش
  • استفاده از Version Number
  • قفل‌گذاری کنترل‌شده
  • ساخت Request ID منحصربه‌فرد
  • مرتب‌سازی براساس Sequence Number

برای چت ساده ممکن است محدود کردن یک درخواست فعال برای هر مکالمه کافی باشد.

ذخیره پرامپت سیستم

در پروژه آموزشی System Prompt داخل کد قرار دارد:

{
    "role": "system",
    "content": "شما یک دستیار فارسی..."
}

در محصول بزرگ‌تر می‌توان نسخه پرامپت را ذخیره کرد:

CREATE TABLE prompt_versions (
    id BIGINT UNSIGNED
        AUTO_INCREMENT
        PRIMARY KEY,

    name VARCHAR(100)
        NOT NULL,

    version INT UNSIGNED
        NOT NULL,

    content TEXT
        NOT NULL,

    is_active BOOLEAN
        NOT NULL
        DEFAULT TRUE,

    created_at DATETIME
        NOT NULL
        DEFAULT CURRENT_TIMESTAMP,

    UNIQUE KEY
        uq_prompt_name_version (
            name,
            version
        )
);

این ساختار کمک می‌کند بدانید هر پاسخ با کدام نسخه پرامپت تولید شده است.

آیا متن کامل مکالمه را ذخیره کنیم؟

این تصمیم به کاربرد محصول، رضایت کاربر و سیاست نگهداری داده بستگی دارد.

پیش از ذخیره‌سازی باید موارد زیر مشخص شوند:

  • هدف ذخیره اطلاعات
  • مدت نگهداری
  • امکان حذف توسط کاربر
  • سطح دسترسی کارکنان
  • داده‌های حساس
  • Backup
  • Logging
  • محیط توسعه و Production
  • سیاست حریم خصوصی محصول

از ذخیره اطلاعات حساس غیرضروری خودداری کنید و دسترسی پایگاه داده را محدود نگه دارید.

MySQL یا PostgreSQL؟

هر دو برای بسیاری از برنامه‌ها مناسب هستند.

معیارMySQLPostgreSQL
پایگاه داده رابطه‌ایبلهبله
پشتیبانی از Transactionبلهبله
JSONبلهبسیار قدرتمند
یادگیری اولیهنسبتاً سادهنسبتاً ساده
اکوسیستم میزبانیگستردهگسترده
قابلیت‌های پیشرفته SQLمناسببسیار گسترده
پروژه چت و APIمناسبمناسب

انتخاب باید براساس تجربه تیم، زیرساخت، نیازهای Query، امکانات میزبانی و مقیاس پروژه انجام شود.

SQL یا NoSQL؟

هیچ‌کدام همیشه بهتر نیستند.

SQL برای این موارد مناسب است:

  • داده‌های دارای ارتباط مشخص
  • Transaction
  • گزارش‌گیری ساختاریافته
  • محدودیت‌های دقیق
  • کاربران، سفارش‌ها و پرداخت‌ها
  • مدیریت مصرف و صورتحساب
  • مکالمات دارای Metadata

NoSQL می‌تواند برای این موارد مناسب باشد:

  • Schema بسیار متغیر
  • Documentهای مستقل
  • بعضی بارهای کاری توزیع‌شده
  • داده‌هایی با ساختار غیرثابت

بسیاری از سیستم‌های واقعی از ترکیب چند نوع ذخیره‌ساز استفاده می‌کنند:

MySQL یا PostgreSQL:
داده اصلی و تراکنش‌ها

Redis:
Cache و داده موقت

Vector Database:
Embedding و جست‌وجوی معنایی

Object Storage:
فایل و سند

خطاهای رایج MySQL و FastAPI

خطای Connection refused

موارد زیر را بررسی کنید:

  • MySQL اجرا شده باشد.
  • Host درست باشد.
  • پورت 3306 درست باشد.
  • Containerها در شبکه مشترک باشند.
  • نام سرویس Docker به‌جای localhost استفاده شود.

داخل Docker Compose:

db

روی سیستم Host:

localhost

خطای Access denied

نام کاربری و رمز را بررسی کنید:

DATABASE_URL=mysql+pymysql://ai_user:ai_password@db:3306/ai_chat

اگر Volume قبلاً با رمز دیگری ساخته شده باشد، تغییر Environment Variable الزاماً رمز Database موجود را تغییر نمی‌دهد.

خطای Unknown database

پایگاه داده ساخته نشده یا نام آن اشتباه است:

SHOW DATABASES;

نمایش نادرست متن فارسی

از utf8mb4 استفاده کنید:

?charset=utf8mb4

و در MySQL:

--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci

خطای Table does not exist

بررسی کنید:

  • Lifespan اجرا شده باشد.
  • برنامه به Database درست متصل باشد.
  • کاربر اجازه ساخت جدول داشته باشد.
  • Migration اجرا شده باشد.

کند شدن Query پیام‌ها

ابتدا Query را با EXPLAIN بررسی کنید:

EXPLAIN
SELECT *
FROM messages
WHERE conversation_id = 100
ORDER BY created_at DESC
LIMIT 20;

سپس وجود Index مناسب را بررسی کنید:

SHOW INDEX
FROM messages;

خطای 502 از Endpoint چت

موارد احتمالی:

  • کلید درواره اشتباه است.
  • Model ID معتبر نیست.
  • ارتباط شبکه برقرار نشده است.
  • پاسخ مدل Timeout شده است.
  • موجودی یا محدودیت حساب نیاز به بررسی دارد.

چک‌لیست یادگیری SQL

  • تفاوت SQL و MySQL را می‌دانم.
  • می‌توانم Database و Table بسازم.
  • Primary Key را می‌شناسم.
  • Foreign Key را می‌شناسم.
  • با INSERT داده اضافه می‌کنم.
  • با SELECT داده می‌خوانم.
  • با WHERE فیلتر می‌کنم.
  • با ORDER BY مرتب می‌کنم.
  • با LIMIT نتیجه را محدود می‌کنم.
  • با UPDATE داده را تغییر می‌دهم.
  • با DELETE رکورد حذف می‌کنم.
  • تفاوت NULL و رشته خالی را می‌دانم.
  • از GROUP BY استفاده می‌کنم.
  • تفاوت WHERE و HAVING را می‌دانم.
  • INNER JOIN و LEFT JOIN را می‌شناسم.
  • مفهوم Index را می‌دانم.
  • Query را با EXPLAIN بررسی می‌کنم.
  • Transaction و Rollback را می‌شناسم.
  • از Query پارامتری استفاده می‌کنم.
  • Secret را داخل Source Code قرار نمی‌دهم.
  • برای تغییر Schema از Migration استفاده می‌کنم.

پرسش‌های متداول

SQL چیست؟

SQL زبان استاندارد کار با پایگاه‌های داده رابطه‌ای است. با آن می‌توان جدول ساخت، اطلاعات را خواند، تغییر داد، حذف کرد و میان جدول‌ها ارتباط ایجاد کرد.

تفاوت SQL و MySQL چیست؟

SQL یک زبان است، اما MySQL نرم‌افزاری است که Queryهای SQL را اجرا و داده‌ها را مدیریت می‌کند.

آیا یادگیری SQL سخت است؟

دستورات پایه مانند SELECT، INSERT، UPDATE و DELETE ساده‌اند. طراحی Schema، بهینه‌سازی Query، Transaction و مدیریت هم‌زمانی به تمرین بیشتری نیاز دارند.

برای یادگیری SQL از MySQL استفاده کنیم یا SQLite؟

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

آیا برای هوش مصنوعی باید SQL بلد باشیم؟

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

آیا SQL می‌تواند متن فارسی ذخیره کند؟

بله. در MySQL بهتر است از Character Set مناسب مانند utf8mb4 استفاده شود.

آیا ORM جای SQL را می‌گیرد؟

خیر. ORM نوشتن بخشی از کد را ساده می‌کند، اما برای طراحی جدول، ساخت Index، تحلیل Performance و رفع خطا همچنان دانش SQL لازم است.

آیا SQLModel فقط با SQLite کار می‌کند؟

خیر. SQLModel روی SQLAlchemy ساخته شده و می‌تواند با پایگاه‌های داده‌ای که Driver سازگار دارند کار کند. در این آموزش از MySQL و PyMySQL استفاده کردیم.

چطور تاریخچه چت را ذخیره کنیم؟

معمولاً یک جدول Conversation و یک جدول Message ساخته می‌شود. هر Message با Foreign Key به Conversation متصل است و فیلدهایی مانند Role، Content، Model ID و Token Usage دارد.

آیا باید همه پیام‌های قبلی را برای مدل ارسال کنیم؟

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

هزینه استفاده از مدل چگونه محاسبه می‌شود؟

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

جمع‌بندی

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

در این مقاله موارد زیر را یاد گرفتیم:

  • SQL و پایگاه داده رابطه‌ای چیست.
  • تفاوت SQL و MySQL چیست.
  • چگونه Database و Table بسازیم.
  • Primary Key و Foreign Key چه هستند.
  • چگونه از CRUD استفاده کنیم.
  • WHERE، LIKE، ORDER BY و LIMIT چگونه کار می‌کنند.
  • GROUP BY و توابع آماری چه کاربردی دارند.
  • INNER JOIN و LEFT JOIN چه تفاوتی دارند.
  • Index و EXPLAIN چگونه به Performance کمک می‌کنند.
  • Transaction و Rollback چه هستند.
  • چگونه از SQL Injection جلوگیری کنیم.
  • چگونه FastAPI را به MySQL متصل کنیم.
  • چگونه تاریخچه مکالمات هوش مصنوعی را ذخیره کنیم.
  • چگونه Python، SQLModel و API درواره را در یک پروژه واقعی ترکیب کنیم.
  • چگونه پروژه را با Docker Compose اجرا کنیم.

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

منابع

مقالات مرتبط

برای مطالعه شرایط استفاده و محدودیت‌های مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.

Read more

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

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

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce شیوه‌ای جدید برای خرید اینترنتی است که در آن ایجنت هوش مصنوعی می‌تواند نیاز کاربر را بفهمد، محصولات را جست‌وجو و مقایسه کند و فرایند خرید را پیش ببرد. در این راهنما با معماری، UCP، ACP و پیاده‌سازی آن با API درواره آشنا می‌شوید.