ভূমিকা

এই পোস্টে বাস্তব ইন্টারভিউতে আসা ১০টি SQL কুয়েরি সমস্যা এবং তাদের step-by-step সমাধান বাংলায় দেওয়া হয়েছে। প্রতিটি সমস্যায় একটি scenario, প্রয়োজনীয় টেবিল structure এবং সম্পূর্ণ SQL solution রয়েছে।


সমস্যা ১: বিভাগ অনুযায়ী সর্বোচ্চ বেতনের কর্মী খুঁজুন

সমস্যা: employees টেবিল থেকে প্রতিটি বিভাগের সর্বোচ্চ বেতনপ্রাপ্ত কর্মীর নাম, বিভাগ এবং বেতন বের করুন।

টেবিল:

employees (emp_id, name, dept_id, salary)
departments (dept_id, dept_name)

সমাধান:

-- পদ্ধতি ১: Subquery দিয়ে
SELECT e.name, d.dept_name, e.salary
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
WHERE e.salary = (
    SELECT MAX(salary)
    FROM employees e2
    WHERE e2.dept_id = e.dept_id
);

-- পদ্ধতি ২: Window Function দিয়ে (আরও efficient)
SELECT name, dept_name, salary
FROM (
    SELECT
        e.name,
        d.dept_name,
        e.salary,
        RANK() OVER (PARTITION BY e.dept_id ORDER BY e.salary DESC) AS rnk
    FROM employees e
    JOIN departments d ON e.dept_id = d.dept_id
) ranked
WHERE rnk = 1;

ব্যাখ্যা: পদ্ধতি ২ বেশি efficient কারণ এটি একবার টেবিল scan করে RANK() দিয়ে প্রতিটি বিভাগের শীর্ষ কর্মী বের করে।


সমস্যা ২: প্রতিটি মাসে মোট বিক্রয় পরিমাণ বের করুন

সমস্যা: orders টেবিল থেকে বছর ও মাস অনুযায়ী মোট বিক্রয় (total_amount) এবং অর্ডারের সংখ্যা বের করুন, নতুন থেকে পুরনো ক্রমে সাজান।

টেবিল:

orders (order_id, customer_id, order_date, total_amount, status)

সমাধান:

SELECT
    YEAR(order_date)  AS বছর,
    MONTH(order_date) AS মাস,
    COUNT(order_id)   AS মোট_অর্ডার,
    SUM(total_amount) AS মোট_বিক্রয়,
    AVG(total_amount) AS গড়_অর্ডার_মূল্য
FROM orders
WHERE status = 'completed'
GROUP BY YEAR(order_date), MONTH(order_date)
ORDER BY বছর DESC, মাস DESC;

ব্যাখ্যা: GROUP BY দিয়ে বছর ও মাস ভাগ করা হয়েছে। WHERE status = 'completed' দিয়ে শুধু সম্পন্ন অর্ডার নেওয়া হয়েছে।


সমস্যা ৩: পরপর তিন দিন বা তার বেশি Login করা Users খুঁজুন

সমস্যা: user_logins টেবিল থেকে সেই সব user খুঁজুন যারা কমপক্ষে ৩ দিন পরপর (consecutive) login করেছেন।

টেবিল:

user_logins (user_id, login_date)  -- login_date unique per user per day

সমাধান:

এখন SQL সমাধান এবং একটি ভিজ্যুয়াল ব্যাখ্যা তৈরি করছি।

এই সমস্যাটি সমাধানের মূল কৌশল হলো Gap and Islands problem। প্রতিটি login_date থেকে ROW_NUMBER() বাদ দিলে consecutive দিনগুলো একই গ্রুপে পড়ে।

কেন এটি কাজ করে?

যদি কেউ ৩ দিন পরপর login করেন (Jan 1, 2, 3), তাহলে:

  • login_date - ROW_NUMBER() = একই মান (ধ্রুবক)
  • ফলে এই ৩টি row একই "island" গ্রুপে পড়বে

