অধ্যায় 5 · টেবিল জোড়া লাগানো
অনেক টেবিল জয়েন, আর ডুপ্লিকেট সারির ফাঁদ
- পৃষ্ঠা 18 / 22
- 14 মিনিট পড়া
আসল প্রশ্ন খুব কমই দুটো টেবিলে আটকে থাকে। "ক্যাটাগরি অনুযায়ী আয়" জানতে লাগে অর্ডার আইটেম (পরিমাণ আর দাম), প্রোডাক্ট (কোন ক্যাটাগরি) আর ক্যাটাগরি (নাম), আর সাধারণত অর্ডারও (ক্যানসেল হওয়াগুলো বাদ দিতে)। উত্তর আসে জয়েন শিকলের মতো জুড়ে: প্রতিটা নতুন JOIN আপনার হাতে থাকা সারিগুলোর সাথে আরও একটা টেবিল যোগ করে।
এখান থেকেই জয়েন চুপচাপ ভুল সংখ্যা দিতে শুরু করে। একই কোয়েরিতে একটা অর্ডারকে তার আইটেম আর তার পেমেন্ট দুটোর সাথে জয়েন করলে প্রতিটা মোট অঙ্ক দ্বিগুণ হয়ে যেতে পারে। মেশিন লার্নিংয়ের ফিচার টেবিল ঠিক এভাবেই বানানো হয় — পাঁচটা টেবিল থেকে প্রতি কাস্টমারে এক সারি — তাই এখানে ফ্যান-আউটের বাগ মানে মডেলের ট্রেনিং ডেটায় বাগ, কোথাও কোনো এরর ছাড়াই।
যা শিখবেন
- ফরেন কি ধরে ধরে তিন, চার, পাঁচটা টেবিল জয়েন করা
- একাধিক পরীক্ষাসহ জয়েনের শর্ত
GROUP BY-এর সাথে জয়েন: ক্যাটাগরি অনুযায়ী আয়, কাস্টমার অনুযায়ী প্রোডাক্টorder_itemsহয়ে মেনি-টু-মেনি, "একসাথে কেনা" জোড়াসহ- যে ফ্যান-আউট ফাঁদ অর্ডার ৬-কে দুবার গোনে, আর তার সমাধান
কি ধরে এগোন
অনেক টেবিলের জয়েন লেখার আগে যে টেবিলগুলো লাগবে তাদের মধ্যে পথটা খুঁজে নিন। BitByte Shop-এ পথটা এরকম (─< মানে "এক থেকে অনেক"):
customers ─< orders ─< order_items >─ products >─ categories
│
└─< paymentsএকজন কাস্টমার থেকে ক্যাটাগরির নাম পর্যন্ত যেতে হাঁটতে হয় customers → orders → order_items → products → categories, প্রতিটা তীরে একটা করে জয়েন, প্রতিটাতে একটা ফরেন কি জোড়া হয় সে যে প্রাইমারি কি-কে রেফার করে তার সাথে।
তিন বা তার বেশি টেবিল
প্রতিটা JOIN এ পর্যন্ত তৈরি হওয়া সারিগুলো নিয়ে তার সাথে আরও একটা টেবিল জোড়ে:
SELECT o.order_id, c.name AS customer, p.name AS product, oi.quantity, oi.unit_price
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p ON p.product_id = oi.product_id
WHERE o.order_id IN (1, 4, 9)
ORDER BY o.order_id, p.product_id;+----------+---------------+---------------------------+----------+------------+
| order_id | customer | product | quantity | unit_price |
+----------+---------------+---------------------------+----------+------------+
| 1 | Nadia Rahman | Python Crash Course | 1 | 1200 |
| 1 | Nadia Rahman | Wireless Mouse | 1 | 900 |
| 4 | Farhana Akter | Hands-On Machine Learning | 1 | 2500 |
| 4 | Farhana Akter | Laptop Stand | 1 | 1500 |
| 9 | Imran Hossain | Mechanical Keyboard | 1 | 4050 |
| 9 | Imran Hossain | Wireless Mouse | 1 | 900 |
+----------+---------------+---------------------------+----------+------------+- এই ফলাফলের গ্রেইন (grain) — এক সারি কী বোঝায় — হলো একটা অর্ডার আইটেম। একটা অর্ডারে যতগুলো আইটেম, অর্ডারটা ততবার আসে, সাথে কাস্টমারও। অ্যাগ্রিগেট করার আগে সবসময় জেনে নিন জয়েনের গ্রেইন কী।
- প্রতিটা
ONআগে জয়েন হওয়া যেকোনো টেবিল ব্যবহার করতে পারে:p.product_id = oi.product_idকাজ করে কারণoiআগেই আছে। - ইনার জয়েনে কোন ক্রমে লিখলেন তাতে ফলাফল বদলায় না, আর ডেটাবেসের প্ল্যানার এমনিতেও নিজের ক্রম বেছে নেয়। কি-এর পথ ধরে লিখুন, যাতে পাঠক অনুসরণ করতে পারেন।
অ্যাগ্রিগেটের সাথে জয়েন: ক্যাটাগরি অনুযায়ী আয়
আয় হলো order_items-এর quantity × unit_price (যে দাম আসলে নেওয়া হয়েছে, তালিকার দাম নয়)। চারটা টেবিল, একটা GROUP BY:
SELECT cat.name AS category,
SUM(oi.quantity) AS units,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM order_items oi
JOIN orders o ON o.order_id = oi.order_id
JOIN products p ON p.product_id = oi.product_id
JOIN categories cat ON cat.category_id = p.category_id
WHERE o.status IN ('delivered', 'shipped')
GROUP BY cat.category_id, cat.name
ORDER BY revenue DESC;+-------------+-------+---------+
| category | units | revenue |
+-------------+-------+---------+
| Electronics | 8 | 31550 |
| Courses | 3 | 21000 |
| Accessories | 5 | 8900 |
| Books | 5 | 8600 |
+-------------+-------+---------+গ্রুপ করার আগে গ্রেইন হলো একটা অর্ডার আইটেম, আর প্রতিটা আইটেম ঠিক একটা প্রোডাক্ট আর একটা ক্যাটাগরির, তাই কিছুই দুবার গোনা হয় না। ক্যানসেল হওয়া অর্ডার ৫ আর পেন্ডিং অর্ডার ১৩ বাদ পড়েছে orders-এর স্ট্যাটাস পরীক্ষায় — এজন্যই orders-এর কোনো কলাম সিলেক্ট না করলেও টেবিলটা কোয়েরিতে আছে।
একাধিক জয়েন শর্ত
ON-এ যেকোনো শর্ত রাখা যায়, AND দিয়ে জুড়ে। কোন আইটেমগুলো প্রোডাক্টের তালিকার দামের চেয়ে কমে বিক্রি হয়েছে?
SELECT oi.order_id, p.name, p.price AS list_price, oi.unit_price
FROM order_items oi
JOIN products p ON p.product_id = oi.product_id AND oi.unit_price < p.price
ORDER BY oi.order_id;+----------+---------------------+------------+------------+
| order_id | name | list_price | unit_price |
+----------+---------------------+------------+------------+
| 9 | Mechanical Keyboard | 4500 | 4050 |
+----------+---------------------+------------+------------+শুধু অর্ডার ৯ ছাড় পেয়েছে। ইনার জয়েনে বাড়তি পরীক্ষাটা WHERE-এও যেতে পারত; LEFT JOIN-এ এটা ON-এই থাকতে হবে, আগের পাতায় যেমন দেখেছেন। আরেকটা সাধারণ ক্ষেত্র হলো কম্পোজিট কি: কোনো টেবিলের কি দুটো কলাম হলে, যেমন order_items (order_id, product_id), তার সাথে জয়েনে দুটোই লাগে: ON x.order_id = oi.order_id AND x.product_id = oi.product_id।
order_items হয়ে মেনি-টু-মেনি
একজন কাস্টমার অনেক প্রোডাক্ট কেনেন; একটা প্রোডাক্ট অনেক কাস্টমার কেনেন। এদের জোড়ে যে জাংশন টেবিল (junction table), সেটা হলো order_items (orders হয়ে):
SELECT c.name AS customer, p.name AS product, SUM(oi.quantity) AS units
FROM customers c
JOIN orders o ON o.customer_id = c.customer_id
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p ON p.product_id = oi.product_id
WHERE c.customer_id IN (1, 3)
GROUP BY c.customer_id, c.name, p.product_id, p.name
ORDER BY c.customer_id, p.product_id;+---------------+---------------------------+-------+
| customer | product | units |
+---------------+---------------------------+-------+
| Nadia Rahman | Python Crash Course | 1 |
| Nadia Rahman | Wireless Mouse | 1 |
| Nadia Rahman | 27-inch Monitor | 1 |
| Nadia Rahman | SQL Masterclass | 1 |
| Farhana Akter | Python Crash Course | 2 |
| Farhana Akter | Hands-On Machine Learning | 1 |
| Farhana Akter | Wireless Mouse | 1 |
| Farhana Akter | Laptop Stand | 1 |
| Farhana Akter | USB-C Hub | 1 |
+---------------+---------------------------+-------+কাস্টমার × প্রোডাক্টের এই টেবিলটাই সেই "ইন্টারঅ্যাকশন ম্যাট্রিক্স", যেখান থেকে একটা রিকমেন্ডার সিস্টেম শুরু করে: কে কী কিনেছে, আর কতটা।
একসাথে কেনা: order_items-কে নিজের সাথে জয়েন
কোন প্রোডাক্ট জোড়াগুলো একই অর্ডারে আসে? একই order_id-তে order_items-কে নিজের সাথে জয়েন করুন। বাড়তি শর্ত a.product_id < b.product_id প্রতিটা জোড়া একবারই রাখে (আর একটা প্রোডাক্টকে নিজের সাথে জোড়া হতে দেয় না):
SELECT pa.name AS product_a, pb.name AS product_b, COUNT(*) AS times_together
FROM order_items a
JOIN order_items b ON a.order_id = b.order_id AND a.product_id < b.product_id
JOIN products pa ON pa.product_id = a.product_id
JOIN products pb ON pb.product_id = b.product_id
GROUP BY a.product_id, b.product_id, pa.name, pb.name
ORDER BY times_together DESC, a.product_id, b.product_id
LIMIT 3;+---------------------------+-----------------+----------------+
| product_a | product_b | times_together |
+---------------------------+-----------------+----------------+
| Wireless Mouse | USB-C Hub | 2 |
| Python Crash Course | Wireless Mouse | 1 |
| Hands-On Machine Learning | SQL Masterclass | 1 |
+---------------------------+-----------------+----------------+মাউস আর USB-C হাব দুবার একসাথে কেনা হয়েছে (অর্ডার ৭ আর ১৪)। আসল ডেটায় একসাথে আসার এই গণনাই "যারা এটা কিনেছেন তারা আরও কিনেছেন…" আর মার্কেট-বাস্কেট অ্যানালিসিসের প্রথম ধাপ।
ফ্যান-আউটের ফাঁদ
প্রায় সবাই এই বাগে একবার না একবার পড়েন। আমরা প্রতি অর্ডারে চাই আইটেমের মূল্য আর কত টাকা পেমেন্ট হয়েছে। দুটোই orders-এর নিচের "অনেক" টেবিলে, তাই দুটোকেই জয়েন করি:
SELECT o.order_id,
SUM(oi.quantity * oi.unit_price) AS items_total,
SUM(pay.amount) AS paid_total
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
JOIN payments pay ON pay.order_id = o.order_id
WHERE o.order_id IN (1, 2, 6, 9)
GROUP BY o.order_id
ORDER BY o.order_id;+----------+-------------+------------+
| order_id | items_total | paid_total |
+----------+-------------+------------+
| 1 | 2100 | 4200 |
| 2 | 4500 | 4500 |
| 6 | 30000 | 15000 |
| 9 | 4950 | 9900 |
+----------+-------------+------------+অর্ডার ৬-এ একটা কোর্স বিক্রি হয়েছে ১৫,০০০ টাকায়, কিন্তু আইটেম দেখাচ্ছে ৩০,০০০। অর্ডার ১-এ একবার ২,১০০ টাকা পেমেন্ট হয়েছে, কিন্তু দেখাচ্ছে ৪,২০০। ঠিক আছে শুধু অর্ডার ২ (এক আইটেম, এক পেমেন্ট)। গ্রুপ করার আগের সারিগুলো দেখুন:
SELECT o.order_id, oi.product_id, oi.quantity * oi.unit_price AS item_value, pay.payment_id, pay.amount
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
JOIN payments pay ON pay.order_id = o.order_id
WHERE o.order_id IN (1, 6)
ORDER BY o.order_id, oi.product_id, pay.payment_id;+----------+------------+------------+------------+--------+
| order_id | product_id | item_value | payment_id | amount |
+----------+------------+------------+------------+--------+
| 1 | 1 | 1200 | 1 | 2100 |
| 1 | 4 | 900 | 1 | 2100 |
| 6 | 7 | 15000 | 5 | 5000 |
| 6 | 7 | 15000 | 6 | 10000 |
+----------+------------+------------+------------+--------+| অর্ডার | আইটেম | পেমেন্ট | জয়েনের পর সারি | কী বারবার আসে |
|---|---|---|---|---|
| 1 | 2 | 1 | 2 × 1 = 2 | পেমেন্ট ১ দুবার → পেমেন্ট ৪,২০০ |
| 6 | 1 | 2 | 1 × 2 = 2 | আইটেমটা দুবার → আইটেম ৩০,০০০ |
| 2 | 1 | 1 | 1 × 1 = 1 | কিছুই না → ভাগ্যক্রমে ঠিক |
আইটেম আর পেমেন্টের মধ্যে নিজেদের কোনো সম্পর্ক নেই; তারা শুধু অর্ডারটা ভাগ করে। দুটোকেই জয়েন করলে একই অর্ডারের প্রতিটা আইটেম সেই অর্ডারের প্রতিটা পেমেন্টের সাথে জোড়া লাগে — প্রতি অর্ডারের ভেতরে একটা ছোট্ট ক্রস জয়েন। এটাই ফ্যান-আউট (fan-out): গ্রেইন হয়ে গেছে "আইটেম × পেমেন্ট", আর সেই গ্রেইনে কোনো যোগফলেরই মানে নেই। কোনো এরর আসে না, আর বেশিরভাগ অর্ডারে এক আইটেম, এক পেমেন্ট থাকলে মোট অঙ্কগুলো বিশ্বাসযোগ্যও দেখায়।
সমাধান: প্রতিটা "অনেক" টেবিল আগে অ্যাগ্রিগেট, তারপর জয়েন
জয়েনের আগেই প্রতিটা টেবিলকে প্রতি অর্ডারে এক সারিতে নামিয়ে আনুন। FROM-এর ভেতরে ব্র্যাকেটে লেখা একটা কোয়েরি টেবিলের মতো কাজ করে, একে বলে ডিরাইভড টেবিল (derived table)। এটা আগাম একটু ঝলক: এ ধরনের সাবকোয়েরি ঠিকমতো শিখবেন পরের পাতা সাবকোয়েরি, EXISTS আর কোরিলেটেড কোয়েরি-তে; আপাতত প্রতিটা ব্র্যাকেটকে পড়ুন "প্রতি অর্ডারে এক সারির একটা ছোট টেবিল" হিসেবে:
SELECT o.order_id, i.items_total, p.paid_total
FROM orders o
JOIN (SELECT order_id, SUM(quantity * unit_price) AS items_total
FROM order_items
GROUP BY order_id) i ON i.order_id = o.order_id
JOIN (SELECT order_id, SUM(amount) AS paid_total
FROM payments
GROUP BY order_id) p ON p.order_id = o.order_id
WHERE o.order_id IN (1, 2, 6, 9)
ORDER BY o.order_id;+----------+-------------+------------+
| order_id | items_total | paid_total |
+----------+-------------+------------+
| 1 | 2100 | 2100 |
| 2 | 4500 | 4500 |
| 6 | 15000 | 15000 |
| 9 | 4950 | 4950 |
+----------+-------------+------------+প্রতিটা ডিরাইভড টেবিলে প্রতি অর্ডারে ঠিক এক সারি, তাই orders-এর সাথে জয়েন করলে কিছুই গুণ হয় না। গণনার ক্ষেত্রে COUNT(DISTINCT …) ফ্যান-আউট সামলায়, কিন্তু SUM(DISTINCT …)-এর দিকে হাত বাড়াবেন না: এটা সত্যিই বারবার আসা মানও ফেলে দেয়, যেমন ৫,০০০ টাকার দুটো আলাদা পেমেন্ট।
সমাধানটা বসানোর পর কোয়েরিটা একটা কাজের রিকনসিলিয়েশন চেক হয়ে যায়: কোন অর্ডারের পুরো টাকা আসেনি? পেমেন্টের সাথে LEFT JOIN দিন যাতে পেমেন্টহীন অর্ডারও থাকে:
SELECT o.order_id, o.status, i.items_total, COALESCE(p.paid_total, 0) AS paid_total,
i.items_total - COALESCE(p.paid_total, 0) AS balance
FROM orders o
JOIN (SELECT order_id, SUM(quantity * unit_price) AS items_total
FROM order_items GROUP BY order_id) i ON i.order_id = o.order_id
LEFT JOIN (SELECT order_id, SUM(amount) AS paid_total
FROM payments GROUP BY order_id) p ON p.order_id = o.order_id
WHERE i.items_total <> COALESCE(p.paid_total, 0)
ORDER BY o.order_id;+----------+-----------+-------------+------------+---------+
| order_id | status | items_total | paid_total | balance |
+----------+-----------+-------------+------------+---------+
| 5 | cancelled | 18500 | 0 | 18500 |
| 13 | pending | 15000 | 0 | 15000 |
+----------+-----------+-------------+------------+---------+ডেলিভারড আর শিপড প্রতিটা অর্ডারের হিসাব মিলে গেছে; যে দুটো মেলেনি, ঠিক সেগুলোরই পেমেন্ট হওয়ার কথা ছিল না।
একটা কাজের রিপোর্ট: প্রতি কাস্টমারে এক সারি
সব মিলিয়ে একটা কাস্টমার সারাংশ বানাই — চার্ন বা লাইফটাইম-ভ্যালু মডেলের ফিচার টেবিলের আকার। গ্রেইন হলো কাস্টমার, তাই customers থেকে শুরু করে LEFT JOIN দিন, যাতে অর্ডারহীন কাস্টমাররাও থাকেন। অর্ডার আর আইটেম একটাই শিকল (প্রতিটা আইটেম একটা অর্ডারের), তাই ফ্যান-আউট নেই; COUNT(DISTINCT o.order_id) আইটেম নয়, অর্ডার গোনে:
SELECT c.customer_id, c.name, c.city,
COUNT(DISTINCT o.order_id) AS orders,
COALESCE(SUM(oi.quantity * oi.unit_price), 0) AS revenue,
COUNT(DISTINCT oi.product_id) AS distinct_products,
MAX(o.order_date) AS last_order
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id AND o.status <> 'cancelled'
LEFT JOIN order_items oi ON oi.order_id = o.order_id
GROUP BY c.customer_id, c.name, c.city
ORDER BY revenue DESC, c.customer_id;+-------------+-----------------+------------+--------+---------+-------------------+------------+
| customer_id | name | city | orders | revenue | distinct_products | last_order |
+-------------+-----------------+------------+--------+---------+-------------------+------------+
| 1 | Nadia Rahman | Dhaka | 3 | 23600 | 4 | 2026-04-19 |
| 2 | Tanvir Ahmed | Chattogram | 3 | 22500 | 3 | 2026-05-21 |
| 6 | Imran Hossain | Khulna | 2 | 19950 | 3 | 2026-06-02 |
| 3 | Farhana Akter | Dhaka | 3 | 9500 | 5 | 2026-06-09 |
| 7 | Mitu Das | Dhaka | 1 | 5500 | 2 | 2026-05-06 |
| 5 | Sadia Chowdhury | NULL | 1 | 4000 | 2 | 2026-03-15 |
| 4 | Rafiq Islam | Sylhet | 0 | 0 | 0 | NULL |
| 8 | Karim Uddin | Rajshahi | 0 | 0 | 0 | NULL |
+-------------+-----------------+------------+--------+---------+-------------------+------------+খেয়াল করুন, স্ট্যাটাসের পরীক্ষাটা প্রথম ON-এ, তাই Rafiq (শুধু একটা ক্যানসেল অর্ডার) শূন্যসহ তার সারিটা রাখেন। এই কোয়েরিতে পেমেন্ট যোগ করলেই আবার ফ্যান-আউটের ফাঁদে পড়বেন — সেগুলো আগে আলাদা করে অ্যাগ্রিগেট করুন। Python-এ এই কোয়েরি সোজা একটা DataFrame-এ চলে যায়, scikit-learn-এর জন্য তৈরি (pandas 3.0 টেক্সট কলামের dtype দেখায় str):
import pandas as pd
from sqlhelp import con
features = pd.read_sql("""
SELECT c.customer_id, c.city,
COUNT(DISTINCT o.order_id) AS orders,
COALESCE(SUM(oi.quantity * oi.unit_price), 0) AS revenue,
COUNT(DISTINCT oi.product_id) AS distinct_products
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id AND o.status <> 'cancelled'
LEFT JOIN order_items oi ON oi.order_id = o.order_id
GROUP BY c.customer_id, c.city
ORDER BY c.customer_id
""", con)
print(features.shape)
print(features.dtypes)
print(features["revenue"].sum(), (features["orders"] == 0).sum())(8, 5)
customer_id int64
city str
orders int64
revenue int64
distinct_products int64
dtype: object
85050 2আটজন কাস্টমারের জন্য আটটা সারি: shape-এর এমন দ্রুত চেক দুটো ভুলই ধরে — বাদ পড়া সারি (যেখানে LEFT লাগত সেখানে ইনার জয়েন) আর ফ্যান-আউট (কাস্টমারের চেয়ে বেশি সারি)।
সাধারণ ভুল
- একই প্যারেন্টের দুটো "অনেক" টেবিল জয়েন করে যোগ করা। আইটেম × পেমেন্ট অর্ডার ৬-এর আইটেম আর অর্ডার ১-এর পেমেন্ট দ্বিগুণ করে। সমাধান: প্রতিটা চাইল্ড টেবিলকে ডিরাইভড টেবিলে প্রতি প্যারেন্টে এক সারিতে অ্যাগ্রিগেট করে তারপর জয়েন করুন।
- গ্রেইন না জানা।
orders JOIN order_items-এর পর এক সারি মানে একটা আইটেম, তাইCOUNT(*)অর্ডার নয়, আইটেম গোনে। সমাধান: অ্যাগ্রিগেট করার আগে গ্রেইনটা মুখে বলুন; অর্ডারের জন্যCOUNT(DISTINCT o.order_id)। - LEFT JOIN-এর শিকলে একটা ইনার জয়েন।
customers LEFT JOIN orders … JOIN order_items …আবার Karim-কে বাদ দেয়, কারণ ইনার জয়েনের একটা অর্ডার লাগে। সমাধান: সারি রাখতে শিকল একবার LEFT JOIN দিয়ে শুরু হলে পরের জয়েনগুলোও LEFT রাখুন। - শিকলের একটা কড়া বাদ।
order_items-কে সরাসরিcustomers-এর সাথে এমন কোনো কলামে জয়েন করা যা ঘটনাচক্রে সংখ্যা (oi.order_id = c.customer_id) — চলে, কিন্তু অর্থহীন। সমাধান: কি-এর পথ এঁকে নিন আর শুধু সেই পথ ধরেই জয়েন করুন। - কিছুই যাচাই না করা। সমাধান: প্রতিটা নতুন জয়েনের পর সারির সংখ্যা আর একটা জানা মোট অঙ্ক মিলিয়ে নিন (যেমন অর্ডার ৬-এর পেমেন্ট মিলে ১৫,০০০)।
নিজে চেষ্টা করুন
- সহজ: প্রতিটা পেমেন্টের সাথে কাস্টমারের নাম দেখান:
payment_id, কাস্টমারের নাম,amount,method,payment_idক্রমে (তিনটা টেবিল)। - মাঝারি: ডেলিভারড অর্ডারের শহরভিত্তিক আয়, সবচেয়ে বেশিটা আগে। শহরের জন্য কোন টেবিল লাগবে, আর যার শহর NULL সেই Sadia-র কী হয়?
- কঠিন: প্রতিটা পেমেন্ট মেথডে পেমেন্টের সংখ্যা আর মোট টাকা দেখান, সাথে কতজন আলাদা কাস্টমার মেথডটা ব্যবহার করেছেন। কোনো মোট অঙ্ক যেন ফুলে না যায় (payments → orders → customers শিকলটা "এক" দিকের দিকে যায়, তাই ফ্যান-আউট হতে পারে কিনা যাচাই করুন)।
উত্তর
-- 1
SELECT pay.payment_id, c.name, pay.amount, pay.method
FROM payments pay
JOIN orders o ON o.order_id = pay.order_id
JOIN customers c ON c.customer_id = o.customer_id
ORDER BY pay.payment_id;-- 2
SELECT c.city, SUM(oi.quantity * oi.unit_price) AS revenue
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
JOIN order_items oi ON oi.order_id = o.order_id
WHERE o.status = 'delivered'
GROUP BY c.city
ORDER BY revenue DESC;+------------+---------+
| city | revenue |
+------------+---------+
| Dhaka | 38600 |
| Chattogram | 19500 |
| Khulna | 4950 |
| NULL | 4000 |
+------------+---------+Sadia-র আয় NULL শহরের নিচে গ্রুপ হয়েছে: GROUP BY সব NULL-কে একটা গ্রুপে রাখে। রিপোর্টটা মানুষের জন্য হলে COALESCE(c.city, 'Unknown') দিয়ে দেখান।
-- 3
SELECT pay.method,
COUNT(*) AS payments,
SUM(pay.amount) AS total,
COUNT(DISTINCT o.customer_id) AS customers
FROM payments pay
JOIN orders o ON o.order_id = pay.order_id
GROUP BY pay.method
ORDER BY total DESC;প্রতিটা পেমেন্টের ঠিক একটা অর্ডার, তাই orders-এর দিকে জয়েন পেমেন্টকে গুণ করতে পারে না; কাস্টমার আইডি orders-এই আছে, তাই customers টেবিল লাগেই না।
সারসংক্ষেপ
- কি-এর পথ ধরে জয়েন জুড়ুন, প্রতিটা রিলেশনশিপে একটা
JOIN … ON; প্রতিটাONআগে জয়েন হওয়া যেকোনো টেবিল ব্যবহার করতে পারে। - অ্যাগ্রিগেট করার আগে জোড়া লাগানো সারির গ্রেইন জানুন; "এক" দিকের দিকে জয়েন গ্রেইন ঠিক রাখে, "অনেক" দিকের দিকে জয়েন তা বদলে দেয়।
- একই প্যারেন্টের সাথে জয়েন করা দুটো "অনেক" টেবিল একে অপরকে গুণ করে (ফ্যান-আউট): আগে প্রতিটাকে প্রতি প্যারেন্টে এক সারিতে অ্যাগ্রিগেট করুন, তারপর জয়েন।
- মেনি-টু-মেনি রিলেশনশিপ যায় জাংশন টেবিল হয়ে;
order_items-এর সেলফ জয়েন দেয় "একসাথে কেনা" জোড়া। - প্রতিটা মাল্টি-জয়েন একটা সারির সংখ্যা আর একটা জানা মোট অঙ্ক দিয়ে যাচাই করুন।
এরপর: সাবকোয়েরি, EXISTS আর কোরিলেটেড কোয়েরি ফ্যান-আউট সারাতে যে "ব্র্যাকেটের ভেতরের কোয়েরি" ব্যবহার করেছেন সেটাকে পূর্ণ হাতিয়ার বানায়: WHERE, FROM আর SELECT-এ সাবকোয়েরি, EXISTS, আর কখন জয়েনের চেয়ে সাবকোয়েরি বেশি পরিষ্কার।