← همه آموزش‌ها

آموزش SQL از پایه تا Window Function

از مفهوم دیتابیس و اولین SELECT تا JOIN، Aggregate، CTE، Index، Transaction و Window Functionهای حرفه‌ای

بنر آموزش SQL: جدول سفارش‌ها و توابع پنجره‌ای OVER و PARTITION BY در کنار کارت‌های پایه، تجمیع، پیشرفته و پنجره.
از کوئری ساده تا Window Function، بدون جا ماندن از JOIN، ایندکس و تراکنش.

در این آموزش چه یاد می‌گیریم؟

این مطلب یک دوره کامل SQL است؛ از صفر شروع می‌کنیم و کم‌کم به مباحث حرفه‌ای می‌رسیم. تمام مثال‌ها روی یک دیتابیس فروشگاهی ساده با چهار جدول customers، products، orders و order_items نوشته شده‌اند تا بتوانید همان منطق را در پروژه واقعی خودتان پیاده کنید.

در کنار دستورهای پایه مثل SELECT و JOIN، به سراغ Index، Transaction و در نهایت Window Functionها می‌رویم؛ ابزاری قدرتمند که خیلی از توسعه‌دهندگان دیر با آن آشنا می‌شوند.

۱. SQL و دیتابیس چیست؟

دیتابیس (Database) جایی است که داده‌های یک برنامه به‌صورت منظم و پایدار نگه‌داری می‌شود؛ مثل مشتری‌ها، محصولات و سفارش‌های یک فروشگاه. برخلاف متغیرهای داخل حافظه برنامه، داده‌های دیتابیس بعد از خاموش شدن برنامه یا حتی ری‌استارت سرور از بین نمی‌روند.

SQL مخفف Structured Query Language است؛ یعنی زبانی برای صحبت کردن با دیتابیس‌های رابطه‌ای. با SQL می‌توانید داده تعریف کنید (ساخت جدول)، داده اضافه یا تغییر دهید (INSERT، UPDATE، DELETE) و از همه مهم‌تر، داده را بخوانید و فیلتر کنید (SELECT).

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

SQL هم یک استاندارد زبانی است و هم مجموعه‌ای از Dialectها؛ یعنی هر موتور دیتابیس (PostgreSQL، MySQL، SQL Server و بقیه) بخش بزرگی از دستورها را یکسان پیاده‌سازی می‌کند، اما در جزئیاتی مثل paging یا نوع داده تفاوت‌های کوچکی دارد. در بخش ۱۴ همین آموزش این تفاوت‌ها را می‌بینیم.

۲. انواع دیتابیس‌ها و تفاوت‌شان

وقتی می‌گوییم «دیتابیس»، منظور فقط دیتابیس رابطه‌ای نیست. مدل‌های مختلفی برای ذخیره داده وجود دارد که هرکدام برای نوع خاصی از مسئله بهینه شده‌اند:

  • Relational (مثل PostgreSQL، MySQL، SQL Server، Oracle، SQLite): داده در جدول‌های با ستون‌های ثابت ذخیره می‌شود و رابطه بین جدول‌ها با کلید خارجی تعریف می‌شود. زبان اصلی آن SQL است.
  • Document (مثل MongoDB): هر رکورد یک سند شبیه JSON است و ساختار آن می‌تواند بین رکوردها فرق کند. برای داده با شکل متغیر یا Nested مناسب است.
  • Key-Value (مثل Redis): داده به‌صورت جفت کلید و مقدار در حافظه یا دیسک نگه‌داری می‌شود. سرعت خواندن و نوشتن بسیار بالاست و بیشتر برای Cache و Session استفاده می‌شود.
  • Graph (مثل Neo4j): داده به‌شکل گره و رابطه بین گره‌ها مدل می‌شود. برای شبکه اجتماعی، سیستم پیشنهاد و مسیرهای پیچیده بین موجودیت‌ها مناسب است.
  • Column-family (مثل Cassandra): داده ستونی و توزیع‌شده نگه‌داری می‌شود و برای نوشتن حجم بسیار بالا در مقیاس چند سرور طراحی شده است.
  • Time-series (مثل InfluxDB یا TimescaleDB): برای داده‌ای که همراه با زمان ثبت می‌شود، مثل Metric و Log سنسورها، و Query آن معمولاً روی بازه زمانی و Aggregate انجام می‌شود.