**সমাধান:**প্রতিটি ধাপে ক্লিক করে logic বুঝুন। নিচে সম্পূর্ণ SQL এর ব্যাখ্যা:


মূল কৌশল: Gap and Islands

user_logins টেবিলে প্রতিটি user-এর login_date রয়েছে। ধরুন ৩ জন user:

user_id login_date
U1 2024-01-01
U1 2024-01-02
U1 2024-01-03
U1 2024-01-07
U2 2024-01-05
U2 2024-01-06
U3 2024-01-10
U3 2024-01-11
U3 2024-01-12

প্রতিটি user-এর login_date-এ ROW_NUMBER() যোগ করি। PARTITION BY user_id ORDER BY login_date:

user_id login_date rn
U1 2024-01-01 1
U1 2024-01-02 2
U1 2024-01-03 3
U1 2024-01-07 4
U2 2024-01-05 1
U2 2024-01-06 2
U3 2024-01-10 1
U3 2024-01-11 2
U3 2024-01-12 3

login_date - rn হিসাব করি। Consecutive দিনগুলোর জন্য এই মান একই থাকে — এটাই গ্রুপ key!

user_id login_date rn grp (date − rn)
U1 Jan 01 1 Dec 31
U1 Jan 02 2 Dec 31
U1 Jan 03 3 Dec 31
U1 Jan 07 4 Jan 03
U2 Jan 05 1 Jan 04
U2 Jan 06 2 Jan 04
U3 Jan 10 1 Jan 09
U3 Jan 11 2 Jan 09
U3 Jan 12 3 Jan 09

এখন GROUP BY user_id, grp করে COUNT(*) ≥ 3 দিয়ে ফিল্টার করি।

user_id grp streak ✓ ≥3?
U1 Dec 31 3 ✓ হ্যাঁ
U1 Jan 03 1 ✗ না
U2 Jan 04 2 ✗ না
U3 Jan 09 3 ✓ হ্যাঁ

কমপক্ষে ৩ দিন বা তার বেশি একটানা (consecutive) লগইন করা ইউজাররা হলেন: U1 এবং U3

-- ধাপ 1: প্রতিটি user-এর login_date-এ ROW_NUMBER() যোগ করি। 
WITH numbered AS (
  SELECT
    user_id,
    login_date,
    ROW_NUMBER() OVER (
      PARTITION BY user_id
      ORDER BY login_date
    ) AS rn
  FROM user_logins
),

-- ধাপ ২: Consecutive গ্রুপ চিহ্নিত করা
grouped AS (
  SELECT
    user_id,
    login_date,
    login_date - INTERVAL (rn - 1) DAY AS grp
  FROM numbered
)

-- ধাপ ৩: ≥৩ দিন streak আছে এমন user বের করা
SELECT DISTINCT user_id
FROM grouped
GROUP BY user_id, grp
HAVING COUNT(*) >= 3;

সমস্যা ৪: Duplicate Records খুঁজুন এবং মুছুন

সমস্যা: customers টেবিলে একই email-এ duplicate rows আছে। সবচেয়ে পুরনো record রেখে বাকিগুলো delete করুন।

টেবিল:

customers (id, name, email, created_at)

সমাধান:

-- ধাপ ১: Duplicate খোঁজা
SELECT email, COUNT(*) AS সংখ্যা
FROM customers
GROUP BY email
HAVING COUNT(*) > 1;

-- ধাপ ২: Duplicate delete করা (সবচেয়ে পুরনো রেখে)
DELETE FROM customers
WHERE id NOT IN (
    SELECT MIN(id)
    FROM customers
    GROUP BY email
);

-- MySQL-এ self-reference সমস্যার জন্য alternative:
DELETE c1
FROM customers c1
INNER JOIN customers c2
ON c1.email = c2.email
AND c1.id > c2.id;

