اتصال جدولها (JOIN)
دادهی چند جدول را کنار هم بگذار: کلید خارجی با REFERENCES و محافظتش از داده، JOIN ... ON برای وصل کردن سفارش به مشتری، اسم مستعار جدول، اتصال سه جدول، LEFT JOIN برای پیدا کردن مشتریهای بیسفارش، JOIN همراه GROUP BY برای گزارش خرید هر مشتری، و خطاهای ambiguous و ضرب دکارتی.
- حدود ۲۱ دقیقه مطالعه
- ۶ مثال اجراشده
- ۳ تمرین با جواب
- مقدماتی تا متوسط
- بهروزرسانی: ۲۹ مهر ۱۴۰۵
توی این درس یاد میگیری با چند جدول مرتبط کار کنی. فروشگاه واقعی فقط جدول محصول ندارد؛ مشتری، سفارش و اقلام سفارش هم دارد و اینها با کلید خارجی (REFERENCES) به هم وصلاند. با JOIN سطرهای این جدولها را کنار هم میگذاری («سفارش ۳ مال کیست و چه چیزهایی دارد؟»)، با LEFT JOIN چیزهایی را پیدا میکنی که ندارند («مشتریهایی که هیچ سفارشی ندادهاند»)، و با ترکیب JOIN و GROUP BY گزارش واقعی میسازی: «هر مشتری چقدر خرید کرده؟».
مسئله: چرا همهچیز در یک جدول نیست؟
Section titled “مسئله: چرا همهچیز در یک جدول نیست؟”میشد همه را در یک جدول بزرگ نوشت: هر سطر یک قلم خرید با اسم مشتری، شهرش، اسم محصول و قیمت. ولی آنوقت اسم و شهر سارا در هر خریدش تکرار میشود. اگر سارا به اصفهان برود، باید ده سطر را درست عوض کنی و اگر یکی جا بماند، دادهات با خودش تناقض دارد. راه درست: هر چیز یک بار در جدول خودش، و جدولها با شناسه به هم اشاره میکنند. سفارش فقط میگوید «مشتری شمارهی ۱». (دلیلها و قانونهایش را در درس طراحی کامل میبینی.) قیمتش این است که برای جواب دادن باید جدولها را دوباره به هم وصل کنی، و این کار JOIN است.
تشبیه: پرونده و شمارهی پرونده
Section titled “تشبیه: پرونده و شمارهی پرونده”در یک مطب، پروندهی هر بیمار یک بار در قفسه است و شماره دارد. وقتی بیمار برای ویزیت میآید، در دفتر نوبتها فقط شمارهی پرونده را مینویسند، نه اسم و آدرس و سابقهاش را. منشی وقتی میخواهد بداند نوبت ساعت ۱۰ مال کیست، شماره را از دفتر برمیدارد و پرونده را از قفسه بیرون میکشد. JOIN همین کار است: «برای هر سطر این جدول، سطر جدول دیگر را که شمارهاش با این جور است پیدا کن و کنارش بگذار». و کلید خارجی قانون منشی است: «شمارهای که در قفسه نیست، در دفتر نوبت نوشته نمیشود».
آماده کردن داده
Section titled “آماده کردن داده”یک فایل جدید ~/shop.sql بساز. این فایل اول جدولهای قبلی را پاک میکند، بعد products.sql را صدا میزند (\ir یعنی «فایل کنار همین فایل را اجرا کن») و سه جدول جدید میسازد:
DROP TABLE IF EXISTS order_items, orders, customers;\ir products.sql
CREATE TABLE customers ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, city TEXT NOT NULL, joined_on DATE NOT NULL DEFAULT CURRENT_DATE);CREATE TABLE orders ( id SERIAL PRIMARY KEY, customer_id INTEGER NOT NULL REFERENCES customers (id), created_on DATE NOT NULL DEFAULT CURRENT_DATE, status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'paid', 'cancelled')));CREATE TABLE order_items ( order_id INTEGER NOT NULL REFERENCES orders (id), product_id INTEGER NOT NULL REFERENCES products (id), qty INTEGER NOT NULL CHECK (qty > 0), unit_price INTEGER NOT NULL, PRIMARY KEY (order_id, product_id));
INSERT INTO customers (name, city, joined_on) VALUES ('سارا احمدی', 'تهران', '2026-02-11'), ('رضا کریمی', 'اصفهان', '2026-04-27'), ('مریم حسینی', 'شیراز', '2026-06-03'), ('علی رضایی', 'تهران', '2026-08-19'), ('نگار محمدی', 'تبریز', '2026-10-08');INSERT INTO orders (customer_id, created_on, status) VALUES (1, '2026-09-02', 'paid'), (2, '2026-09-05', 'paid'), (1, '2026-09-20', 'paid'), (3, '2026-10-01', 'pending'), (4, '2026-10-03', 'cancelled'), (2, '2026-10-10', 'paid');INSERT INTO order_items (order_id, product_id, qty, unit_price) VALUES (1, 1, 1, 320000), (1, 4, 2, 450000), (2, 5, 1, 1890000), (3, 8, 3, 290000), (3, 10, 5, 95000), (4, 7, 1, 3200000), (5, 6, 1, 1250000), (6, 9, 1, 2750000), (6, 4, 1, 450000);چند نکتهی تازه در این فایل:
REFERENCES customers (id)یعنی ستونcustomer_idکلید خارجی است: هر مقدارش بایدidیک مشتری موجود باشد.PRIMARY KEY (order_id, product_id)کلید اصلی دوتایی است: یک محصول در یک سفارش فقط یک سطر دارد (برای دو عدد،qtyرا ۲ کن).unit_priceقیمت لحظهی خرید است. قیمت محصول فردا عوض میشود، ولی سفارش دیروز باید با قیمت دیروز بماند؛ برای همین قیمت در اقلام سفارش کپی میشود.
اجرایش کن:
psql shop -f ~/shop.sqlpsql shop\dt List of relations Schema | Name | Type | Owner--------+-------------+-------+------- public | customers | table | ali public | order_items | table | ali public | orders | table | ali public | products | table | ali(4 rows)مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: کلید خارجی از داده محافظت میکند
Section titled “مثال ۱: کلید خارجی از داده محافظت میکند”INSERT INTO orders (customer_id) VALUES (99);DELETE FROM customers WHERE id = 1;ERROR: insert or update on table "orders" violates foreign key constraint "orders_customer_id_fkey"DETAIL: Key (customer_id)=(99) is not present in table "customers".ERROR: update or delete on table "customers" violates foreign key constraint "orders_customer_id_fkey" on table "orders"DETAIL: Key (id)=(1) is still referenced from table "orders".سفارش برای مشتریای که وجود ندارد ثبت نمیشود، و مشتریای که سفارش دارد حذف نمیشود. بدون این قید، بعد از چند ماه سفارشهایی پیدا میکنی که «مال هیچکس» نیستند (به آنها یتیم میگویند).
مثال ۲: اولین JOIN
Section titled “مثال ۲: اولین JOIN”جدول سفارشها خودش فقط عدد دارد:
SELECT * FROM orders; id | customer_id | created_on | status----+-------------+------------+----------- 1 | 1 | 2026-09-02 | paid 2 | 2 | 2026-09-05 | paid 3 | 1 | 2026-09-20 | paid 4 | 3 | 2026-10-01 | pending 5 | 4 | 2026-10-03 | cancelled 6 | 2 | 2026-10-10 | paid(6 rows)اسم مشتری را از جدول customers کنارش بگذار:
SELECT orders.id, orders.created_on, customers.name, orders.statusFROM ordersJOIN customers ON customers.id = orders.customer_idORDER BY orders.id; id | created_on | name | status----+------------+------------+----------- 1 | 2026-09-02 | سارا احمدی | paid 2 | 2026-09-05 | رضا کریمی | paid 3 | 2026-09-20 | سارا احمدی | paid 4 | 2026-10-01 | مریم حسینی | pending 5 | 2026-10-03 | علی رضایی | cancelled 6 | 2026-10-10 | رضا کریمی | paid(6 rows)بخوانش اینطور: «از orders، و برای هر سفارش سطری از customers که customers.id = orders.customer_id باشد را کنارش بگذار». شرط ON میگوید کدام ستونها باید جور باشند. وقتی دو جدول داری، ستونها را با جدول.ستون نام ببر.
مثال ۳: اسم مستعار جدول و ستون مبهم
Section titled “مثال ۳: اسم مستعار جدول و ستون مبهم”نوشتن orders. و customers. خستهکننده است. بعد از اسم جدول یک اسم کوتاه بگذار:
SELECT o.id, c.name, c.cityFROM orders oJOIN customers c ON c.id = o.customer_idWHERE c.city = 'تهران'ORDER BY o.id; id | name | city----+------------+------- 1 | سارا احمدی | تهران 3 | سارا احمدی | تهران 5 | علی رضایی | تهران(3 rows)WHERE میتواند روی ستونهای هر دو جدول شرط بگذارد. ولی اگر اسم جدول را جا بیندازی:
SELECT id, name FROM orders JOIN customers ON customers.id = orders.customer_id;ERROR: column reference "id" is ambiguousLINE 1: SELECT id, name FROM orders JOIN customers ON customers.id =... ^هر دو جدول ستون id دارند و دیتابیس نمیداند منظورت کدام است (ambiguous یعنی مبهم). name مشکلی نداشت، چون فقط در customers هست؛ ولی عادت خوب این است که در هر کوئری با JOIN، همهی ستونها را با اسم جدول بنویسی.
مثال ۴: سه جدول
Section titled “مثال ۴: سه جدول”جزئیات سفارشهای ۱ و ۳: هر قلم با اسم محصول و جمع آن ردیف:
SELECT o.id AS order_id, p.name AS product, oi.qty, oi.unit_price, oi.qty * oi.unit_price AS line_totalFROM order_items oiJOIN orders o ON o.id = oi.order_idJOIN products p ON p.id = oi.product_idWHERE o.id IN (1, 3)ORDER BY o.id, p.name; order_id | product | qty | unit_price | line_total----------+-------------+-----+------------+------------ 1 | ماوس بیسیم | 2 | 450000 | 900000 1 | کتاب پایتون | 1 | 320000 | 320000 3 | فلش 64 گیگ | 3 | 290000 | 870000 3 | کابل شبکه | 5 | 95000 | 475000(4 rows)هر JOIN یک جدول دیگر را با شرط ON خودش وصل میکند. از جدول «وسط» (order_items) شروع کردیم، چون به هر دو طرف اشاره میکند.
مثال ۵: LEFT JOIN، چیزهایی که ندارند
Section titled “مثال ۵: LEFT JOIN، چیزهایی که ندارند”JOIN معمولی (اسم کاملش INNER JOIN) فقط سطرهایی را نگه میدارد که در هر دو طرف جفت داشته باشند. مشتریای که سفارش ندارد، در نتیجه نیست. LEFT JOIN همهی سطرهای جدول سمت چپ را نگه میدارد و اگر جفت نداشتند، ستونهای طرف راست را NULL میگذارد:
SELECT c.name, o.id AS order_idFROM customers cLEFT JOIN orders o ON o.customer_id = c.idORDER BY c.id, o.id; name | order_id------------+---------- سارا احمدی | 1 سارا احمدی | 3 رضا کریمی | 2 رضا کریمی | 6 مریم حسینی | 4 علی رضایی | 5 نگار محمدی |(7 rows)نگار سفارشی ندارد، ولی در نتیجه هست و order_idش خالی است. حالا «مشتریهای بیسفارش» یعنی همان سطرهایی که طرف راستشان NULL است:
SELECT c.nameFROM customers cLEFT JOIN orders o ON o.customer_id = c.idWHERE o.id IS NULL; name------------ نگار محمدی(1 row)این الگو (LEFT JOIN و بعد WHERE طرف_راست.id IS NULL) برای سؤالهای «کدام … هیچ … ندارد» خیلی پرکاربرد است: محصولهای هرگز فروختهنشده، کاربرهای بیفعالیت، سرورهای بدون مانیتورینگ.
میخواهی همهی محصولها را ببینی، حتی آنهایی که هیچوقت فروخته نشدهاند. کدام؟
جواب درست: products p LEFT JOIN order_items oi ...
جدولی که همهی سطرهایش را میخواهی سمت چپ LEFT JOIN میآید.
مثال ۶: JOIN و GROUP BY، گزارش واقعی
Section titled “مثال ۶: JOIN و GROUP BY، گزارش واقعی”تعداد سفارش هر مشتری، شامل آنهایی که صفر سفارش دارند:
SELECT c.name, count(o.id) AS ordersFROM customers cLEFT JOIN orders o ON o.customer_id = c.idGROUP BY c.id, c.nameORDER BY orders DESC, c.name; name | orders------------+-------- رضا کریمی | 2 سارا احمدی | 2 علی رضایی | 1 مریم حسینی | 1 نگار محمدی | 0(5 rows)دقت کن: count(o.id)، نه count(*). سطر نگار وجود دارد (با o.id خالی)، پس count(*) آن را یک میشمرد؛ count(o.id) مقدار NULL را نمیشمارد و صفر درست را میدهد (همان قانون درس قبل). GROUP BY c.id, c.name: بر اساس id گروه کردیم چون دو مشتری ممکن است همنام باشند.
و گزارش اصلی، «هر مشتری چقدر پرداختشده خرید کرده»:
SELECT c.name, sum(oi.qty * oi.unit_price) AS spentFROM customers cJOIN orders o ON o.customer_id = c.idJOIN order_items oi ON oi.order_id = o.idWHERE o.status = 'paid'GROUP BY c.id, c.nameORDER BY spent DESC; name | spent------------+--------- رضا کریمی | 5090000 سارا احمدی | 2565000(2 rows)ترتیب اجرا همان درس قبل است، فقط FROM حالا چند جدول وصلشده است: اول جدولها وصل میشوند، WHERE سفارشهای پرداختنشده را حذف میکند، بعد گروهبندی و جمع.
پشت پرده: JOIN در عمل چه میکند
Section titled “پشت پرده: JOIN در عمل چه میکند”از نظر منطقی، JOIN یعنی: «همهی جفتهای ممکن از دو جدول را بساز و فقط آنهایی را نگه دار که شرط ON درست است». همهی جفتها را ضرب دکارتی میگویند و اگر شرط را جا بیندازی، دقیقاً همان را میگیری:
SELECT count(*) FROM customers, orders; count------- 30(1 row)۵ مشتری × ۶ سفارش = ۳۰ سطر بیمعنی (هر سفارش کنار هر مشتری). روی جدولهای واقعی (ده هزار × یک میلیون) این کوئری دیتابیس را از پا درمیآورد. (کاما بین جدولها شکل قدیمی JOIN است؛ همیشه JOIN ... ON بنویس تا شرط جا نیفتد. اگر با JOIN شرط ON را ننویسی، PostgreSQL خطای نحوی میدهد.)
در عمل، دیتابیس همهی جفتها را نمیسازد. سه روش اصلی دارد: Nested Loop (برای هر سطر یکی، جفتش را در دیگری، معمولاً با ایندکس، پیدا کن؛ برای جدولهای کوچک یا وقتی ایندکس هست)، Hash Join (از جدول کوچکتر یک جدول درهمسازی در حافظه بساز و جدول بزرگ را یک بار بخوان) و Merge Join (هر دو را بر اساس کلید مرتب کن و مثل زیپ کنار هم ببند). کدام را انتخاب کند به اندازهی جدولها و ایندکسها بستگی دارد. در درس ایندکس با EXPLAIN این انتخاب را میبینی.
جدولهای مرجع
Section titled “جدولهای مرجع”| شکل | نتیجه |
|---|---|
a JOIN b ON ... |
فقط سطرهای جفتدار (INNER) |
a LEFT JOIN b ON ... |
همهی a، جای خالی b با NULL |
a RIGHT JOIN b ON ... |
همهی b (همان LEFT با جای عوضشده؛ کمتر استفاده میشود) |
a FULL JOIN b ON ... |
همهی هر دو طرف |
LEFT JOIN ... WHERE b.id IS NULL |
سطرهای a که در b جفت ندارند |
FROM a, b (بدون شرط) |
ضرب دکارتی؛ تقریباً همیشه اشتباه |
| قید | معنی |
|---|---|
REFERENCES t (id) |
کلید خارجی: مقدار باید در t.id باشد |
PRIMARY KEY (a, b) |
کلید اصلی چندستونی |
ON DELETE CASCADE |
با حذف والد، فرزندها هم حذف شوند (درس طراحی) |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) جا انداختن اسم جدول
Section titled “۱) جا انداختن اسم جدول”column reference "id" is ambiguous. راهحل: با JOIN، همهی ستونها را alias.ستون بنویس.
۲) JOIN بهجای LEFT JOIN
Section titled “۲) JOIN بهجای LEFT JOIN”گزارش «تعداد سفارش هر مشتری» با JOIN معمولی، مشتریهای بیسفارش را بیصدا حذف میکند و هیچ خطایی نمیبینی. راهحل: هر وقت «همهی X، حتی آنهایی که Y ندارند» خواستی، LEFT JOIN با X در سمت چپ.
۳) count(*) بعد از LEFT JOIN
Section titled “۳) count(*) بعد از LEFT JOIN”مثال ۶: مشتری بیسفارش ۱ شمرده میشود. راهحل: count(طرف_راست.id).
۴) اجرای products.sql بعد از ساختن کلیدهای خارجی
Section titled “۴) اجرای products.sql بعد از ساختن کلیدهای خارجی”psql shop -f ~/products.sqlpsql:/home/ali/products.sql:1: ERROR: cannot drop table products because other objects depend on itDETAIL: constraint order_items_product_id_fkey on table order_items depends on table productsHINT: Use DROP ... CASCADE to drop the dependent objects too.psql:/home/ali/products.sql:10: ERROR: relation "products" already existsINSERT 0 12DROP بهخاطر کلید خارجی رد شد، CREATE هم چون جدول هنوز هست، ولی INSERT اجرا شد و حالا ۱۲ سطر تکراری داری! psql بهطور پیشفرض بعد از خطا ادامه میدهد. راهحل: shop.sql را اجرا کن (اول جدولهای وابسته را پاک میکند) و برای اسکریپتهای خودت psql -v ON_ERROR_STOP=1 -f ... بزن تا با اولین خطا بایستد. CASCADE هم راهحل نیست: کلیدهای خارجی را بیصدا حذف میکند.
۵) جمع زدن بعد از JOIN چندتایی
Section titled “۵) جمع زدن بعد از JOIN چندتایی”اگر جدولی را به دو جدول «چندتایی» وصل کنی (مثلاً مشتری به سفارشها و به آدرسهایش)، هر سفارش بهازای هر آدرس تکرار میشود و sum چند برابر میشود. راهحل: اول تعداد سطرها را بدون GROUP BY ببین؛ اگر بیشتر از انتظار بود، هر جمع را جدا حساب کن.
فهرست همهی سفارشها را با تاریخ، اسم مشتری، شهر و وضعیت بگیر، ولی فقط سفارشهای ماه اکتبر ۲۰۲۶ (از 2026-10-01 به بعد).
دیدن جواب
SELECT o.id, o.created_on, c.name, c.city, o.statusFROM orders oJOIN customers c ON c.id = o.customer_idWHERE o.created_on >= '2026-10-01'ORDER BY o.created_on; id | created_on | name | city | status----+------------+------------+--------+----------- 4 | 2026-10-01 | مریم حسینی | شیراز | pending 5 | 2026-10-03 | علی رضایی | تهران | cancelled 6 | 2026-10-10 | رضا کریمی | اصفهان | paid(3 rows)محصولهایی را پیدا کن که هرگز در هیچ سفارشی نبودهاند. بعد سه محصول پرفروش (بر اساس جمع qty) را نشان بده.
دیدن جواب
SELECT p.nameFROM products pLEFT JOIN order_items oi ON oi.product_id = p.idWHERE oi.product_id IS NULLORDER BY p.id;
SELECT p.name, sum(oi.qty) AS soldFROM products pJOIN order_items oi ON oi.product_id = p.idGROUP BY p.id, p.nameORDER BY sold DESC, p.nameLIMIT 3; name-------------- کتاب لینوکس کتاب داکر روتر وایفای سوئیچ 8 پورت(4 rows)
name | sold------------+------ کابل شبکه | 5 فلش 64 گیگ | 3 ماوس بیسیم | 3(3 rows)ماوس در دو سفارش بود (۲ + ۱). p.name در ORDER BY تساوی فلش و ماوس را قطعی کرد.
برای هر سفارش یک سطر خلاصه بساز: شماره، اسم مشتری، وضعیت، تعداد قلمها، تعداد کل کالاها و مبلغ کل. سفارشها را از جدیدترین مرتب کن. بعد با HAVING فقط سفارشهای بالای ۲ میلیون تومان را نگه دار.
دیدن جواب
SELECT o.id, c.name, o.status, count(*) AS lines, sum(oi.qty) AS units, sum(oi.qty * oi.unit_price) AS totalFROM orders oJOIN customers c ON c.id = o.customer_idJOIN order_items oi ON oi.order_id = o.idGROUP BY o.id, c.name, o.statusHAVING sum(oi.qty * oi.unit_price) > 2000000ORDER BY o.created_on DESC; id | name | status | lines | units | total----+------------+---------+-------+-------+--------- 6 | رضا کریمی | paid | 2 | 2 | 3200000 4 | مریم حسینی | pending | 1 | 1 | 3200000(2 rows)بدون HAVING شش سطر میگیری (سفارش ۱: ۱٬۲۲۰٬۰۰۰، سفارش ۳: ۱٬۳۴۵٬۰۰۰، …). count(*) اینجا درست است، چون JOIN معمولی است و سطر خالی ندارد. ORDER BY o.created_on با اینکه در GROUP BY نیست کار میکند، چون o.id کلید اصلی است و PostgreSQL میداند هر id فقط یک تاریخ دارد.
آزمونک
Section titled “آزمونک”کلید خارجی customer_id REFERENCES customers (id) چه چیزی را تضمین میکند؟
جواب درست: هر customer_id یک مشتری موجود است
و مشتریای که سفارش دارد هم حذف نمیشود.
شرط ON در JOIN چه میگوید؟
جواب درست: کدام سطرهای دو جدول با هم جفت شوند
مثلاً c.id = o.customer_id
مشتری بدون سفارش در نتیجهی customers JOIN orders هست؟
جواب درست: خیر؛ برای آن LEFT JOIN لازم است
INNER JOIN فقط جفتدارها را نگه میدارد.
خطای column reference "id" is ambiguous یعنی…
جواب درست: چند جدول ستون id دارند و معلوم نیست کدام
o.id یا c.id بنویس.
FROM customers, orders بدون شرط با ۵ و ۶ سطر چند سطر میدهد؟
جواب درست: 30
ضرب دکارتی: همهی جفتها.
بعد از LEFT JOIN، برای شمردن سفارشها با صفر درست، چه بنویسیم؟
جواب درست: count(o.id)
count(ستون) مقدار NULL را نمیشمارد.
جمعبندی
Section titled “جمعبندی”- دادهی هر چیز یک بار در جدول خودش؛ جدولها با کلید خارجی (
REFERENCES) به هم اشاره میکنند و دیتابیس از سطرهای یتیم جلوگیری میکند. JOIN b ON شرطسطرهای جفت را کنار هم میگذارد؛ اسم مستعار (orders o) وo.ستونکوئری را خوانا و بیابهام میکند.LEFT JOINهمهی سطرهای سمت چپ را نگه میدارد؛ باWHERE b.id IS NULLچیزهای «بدون» پیدا میشوند.JOIN+GROUP BY= گزارش؛ بعد ازLEFT JOINازcount(b.id)استفاده کن.- شرط
ONرا هرگز جا نینداز (ضرب دکارتی)؛ از این به بعد داده را باshop.sqlتازه کن.
| دستور | کاری که میکند |
|---|---|
col INTEGER REFERENCES t (id) | کلید خارجی |
FROM a JOIN b ON b.id = a.b_id | اتصال |
FROM orders o JOIN customers c ON ... | اسم مستعار جدول |
a LEFT JOIN b ON ... | همهی a |
... WHERE b.id IS NULL | a هایی که b ندارند |
count(b.id) | شمارش درست بعد از LEFT JOIN |
\ir file.sql | اجرای فایل کناری در psql |
psql -v ON_ERROR_STOP=1 -f f.sql | توقف با اولین خطا |