অধ্যায় 6 · অনুশীলন ও ক্যাপস্টোন
SQL ইন্টারভিউ ও সমস্যা সমাধানের অনুশীলন
- পৃষ্ঠা 21 / 22
- 18 মিনিট পড়া
ডেটা, ML আর AI ইঞ্জিনিয়ারিংয়ের SQL ইন্টারভিউ আর অনলাইন অ্যাসেসমেন্ট ঘুরেফিরে একই ডজনখানেক প্যাটার্নে আসে: NULL-সহ ফিল্টার, দুবার না গুনে অ্যাগ্রিগেট, কী নেই তা খুঁজে বের করা, দলের ভেতরে র্যাঙ্ক, একটা সারিকে আগেরটার সাথে তুলনা, আর একটা কোয়েরি কেন ভুল সংখ্যা দিচ্ছে তা ধরা। প্যাটার্নটা চিনে ফেললে কোয়েরি প্রায় নিজেই লেখা হয়ে যায়। এই পাতাটা একটা অনুশীলনের সেশন: শপের ডেটার ওপর ধাপে ধাপে কঠিন সমস্যা, প্রতিটার সমাধান, আর তার চেয়েও জরুরি — ইন্টারভিউয়ার যে যুক্তিটা শুনতে চান।
সমাধান পড়ার আগে প্রতিটা সমস্যা নিজে চেষ্টা করুন। প্রতিটা কোয়েরি SQLite-এ চলে (উইন্ডো ফাংশনের জন্য 3.25+); MySQL বা PostgreSQL যেখানে আলাদা, নোটে বলা আছে।
যা শিখবেন
- লজিক্যাল এক্সিকিউশন অর্ডারের ওপর দাঁড়ানো, যেকোনো SQL সমস্যার জন্য একটা পুনরাবৃত্তিযোগ্য পদ্ধতি
- ক্লাসিক প্যাটার্ন: NULL-নিরাপদ ফিল্টার, অ্যান্টি-জয়েন, ফ্যান-আউট ঠেকাতে আগে অ্যাগ্রিগেট, কোরিলেটেড সাবকোয়েরি, N-তম সর্বোচ্চ, প্রতি দলে সেরা N
- উইন্ডো ফাংশনের প্যাটার্ন: রানিং টোটাল, মাস-থেকে-মাস গ্রোথ, গ্যাপ-অ্যান্ড-আইল্যান্ড, রিটেনশন, ডুপ্লিকেট সরানো
- ভুল কোয়েরি কীভাবে ডিবাগ করবেন, আর ধীর কোয়েরিকে কীভাবে sargable (ইনডেক্স ব্যবহার করতে পারে এমন) করবেন
- সময়-বাঁধা অ্যাসেসমেন্ট কীভাবে সামলাবেন
প্রতিটা সমস্যার জন্য একটা পদ্ধতি
- প্রশ্নটা নিজের ভাষায় বলুন, আর উত্তরের গ্রেইন ঠিক করুন: "প্রতি ক্যাটাগরিতে একটা সারি", "প্রতি গ্রাহকের প্রতি মাসে একটা সারি"।
- এজ কেস জিজ্ঞেস করুন: বাতিল অর্ডার? টাই? NULL? বিক্রি নেই এমন ক্যাটাগরি? জিজ্ঞেস করার সুযোগ না থাকলে আপনার ধরে নেওয়াটা একটা কমেন্টে লিখুন।
- ধাপে ধাপে বানান: আগে শুধু
FROM/JOINঅংশ চালিয়ে সারির সংখ্যা দেখুন, তারপর ফিল্টার, তারপর গ্রুপিং। CTE প্রতিটা ধাপকে পড়ার মতো রাখে। - ফলাফল যাচাই করুন জানা কিছুর সাথে: একটা টোটাল, একটা গণনা, হাতে হিসাব করা একটা সারি।
আর লজিক্যাল ক্রমটা মাথায় রাখুন; "এখানে এই অ্যালিয়াস কেন চলছে না?" ধরনের বেশিরভাগ এররের ব্যাখ্যা এটাই:
FROM / JOIN → WHERE → GROUP BY → HAVING → SELECT (উইন্ডো ফাংশন এখানে) → DISTINCT → ORDER BY → LIMITউইন্ডো ফাংশন চলে WHERE আর GROUP BY-এর পরে, তাই সরাসরি এগুলো দিয়ে ফিল্টার করা যায় না — আগে একটা CTE বা সাবকোয়েরিতে মুড়ে নিন। এই একটা তথ্যই নিচের কয়েকটা সমস্যার উত্তর।
লেভেল ১: ফিল্টার আর NULL
সমস্যা ১. ঢাকার বাইরের গ্রাহক
যে গ্রাহকেরা ঢাকায় নন, তাঁদের তালিকা দিন। সহজ-সরল কোয়েরিটা ভুল:
SELECT customer_id, name, city FROM customers WHERE city <> 'Dhaka' ORDER BY customer_id;
SELECT customer_id, name, city
FROM customers
WHERE city <> 'Dhaka' OR city IS NULL
ORDER BY customer_id;+-------------+---------------+------------+
| customer_id | name | city |
+-------------+---------------+------------+
| 2 | Tanvir Ahmed | Chattogram |
| 4 | Rafiq Islam | Sylhet |
| 6 | Imran Hossain | Khulna |
| 8 | Karim Uddin | Rajshahi |
+-------------+---------------+------------+
+-------------+-----------------+------------+
| customer_id | name | city |
+-------------+-----------------+------------+
| 2 | Tanvir Ahmed | Chattogram |
| 4 | Rafiq Islam | Sylhet |
| 5 | Sadia Chowdhury | NULL |
| 6 | Imran Hossain | Khulna |
| 8 | Karim Uddin | Rajshahi |
+-------------+-----------------+------------+যুক্তি: NULL <> 'Dhaka' সত্য নয়, অজানা — তাই প্রথম কোয়েরি থেকে সাদিয়া (শহর অজানা) হারিয়ে যান। তিনি উত্তরে থাকবেন কি না, সেটা ব্যবসার প্রশ্ন — সেটা বলুন। SQLite আর PostgreSQL-এ city IS NOT 'Dhaka' / city IS DISTINCT FROM 'Dhaka'-ও আছে।
সমস্যা ২. এখনই বিক্রি করা যায় এমন প্রোডাক্ট
স্টকে আছে আর দাম ৩,০০০ টাকার কম, এমন প্রোডাক্ট, সবচেয়ে সস্তাটা আগে।
SELECT product_id, name, price, stock
FROM products
WHERE price < 3000
AND (stock > 0 OR stock IS NULL) -- NULL স্টক = ডিজিটাল প্রোডাক্ট, কখনো ফুরায় না
ORDER BY price, product_id;+------------+---------------------------+-------+-------+
| product_id | name | price | stock |
+------------+---------------------------+-------+-------+
| 4 | Wireless Mouse | 900 | 60 |
| 11 | Gift Card | 1000 | NULL |
| 1 | Python Crash Course | 1200 | 40 |
| 8 | Laptop Stand | 1500 | 25 |
| 2 | Hands-On Machine Learning | 2500 | 15 |
+------------+---------------------------+-------+-------+যুক্তি: USB-C Hub (স্টক ০) বাদ। কোর্স আর গিফট কার্ডের স্টক NULL, কারণ সেগুলো ডিজিটাল; শুধু stock > 0 লিখলে চুপচাপ বাদ পড়ত। টাই ভাঙার জন্য product_id ক্রমটা পুরোপুরি নির্দিষ্ট করে।
লেভেল ২: অ্যাগ্রিগেশন আর জয়েন
সমস্যা ৩. প্রতি ক্যাটাগরির আয়, ফাঁকাগুলোসহ
বাতিল নয় এমন অর্ডার থেকে প্রতি ক্যাটাগরির আয়। বিক্রি না থাকলেও প্রতিটা ক্যাটাগরি দেখান।
SELECT c.name AS category,
COALESCE(SUM(CASE WHEN o.status <> 'cancelled'
THEN oi.quantity * oi.unit_price END), 0) AS revenue
FROM categories AS c
LEFT JOIN products AS p ON p.category_id = c.category_id
LEFT JOIN order_items AS oi ON oi.product_id = p.product_id
LEFT JOIN orders AS o ON o.order_id = oi.order_id
GROUP BY c.category_id, c.name
ORDER BY revenue DESC, category;+-------------+---------+
| category | revenue |
+-------------+---------+
| Courses | 36000 |
| Electronics | 31550 |
| Accessories | 8900 |
| Books | 8600 |
| Furniture | 0 |
+-------------+---------+যুক্তি: categories থেকে শুরু করুন, যাতে Furniture টিকে থাকে; COALESCE তার NULL যোগফলকে 0 বানায়। বাতিল অর্ডার বাদ দেওয়ার শর্তটা WHERE-এ নয়, SUM-এর ভেতরে বসানো হয়েছে: WHERE o.status <> 'cancelled' লিখলে খালি ক্যাটাগরির NULL সারিগুলো বাদ পড়ত আর LEFT JOIN আবার ইনার জয়েন হয়ে যেত; এমনকি WHERE o.status IS NULL OR … লিখলেও যে ক্যাটাগরির সব বিক্রিই বাতিল হয়েছে, সেটা হারিয়ে যেত। শর্তসাপেক্ষ অ্যাগ্রিগেশন প্রতিটা ক্যাটাগরি রাখে, শুধু বাতিল লাইনগুলোর জন্য কিছু যোগ করে না। Electronics থেকে বাতিল মনিটরটা (অর্ডার ৫) বাদ গেছে। Gift Card-এর কোনো ক্যাটাগরি নেই, তাই সে কোনো সারিতে নেই — এটা উল্লেখ করুন।
সমস্যা ৪. যে গ্রাহকেরা কখনো অর্ডার করেননি
SELECT c.customer_id, c.name
FROM customers AS c
WHERE NOT EXISTS (SELECT 1 FROM orders AS o WHERE o.customer_id = c.customer_id)
ORDER BY c.customer_id;+-------------+-------------+
| customer_id | name |
+-------------+-------------+
| 8 | Karim Uddin |
+-------------+-------------+যুক্তি: এটা একটা অ্যান্টি-জয়েন। LEFT JOIN … WHERE o.order_id IS NULL-ও চলে। NOT IN (SELECT customer_id …) এড়িয়ে চলুন: সাবকোয়েরি কখনো একটা NULL ফেরত দিলে পুরো ফলাফল ফাঁকা হয়ে যায়।
সমস্যা ৫. যে অর্ডারের পেমেন্ট মেলে না
যে অর্ডারে পরিশোধিত টাকা অর্ডারের মোট দামের সাথে মেলে না, সেগুলো বের করুন। লোভনীয় কোয়েরিটা আইটেম আর পেমেন্ট একসাথে জয়েন করে:
-- ভুল: orders-এর সাথে দুটো "many" টেবিল জয়েন করলে সারি গুণ হয়ে যায়
SELECT o.order_id,
SUM(oi.quantity * oi.unit_price) AS order_total,
COALESCE(SUM(p.amount), 0) AS paid
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id
LEFT JOIN payments AS p ON p.order_id = o.order_id
GROUP BY o.order_id
HAVING order_total <> paid
ORDER BY o.order_id;+----------+-------------+-------+
| order_id | order_total | paid |
+----------+-------------+-------+
| 1 | 2100 | 4200 |
| 4 | 4000 | 8000 |
| 5 | 18500 | 0 |
| 6 | 30000 | 15000 |
| 7 | 4000 | 8000 |
| 9 | 4950 | 9900 |
| 11 | 5500 | 11000 |
| 13 | 15000 | 0 |
| 14 | 3100 | 6200 |
+----------+-------------+-------+নয়টা "অমিল" — প্রায় সবই ভুয়া। দুটো আইটেম আর একটা পেমেন্টের অর্ডারে পেমেন্ট দুবার গোনা হয়; অর্ডার ৬-এ (একটা আইটেম, দুটো পেমেন্ট) আইটেম দুবার গোনা হয়। সমাধান: আগে প্রতিটা দিককে প্রতি অর্ডারে একটা সারিতে অ্যাগ্রিগেট করুন, তারপর জয়েন:
WITH totals AS (
SELECT order_id, SUM(quantity * unit_price) AS order_total
FROM order_items GROUP BY order_id
),
paid AS (
SELECT order_id, SUM(amount) AS paid
FROM payments GROUP BY order_id
)
SELECT o.order_id, o.status, t.order_total, COALESCE(p.paid, 0) AS paid
FROM orders AS o
JOIN totals AS t ON t.order_id = o.order_id
LEFT JOIN paid AS p ON p.order_id = o.order_id
WHERE t.order_total <> COALESCE(p.paid, 0)
ORDER BY o.order_id;+----------+-----------+-------------+------+
| order_id | status | order_total | paid |
+----------+-----------+-------------+------+
| 5 | cancelled | 18500 | 0 |
| 13 | pending | 15000 | 0 |
+----------+-----------+-------------+------+যুক্তি: শুধু বাতিল আর পেন্ডিং অর্ডার দুটোর পেমেন্ট হয়নি; বাকি সব মিলে যায়, অর্ডার ৬-এর ভাগ করা পেমেন্টসহ। "জয়েনের আগে অ্যাগ্রিগেট" — SQL ইন্টারভিউয়ের সবচেয়ে কাজের বাক্য।
লেভেল ৩: সাবকোয়েরি আর র্যাঙ্কিং
সমস্যা ৬. ক্যাটাগরির গড়ের চেয়ে বেশি দামের প্রোডাক্ট
SELECT p.name, p.category_id, p.price
FROM products AS p
WHERE p.price > (SELECT AVG(p2.price) FROM products AS p2
WHERE p2.category_id = p.category_id)
ORDER BY p.category_id, p.price DESC;+---------------------------+-------------+-------+
| name | category_id | price |
+---------------------------+-------------+-------+
| Hands-On Machine Learning | 1 | 2500 |
| 27-inch Monitor | 2 | 18500 |
| AI Engineering Bootcamp | 3 | 15000 |
| USB-C Hub | 4 | 2200 |
+---------------------------+-------------+-------+যুক্তি: একটা কোরিলেটেড সাবকোয়েরি বাইরের প্রতিটা সারির জন্য সেই সারির ক্যাটাগরি নিয়ে একবার চলে। উইন্ডো দিয়েও হয়: একটা CTE-তে AVG(price) OVER (PARTITION BY category_id) হিসাব করে বাইরে ফিল্টার। গিফট কার্ডকে (NULL ক্যাটাগরি) তুলনা করা হয় কিছুই না থাকার গড়ের (NULL) সাথে, তাই সে কখনো আসে না।
সমস্যা ৭. দ্বিতীয় সর্বোচ্চ বেতন
SELECT MAX(salary) AS second_highest
FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);
WITH ranked AS (
SELECT name, department, salary,
DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rnk
FROM employees
)
SELECT department, name, salary
FROM ranked
WHERE rnk = 2
ORDER BY department, name;+----------------+
| second_highest |
+----------------+
| 180000 |
+----------------+
+-------------+--------------+--------+
| department | name | salary |
+-------------+--------------+--------+
| Data | Priya Sen | 110000 |
| Engineering | Nusrat Jahan | 120000 |
| Engineering | Zahid Hasan | 120000 |
+-------------+--------------+--------+যুক্তি: দ্বিতীয় কোনো মান না থাকলে প্রথম রূপটা ফাঁকা ফলাফল নয়, NULL ফেরত দেয় — ইন্টারভিউয়াররা এটা পছন্দ করেন। "প্রতি দলে N-তম সর্বোচ্চ"-এর জন্য DENSE_RANK টাই সামলায়: নুসরাত আর জাহিদ দুজনেই ১,২০,০০০ পান, তাই Engineering-এ দুজনেই দ্বিতীয়। ROW_NUMBER ইচ্ছেমতো একজনকে বেছে নিত, RANK টাইয়ের পরের সংখ্যাটা লাফিয়ে যেত।
সমস্যা ৮. প্রতি ক্যাটাগরিতে আয়ের হিসাবে সেরা ২টা প্রোডাক্ট
WITH product_revenue AS (
SELECT p.category_id, p.name, SUM(oi.quantity * oi.unit_price) AS revenue
FROM products AS p
JOIN order_items AS oi ON oi.product_id = p.product_id
JOIN orders AS o ON o.order_id = oi.order_id AND o.status <> 'cancelled'
GROUP BY p.product_id, p.category_id, p.name
),
ranked AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY revenue DESC, name) AS rn
FROM product_revenue
)
SELECT category_id, name, revenue, rn
FROM ranked
WHERE rn <= 2
ORDER BY category_id, rn;+-------------+---------------------------+---------+----+
| category_id | name | revenue | rn |
+-------------+---------------------------+---------+----+
| 1 | Hands-On Machine Learning | 5000 | 1 |
| 1 | Python Crash Course | 3600 | 2 |
| 2 | 27-inch Monitor | 18500 | 1 |
| 2 | Mechanical Keyboard | 8550 | 2 |
| 3 | AI Engineering Bootcamp | 30000 | 1 |
| 3 | SQL Masterclass | 6000 | 2 |
| 4 | Laptop Stand | 4500 | 1 |
| 4 | USB-C Hub | 4400 | 2 |
+-------------+---------------------------+---------+----+যুক্তি: আগে অ্যাগ্রিগেট, তারপর র্যাঙ্ক, তারপর ফিল্টার — ফিল্টারকে CTE-র বাইরে থাকতেই হবে, কারণ উইন্ডো ফাংশন চলে WHERE-এর পরে। উইন্ডোর ORDER BY-তে name যোগ করলে টাই হলেও ফল নির্দিষ্ট থাকে।
লেভেল ৪: সময়ের ধারার প্যাটার্ন
সমস্যা ৯. মাসিক আয়, রানিং টোটাল আর মাস-থেকে-মাস গ্রোথ
WITH monthly AS (
SELECT strftime('%Y-%m', o.order_date) AS month,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id
WHERE o.status <> 'cancelled'
GROUP BY month
)
SELECT month, revenue,
SUM(revenue) OVER (ORDER BY month) AS running_total,
ROUND(100.0 * (revenue - LAG(revenue) OVER (ORDER BY month))
/ LAG(revenue) OVER (ORDER BY month), 1) AS mom_pct
FROM monthly
ORDER BY month;+---------+---------+---------------+---------+
| month | revenue | running_total | mom_pct |
+---------+---------+---------------+---------+
| 2026-01 | 6600 | 6600 | NULL |
| 2026-02 | 7000 | 13600 | 6.1 |
| 2026-03 | 21400 | 35000 | 205.7 |
| 2026-04 | 23450 | 58450 | 9.6 |
| 2026-05 | 8500 | 66950 | -63.8 |
| 2026-06 | 18100 | 85050 | 112.9 |
+---------+---------+---------------+---------+যুক্তি: 100.0 দশমিক ভাগ নিশ্চিত করে (পূর্ণসংখ্যার ভাগ কেটে ফেলত)। জানুয়ারির গ্রোথ NULL, কারণ আগের কোনো মাস নেই — এটাই ঠিক, ০ বানাবেন না। আসল ইন্টারভিউতে বাদ পড়া মাসের কথা তুলুন: যে মাসে অর্ডার নেই তার কোনো সারিই হয় না, তখন LAG দুই মাস আগের সাথে তুলনা করবে। একটা ক্যালেন্ডারের (রিকার্সিভ CTE) সাথে জয়েন করে এটা ঠিক করুন, আর কোনো মাসের আয় শূন্য হতে পারে বলে ভাগটাকে NULLIF(…, 0) দিয়ে সুরক্ষিত রাখুন — দুটোই অ্যানালিটিক্স প্যাটার্ন: গ্রোথ, কোহর্ট, রিটেনশন আর গ্যাপ-এর মতো।
সমস্যা ১০. সবচেয়ে লম্বা লগইন স্ট্রিক (গ্যাপ আর আইল্যান্ড)
প্রত্যেক ব্যবহারকারীর টানা লগইনের সবচেয়ে লম্বা পর্ব খুঁজুন।
CREATE TABLE logins (user_id INTEGER, login_date TEXT);
INSERT INTO logins VALUES
(1, '2026-06-01'), (1, '2026-06-02'), (1, '2026-06-03'), (1, '2026-06-05'), (1, '2026-06-06'),
(2, '2026-06-01'), (2, '2026-06-03'), (2, '2026-06-04'), (2, '2026-06-05'), (2, '2026-06-06'),
(2, '2026-06-06'), (3, '2026-06-02');
WITH days AS (
SELECT DISTINCT user_id, login_date FROM logins
),
islands AS (
SELECT user_id, login_date,
julianday(login_date)
- ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) AS grp
FROM days
),
streaks AS (
SELECT user_id, MIN(login_date) AS start_day, MAX(login_date) AS end_day, COUNT(*) AS length
FROM islands
GROUP BY user_id, grp
)
SELECT user_id, start_day, end_day, length
FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY length DESC, start_day) AS rn
FROM streaks)
WHERE rn = 1
ORDER BY user_id;+---------+------------+------------+--------+
| user_id | start_day | end_day | length |
+---------+------------+------------+--------+
| 1 | 2026-06-01 | 2026-06-03 | 3 |
| 2 | 2026-06-03 | 2026-06-06 | 4 |
| 3 | 2026-06-02 | 2026-06-02 | 1 |
+---------+------------+------------+--------+যুক্তি: টানা দিনগুলোর মধ্যে তারিখ আর সারি নম্বর দুটোই একটা করে বাড়ে, তাই তাদের পার্থক্য স্থির থাকে; প্রতিটা স্থির মান একটা আইল্যান্ড। আগে ডুপ্লিকেট সরান (ব্যবহারকারী ২ ৬ জুন দুবার লগইন করেছেন), নইলে সারি নম্বর সরে যায়। একই কৌশলে আউটেজ, LLM কলের টানা ব্যর্থতা আর সেশন খুঁজে পাওয়া যায়।
সমস্যা ১১. প্রথম অর্ডারের মাস ধরে আবার কেনার হার (রিটেনশন)
WITH valid AS (
SELECT customer_id, order_date FROM orders WHERE status <> 'cancelled'
),
firsts AS (
SELECT customer_id, MIN(order_date) AS first_date FROM valid GROUP BY customer_id
)
SELECT strftime('%Y-%m', f.first_date) AS cohort,
COUNT(*) AS customers,
SUM(EXISTS (SELECT 1 FROM valid AS v
WHERE v.customer_id = f.customer_id
AND strftime('%Y-%m', v.order_date) > strftime('%Y-%m', f.first_date))) AS came_back,
ROUND(100.0 * SUM(EXISTS (SELECT 1 FROM valid AS v
WHERE v.customer_id = f.customer_id
AND strftime('%Y-%m', v.order_date) > strftime('%Y-%m', f.first_date))) / COUNT(*), 0) AS pct
FROM firsts AS f
GROUP BY cohort
ORDER BY cohort;+---------+-----------+-----------+-------+
| cohort | customers | came_back | pct |
+---------+-----------+-----------+-------+
| 2026-01 | 2 | 2 | 100.0 |
| 2026-02 | 1 | 1 | 100.0 |
| 2026-03 | 1 | 0 | 0.0 |
| 2026-04 | 1 | 1 | 100.0 |
| 2026-05 | 1 | 0 | 0.0 |
+---------+-----------+-----------+-------+যুক্তি: কোহর্ট ঠিক হয় প্রথম বৈধ অর্ডার দিয়ে (রফিকের একমাত্র অর্ডার বাতিল, তাই তিনি কোনো কোহর্টে নেই)। "ফিরে এসেছেন" মানে পরের কোনো মাসে অর্ডার। SQLite-এ EXISTS 0/1 দেয়, তাই যোগ করা যায়; PostgreSQL-এ লিখুন COUNT(*) FILTER (WHERE …) বা SUM(CASE …)। পূর্ণ রিটেনশন টেবিলে "প্রথম অর্ডারের পর কত মাস" ধরে প্রতিটার জন্য একটা কলাম থাকে (অ্যানালিটিক্স প্যাটার্ন: গ্রোথ, কোহর্ট, রিটেনশন আর গ্যাপ দেখুন)।
লেভেল ৫: পরিষ্কার করা, ডিবাগ আর গতি
সমস্যা ১২. শুধু ছোট-বড় হাতের অক্ষরে আলাদা ডুপ্লিকেট
INSERT INTO customers (customer_id, name, email, city, joined_on) VALUES
(9, 'Nadia R.', ' NADIA@example.com', 'Dhaka', '2026-06-20');
SELECT LOWER(TRIM(email)) AS clean_email, COUNT(*) AS n, GROUP_CONCAT(customer_id ORDER BY customer_id) AS ids
FROM customers
GROUP BY clean_email
HAVING COUNT(*) > 1;
WITH ranked AS (
SELECT customer_id,
ROW_NUMBER() OVER (PARTITION BY LOWER(TRIM(email)) ORDER BY joined_on, customer_id) AS rn
FROM customers
)
SELECT customer_id AS duplicate_to_remove FROM ranked WHERE rn > 1;+-------------------+---+-----+
| clean_email | n | ids |
+-------------------+---+-----+
| nadia@example.com | 2 | 1,9 |
+-------------------+---+-----+
+---------------------+
| duplicate_to_remove |
+---------------------+
| 9 |
+---------------------+যুক্তি: UNIQUE কনস্ট্রেইন্ট এটা আটকায়নি, কারণ ' NADIA@example.com' একটা আলাদা স্ট্রিং। (GROUP_CONCAT-এর ভেতরে ORDER BY-এর জন্য SQLite 3.44+ লাগে; PostgreSQL-এ লেখা হয় STRING_AGG(customer_id::text, ',' ORDER BY customer_id)।) নরমালাইজ করুন, গ্রুপ করুন, তারপর ROW_NUMBER দিয়ে প্রতি দলে একটা সারি রাখুন — পুরোনোটা আগে। আসল সিস্টেমে মোছার আগে ডুপ্লিকেটের অর্ডারগুলোও রেখে দেওয়া গ্রাহকের নামে সরিয়ে নেবেন, আর LOWER(TRIM(email))-এর ওপর একটা ইউনিক ইনডেক্স যোগ করবেন।
সমস্যা ১৩. ডিবাগ: "শূন্য অর্ডারের গ্রাহক দেখাচ্ছে ১টা অর্ডার"
একজন সহকর্মী চান প্রত্যেক গ্রাহকের ডেলিভারড অর্ডারের সংখ্যা, শূন্যগুলোসহ:
SELECT c.customer_id, COUNT(*) AS delivered_orders
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.customer_id
WHERE o.status = 'delivered'
GROUP BY c.customer_id
ORDER BY c.customer_id;+-------------+------------------+
| customer_id | delivered_orders |
+-------------+------------------+
| 1 | 3 |
| 2 | 2 |
| 3 | 3 |
| 5 | 1 |
| 6 | 1 |
| 7 | 1 |
+-------------+------------------+দুটো বাগ। ডান দিকের টেবিলের ওপর WHERE ডেলিভারড অর্ডার না থাকা গ্রাহকদের বাদ দেয় (৪, ৮, ৯ নেই), LEFT JOIN-কে ইনার জয়েন বানিয়ে দেয়। আর তাঁরা থাকলেও COUNT(*) তাঁদের NULL-ভরা একটা সারিকে ১ গুনত। শর্তটা ON-এ সরান, আর ডান টেবিলের একটা কলাম গুনুন:
SELECT c.customer_id, COUNT(o.order_id) AS delivered_orders
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.customer_id AND o.status = 'delivered'
GROUP BY c.customer_id
ORDER BY c.customer_id;+-------------+------------------+
| customer_id | delivered_orders |
+-------------+------------------+
| 1 | 3 |
| 2 | 2 |
| 3 | 3 |
| 4 | 0 |
| 5 | 1 |
| 6 | 1 |
| 7 | 1 |
| 8 | 0 |
| 9 | 0 |
+-------------+------------------+সমস্যা ১৪. দ্রুত করুন: sargable ফিল্টার
"৫ কোটি অর্ডারে এই কোয়েরি ধীর। কেন?"
CREATE INDEX idx_orders_date ON orders (order_date);
EXPLAIN QUERY PLAN
SELECT COUNT(*) FROM orders WHERE strftime('%Y-%m', order_date) = '2026-03';
EXPLAIN QUERY PLAN
SELECT COUNT(*) FROM orders WHERE order_date >= '2026-03-01' AND order_date < '2026-04-01';+----+--------+---------+--------------------------------------------------+
| id | parent | notused | detail |
+----+--------+---------+--------------------------------------------------+
| 3 | 0 | 0 | SCAN orders USING COVERING INDEX idx_orders_date |
+----+--------+---------+--------------------------------------------------+
+----+--------+---------+------------------------------------------------------------------------------------+
| id | parent | notused | detail |
+----+--------+---------+------------------------------------------------------------------------------------+
| 3 | 0 | 0 | SEARCH orders USING COVERING INDEX idx_orders_date (order_date>? AND order_date<?) |
+----+--------+---------+------------------------------------------------------------------------------------+যুক্তি: কলামটাকে ফাংশনে মুড়লে ইনডেক্সের কাছে তার ক্রম লুকিয়ে যায়, তাই SQLite সরাসরি মার্চে লাফ দিতে পারে না: সে প্রতিটা এন্ট্রি পড়ে (SCAN — এখানে ইনডেক্সের ভেতর দিয়ে, যেটা টেবিলের চেয়ে ছোট, কিন্তু তবু পুরোটা)। খালি কলামের ওপর অর্ধ-খোলা পরিসর sargable: প্ল্যান হয়ে যায় SEARCH, আর পড়ে শুধু মার্চ। ইন্টারভিউয়াররা আর যা শুনতে চান: শুধু দরকারি কলাম আনুন, শুরুতে % দেওয়া LIKE এড়ান, জয়েনের আগে ফিল্টার করুন, EXPLAIN দিয়ে প্ল্যান দেখুন (EXPLAIN আর কোয়েরি অপ্টিমাইজেশন দেখুন)।
সময়-বাঁধা অ্যাসেসমেন্ট
- আগে সব প্রশ্ন পড়ুন, আর যেগুলো শেষ করতে পারবেন সেগুলো দিয়ে শুরু করুন; নম্বর প্রায়ই প্রশ্ন ধরে আসে।
- লেখার আগে ডেটা দেখুন: প্রতিটা টেবিলে
SELECT * … LIMIT 5, আর কি (key) কলামগুলোতে NULL বা ডুপ্লিকেট আছে কি না দেখুন। - CTE-র ধাপে লিখুন আর প্রতিটা চালান। ডিবাগ করতে না পারা চালাক এক-লাইনের চেয়ে সঠিক, পড়ার মতো উত্তর ভালো।
- ডায়ালেক্ট দেখে নিন।
LIMITবনামTOP,strftimeবনামDATE_TRUNC/DATE_FORMAT,||বনামCONCAT। অনেক প্ল্যাটফর্ম MySQL বা PostgreSQL চালায়। - প্রতি প্রশ্নে দুই মিনিট রাখুন যাচাইয়ের জন্য: সারির সংখ্যা কি গ্রেইনের সাথে মেলে? টোটাল কি একটা সাধারণ
SUM-এর সাথে মেলে? - কথা বলুন (লাইভ ইন্টারভিউতে): ধরে নেওয়া বিষয়, প্যাটার্ন আর ট্রেড-অফ বলুন। কোয়েরির মতোই যুক্তিরও নম্বর আছে।
সাধারণ ভুল
- ফিল্টারে NULL ভুলে যাওয়া।
<>,NOT INআর তুলনাগুলো NULL সারি চুপচাপ বাদ দেয়। সমাধান:IS NULL,COALESCEবাIS DISTINCT FROMদিয়ে সচেতনভাবে সিদ্ধান্ত নিন। - একসাথে দুটো one-to-many টেবিল জয়েন। টোটাল গুণ হয়ে যায়। সমাধান: প্রতিটা দিক একটা CTE-তে কি ধরে অ্যাগ্রিগেট করুন, তারপর জয়েন।
- LEFT JOIN-এর ডান টেবিল
WHERE-এ ফিল্টার। এটা ইনার জয়েন হয়ে যায়। সমাধান: শর্তটাON-এ দিন। WHERE-এ উইন্ডো ফাংশন। তখনো সেটা হিসাবই হয়নি। সমাধান: CTE-তে হিসাব করে বাইরে ফিল্টার করুন।- অনির্দিষ্ট ক্রম। টাইসহ
ORDER BY revenue, বা টাই-ভাঙা ছাড়াROW_NUMBER, আলাদা রানে আলাদা উত্তর দেয়। সমাধান: প্রতিটাORDER BY-তে একটা ইউনিক কলাম যোগ করুন।
নিজে চেষ্টা করুন
- সহজ: প্রতিটা পেমেন্ট মাধ্যমের জন্য পেমেন্টের সংখ্যা আর মোট টাকা দেখান, সবচেয়ে বড় টোটাল আগে।
- মাঝারি: অন্তত একটা বাতিল-নয় অর্ডার আছে এমন প্রত্যেক গ্রাহকের প্রথম আর সর্বশেষ বাতিল-নয় অর্ডারের মধ্যে কত দিন, তা দেখান।
- কঠিন: প্রতিটা অর্ডারের মোট দাম আর সেটা ওই গ্রাহকের মোট খরচের কত শতাংশ, তা দেখান — শুধু উইন্ডো ফাংশন দিয়ে (সেলফ-জয়েন ছাড়া)। বাতিল অর্ডার বাদ দিন।
উত্তর
-- ১.
SELECT method, COUNT(*) AS payments, SUM(amount) AS total
FROM payments
GROUP BY method
ORDER BY total DESC, method;
-- ২.
SELECT customer_id,
CAST(julianday(MAX(order_date)) - julianday(MIN(order_date)) AS INTEGER) AS days_between
FROM orders
WHERE status <> 'cancelled'
GROUP BY customer_id
ORDER BY customer_id;
-- ৩.
WITH order_totals AS (
SELECT o.order_id, o.customer_id, SUM(oi.quantity * oi.unit_price) AS total
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id
WHERE o.status <> 'cancelled'
GROUP BY o.order_id, o.customer_id
)
SELECT customer_id, order_id, total,
ROUND(100.0 * total / SUM(total) OVER (PARTITION BY customer_id), 1) AS pct_of_customer
FROM order_totals
ORDER BY customer_id, order_id;সারসংক্ষেপ
- প্রশ্নটা নিজের ভাষায় বলুন, গ্রেইন ঠিক করুন, এজ কেসের তালিকা করুন, CTE-র ধাপে বানান, আর জানা একটা সংখ্যার সাথে ফলাফল মিলিয়ে নিন।
- বেশিরভাগ ভুল উত্তরের কারণ NULL, দুটো "many" টেবিল জয়েনের ফ্যান-আউট, আর LEFT JOIN-এর ডান দিকে
WHERE। - CTE-তে
ROW_NUMBER/RANK/DENSE_RANKদিয়ে র্যাঙ্ক করুন, তারপর ফিল্টার; টাই কেমন আচরণ করবে তা দেখে ফাংশন বাছুন। LAG, রানিংSUM() OVER, তারিখ বিয়োগ সারি নম্বর (আইল্যান্ড) আর প্রথম-ঘটনার কোহর্ট দিয়ে সময়ের ধারার বেশিরভাগ প্রশ্নের উত্তর হয়।- ফিল্টার sargable রাখুন, আর সময়-বাঁধা পরীক্ষায় চালাক উত্তরের চেয়ে পরিষ্কার, যাচাই করা উত্তর বেছে নিন।
এরপর: ক্যাপস্টোন প্রজেক্ট, চিট শিট আর এরপর কী এই দক্ষতাগুলোকে চারটা পোর্টফোলিও প্রজেক্টে রূপ দেয়, অ্যাডভান্সড টিউটোরিয়ালের এক পাতার একটা রেফারেন্স দেয়, আর দেখায় এখান থেকে AI পথ কোন দিকে যায়।