نوع دیتابیسنمونهمدل دادهکجا مناسب است
RelationalPostgreSQL, MySQL, SQL Serverجدول با Schema ثابتداده تراکنشی، رابطه‌های پیچیده، نیاز به یکپارچگی داده
DocumentMongoDBسند JSON با Schema متغیرمحتوای Nested، ساختار داده در حال تغییر
Key-ValueRedisکلید و مقدارCache، Session، Rate Limiting
GraphNeo4jگره و رابطهشبکه اجتماعی، تشخیص تقلب، پیشنهاد محصول
Column-familyCassandraستون توزیع‌شدهنوشتن بسیار زیاد، مقیاس افقی چند دیتاسنتر
Time-seriesInfluxDB, TimescaleDBمقدار همراه با TimestampMetric، Log، داده سنسور و مانیتورینگ
این آموزش روی SQL و دیتابیس‌های رابطه‌ای تمرکز دارد. با این حال بدانید که بعضی از دیتابیس‌های غیررابطه‌ای هم زبان شبیه SQL دارند؛ مثلاً Cassandra با CQL و بعضی سرویس‌های Time-series با SQL-like query کار می‌کنند، ولی مدل داده و رفتار زیر پوسته آن‌ها با دیتابیس رابطه‌ای فرق دارد.
مقایسه شش نوع دیتابیس: Relational، Document، Key-Value، Graph، Column-family و Time-series و کاربرد هرکدام
هر مدل دیتابیس برای شکل خاصی از داده و الگوی دسترسی طراحی شده؛ انتخاب درست به نوع داده و نحوه Queryهای شما بستگی دارد.

۳. مفاهیم پایه رابطه‌ای

قبل از نوشتن اولین Query، چند اصطلاح پایه را می‌شناسیم:

  • جدول (Table): مجموعه‌ای از داده‌های هم‌نوع با ساختار ثابت؛ مثل جدول products برای نگه‌داری محصولات.
  • سطر (Row): یک رکورد مشخص داخل جدول؛ مثلاً یک محصول خاص با تمام مشخصاتش.
  • ستون (Column): یک ویژگی از رکورد با نوع داده معین؛ مثل price که همیشه عدد است.
  • Primary Key (PK): ستون یا مجموعه ستونی که هر سطر را به‌طور یکتا مشخص می‌کند. دو سطر نمی‌توانند PK یکسان داشته باشند.
  • Foreign Key (FK): ستونی در یک جدول که به Primary Key جدول دیگر اشاره می‌کند و رابطه بین دو جدول را برقرار می‌کند؛ مثلاً customer_id در جدول orders به customers اشاره دارد.
  • NULL: یعنی نبود مقدار، نه صفر و نه رشته خالی. اگر شماره تلفن مشتری ثبت نشده باشد، مقدار آن ستون NULL است.
Foreign Key فقط یک نکته اسنادی نیست؛ دیتابیس با آن جلوگیری می‌کند سفارشی برای مشتری‌ای که وجود ندارد ثبت شود. این ویژگی را Referential Integrity می‌نامند.

۴. نمونه Schema فروشگاه

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

CREATE TABLE customers (
    customer_id   INT PRIMARY KEY,
    full_name     VARCHAR(100) NOT NULL,
    email         VARCHAR(150) NOT NULL UNIQUE,
    city          VARCHAR(80),
    created_at    DATE NOT NULL
);

CREATE TABLE products (
    product_id    INT PRIMARY KEY,
    product_name  VARCHAR(120) NOT NULL,
    category      VARCHAR(60),
    price         DECIMAL(10,2) NOT NULL,
    stock_qty     INT NOT NULL DEFAULT 0
);

CREATE TABLE orders (
    order_id      INT PRIMARY KEY,
    customer_id   INT NOT NULL,
    order_date    DATE NOT NULL,
    status        VARCHAR(20) NOT NULL DEFAULT 'pending',
    is_deleted    INT NOT NULL DEFAULT 0,
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);