ব্যাখ্যা: MIN(id) দিয়ে প্রতিটি email-এর সবচেয়ে পুরনো record রাখা হয়। JOIN পদ্ধতি MySQL-এ subquery সমস্যা এড়াতে ব্যবহার করা হয়।


সমস্যা ৫: Nth সর্বোচ্চ বেতন বের করুন

সমস্যা: employees টেবিল থেকে 3rd সর্বোচ্চ বেতন বের করুন (duplicate বেতন আলাদা হিসেবে গণ্য করবেন না)।

টেবিল:

employees (emp_id, name, salary)

সমাধান:

-- পদ্ধতি ১: DENSE_RANK দিয়ে (সবচেয়ে reliable)
SELECT salary
FROM (
    SELECT salary,
           DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
    FROM employees
) ranked
WHERE rnk = 3
LIMIT 1;

-- পদ্ধতি ২: Subquery দিয়ে (N = 3)
SELECT MIN(salary) AS third_highest
FROM (
    SELECT DISTINCT salary
    FROM employees
    ORDER BY salary DESC
    LIMIT 3
) top3;

-- পদ্ধতি ৩: Generic N-এর জন্য (N এর জায়গায় যেকোনো সংখ্যা দিন)
SELECT DISTINCT salary
FROM employees
ORDER BY salary DESC
LIMIT 1 OFFSET 2;   -- OFFSET = N-1

ব্যাখ্যা: DENSE_RANK() সবচেয়ে ভালো কারণ এটি duplicate salary handle করে সঠিকভাবে। OFFSET 2 মানে প্রথম ২টি skip করে ৩য়টি নেবে।


সমস্যা ৬: Manager এবং তাদের অধীনস্থ কর্মীদের তালিকা তৈরি করুন

সমস্যা: employees টেবিলে self-referencing foreign key আছে। প্রতিটি manager-এর নাম এবং তাদের direct reportee-দের সংখ্যা বের করুন।

টেবিল:

employees (emp_id, name, salary, manager_id)  -- manager_id references emp_id

সমাধান:

-- Manager এবং reportee সংখ্যা
SELECT
    m.name        AS ম্যানেজার_নাম,
    COUNT(e.emp_id) AS সরাসরি_অধীনস্থ_সংখ্যা,
    GROUP_CONCAT(e.name ORDER BY e.name SEPARATOR ', ') AS অধীনস্থ_কর্মীরা
FROM employees e
JOIN employees m ON e.manager_id = m.emp_id
GROUP BY m.emp_id, m.name
ORDER BY সরাসরি_অধীনস্থ_সংখ্যা DESC;

-- Recursive CTE দিয়ে সম্পূর্ণ hierarchy (multi-level)
WITH RECURSIVE org_chart AS (
    -- Base case: top-level manager (manager_id IS NULL)
    SELECT emp_id, name, manager_id, 0 AS level, name AS path
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    -- Recursive case
    SELECT e.emp_id, e.name, e.manager_id, oc.level + 1,
           CONCAT(oc.path, ' > ', e.name)
    FROM employees e
    JOIN org_chart oc ON e.manager_id = oc.emp_id
)
SELECT emp_id, name, level, path
FROM org_chart
ORDER BY path;

ব্যাখ্যা: Self-JOIN দিয়ে এক স্তরের hierarchy বের করা যায়। Multi-level hierarchy-র জন্য Recursive CTE ব্যবহার করা হয়।


সমস্যা ৭: Rolling/Cumulative Total বের করুন

সমস্যা: sales টেবিল থেকে প্রতিটি দিনের বিক্রয়ের পাশাপাশি সেই মাসের cumulative (চলমান মোট) বিক্রয় বের করুন।

টেবিল:

sales (sale_id, sale_date, amount, product_id)

সমাধান:

