رفتن به محتوا
دیتابیس و SQLSQL از صفردرس ۴ از ۱۰

اتصال جدول‌ها (JOIN)

داده‌ی چند جدول را کنار هم بگذار: کلید خارجی با REFERENCES و محافظتش از داده، JOIN ... ON برای وصل کردن سفارش به مشتری، اسم مستعار جدول، اتصال سه جدول، LEFT JOIN برای پیدا کردن مشتری‌های بی‌سفارش، JOIN همراه GROUP BY برای گزارش خرید هر مشتری، و خطاهای ambiguous و ضرب دکارتی.

  • حدود ۲۱ دقیقه مطالعه
  • ۶ مثال اجراشده
  • ۳ تمرین با جواب
  • مقدماتی تا متوسط
  • به‌روزرسانی: ۲۹ مهر ۱۴۰۵

توی این درس یاد می‌گیری با چند جدول مرتبط کار کنی. فروشگاه واقعی فقط جدول محصول ندارد؛ مشتری، سفارش و اقلام سفارش هم دارد و این‌ها با کلید خارجی (REFERENCES) به هم وصل‌اند. با JOIN سطرهای این جدول‌ها را کنار هم می‌گذاری («سفارش ۳ مال کیست و چه چیزهایی دارد؟»)، با LEFT JOIN چیزهایی را پیدا می‌کنی که ندارند («مشتری‌هایی که هیچ سفارشی نداده‌اند»)، و با ترکیب JOIN و GROUP BY گزارش واقعی می‌سازی: «هر مشتری چقدر خرید کرده؟».

مسئله: چرا همه‌چیز در یک جدول نیست؟

Section titled “مسئله: چرا همه‌چیز در یک جدول نیست؟”

می‌شد همه را در یک جدول بزرگ نوشت: هر سطر یک قلم خرید با اسم مشتری، شهرش، اسم محصول و قیمت. ولی آن‌وقت اسم و شهر سارا در هر خریدش تکرار می‌شود. اگر سارا به اصفهان برود، باید ده سطر را درست عوض کنی و اگر یکی جا بماند، داده‌ات با خودش تناقض دارد. راه درست: هر چیز یک بار در جدول خودش، و جدول‌ها با شناسه به هم اشاره می‌کنند. سفارش فقط می‌گوید «مشتری شماره‌ی ۱». (دلیل‌ها و قانون‌هایش را در درس طراحی کامل می‌بینی.) قیمتش این است که برای جواب دادن باید جدول‌ها را دوباره به هم وصل کنی، و این کار JOIN است.

تشبیه: پرونده و شماره‌ی پرونده

Section titled “تشبیه: پرونده و شماره‌ی پرونده”

در یک مطب، پرونده‌ی هر بیمار یک بار در قفسه است و شماره دارد. وقتی بیمار برای ویزیت می‌آید، در دفتر نوبت‌ها فقط شماره‌ی پرونده را می‌نویسند، نه اسم و آدرس و سابقه‌اش را. منشی وقتی می‌خواهد بداند نوبت ساعت ۱۰ مال کیست، شماره را از دفتر برمی‌دارد و پرونده را از قفسه بیرون می‌کشد. JOIN همین کار است: «برای هر سطر این جدول، سطر جدول دیگر را که شماره‌اش با این جور است پیدا کن و کنارش بگذار». و کلید خارجی قانون منشی است: «شماره‌ای که در قفسه نیست، در دفتر نوبت نوشته نمی‌شود».

جدول‌های فروشگاه و کلیدهای خارجی بینشان: هر فلش یعنی «این ستون به شناسه‌ی آن جدول اشاره می‌کند».

یک فایل جدید ~/shop.sql بساز. این فایل اول جدول‌های قبلی را پاک می‌کند، بعد products.sql را صدا می‌زند (\ir یعنی «فایل کنار همین فایل را اجرا کن») و سه جدول جدید می‌سازد:

shop.sql
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 قیمت لحظه‌ی خرید است. قیمت محصول فردا عوض می‌شود، ولی سفارش دیروز باید با قیمت دیروز بماند؛ برای همین قیمت در اقلام سفارش کپی می‌شود.

اجرایش کن:

Terminal window
psql shop -f ~/shop.sql
psql 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 “مثال ۱: کلید خارجی از داده محافظت می‌کند”
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".

سفارش برای مشتری‌ای که وجود ندارد ثبت نمی‌شود، و مشتری‌ای که سفارش دارد حذف نمی‌شود. بدون این قید، بعد از چند ماه سفارش‌هایی پیدا می‌کنی که «مال هیچ‌کس» نیستند (به آن‌ها یتیم می‌گویند).

جدول سفارش‌ها خودش فقط عدد دارد:

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.status
FROM orders
JOIN customers ON customers.id = orders.customer_id
ORDER 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.city
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE 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 ambiguous
LINE 1: SELECT id, name FROM orders JOIN customers ON customers.id =...
^

هر دو جدول ستون id دارند و دیتابیس نمی‌داند منظورت کدام است (ambiguous یعنی مبهم). name مشکلی نداشت، چون فقط در customers هست؛ ولی عادت خوب این است که در هر کوئری با JOIN، همه‌ی ستون‌ها را با اسم جدول بنویسی.