CREATE TABLE order_items (
    order_item_id INT PRIMARY KEY,
    order_id      INT NOT NULL,
    product_id    INT NOT NULL,
    quantity      INT NOT NULL,
    unit_price    DECIMAL(10,2) NOT NULL,
    FOREIGN KEY (order_id) REFERENCES orders(order_id),
    FOREIGN KEY (product_id) REFERENCES products(product_id)
);

این نسخه از Schema عمداً ساده و قابل‌حمل بین موتورهای مختلف نوشته شده است. برای شناسه‌های خودافزا (Auto Increment)، هر Dialect syntax خودش را دارد:

  • PostgreSQL: customer_id SERIAL PRIMARY KEY یا GENERATED ALWAYS AS IDENTITY
  • MySQL: customer_id INT AUTO_INCREMENT PRIMARY KEY
  • SQL Server: customer_id INT IDENTITY(1,1) PRIMARY KEY
  • SQLite: customer_id INTEGER PRIMARY KEY AUTOINCREMENT
  • Oracle: customer_id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY

۵. SELECT پایه، WHERE، ORDER BY و صفحه‌بندی

ساده‌ترین Query خواندن همه ستون‌ها یا چند ستون خاص است:

SELECT * FROM products;

SELECT product_name, price
FROM products;
در کد واقعی از SELECT * بپرهیزید. ستون‌های لازم را صریح بنویسید تا هم Query سریع‌تر باشد و هم با تغییر Schema، برنامه شما به‌طور غیرمنتظره خراب نشود.

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

SELECT product_name, price, category
FROM products
WHERE category = 'Electronics' AND price > 100;

SELECT * FROM orders
WHERE status IN ('pending', 'processing');

SELECT * FROM customers
WHERE city LIKE 'Tehran%';

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

SELECT product_name, price
FROM products
ORDER BY price DESC;

SELECT product_name, price
FROM products
ORDER BY category ASC, price DESC;

حذف تکراری با DISTINCT

SELECT DISTINCT category
FROM products;

صفحه‌بندی: LIMIT/OFFSET در برابر TOP و OFFSET-FETCH

وقتی فقط تعدادی از سطرها یا یک صفحه از نتیجه را می‌خواهید، هر Dialect syntax خودش را دارد:

-- PostgreSQL, MySQL, SQLite
SELECT product_name, price
FROM products
ORDER BY price DESC
LIMIT 5 OFFSET 10;

-- SQL Server: نسخه ساده با TOP
SELECT TOP 5 product_name, price
FROM products
ORDER BY price DESC;

-- SQL Server: نسخه استاندارد با صفحه‌بندی واقعی
SELECT product_name, price
FROM products
ORDER BY price DESC
OFFSET 10 ROWS FETCH NEXT 5 ROWS ONLY;
TOP بدون ORDER BY معنای مشخصی ندارد؛ همیشه قبل از TOP، LIMIT یا OFFSET/FETCH یک ترتیب مشخص تعریف کنید، وگرنه نتیجه بین اجراهای مختلف می‌تواند فرق کند.

۶. INSERT، UPDATE و DELETE

افزودن داده با INSERT

INSERT INTO customers (customer_id, full_name, email, city, created_at)
VALUES (1, 'Sara Ahmadi', 'sara@example.com', 'Tehran', '2026-01-15');

INSERT INTO products (product_id, product_name, category, price, stock_qty)
VALUES (101, 'Wireless Mouse', 'Electronics', 249000, 40);

به‌روزرسانی داده با UPDATE

UPDATE products
SET stock_qty = stock_qty - 1
WHERE product_id = 101;

UPDATE orders
SET status = 'shipped'
WHERE order_id = 5001;

حذف داده با DELETE

DELETE FROM order_items
WHERE order_item_id = 9001;
UPDATE و DELETE بدون WHERE روی همه سطرهای جدول اجرا می‌شوند. قبل از فشردن Enter، شرط WHERE را دوباره بخوانید و اگر ابزار اجازه می‌دهد، ابتدا با یک SELECT با همان شرط، سطرهای هدف را ببینید.

ترفند Soft Delete

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