SELECT
    sale_date,
    amount                              AS দৈনিক_বিক্রয়,
    SUM(amount) OVER (
        PARTITION BY YEAR(sale_date), MONTH(sale_date)
        ORDER BY sale_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    )                                   AS মাসিক_চলমান_মোট,
    SUM(amount) OVER (
        ORDER BY sale_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    )                                   AS সার্বিক_চলমান_মোট,
    AVG(amount) OVER (
        ORDER BY sale_date
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    )                                   AS ৭_দিনের_গড়
FROM sales
ORDER BY sale_date;

ব্যাখ্যা: ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW মানে শুরু থেকে বর্তমান row পর্যন্ত sum করে। ROWS BETWEEN 6 PRECEDING AND CURRENT ROW দিয়ে গত ৭ দিনের moving average বের করা হয়।


সমস্যা ৮: দুটি টেবিলের মধ্যে Missing Records খুঁজুন

সমস্যা: products টেবিলে সব পণ্য আছে, কিন্তু order_items টেবিলে শুধু বিক্রি হওয়া পণ্য আছে। যেসব পণ্য গত ৩০ দিনে একবারও বিক্রি হয়নি তাদের খুঁজুন।

টেবিল:

products (product_id, product_name, category, price)
order_items (item_id, order_id, product_id, quantity, order_date)

সমাধান:

-- পদ্ধতি ১: LEFT JOIN + NULL check (সবচেয়ে efficient)
SELECT p.product_id, p.product_name, p.category, p.price
FROM products p
LEFT JOIN order_items oi
    ON p.product_id = oi.product_id
    AND oi.order_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
WHERE oi.product_id IS NULL
ORDER BY p.category, p.product_name;

-- পদ্ধতি ২: NOT EXISTS
SELECT product_id, product_name, category
FROM products p
WHERE NOT EXISTS (
    SELECT 1
    FROM order_items oi
    WHERE oi.product_id = p.product_id
    AND oi.order_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
);

-- পদ্ধতি ৩: NOT IN (NULL থাকলে সমস্যা হয়, এড়িয়ে চলুন)
SELECT product_id, product_name
FROM products
WHERE product_id NOT IN (
    SELECT DISTINCT product_id
    FROM order_items
    WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
    AND product_id IS NOT NULL  -- NULL handle করতে হবে
);

ব্যাখ্যা: LEFT JOIN + IS NULL সবচেয়ে efficient। NOT IN-এ subquery-তে NULL থাকলে পুরো query কোনো result দেয় না, তাই সতর্ক থাকুন।


সমস্যা ৯: Pivot Table তৈরি করুন (Row থেকে Column)

সমস্যা: monthly_sales টেবিলে বছর, মাস এবং বিক্রয় আছে। Row-তে থাকা মাসগুলোকে column-এ রূপান্তর করে একটি pivot table তৈরি করুন।

টেবিল:

monthly_sales (year, month, total_sales)

প্রত্যাশিত ফলাফল:

year Jan Feb Mar Apr ...
2023 50000 45000 60000 55000 ...
2024 62000 58000 71000 68000 ...

সমাধান:

-- MySQL-এ CASE WHEN দিয়ে Pivot
SELECT
    year,
    SUM(CASE WHEN month = 1  THEN total_sales ELSE 0 END) AS জানুয়ারি,
    SUM(CASE WHEN month = 2  THEN total_sales ELSE 0 END) AS ফেব্রুয়ারি,
    SUM(CASE WHEN month = 3  THEN total_sales ELSE 0 END) AS মার্চ,
    SUM(CASE WHEN month = 4  THEN total_sales ELSE 0 END) AS এপ্রিল,
    SUM(CASE WHEN month = 5  THEN total_sales ELSE 0 END) AS মে,
    SUM(CASE WHEN month = 6  THEN total_sales ELSE 0 END) AS জুন,
    SUM(CASE WHEN month = 7  THEN total_sales ELSE 0 END) AS জুলাই,
    SUM(CASE WHEN month = 8  THEN total_sales ELSE 0 END) AS আগস্ট,
    SUM(CASE WHEN month = 9  THEN total_sales ELSE 0 END) AS সেপ্টেম্বর,
    SUM(CASE WHEN month = 10 THEN total_sales ELSE 0 END) AS অক্টোবর,
    SUM(CASE WHEN month = 11 THEN total_sales ELSE 0 END) AS নভেম্বর,
    SUM(CASE WHEN month = 12 THEN total_sales ELSE 0 END) AS ডিসেম্বর,
    SUM(total_sales)                                       AS বার্ষিক_মোট