جزئیات سفارش‌های ۱ و ۳: هر قلم با اسم محصول و جمع آن ردیف:

SELECT o.id AS order_id, p.name AS product, oi.qty, oi.unit_price,
oi.qty * oi.unit_price AS line_total
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
JOIN products p ON p.id = oi.product_id
WHERE 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_id
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
ORDER BY c.id, o.id;
خروجی
name | order_id
------------+----------
سارا احمدی | 1
سارا احمدی | 3
رضا کریمی | 2
رضا کریمی | 6
مریم حسینی | 4
علی رضایی | 5
نگار محمدی |
(7 rows)

نگار سفارشی ندارد، ولی در نتیجه هست و order_idش خالی است. حالا «مشتری‌های بی‌سفارش» یعنی همان سطرهایی که طرف راستشان NULL است:

SELECT c.name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL;
خروجی
name
------------
نگار محمدی
(1 row)

این الگو (LEFT JOIN و بعد WHERE طرف_راست.id IS NULL) برای سؤال‌های «کدام … هیچ … ندارد» خیلی پرکاربرد است: محصول‌های هرگز فروخته‌نشده، کاربرهای بی‌فعالیت، سرورهای بدون مانیتورینگ.

⚡ بررسی سریع

می‌خواهی همه‌ی محصول‌ها را ببینی، حتی آن‌هایی که هیچ‌وقت فروخته نشده‌اند. کدام؟

مثال ۶: JOIN و GROUP BY، گزارش واقعی

Section titled “مثال ۶: JOIN و GROUP BY، گزارش واقعی”

تعداد سفارش هر مشتری، شامل آن‌هایی که صفر سفارش دارند:

SELECT c.name, count(o.id) AS orders
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER 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 spent
FROM customers c
JOIN orders o ON o.customer_id = c.id
JOIN order_items oi ON oi.order_id = o.id
WHERE o.status = 'paid'
GROUP BY c.id, c.name
ORDER 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 این انتخاب را می‌بینی.

INNER JOIN فقط جفت‌های جورشده را نگه می‌دارد؛ LEFT JOIN همه‌ی سطرهای سمت چپ را، و جای خالی طرف راست را NULL می‌گذارد.
شکل نتیجه
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 با حذف والد، فرزندها هم حذف شوند (درس طراحی)

column reference "id" is ambiguous. راه‌حل: با JOIN، همه‌ی ستون‌ها را alias.ستون بنویس.

گزارش «تعداد سفارش هر مشتری» با JOIN معمولی، مشتری‌های بی‌سفارش را بی‌صدا حذف می‌کند و هیچ خطایی نمی‌بینی. راه‌حل: هر وقت «همه‌ی X، حتی آن‌هایی که Y ندارند» خواستی، LEFT JOIN با X در سمت چپ.

مثال ۶: مشتری بی‌سفارش ۱ شمرده می‌شود. راه‌حل: count(طرف_راست.id).

۴) اجرای products.sql بعد از ساختن کلیدهای خارجی

Section titled “۴) اجرای products.sql بعد از ساختن کلیدهای خارجی”
Terminal window
psql shop -f ~/products.sql
خروجی
psql:/home/ali/products.sql:1: ERROR: cannot drop table products because other objects depend on it
DETAIL: constraint order_items_product_id_fkey on table order_items depends on table products
HINT: Use DROP ... CASCADE to drop the dependent objects too.
psql:/home/ali/products.sql:10: ERROR: relation "products" already exists
INSERT 0 12

DROP به‌خاطر کلید خارجی رد شد، 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.status
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE 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.name
FROM products p
LEFT JOIN order_items oi ON oi.product_id = p.id
WHERE oi.product_id IS NULL
ORDER BY p.id;
SELECT p.name, sum(oi.qty) AS sold
FROM products p
JOIN order_items oi ON oi.product_id = p.id
GROUP BY p.id, p.name
ORDER BY sold DESC, p.name
LIMIT 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 total
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id, c.name, o.status
HAVING sum(oi.qty * oi.unit_price) > 2000000
ORDER 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 فقط یک تاریخ دارد.

آزمونک
  1. کلید خارجی customer_id REFERENCES customers (id) چه چیزی را تضمین می‌کند؟

  2. شرط ON در JOIN چه می‌گوید؟

  3. مشتری بدون سفارش در نتیجه‌ی customers JOIN orders هست؟

  4. خطای column reference "id" is ambiguous یعنی…

  5. FROM customers, orders بدون شرط با ۵ و ۶ سطر چند سطر می‌دهد؟

  6. بعد از LEFT JOIN، برای شمردن سفارش‌ها با صفر درست، چه بنویسیم؟

  • داده‌ی هر چیز یک بار در جدول خودش؛ جدول‌ها با کلید خارجی (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 NULLa هایی که b ندارند
count(b.id)شمارش درست بعد از LEFT JOIN
\ir file.sqlاجرای فایل کناری در psql
psql -v ON_ERROR_STOP=1 -f f.sqlتوقف با اولین خطا