UPDATE orders
SET is_deleted = 1
WHERE order_id = 5001;

-- بعد در همه Queryهای عادی این شرط را اضافه می‌کنیم
SELECT * FROM orders
WHERE is_deleted = 0;
الگوی رایج‌تر به‌جای یک فلگ ساده، ستونی مثل deleted_at TIMESTAMP NULL است؛ اگر NULL باشد سطر فعال است، و اگر مقدار داشته باشد، همان مقدار زمان دقیق حذف را هم نشان می‌دهد.

۷. JOIN بین جدول‌ها

JOIN دو یا چند جدول را بر اساس یک ستون مشترک (معمولاً PK و FK) کنار هم می‌گذارد. چهار نوع اصلی آن را با مثال می‌بینیم.

INNER JOIN — فقط سطرهایی که در هر دو طرف تطابق دارند

SELECT o.order_id, c.full_name, o.order_date
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id;

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

LEFT JOIN — همه سطرهای جدول سمت چپ، حتی بدون تطابق

SELECT c.full_name, o.order_id, o.order_date
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id;

این Query همه مشتری‌ها را برمی‌گرداند؛ حتی کسی که هنوز سفارشی ثبت نکرده. برای چنین مشتری‌ها ستون‌های order_id و order_date برابر NULL خواهد بود.

RIGHT JOIN — همه سطرهای جدول سمت راست، حتی بدون تطابق

SELECT c.full_name, o.order_id
FROM orders o
RIGHT JOIN customers c ON o.customer_id = c.customer_id;

این Query دقیقاً همان نتیجه مثال LEFT JOIN بالا را می‌دهد؛ فقط جای دو جدول در نوشتن Query عوض شده است. RIGHT JOIN کمتر استفاده می‌شود، چون معمولاً می‌توان همان منطق را با LEFT JOIN و ترتیب دیگر جدول‌ها نوشت.

FULL JOIN — همه سطرها از هر دو طرف

SELECT c.full_name, o.order_id
FROM customers c
FULL JOIN orders o ON c.customer_id = o.customer_id;

FULL JOIN (یا FULL OUTER JOIN) هم مشتری‌های بدون سفارش و هم سفارش‌های بدون مشتری معتبر (در صورت وجود چنین داده ناسازگاری) را نشان می‌دهد.

MySQL از FULL OUTER JOIN پشتیبانی مستقیم نمی‌کند؛ باید همان نتیجه را با UNION یک LEFT JOIN و یک RIGHT JOIN بسازید. SQLite هم تا نسخه‌های قدیمی‌تر RIGHT JOIN و FULL JOIN نداشت؛ در نسخه‌های جدیدتر این دو اضافه شده‌اند.

۸. توابع Aggregate، GROUP BY و HAVING

توابع Aggregate روی چند سطر یک محاسبه واحد انجام می‌دهند: COUNT، SUM، AVG، MIN و MAX.

SELECT COUNT(*) AS total_orders FROM orders;

SELECT SUM(quantity * unit_price) AS total_revenue
FROM order_items;

SELECT AVG(price) AS avg_price, MIN(price) AS min_price, MAX(price) AS max_price
FROM products;

گروه‌بندی با GROUP BY

برای محاسبه Aggregate به‌ازای هر گروه، از GROUP BY استفاده می‌کنیم:

SELECT p.category, SUM(oi.quantity * oi.unit_price) AS category_revenue
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
GROUP BY p.category
ORDER BY category_revenue DESC;

این Query درآمد فروش را برای هر دسته محصول جمع می‌زند؛ نتیجه به تعداد دسته‌های موجود، یعنی خیلی کمتر از تعداد کل سطرهای order_items، سطر خواهد داشت.

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

SELECT customer_id, COUNT(*) AS orders_count
FROM orders
GROUP BY customer_id
HAVING COUNT(*) > 2;
WHERE قبل از گروه‌بندی روی سطرهای خام فیلتر می‌کند؛ HAVING بعد از GROUP BY روی نتیجه Aggregate هر گروه فیلتر می‌کند. به همین دلیل نمی‌توانید مستقیم در WHERE از COUNT(*) استفاده کنید.

۹. Subquery و CTE

