کوئری داخل کوئری
زیرکوئری میتواند یک مقدار (scalar)، یک فهرست (برای IN)، یک «وجود دارد یا نه» (EXISTS) یا یک جدول موقت (derived table) برگرداند.
-- scalar: محصولات گرانتر از میانگین
SELECT sku, list_price FROM products
WHERE list_price > (SELECT AVG(list_price) FROM products);
-- EXISTS: مشتریهایی که دستکم یک سفارش بالای ۵۰۰ میلیون ریال دارند
SELECT c.id, c.full_name FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.customer_id = c.id AND o.total_amount > 500000000
);
زیرکوئری دوم «همبسته» (correlated) است: به ستون کوئری بیرونی (c.id) ارجاع میدهد. EXISTS به محض پیدا کردن اولین ردیف متوقف میشود و مهم نیست داخلش چه چیزی SELECT کنید.
NOT IN در برابر NOT EXISTS
-- محصولاتی که هرگز فروش نرفتهاند
SELECT p.sku FROM products p
WHERE NOT EXISTS (SELECT 1 FROM order_items oi WHERE oi.product_id = p.id);
اگر ستون زیرکوئری NULL داشته باشد، NOT IN برای همهی ردیفها UNKNOWN میشود و نتیجه خالی میآید. NOT EXISTS این مشکل را ندارد و MySQL 8 آن را به anti-join بهینه تبدیل میکند.
Derived table و LATERAL
-- derived table: میانگین خرید هر مشتری، سپس فقط مشتریهای بالای میانگین کل
SELECT t.customer_id, t.spent
FROM (SELECT customer_id, SUM(total_amount) AS spent
FROM orders GROUP BY customer_id) AS t
WHERE t.spent > 1000000000;
-- LATERAL (8.0.14+): سه سفارش آخر هر مشتری
SELECT c.full_name, last3.id, last3.created_at
FROM customers c
JOIN LATERAL (
SELECT o.id, o.created_at FROM orders o
WHERE o.customer_id = c.id
ORDER BY o.created_at DESC LIMIT 3
) AS last3 ON TRUE;
derived table معمولی نمیتواند به جدولهای کنارش ارجاع دهد؛ LATERAL این اجازه را میدهد و برای «N ردیف آخر هر گروه» با ایندکس (customer_id, created_at) بسیار سریع است.
View: کوئری نامدار
CREATE OR REPLACE VIEW v_order_summary AS
SELECT o.id, o.created_at, o.status, c.full_name, c.mobile, o.total_amount
FROM orders o JOIN customers c ON c.id = o.customer_id;
SELECT * FROM v_order_summary WHERE status = 'paid' ORDER BY created_at DESC LIMIT 20;
-- View قابل بهروزرسانی با محافظ
CREATE VIEW v_draft_orders AS
SELECT id, customer_id, status FROM orders WHERE status = 'draft'
WITH CHECK OPTION; -- UPDATE که ردیف را از شرط View بیرون ببرد رد میشود
View داده ذخیره نمیکند (MySQL «Materialized View» ندارد). مزیتش پنهان کردن پیچیدگی و محدود کردن دسترسی است: میتوانید به کاربر گزارشگیر فقط روی View دسترسی بدهید و ستونهای حساس را نشان ندهید.
نکتههایی که کمتر کسی میداند
SELECT *در تعریف View هنگام ساخت به فهرست ثابت ستونها تبدیل میشود؛ ستونهایی که بعداً به جدول اضافه کنید در View نمیآیند تا View را دوباره بسازید.- View با ALGORITHM=MERGE در کوئری بیرونی ادغام میشود و از ایندکسها استفاده میکند؛ اما اگر GROUP BY، DISTINCT، UNION یا LIMIT داشته باشد به TEMPTABLE تبدیل میشود و فیلتر بیرونی بعد از ساخت کل نتیجه اعمال میشود.
- View بهطور پیشفرض با
SQL SECURITY DEFINERساخته میشود؛ اگر کاربر سازنده حذف شود، View با خطای «The user specified as a definer does not exist» از کار میافتد — دردسر رایج بعد از انتقال بکاپ به سرور دیگر. - زیرکوئری scalar که بیش از یک ردیف برگرداند، خطای 1242 میدهد، اما فقط وقتی دادهی تکراری واقعاً پیدا شود؛ یعنی ممکن است ماهها درست کار کند و ناگهان بشکند.
- بهینهساز MySQL 8 بسیاری از
IN (subquery)ها را به semi-join تبدیل میکند؛ توصیهی قدیمی «همیشه زیرکوئری را به JOIN تبدیل کن» دیگر قانون نیست — با EXPLAIN بسنجید.