অধ্যায় 1 · অ্যাডভান্সড কোয়েরি
উইন্ডো ফাংশন ১: PARTITION BY, ROW_NUMBER ও RANK
- পৃষ্ঠা 2 / 22
- 15 মিনিট পড়া
GROUP BY "প্রতি ডিপার্টমেন্টে গড় বেতন কত?" প্রশ্নের উত্তর দেয় প্রতিটা ডিপার্টমেন্টকে একটা সারিতে গুটিয়ে এনে। প্রায়ই আপনার দুটোই একসাথে দরকার: প্রত্যেক কর্মী, আর তাঁর পাশে তাঁর ডিপার্টমেন্টের গড়; প্রতিটা প্রোডাক্ট, আর নিজের ক্যাটাগরিতে তার র্যাংক। উইন্ডো ফাংশন (window function) ঠিক এটাই করে। এটা পরস্পর-সম্পর্কিত কিছু সারির (উইন্ডো) ওপর একটা মান হিসাব করে, আর কোনো সারি না গুটিয়ে ফলটা প্রতিটা সারির পাশে বসিয়ে দেয়।
ডেটার কাজে উইন্ডো সবচেয়ে দরকারি টুলগুলোর একটা: ফিচার বানানোর আগে প্রতি ব্যবহারকারীর শুধু সর্বশেষ রেকর্ড রাখা, এমবেড করার আগে স্ক্র্যাপ করা ডকুমেন্টের ডুপ্লিকেট সরানো, প্রতিটা সোর্স ডকুমেন্ট থেকে খুঁজে পাওয়া সেরা ৩টা চাঙ্ক নেওয়া, ইভ্যালুয়েশন লগ থেকে প্রতি এক্সপেরিমেন্টের সেরা রান বাছাই। এই পাতায় থাকছে র্যাংকিং; পরের পাতায় রানিং টোটাল, LAG আর ফ্রেম।
যা শিখবেন
- উইন্ডো
GROUP BYথেকে কীভাবে আলাদা, আরOVER(),PARTITION BYওORDER BYকীভাবে উইন্ডো ঠিক করে। ROW_NUMBER,RANK,DENSE_RANKআরNTILE, আর এরা টাই (সমান মান) কীভাবে সামলায়।- প্রতি গ্রুপে সেরা N, আর প্রতিটা এন্টিটির সর্বশেষ সারি।
ROW_NUMBERদিয়ে ডুপ্লিকেট সারি সরানো।
উইন্ডো বনাম GROUP BY
একই প্রশ্ন, দুইভাবে। GROUP BY দিলে প্রতি ডিপার্টমেন্টে একটা সারি:
SELECT department, ROUND(AVG(salary), 0) AS dept_avg
FROM employees
GROUP BY department
ORDER BY department;+-------------+----------+
| department | dept_avg |
+-------------+----------+
| Data | 121667.0 |
| Engineering | 140000.0 |
| Management | 250000.0 |
+-------------+----------+উইন্ডো দিলে প্রত্যেক কর্মী থেকে যান, আর প্রতিটা সারিতে ডিপার্টমেন্টের গড় যোগ হয়:
SELECT name,
department,
salary,
ROUND(AVG(salary) OVER (PARTITION BY department), 0) AS dept_avg,
salary - ROUND(AVG(salary) OVER (PARTITION BY department), 0) AS vs_avg,
ROUND(100.0 * salary / SUM(salary) OVER (), 1) AS pct_of_payroll
FROM employees
ORDER BY department, salary DESC, employee_id;+-----------------+-------------+--------+----------+----------+----------------+
| name | department | salary | dept_avg | vs_avg | pct_of_payroll |
+-----------------+-------------+--------+----------+----------+----------------+
| Lamia Karim | Data | 160000 | 121667.0 | 38333.0 | 15.5 |
| Priya Sen | Data | 110000 | 121667.0 | -11667.0 | 10.6 |
| Shuvo Roy | Data | 95000 | 121667.0 | -26667.0 | 9.2 |
| Hasan Mahmud | Engineering | 180000 | 140000.0 | 40000.0 | 17.4 |
| Nusrat Jahan | Engineering | 120000 | 140000.0 | -20000.0 | 11.6 |
| Zahid Hasan | Engineering | 120000 | 140000.0 | -20000.0 | 11.6 |
| Ayesha Siddiqua | Management | 250000 | 250000.0 | 0.0 | 24.2 |
+-----------------+-------------+--------+----------+----------+----------------+যা খেয়াল করবেন:
OVER (PARTITION BY department)মানে "একই ডিপার্টমেন্টের সারিগুলো নিয়ে হিসাব"। উইন্ডোকে ভাবতে পারেনGROUP BY-এর এমন একটা গ্রুপ হিসেবে, যা এক সারিতে গুটিয়ে যায় না।- ভেতরে কিছু না থাকা
OVER ()মানে "সব সারির ওপর":SUM(salary) OVER ()হলো পুরো বেতনের যোগফল, যা প্রতিটা সারিতে পাওয়া যায়, তাই মোটের শতাংশ বের করতে কোনো সাবকোয়েরি লাগে না। - সাধারণ অ্যাগ্রিগেট (
SUM,AVG,COUNT,MIN,MAX)OVERযোগ করলেই উইন্ডো ফাংশন হিসেবে কাজ করে।
উইন্ডো ফাংশনের গঠন
RANK() OVER ( PARTITION BY category_id ORDER BY price DESC )
| | |
| | +-- প্রতিটা উইন্ডোর ভেতরের ক্রম: কে প্রথম, কে দ্বিতীয় …
| +-- সারিগুলোকে আলাদা আলাদা উইন্ডোতে ভাগ (ঐচ্ছিক; না দিলে একটাই উইন্ডো)
+-- প্রতিটা সারির জন্য কী হিসাব হবেকোয়েরির লজিক্যাল ক্রমে উইন্ডো ফাংশন চলে বেশ পরে: FROM, WHERE, GROUP BY আর HAVING-এর পরে, SELECT-এর সাথে, আর ORDER BY ও LIMIT-এর আগে। এর দুটো ফল নিচে দেখবেন: GROUP BY-এর ফলাফলকে র্যাংক করা যায়, আর WHERE-এ উইন্ডো ফাংশন দিয়ে ফিল্টার করা যায় না।
কোথায় চলে: SQLite 3.25+, MySQL 8.0+, PostgreSQL 8.4+, SQL Server (র্যাংকিং ফাংশন ২০০৫ থেকে, বাকিগুলো ২০১২ থেকে)। MySQL 5.7-এ একটাও নেই, অথচ পুরোনো হোস্টিংয়ে সেটা এখনো চালু আছে।
ROW_NUMBER, RANK আর DENSE_RANK
তিনটাই উইন্ডোর সারিগুলোকে ORDER BY-এর ক্রমে নম্বর দেয়। পার্থক্য দেখা যায় শুধু দুটো সারির মান সমান (টাই) হলে। Nusrat আর Zahid দুজনেই ১,২০,০০০ পান:
SELECT name,
salary,
ROW_NUMBER() OVER (ORDER BY salary DESC, employee_id) AS row_num,
RANK() OVER (ORDER BY salary DESC) AS rnk,
DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rnk
FROM employees
ORDER BY salary DESC, employee_id;+-----------------+--------+---------+-----+-----------+
| name | salary | row_num | rnk | dense_rnk |
+-----------------+--------+---------+-----+-----------+
| Ayesha Siddiqua | 250000 | 1 | 1 | 1 |
| Hasan Mahmud | 180000 | 2 | 2 | 2 |
| Lamia Karim | 160000 | 3 | 3 | 3 |
| Nusrat Jahan | 120000 | 4 | 4 | 4 |
| Zahid Hasan | 120000 | 5 | 4 | 4 |
| Priya Sen | 110000 | 6 | 6 | 5 |
| Shuvo Roy | 95000 | 7 | 7 | 6 |
+-----------------+--------+---------+-----+-----------+| ফাংশন | টাই হলে | কীসের জন্য |
|---|---|---|
ROW_NUMBER() | তবুও আলাদা নম্বর (১, ২, ৩, ৪, ৫); কে আগে যাবে, তা টাই ভাঙার কলাম দিয়ে আপনাকেই ঠিক করতে হবে | প্রতি গ্রুপে ঠিক একটা সারি: সর্বশেষ রেকর্ড, ডুপ্লিকেট সরানো, পেজিনেশন |
RANK() | একই নম্বর, তারপর একটা ফাঁক (৪, ৪, ৬): "দুজন ৪র্থ স্থানে, ৫ম কেউ নেই" | প্রতিযোগিতার মতো র্যাংকিং, "টাইসহ সেরা ৩" |
DENSE_RANK() | একই নম্বর, কোনো ফাঁক নেই (৪, ৪, ৫) | "২য় সর্বোচ্চ আলাদা বেতন", দামের স্তর, টিয়ার |
ROW_NUMBER-এ employee_id দিয়ে টাই ভাঙাটা জরুরি। এটা না দিলে ডেটাবেস আজ Nusrat-কে ৪ দিতে পারে আর কাল Zahid-কে, তখন "১ নম্বর সারি"-র ওপর নির্ভর করা যেকোনো ফলাফল এক রান থেকে আরেক রানে বদলে যায়। ROW_NUMBER-এর ক্রম সবসময় একটা ইউনিক কলাম দিয়ে শেষ করুন।
PARTITION BY: গ্রুপের ভেতরে র্যাংকিং
PARTITION BY যোগ করলে প্রতিটা গ্রুপে নম্বর আবার শুরু হয়। নিজের ক্যাটাগরির ভেতরে দাম অনুযায়ী প্রোডাক্টের র্যাংক:
SELECT COALESCE(c.name, 'Uncategorised') AS category,
p.name,
p.price,
RANK() OVER (PARTITION BY p.category_id ORDER BY p.price DESC) AS price_rank
FROM products AS p
LEFT JOIN categories AS c ON c.category_id = p.category_id
ORDER BY category, price_rank, p.product_id;+---------------+-----------------------------+-------+------------+
| category | name | price | price_rank |
+---------------+-----------------------------+-------+------------+
| Accessories | USB-C Hub | 2200 | 1 |
| Accessories | Laptop Stand | 1500 | 2 |
| Books | Hands-On Machine Learning | 2500 | 1 |
| Books | Python Crash Course | 1200 | 2 |
| Courses | AI Engineering Bootcamp | 15000 | 1 |
| Courses | SQL Masterclass | 3000 | 2 |
| Electronics | 27-inch Monitor | 18500 | 1 |
| Electronics | Noise-Cancelling Headphones | 7500 | 2 |
| Electronics | Mechanical Keyboard | 4500 | 3 |
| Electronics | Wireless Mouse | 900 | 4 |
| Uncategorised | Gift Card | 1000 | 1 |
+---------------+-----------------------------+-------+------------+Gift Card-এর কোনো ক্যাটাগরি নেই, তাই এর category_id NULL। উইন্ডোর পার্টিশন সব NULL-কে একই গ্রুপে রাখে (GROUP BY-এর মতোই), তাই এটা নিজের আলাদা উইন্ডো আর র্যাংক ১ পায়।
প্রতি গ্রুপে সেরা N
"প্রতিটা ক্যাটাগরির সবচেয়ে বেশি বিক্রি হওয়া দুটো প্রোডাক্ট" — ইন্টারভিউয়ের ক্লাসিক প্রশ্ন, আবার রোজকার কাজও। প্রথমেই যেভাবে লিখতে ইচ্ছে করে (এখানে দাম দিয়ে দেখানো), সেটা চলে না:
SELECT name, category_id,
RANK() OVER (PARTITION BY category_id ORDER BY price DESC) AS rnk
FROM products
WHERE rnk <= 2;Error: misuse of aliased window function rnkকোনো উইন্ডো হিসাব হওয়ার আগেই WHERE চলে, তাই সেখানে উইন্ডো ফাংশন (বা তার অ্যালিয়াস) ব্যবহার করা যায় না; PostgreSQL আর MySQL-ও নিজেদের এরর মেসেজ দিয়ে এটা আটকে দেয়। র্যাংকটা একটা CTE (বা সাবকোয়েরি)-তে হিসাব করুন, তারপর বাইরে ফিল্টার করুন:
WITH product_revenue AS (
SELECT p.product_id, p.name, c.name AS category,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM order_items AS oi
JOIN orders AS o ON o.order_id = oi.order_id
JOIN products AS p ON p.product_id = oi.product_id
JOIN categories AS c ON c.category_id = p.category_id
WHERE o.status IN ('delivered', 'shipped')
GROUP BY p.product_id, p.name, c.name
),
ranked AS (
SELECT *,
RANK() OVER (PARTITION BY category ORDER BY revenue DESC) AS rnk
FROM product_revenue
)
SELECT category, rnk, name, revenue
FROM ranked
WHERE rnk <= 2
ORDER BY category, rnk, product_id;+-------------+-----+---------------------------+---------+
| category | rnk | name | revenue |
+-------------+-----+---------------------------+---------+
| Accessories | 1 | Laptop Stand | 4500 |
| Accessories | 2 | USB-C Hub | 4400 |
| Books | 1 | Hands-On Machine Learning | 5000 |
| Books | 2 | Python Crash Course | 3600 |
| Courses | 1 | AI Engineering Bootcamp | 15000 |
| Courses | 2 | SQL Masterclass | 6000 |
| Electronics | 1 | 27-inch Monitor | 18500 |
| Electronics | 2 | Mechanical Keyboard | 8550 |
+-------------+-----+---------------------------+---------+টাই হওয়া সবাইকে রাখতে চাইলে (২য় স্থানে টাই করা দুটো প্রোডাক্টই আসবে) RANK, আর প্রতি গ্রুপে ঠিক N-টা সারি চাইলে ROW_NUMBER। পুরো ফলাফলের একটাই top-N-এর জন্য PostgreSQL 13+-এ FETCH FIRST n ROWS WITH TIES আছে (SQL Server-এ TOP n WITH TIES), কিন্তু প্রতি গ্রুপে নয়, তাই এই CTE প্যাটার্নটাই সব ডেটাবেসে চলে।
প্রতিটা এন্টিটির সর্বশেষ সারি
একই প্যাটার্নে ROW_NUMBER আর নতুন থেকে পুরোনো ক্রমে তারিখ দিলে পাওয়া যায় প্রত্যেক গ্রাহকের সবচেয়ে সাম্প্রতিক অর্ডার। ইতিহাস হিসেবে রাখা যেকোনো কিছুর "বর্তমান" অবস্থা এভাবেই বের করা হয়: ব্যবহারকারীর সর্বশেষ প্ল্যান, মডেলের সর্বশেষ ইভ্যালুয়েশন, ডকুমেন্টের সর্বশেষ সংস্করণ।
WITH ordered AS (
SELECT o.*,
ROW_NUMBER() OVER (PARTITION BY customer_id
ORDER BY order_date DESC, order_id DESC) AS rn
FROM orders AS o
)
SELECT c.name, ordered.order_id, ordered.order_date, ordered.status
FROM ordered
JOIN customers AS c ON c.customer_id = ordered.customer_id
WHERE rn = 1
ORDER BY ordered.order_date DESC, ordered.order_id;+-----------------+----------+------------+-----------+
| name | order_id | order_date | status |
+-----------------+----------+------------+-----------+
| Farhana Akter | 14 | 2026-06-09 | delivered |
| Imran Hossain | 13 | 2026-06-02 | pending |
| Tanvir Ahmed | 12 | 2026-05-21 | shipped |
| Mitu Das | 11 | 2026-05-06 | delivered |
| Nadia Rahman | 10 | 2026-04-19 | delivered |
| Sadia Chowdhury | 7 | 2026-03-15 | delivered |
| Rafiq Islam | 5 | 2026-02-25 | cancelled |
+-----------------+----------+------------+-----------+সাতজন গ্রাহক, সাতটা সারি; Karim কখনো অর্ডার করেননি, orders-এ তাঁর কোনো সারি নেই, তাই তিনি নেই। সার্ভারেও কোয়েরিটা কোনো বদল ছাড়াই চলে:
-- PostgreSQL
WITH ordered AS (
SELECT o.*,
ROW_NUMBER() OVER (PARTITION BY customer_id
ORDER BY order_date DESC, order_id DESC) AS rn
FROM orders AS o
)
SELECT customer_id, order_id, order_date
FROM ordered
WHERE rn = 1 AND customer_id <= 3
ORDER BY customer_id;+-------------+----------+------------+
| customer_id | order_id | order_date |
+-------------+----------+------------+
| 1 | 10 | 2026-04-19 |
| 2 | 12 | 2026-05-21 |
| 3 | 14 | 2026-06-09 |
+-------------+----------+------------+NTILE: সমান আকারের ভাগ
NTILE(n) উইন্ডোর সাজানো সারিগুলোকে n-টা (প্রায়) সমান ভাগে (bucket) ভাগ করে আর প্রতিটা সারির ভাগের নম্বর দেয়। প্রোডাক্টগুলোকে দামের চারটা স্তরে:
SELECT name,
price,
NTILE(4) OVER (ORDER BY price DESC, product_id) AS price_tier
FROM products
ORDER BY price DESC, product_id;+-----------------------------+-------+------------+
| name | price | price_tier |
+-----------------------------+-------+------------+
| 27-inch Monitor | 18500 | 1 |
| AI Engineering Bootcamp | 15000 | 1 |
| Noise-Cancelling Headphones | 7500 | 1 |
| Mechanical Keyboard | 4500 | 2 |
| SQL Masterclass | 3000 | 2 |
| Hands-On Machine Learning | 2500 | 2 |
| USB-C Hub | 2200 | 3 |
| Laptop Stand | 1500 | 3 |
| Python Crash Course | 1200 | 3 |
| Gift Card | 1000 | 4 |
| Wireless Mouse | 900 | 4 |
+-----------------------------+-------+------------+১১টা সারি ৪ দিয়ে সমান ভাগ হয় না (১১ = ৪ × ২ + ৩), তাই প্রথম তিনটা ভাগ একটা করে সারি বেশি পায় (৩, ৩, ৩, ২); ভাগের সংখ্যার চেয়ে সারি কম হলে শেষের দিকের নম্বরগুলো কখনো আসেই না। NTILE কাটে সারির সংখ্যা ধরে, মান ধরে নয়: একই দামের দুটো প্রোডাক্ট আলাদা স্তরে পড়তে পারে, যদি ভাগের সীমা ঠিক তাদের মাঝখানে পড়ে। ML-এর কাজে এটা কোয়ান্টাইল বিনিং (quantile binning) — আয় বা দামের মতো একদিকে হেলে থাকা (skewed) একটা সংখ্যাকে কয়েকটা ক্রমিক ক্যাটাগরিতে বদলানোর উপায়; নির্দিষ্ট মানের সীমা চাইলে বরং CASE WHEN ব্যবহার করুন।
ROW_NUMBER দিয়ে ডুপ্লিকেট সরানো
ইমপোর্ট করা ডেটা ডুপ্লিকেটে ভরা: দুবার সাবমিট হওয়া ফর্ম, একই মানুষ ওয়েব আর অ্যাপ দুই জায়গা থেকে সাইন আপ করেছেন, একটা স্ক্র্যাপার একই পাতা দুবার দেখেছে। ট্রেনিং ডেটায় ডুপ্লিকেট থাকলে মডেল সেই উদাহরণগুলোকে বেশি গুরুত্ব দেয়, আর যে ডুপ্লিকেট ট্রেনিং ও টেস্ট দুই সেটেই পড়ে, তা মূল্যায়নকে আসলের চেয়ে ভালো দেখায়। এখানে দুই ধরনের ডুপ্লিকেটসহ সাইনআপের একটা ছোট ইমপোর্ট:
CREATE TABLE signups (
signup_id INTEGER PRIMARY KEY,
email TEXT NOT NULL,
source TEXT NOT NULL,
signed_up TEXT NOT NULL
);
INSERT INTO signups (signup_id, email, source, signed_up) VALUES
(1, 'nadia@example.com', 'web', '2026-03-01'),
(2, 'tanvir@example.com', 'web', '2026-03-02'),
(3, 'Nadia@Example.com', 'app', '2026-03-05'),
(4, 'rafiq@example.com', 'web', '2026-03-02'),
(5, 'rafiq@example.com', 'web', '2026-03-02'),
(6, 'mitu@example.com', 'app', '2026-03-07');
SELECT signup_id, email, signed_up,
ROW_NUMBER() OVER (PARTITION BY LOWER(email)
ORDER BY signed_up DESC, signup_id DESC) AS rn
FROM signups
ORDER BY LOWER(email), rn;+-----------+--------------------+------------+----+
| signup_id | email | signed_up | rn |
+-----------+--------------------+------------+----+
| 6 | mitu@example.com | 2026-03-07 | 1 |
| 3 | Nadia@Example.com | 2026-03-05 | 1 |
| 1 | nadia@example.com | 2026-03-01 | 2 |
| 5 | rafiq@example.com | 2026-03-02 | 1 |
| 4 | rafiq@example.com | 2026-03-02 | 2 |
| 2 | tanvir@example.com | 2026-03-02 | 1 |
+-----------+--------------------+------------+----+আগে ঠিক করুন দুটো সারি কখন "একই" (PARTITION BY LOWER(email), যাতে বড়-ছোট হাতের অক্ষরের পার্থক্য ডুপ্লিকেট লুকিয়ে না ফেলে) আর কোন কপিটা রাখবেন (ORDER BY: সর্বশেষটা, তারপর সবচেয়ে বড় id)। rn > 1 হওয়া প্রতিটা সারি ডুপ্লিকেট। আগে সেগুলো দেখুন, তারপর মুছুন:
DELETE FROM signups
WHERE signup_id IN (
SELECT signup_id
FROM (
SELECT signup_id,
ROW_NUMBER() OVER (PARTITION BY LOWER(email)
ORDER BY signed_up DESC, signup_id DESC) AS rn
FROM signups
) AS t
WHERE rn > 1
);
SELECT signup_id, email, source FROM signups ORDER BY signup_id;+-----------+--------------------+--------+
| signup_id | email | source |
+-----------+--------------------+--------+
| 2 | tanvir@example.com | web |
| 3 | Nadia@Example.com | app |
| 5 | rafiq@example.com | web |
| 6 | mitu@example.com | app |
+-----------+--------------------+--------+প্রায়ই আসলে কিছু মোছা হয় না: কাঁচা টেবিলটা অক্ষত রেখে CREATE TABLE … AS SELECT … WHERE rn = 1 দিয়ে একটা পরিষ্কার টেবিল বানানো হয়, যাতে পরিষ্কারের ধাপটা আবার চালানো আর পরে যাচাই করা যায়। ডেটা পাইপলাইনে সাধারণত এভাবেই করা হয়।
সাধারণ ভুল
WHERE-এ উইন্ডো ফাংশন দিয়ে ফিল্টার। এটা চলে না (ওপরে দেখেছেন), কারণ উইন্ডো হিসাব হয়WHERE-এর পরে। সমাধান: উইন্ডোটা CTE বা সাবকোয়েরিতে হিসাব করে বাইরে ফিল্টার করুন। (এ কাজের জন্য Snowflake, BigQuery আর DuckDB-তেQUALIFYনামে একটা ক্লজ আছে; SQLite, MySQL আর PostgreSQL-এ সেটা নেই।)- টাই ভাঙার কলাম ছাড়া
ROW_NUMBER।ORDER BY-তে টাই থাকলে কোন সারি ১ পাবে তা ডেটাবেসের ইচ্ছা, এক রান থেকে আরেক রানে বদলাতে পারে, তাই "সর্বশেষ অর্ডার" বা "রেখে দেওয়া ডুপ্লিকেট"-ও বদলায়। সমাধান: উইন্ডোরORDER BYপ্রাইমারি কি-র মতো একটা ইউনিক কলাম দিয়ে শেষ করুন। - আলাদা মান বোঝাতে
RANKব্যবহার। "৩য় সর্বোচ্চ বেতন"RANK() = 3দিয়ে এখানে ১,৬০,০০০ দেয়, কিন্তুRANK-এ ওপরে টাই থাকলে ৩ নম্বরটা পুরোপুরি বাদ পড়তে পারে। সমাধান: "N-তম আলাদা মান"-এর জন্যDENSE_RANK, "N-তম সারি"-র জন্যROW_NUMBER। PARTITION BYভুলে যাওয়া। এটা না দিলে প্রোডাক্টগুলো নিজের ক্যাটাগরির ভেতরে না হয়ে সব প্রোডাক্টের সাথে র্যাংক হয়, আর "প্রতি ক্যাটাগরিতে সেরা ২" হয়ে যায় "সবার মধ্যে সেরা ২"। সমাধান: গ্রুপের কলাম দিয়ে পার্টিশন করুন।- উইন্ডোর
ORDER BYফলাফল সাজাবে ভাবা। এটা শুধু উইন্ডোর হিসাবের ভেতরে সারি সাজায়। সমাধান: কোয়েরির শেষে আলাদা একটাORDER BYদিন।
নিজে চেষ্টা করুন
- সহজ: প্রত্যেক গ্রাহকের অর্ডারগুলোকে তারিখের ক্রমে নম্বর দিন (১ = প্রথম অর্ডার), গ্রাহকের id, অর্ডারের id, তারিখ আর নম্বর দেখিয়ে।
- মাঝারি:
DENSE_RANKদিয়ে প্রতিটা ক্যাটাগরির দ্বিতীয় সবচেয়ে দামি প্রোডাক্ট বের করুন। - কঠিন: বিক্রি হওয়া অর্ডার থেকে রেভিনিউ অনুযায়ী গ্রাহকদের
DENSE_RANKদিয়ে র্যাংক করুন, আর প্রত্যেক গ্রাহককে রাখুন: যাঁদের রেভিনিউ নেই, তাঁরা ০ পাবেন আর শেষ র্যাংক ভাগ করে নেবেন। CTE ছাড়া সরাসরিGROUP BY-এর ফলাফলকে র্যাংক করুন।
উত্তর
-- 1. প্রতি গ্রাহকের অর্ডারের ক্রম
SELECT customer_id, order_id, order_date,
ROW_NUMBER() OVER (PARTITION BY customer_id
ORDER BY order_date, order_id) AS order_number
FROM orders
ORDER BY customer_id, order_number;
-- 2. প্রতি ক্যাটাগরিতে দ্বিতীয় সবচেয়ে দামি
SELECT category_id, name, price
FROM (
SELECT category_id, name, price, product_id,
DENSE_RANK() OVER (PARTITION BY category_id ORDER BY price DESC) AS dr
FROM products
WHERE category_id IS NOT NULL
) AS t
WHERE dr = 2
ORDER BY category_id, product_id;
-- 3. অ্যাগ্রিগেটকে র্যাংক: উইন্ডো চলে GROUP BY-এর পরে
SELECT c.name,
COALESCE(SUM(oi.quantity * oi.unit_price), 0) AS revenue,
DENSE_RANK() OVER (ORDER BY COALESCE(SUM(oi.quantity * oi.unit_price), 0) DESC) AS revenue_rank
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
AND o.status IN ('delivered', 'shipped')
LEFT JOIN order_items AS oi ON oi.order_id = o.order_id
GROUP BY c.customer_id, c.name
ORDER BY revenue_rank, c.customer_id;সারসংক্ষেপ
- উইন্ডো ফাংশন সম্পর্কিত সারিগুলোর ওপর হিসাব করা একটা মান প্রতিটা সারিতে যোগ করে;
GROUP BYগুটিয়ে ফেলে, উইন্ডো ফেলে না। OVER ()মানে সব সারি,PARTITION BYআলাদা আলাদা উইন্ডোতে ভাগ করে,OVER-এর ভেতরেরORDER BYর্যাংকিংয়ের ক্রম ঠিক করে।ROW_NUMBERসবসময় আলাদা নম্বর দেয় (একটা ইউনিক টাই-ভাঙা কলাম দিন),RANKভাগ করে আর ফাঁক রাখে,DENSE_RANKভাগ করে ফাঁক ছাড়া,NTILE(n)n-টা ভাগ বানায়।- উইন্ডো হিসাব হয়
WHERE-এর পরে: CTE-তে র্যাংক করুন, বাইরে ফিল্টার করুন। এভাবেই পাওয়া যায় প্রতি গ্রুপে সেরা N, প্রতিটা এন্টিটির সর্বশেষ সারি আর ডুপ্লিকেট সরানো।
এরপর: উইন্ডো ফাংশন ২: LAG, LEAD, রানিং টোটাল আর ফ্রেম একই OVER (…) দিয়ে আগের সারি দেখা, রানিং টোটাল আর মুভিং অ্যাভারেজ বানানো, আর একটা উইন্ডোতে ঠিক কোন সারিগুলো থাকবে তা নিয়ন্ত্রণ করা শেখায়।