گاهی برای پاسخ به یک Query باید نتیجه یک Query دیگر را داخل آن استفاده کنید:

-- Subquery در WHERE: مشتری‌هایی که سفارش دارند
SELECT full_name
FROM customers
WHERE customer_id IN (SELECT DISTINCT customer_id FROM orders);

-- Subquery همبسته (Correlated): تعداد سفارش هر مشتری
SELECT c.full_name,
       (SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.customer_id) AS orders_count
FROM customers c;

وقتی Subqueryها تودرتو و پیچیده می‌شوند، CTE یا Common Table Expression با کلمه WITH خوانایی را بهتر می‌کند:

WITH customer_totals AS (
    SELECT o.customer_id, SUM(oi.quantity * oi.unit_price) AS total_spent
    FROM orders o
    JOIN order_items oi ON oi.order_id = o.order_id
    GROUP BY o.customer_id
)
SELECT c.full_name, ct.total_spent
FROM customer_totals ct
JOIN customers c ON c.customer_id = ct.customer_id
WHERE ct.total_spent > (SELECT AVG(total_spent) FROM customer_totals)
ORDER BY ct.total_spent DESC;

این Query مشتری‌هایی را نشان می‌دهد که هزینه کل خریدشان بیشتر از میانگین هزینه همه مشتری‌ها است. CTE به شما اجازه می‌دهد یک نتیجه میانی را نام‌گذاری کنید و چند بار در Query اصلی از آن استفاده کنید، بدون تکرار همان منطق.

۱۰. Index و اهمیت آن

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

CREATE INDEX idx_orders_customer_id ON orders(customer_id);
CREATE INDEX idx_order_items_order_id ON order_items(order_id);
CREATE UNIQUE INDEX idx_customers_email ON customers(email);

بدون Index مناسب، دیتابیس مجبور است برای هر Query کل جدول را سطر به سطر بررسی کند (Full Table Scan)؛ روی جدولی با چند میلیون سطر این کار می‌تواند بسیار زمان‌بر باشد.

Primary Key و معمولاً ستون‌های UNIQUE به‌طور خودکار Index می‌گیرند. برای بقیه ستون‌ها، Index را روی ستون‌هایی بسازید که در WHERE، JOIN یا ORDER BY پرکاربردترین هستند.
هر Index رایگان نیست؛ حجم دیسک بیشتری می‌گیرد و هر INSERT/UPDATE/DELETE باید Indexها را هم به‌روزرسانی کند. Index زدن روی هر ستون بدون بررسی الگوی واقعی Queryها (Over-indexing) می‌تواند سرعت نوشتن را کاهش دهد.

۱۱. Transaction و ACID

Transaction مجموعه‌ای از دستورهای SQL است که باید همه با هم موفق یا همه با هم لغو شوند؛ نه نصفه‌ونیمه. مثال کلاسیک آن ثبت یک سفارش همراه با کم کردن موجودی انبار است:

BEGIN TRANSACTION;

UPDATE products
SET stock_qty = stock_qty - 1
WHERE product_id = 101 AND stock_qty >= 1;

INSERT INTO orders (order_id, customer_id, order_date, status)
VALUES (5002, 1, '2026-07-20', 'pending');

INSERT INTO order_items (order_item_id, order_id, product_id, quantity, unit_price)
VALUES (9002, 5002, 101, 1, 249000);

COMMIT;

اگر در وسط راه خطایی رخ دهد — مثلاً موجودی کافی نبود — باید با ROLLBACK همه تغییرات همان Transaction را برگردانیم تا موجودی و سفارش ناسازگار نشوند:

ROLLBACK;

چهار ویژگی ACID دلیل اصلی اعتماد به Transaction هستند:

  • Atomicity: همه دستورهای Transaction با هم اجرا می‌شوند یا هیچ‌کدام اجرا نمی‌شوند.
  • Consistency: دیتابیس بعد از Transaction هم باید در وضعیت معتبر و مطابق قوانین (مثل Foreign Key) باقی بماند.
  • Isolation: Transactionهای هم‌زمان تا حد امکان نباید روی نتیجه یکدیگر تأثیر ناخواسته بگذارند.
  • Durability: بعد از COMMIT موفق، تغییرات حتی با خاموش شدن ناگهانی سرور از بین نمی‌روند.