FROM monthly_sales
GROUP BY year
ORDER BY year;

ব্যাখ্যা: MySQL-এ native PIVOT নেই, তাই CASE WHEN + SUM দিয়ে conditional aggregation করা হয়। PostgreSQL-এ crosstab() function ব্যবহার করা যায়।


সমস্যা ১০: Customer Retention Rate বের করুন

সমস্যা: orders টেবিল থেকে প্রতি মাসে নতুন এবং ফিরে আসা (returning) customer-এর সংখ্যা এবং retention rate বের করুন।

টেবিল:

orders (order_id, customer_id, order_date, total_amount)

সমাধান:

-- ধাপ ১: প্রতিটি customer-এর প্রথম order মাস বের করা
WITH first_orders AS (
    SELECT
        customer_id,
        DATE_FORMAT(MIN(order_date), '%Y-%m') AS first_month
    FROM orders
    GROUP BY customer_id
),

-- ধাপ ২: প্রতি মাসে active customers চিহ্নিত করা
monthly_activity AS (
    SELECT DISTINCT
        customer_id,
        DATE_FORMAT(order_date, '%Y-%m') AS activity_month
    FROM orders
),

-- ধাপ ৩: নতুন ও পুরনো customer আলাদা করা
classified AS (
    SELECT
        ma.activity_month,
        ma.customer_id,
        CASE
            WHEN ma.activity_month = fo.first_month THEN 'নতুন'
            ELSE 'ফিরে আসা'
        END AS customer_type
    FROM monthly_activity ma
    JOIN first_orders fo ON ma.customer_id = fo.customer_id
)

-- ধাপ ৪: মাস অনুযায়ী summary
SELECT
    activity_month              AS মাস,
    COUNT(DISTINCT CASE WHEN customer_type = 'নতুন'       THEN customer_id END) AS নতুন_কাস্টমার,
    COUNT(DISTINCT CASE WHEN customer_type = 'ফিরে আসা'  THEN customer_id END) AS ফিরে_আসা_কাস্টমার,
    COUNT(DISTINCT customer_id)                                                   AS মোট_কাস্টমার,
    ROUND(
        100.0 * COUNT(DISTINCT CASE WHEN customer_type = 'ফিরে আসা' THEN customer_id END)
              / NULLIF(COUNT(DISTINCT customer_id), 0),
        2
    )                           AS retention_rate_percent
FROM classified
GROUP BY activity_month
ORDER BY activity_month;

ব্যাখ্যা: CTE (Common Table Expression) দিয়ে ধাপে ধাপে সমস্যাটি সমাধান করা হয়েছে। NULLIF(..., 0) দিয়ে division by zero এড়ানো হয়েছে। এই ধরনের multi-step CTE solution ইন্টারভিউতে analytical thinking দেখানোর সেরা উপায়।


উপসংহার

এই ১০টি সমস্যা বাস্তব প্রজেক্টে এবং ইন্টারভিউতে বারবার আসে। মূল কৌশলগুলো হলো:

  • জটিল সমস্যাকে CTE দিয়ে ছোট ধাপে ভাগ করুন
  • Window Function (RANK, ROW_NUMBER, SUM OVER) শিখুন
  • JOIN vs Subquery-এর trade-off বুঝুন
  • NULL handling সবসময় মাথায় রাখুন
  • EXPLAIN দিয়ে query performance যাচাই করুন
Share