هر جا چند دستور SQL باید با هم یک واحد منطقی را تشکیل دهند — مثل کسر موجودی و ثبت سفارش — آن‌ها را داخل یک Transaction قرار دهید، نه به‌صورت چند Query جدا از هم.

۱۲. Window Functions حرفه‌ای

Window Function برخلاف GROUP BY که سطرها را در یک گروه ادغام می‌کند و تعداد سطر نتیجه را کم می‌کند، محاسبه را برای هر سطر انجام می‌دهد و همه سطرهای اصلی را نگه می‌دارد. کلیدواژه اصلی آن OVER() است.

نمایش عملکرد Window Function با OVER و PARTITION BY در مقایسه با GROUP BY روی سطرهای جدول
در GROUP BY سطرها به یک نتیجه خلاصه تبدیل می‌شوند؛ در Window Function هر سطر اصلی باقی می‌ماند و یک مقدار محاسبه‌شده کنار آن اضافه می‌شود.

PARTITION BY و ORDER BY داخل OVER

PARTITION BY مشخص می‌کند محاسبه در چه گروهی انجام شود (شبیه GROUP BY، اما بدون ادغام سطرها) و ORDER BY داخل OVER ترتیب محاسبه را در همان گروه تعیین می‌کند.

ROW_NUMBER، RANK و DENSE_RANK

SELECT c.city, c.full_name, ct.total_spent,
       ROW_NUMBER() OVER (PARTITION BY c.city ORDER BY ct.total_spent DESC) AS row_num,
       RANK()       OVER (PARTITION BY c.city ORDER BY ct.total_spent DESC) AS rnk,
       DENSE_RANK() OVER (PARTITION BY c.city ORDER BY ct.total_spent DESC) AS dense_rnk
FROM customers c
JOIN (
    SELECT o.customer_id, SUM(oi.quantity * oi.unit_price) AS total_spent
    FROM orders o
    JOIN order_items oi ON oi.order_id = o.order_id
    GROUP BY o.customer_id
) ct ON ct.customer_id = c.customer_id;

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

LAG و LEAD — مقایسه با سطر قبل و بعد

SELECT customer_id, order_id, order_date,
       LAG(order_date)  OVER (PARTITION BY customer_id ORDER BY order_date) AS previous_order_date,
       LEAD(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) AS next_order_date
FROM orders;

LAG مقدار سطر قبلی و LEAD مقدار سطر بعدی همان Partition را برمی‌گرداند؛ خیلی مناسب برای محاسبه فاصله بین دو خرید متوالی یک مشتری است.

SUM و AVG به‌عنوان Window Function — مجموع در حال جمع (Running Total)

SELECT order_id, order_date,
       SUM(total_amount) OVER (ORDER BY order_date) AS running_total,
       AVG(total_amount) OVER (ORDER BY order_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_avg_3
FROM (
    SELECT o.order_id, o.order_date, SUM(oi.quantity * oi.unit_price) AS total_amount
    FROM orders o
    JOIN order_items oi ON oi.order_id = o.order_id
    GROUP BY o.order_id, o.order_date
) order_totals;

ستون running_total مجموع تجمعی فروش تا هر سفارش را نشان می‌دهد و moving_avg_3 میانگین متحرک سه سفارش آخر را محاسبه می‌کند؛ هر دو بدون اینکه سطرهای اصلی گزارش را از دست بدهیم.

GROUP BY در برابر Window Function

ویژگیGROUP BYWindow Function
تعداد سطر نتیجهیک سطر برای هر گروههمان تعداد سطر ورودی
دسترسی به ستون‌های سطر اصلیمحدود، فقط ستون‌های Group یا Aggregateکامل؛ هر ستون سطر اصلی هم قابل انتخاب است
مقایسه با سطرهای دیگر همان گروهندارددارد؛ با LAG، LEAD، RANK و مشابه
مثال مناسبجمع فروش هر دسته کالارتبه‌بندی مشتری‌ها در هر شهر، بدون از دست دادن جزئیات هر مشتری
قانون ساده: اگر فقط یک عدد خلاصه به‌ازای هر گروه می‌خواهید، GROUP BY کافی است. اگر می‌خواهید همه سطرهای اصلی را نگه دارید و کنار هرکدام یک محاسبه مرتبط با گروهش اضافه کنید، Window Function گزینه درست است.

۱۳. نکات مهم و اشتباهات رایج

  • SQL Injection: هرگز مقدار ورودی کاربر را مستقیم داخل رشته SQL نچسبانید. همیشه از Parameterized Query یا Prepared Statement استفاده کنید تا ورودی به‌عنوان داده تفسیر شود، نه بخشی از دستور SQL.
  • SELECT * در کد Production: باعث انتقال داده اضافه، شکستن کد با تغییر Schema و کند شدن Query می‌شود؛ فقط ستون‌های لازم را انتخاب کنید.
  • UPDATE/DELETE بدون WHERE: بدون شرط، دستور روی همه سطرهای جدول اجرا می‌شود. قبل از اجرا با یک SELECT مشابه، سطرهای هدف را بررسی کنید.
  • مقایسه با NULL: column = NULL همیشه نامعلوم (Unknown) است، نه درست. برای بررسی NULL باید از IS NULL یا IS NOT NULL استفاده کنید.
  • مسئله N+1: اجرای یک Query برای گرفتن لیست و بعد یک Query جدا برای هر سطر (مثلاً گرفتن جزئیات هر سفارش با Query مستقل داخل حلقه) تعداد رفت‌وبرگشت به دیتابیس را منفجر می‌کند. به‌جای آن با یک JOIN یا یک Query دسته‌ای همه داده لازم را یک‌جا بگیرید.

۱۴. تفاوت‌های کوتاه بین Dialectها

موضوعPostgreSQLMySQLSQL ServerSQLiteOracle
Auto IncrementSERIAL / IDENTITYAUTO_INCREMENTIDENTITY(1,1)AUTOINCREMENTIDENTITY / Sequence
صفحه‌بندیLIMIT/OFFSETLIMIT/OFFSETTOP یا OFFSET/FETCHLIMIT/OFFSETFETCH FIRST n ROWS ONLY
تاریخ/زمان فعلیCURRENT_DATECURDATE()GETDATE()date('now')SYSDATE
نوع Boolean بومیدارد (BOOLEAN)ندارد؛ معمولاً TINYINT(1)ندارد؛ معمولاً BITندارد؛ عدد ۰ یا ۱ندارد؛ معمولاً NUMBER(1)
FULL OUTER JOINداردندارد (با UNION شبیه‌سازی می‌شود)دارددر نسخه‌های جدید دارددارد
برای پروژه‌های واقعی، Documentation همان موتوری که استفاده می‌کنید را مرجع اصلی قرار دهید؛ این جدول فقط چند تفاوت رایج را نشان می‌دهد، نه فهرست کامل.

۱۵. خلاصه و چک‌لیست

  • SQL زبان کار با دیتابیس رابطه‌ای است؛ برای انتخاب نوع دیتابیس، مدل داده و الگوی Query خود را در نظر بگیرید.
  • جدول، سطر، ستون، Primary Key و Foreign Key پایه هر Schema رابطه‌ای هستند.
  • با SELECT، WHERE، ORDER BY و JOIN می‌توانید بیشتر گزارش‌های روزمره را بسازید.
  • GROUP BY برای خلاصه‌سازی و Window Function برای محاسبه در کنار سطرهای اصلی است.
  • Index سرعت خواندن را بالا می‌برد، اما رایگان نیست؛ فقط جایی که لازم است اضافه کنید.
  • عملیات چندمرحله‌ای که باید با هم موفق یا لغو شوند را در یک Transaction قرار دهید.
  • همیشه ورودی کاربر را Parameterize کنید و قبل از UPDATE/DELETE، شرط WHERE را دوباره بررسی کنید.

بهترین راه یادگیری SQL، نوشتن Query روی داده واقعی است؛ همین Schema فروشگاهی را در دیتابیس دلخواه خود بسازید و مثال‌های این آموزش را دوباره اجرا کنید.