অধ্যায় 1 · অ্যাডভান্সড কোয়েরি
উইন্ডো ফাংশন ২: LAG, LEAD, রানিং টোটাল আর ফ্রেম
- পৃষ্ঠা 3 / 22
- 17 মিনিট পড়া
আগের পাতায় উইন্ডো ফাংশন সারিগুলোকে না গুটিয়েই নম্বর আর র্যাংক দিয়েছে। এই পাতায় সেই একই OVER (…) দিয়ে কয়েকটা সারি মিলিয়ে হিসাব করা হবে: আগের সারির মান, রানিং টোটাল, তিন মাসের মুভিং অ্যাভারেজ, মোটের মধ্যে প্রতিটা সারির ভাগ। প্রতিটা রেভিনিউ ড্যাশবোর্ডের পেছনে এই কোয়েরিগুলোই থাকে। আবার মডেলের জন্য সময়ভিত্তিক ফিচার — "এখন পর্যন্ত মোট খরচ", "শেষ অর্ডারের পর কত দিন", "শেষ তিন মাসের গড়" — ডেটাবেস থেকে না বেরিয়েই এভাবে বানানো যায়।
এসব হিসাবকে নিখুঁতভাবে বোঝার নতুন ধারণাটার নাম ফ্রেম (frame): বর্তমান সারির জন্য পার্টিশনের কোন কোন সারি একটা ফাংশন দেখতে পাবে। উইন্ডো ফাংশনের বেশিরভাগ অপ্রত্যাশিত ফলের পেছনে থাকে ফ্রেম, তাই এই পাতায় ফ্রেম মন দিয়ে দেখা হবে।
যা শিখবেন
LAGআরLEADদিয়ে পেছনের আর সামনের সারি দেখা (অফসেট আর ডিফল্টসহ)- অ্যাগ্রিগেট উইন্ডো ফাংশন দিয়ে রানিং টোটাল, মুভিং অ্যাভারেজ আর মোটের শতাংশ
- ফ্রেম:
ROWS,RANGEআরGROUPS-এর পার্থক্য, ডিফল্ট ফ্রেম, আরLAST_VALUE-এর ফাঁদ WINDOWক্লজে উইন্ডোর একবার নাম দিয়ে বারবার ব্যবহার করা- অর্ডারের ইতিহাস থেকে অ্যানালিটিক্স আর ML-এর জন্য গ্রাহকভিত্তিক ফিচার বানানো
কাজের জন্য একটা মাসিক রেভিনিউ টেবিল
বেশিরভাগ উদাহরণ মাসিক রেভিনিউ নিয়ে: বাতিল হয়নি এমন প্রতিটা অর্ডারের পরিমাণ × একক দাম। একটা CTE (দেখুন CTE আর রিকার্সিভ কোয়েরি) কোয়েরিটা পড়তে সহজ রাখে:
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 FROM monthly ORDER BY month;+---------+---------+
| month | revenue |
+---------+---------+
| 2026-01 | 6600 |
| 2026-02 | 7000 |
| 2026-03 | 21400 |
| 2026-04 | 23450 |
| 2026-05 | 8500 |
| 2026-06 | 18100 |
+---------+---------+৬টা সারি, প্রতি মাসে একটা। "বাতিল নয়" শর্তে pending অর্ডার ১৩-ও ধরা হয়েছে, তাই এখানে জুনের রেভিনিউ ১৮,১০০, CTE আর রিকার্সিভ কোয়েরি পাতার delivered-বা-shipped রিপোর্টের মতো ৩,১০০ নয়। GROUP BY CTE-র ভেতরেই হয়ে গেছে; নিচের প্রতিটা উইন্ডো ফাংশন এই ৬টা সারির ওপর কাজ করবে, সেগুলো না গুটিয়ে।
LAG আর LEAD: আগের সারি আর পরের সারি
LAG(expr) উইন্ডোর আগের সারি থেকে expr-এর মান দেয়; LEAD(expr) দেয় পরের সারি থেকে। ক্রমটা আসে OVER-এর ভেতরের ORDER BY থেকে। এটা সবসময় দিন: ডেটাবেস ORDER BY ছাড়াও LAG চালায়, কিন্তু তখন "আগের সারি" মানে ইঞ্জিন যে সারিটা আগে পড়েছে সেটা, যার কোনো অর্থ নেই।
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,
LAG(revenue) OVER (ORDER BY month) AS prev_month,
revenue - LAG(revenue) OVER (ORDER BY month) AS change,
LEAD(revenue) OVER (ORDER BY month) AS next_month,
LAG(revenue, 2, 0) OVER (ORDER BY month) AS two_back
FROM monthly
ORDER BY month;+---------+---------+------------+--------+------------+----------+
| month | revenue | prev_month | change | next_month | two_back |
+---------+---------+------------+--------+------------+----------+
| 2026-01 | 6600 | NULL | NULL | 7000 | 0 |
| 2026-02 | 7000 | 6600 | 400 | 21400 | 0 |
| 2026-03 | 21400 | 7000 | 14400 | 23450 | 6600 |
| 2026-04 | 23450 | 21400 | 2050 | 8500 | 7000 |
| 2026-05 | 8500 | 23450 | -14950 | 18100 | 21400 |
| 2026-06 | 18100 | 8500 | 9600 | NULL | 23450 |
+---------+---------+------------+--------+------------+----------+- প্রথম সারির আগে কোনো সারি নেই, তাই
LAGদেয়NULL— আর তার ওপর করা হিসাবওNULL। শেষ সারির পরে কোনো সারি নেই। LAG(revenue, 2, 0)দুই সারি পেছনে তাকায়, আর তেমন সারি না থাকলেNULL-এর বদলে0দেয়। ডিফল্ট দিন শুধু তখনই, যখন ০ সত্যিই সঠিক উত্তর; "আগের মাস নেই"-এর জন্য সাধারণতNULL-ই ঠিক উত্তর।LAGগোনে সারি, মাস নয়। কোনো মাসে অর্ডার না থাকলে সেই মাসটা স্রেফ থাকবে না, আরLAGতুলনা করবে দুই মাস আগের সাথে। ফাঁক থাকতে পারলে রিকার্সিভ CTE দিয়ে একটা ক্যালেন্ডার বানিয়ে (দেখুন CTE আর রিকার্সিভ কোয়েরি) তার সাথেLEFT JOINকরুন।
PARTITION BY দিলে আগের সারি খোঁজা হয় প্রতিটা গ্রাহকের নিজের ইতিহাসের ভেতরে। নিচে একজন গ্রাহকের পরপর দুই অর্ডারের মাঝের সময় — চার্ন (churn) মডেলের একটা ক্লাসিক ফিচার:
SELECT customer_id,
order_id,
order_date,
LAG(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) AS prev_order,
CAST(julianday(order_date)
- julianday(LAG(order_date) OVER (PARTITION BY customer_id ORDER BY order_date))
AS INTEGER) AS days_since_prev
FROM orders
WHERE status <> 'cancelled' AND customer_id <= 3
ORDER BY customer_id, order_date;+-------------+----------+------------+------------+-----------------+
| customer_id | order_id | order_date | prev_order | days_since_prev |
+-------------+----------+------------+------------+-----------------+
| 1 | 1 | 2026-01-12 | NULL | NULL |
| 1 | 3 | 2026-02-03 | 2026-01-12 | 22 |
| 1 | 10 | 2026-04-19 | 2026-02-03 | 75 |
| 2 | 2 | 2026-01-20 | NULL | NULL |
| 2 | 6 | 2026-03-08 | 2026-01-20 | 47 |
| 2 | 12 | 2026-05-21 | 2026-03-08 | 74 |
| 3 | 4 | 2026-02-14 | NULL | NULL |
| 3 | 8 | 2026-03-28 | 2026-02-14 | 42 |
| 3 | 14 | 2026-06-09 | 2026-03-28 | 73 |
+-------------+----------+------------+------------+-----------------+উইন্ডো ফাংশন হিসেবে অ্যাগ্রিগেট: রানিং টোটাল
যেকোনো অ্যাগ্রিগেট — SUM, AVG, COUNT, MIN, MAX — OVER যোগ করলেই উইন্ডো ফাংশন হয়ে যায়। ভেতরে ORDER BY দিলে সেটা জমতে থাকে:
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,
SUM(revenue) OVER () AS grand_total,
ROUND(100.0 * revenue / SUM(revenue) OVER (), 1) AS pct_of_total
FROM monthly
ORDER BY month;+---------+---------+---------------+-------------+--------------+
| month | revenue | running_total | grand_total | pct_of_total |
+---------+---------+---------------+-------------+--------------+
| 2026-01 | 6600 | 6600 | 85050 | 7.8 |
| 2026-02 | 7000 | 13600 | 85050 | 8.2 |
| 2026-03 | 21400 | 35000 | 85050 | 25.2 |
| 2026-04 | 23450 | 58450 | 85050 | 27.6 |
| 2026-05 | 8500 | 66950 | 85050 | 10.0 |
| 2026-06 | 18100 | 85050 | 85050 | 21.3 |
+---------+---------+---------------+-------------+--------------+OVER (ORDER BY month)প্রথম সারি থেকে বর্তমান সারি পর্যন্ত যোগ করে: এটাই রানিং টোটাল।OVER ()— ফাঁকা উইন্ডো — মানে পুরো ফলাফল, তাই প্রতিটা সারি গ্র্যান্ড টোটাল দেখে। তা দিয়ে ভাগ করলে এক ধাপেই মোটের শতাংশ পাওয়া যায়, কোনো সাবকোয়েরি ছাড়া।100.0 *লিখলে ভাগফল দশমিকে আসে। দুটো পূর্ণসংখ্যায়revenue / SUM(…)করলে SQLite আর PostgreSQL-এ পূর্ণসংখ্যার ভাগ হতো, উত্তর আসত ০।
"একটা গ্রুপের ভেতরে ভাগ" চাইলে PARTITION BY যোগ করুন। প্রতিটা প্রোডাক্টের নিজের ক্যাটাগরির রেভিনিউতে কত ভাগ:
SELECT c.name AS category,
p.name AS product,
SUM(oi.quantity * oi.unit_price) AS revenue,
ROUND(100.0 * SUM(oi.quantity * oi.unit_price)
/ SUM(SUM(oi.quantity * oi.unit_price)) OVER (PARTITION BY c.name), 1) AS pct_of_category
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 <> 'cancelled' AND c.name IN ('Books', 'Electronics')
GROUP BY c.name, p.name
ORDER BY category, revenue DESC;+-------------+---------------------------+---------+-----------------+
| category | product | revenue | pct_of_category |
+-------------+---------------------------+---------+-----------------+
| Books | Hands-On Machine Learning | 5000 | 58.1 |
| Books | Python Crash Course | 3600 | 41.9 |
| Electronics | 27-inch Monitor | 18500 | 58.6 |
| Electronics | Mechanical Keyboard | 8550 | 27.1 |
| Electronics | Wireless Mouse | 4500 | 14.3 |
+-------------+---------------------------+---------+-----------------+SUM(SUM(…)) OVER (…) খেয়াল করুন। উইন্ডো ফাংশন চলে GROUP BY-এর পরে, তাই ভেতরের SUM হলো প্রতিটা প্রোডাক্টের গ্রুপ-টোটাল, আর বাইরেরটা পার্টিশন জুড়ে সেই টোটালগুলো যোগ করে। প্রথমবার দেখতে অদ্ভুত লাগে, কিন্তু এটা সঠিক আর বেশ প্রচলিত।
ফ্রেম: ফাংশন কোন সারিগুলো দেখে
একটা উইন্ডোর তিনটা অংশ: PARTITION BY (কোন সারিগুলো একসাথে), ORDER BY (তাদের ক্রম) আর ফ্রেম (বর্তমান সারির জন্য তাদের মধ্যে কোনগুলো গোনা হবে)। ফ্রেম লেখা হয় ORDER BY-এর পরে:
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW -- এই সারি আর তার আগের দুটো
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW -- পার্টিশনের শুরু থেকে এখান পর্যন্ত
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING -- পুরো পার্টিশন
RANGE BETWEEN 30 PRECEDING AND CURRENT ROW -- ORDER BY-এর মান এই সারির 30-এর মধ্যে
GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW -- এই পিয়ার গ্রুপ আর তার আগেরটাROWS দিয়ে মুভিং অ্যাভারেজ
তিন মাসের মুভিং অ্যাভারেজ ওঠানামা-ভরা একটা সিরিজকে মসৃণ করে — মডেল ট্রেনিংয়ের সময় লস-কার্ভ মসৃণ করে দেখার মতোই ধারণা:
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,
ROUND(AVG(revenue) OVER (ORDER BY month
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 0) AS avg_3m,
COUNT(*) OVER (ORDER BY month
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS months_in_avg
FROM monthly
ORDER BY month;+---------+---------+---------+---------------+
| month | revenue | avg_3m | months_in_avg |
+---------+---------+---------+---------------+
| 2026-01 | 6600 | 6600.0 | 1 |
| 2026-02 | 7000 | 6800.0 | 2 |
| 2026-03 | 21400 | 11667.0 | 3 |
| 2026-04 | 23450 | 17283.0 | 3 |
| 2026-05 | 8500 | 17783.0 | 3 |
| 2026-06 | 18100 | 16683.0 | 3 |
+---------+---------+---------+---------------+প্রথম দুই সারির গড় তিন মাসের কম দিয়ে হয়েছে (months_in_avg দেখুন)। এগুলো রিপোর্টে রাখুন, বাদ দিন, বা হালকা রঙে দেখান — কিন্তু ব্যাপারটা জেনে রাখুন।
ডিফল্ট ফ্রেম, আর ROWS ও RANGE কেন আলাদা
উইন্ডোতে ORDER BY লিখে ফ্রেম না লিখলে ডিফল্ট হয় RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW; ORDER BY একেবারে না থাকলে ফ্রেম হলো পুরো পার্টিশন। RANGE ORDER BY-এর একই মানের সারিগুলোকে (পিয়ার, peers) এক ধাপ ধরে, তাই টাই হলে সবগুলো একসাথে যোগ হয়। Nusrat আর Zahid দুজনেরই বেতন ১,২০,০০০:
SELECT name,
salary,
SUM(salary) OVER (ORDER BY salary) AS default_range,
SUM(salary) OVER (ORDER BY salary
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS by_rows,
SUM(salary) OVER (ORDER BY salary
GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW) AS this_and_prev_group
FROM employees
ORDER BY salary, name;+-----------------+--------+---------------+---------+---------------------+
| name | salary | default_range | by_rows | this_and_prev_group |
+-----------------+--------+---------------+---------+---------------------+
| Shuvo Roy | 95000 | 95000 | 95000 | 95000 |
| Priya Sen | 110000 | 205000 | 205000 | 205000 |
| Nusrat Jahan | 120000 | 445000 | 325000 | 350000 |
| Zahid Hasan | 120000 | 445000 | 445000 | 350000 |
| Lamia Karim | 160000 | 605000 | 605000 | 400000 |
| Hasan Mahmud | 180000 | 785000 | 785000 | 340000 |
| Ayesha Siddiqua | 250000 | 1035000 | 1035000 | 430000 |
+-----------------+--------+---------------+---------+---------------------+| ফ্রেমের একক | কী গোনে | টাই (পিয়ার) | কোথায় চলে |
|---|---|---|---|
ROWS | আসল সারি | আলাদা হয়ে যায়, টাই-ব্রেকার না দিলে ক্রম অনিশ্চিত | SQLite, MySQL 8, PostgreSQL |
RANGE | ORDER BY-এর মানের দূরত্ব | সবসময় একসাথে | তিনটাতেই; মানের অফসেটের (30 PRECEDING) জন্য একটাই সংখ্যা বা তারিখের ORDER BY লাগে, আর SQLite 3.28+ / PostgreSQL 11+ |
GROUPS | পিয়ারদের গ্রুপ | সবসময় একসাথে | SQLite 3.28+, PostgreSQL 11+; MySQL-এ নেই |
by_rows-এ Nusrat-এর সারিতে ১,২০,০০০ বেতনের দুজনের মধ্যে শুধু তিনি নিজে আছেন, আর দুজনের কে আগে আসবে তার কোনো নিশ্চয়তা নেই। প্রতিবার একই রানিং টোটাল চাইলে হয় RANGE রাখুন, নয়তো ক্রমটা ইউনিক করুন (ORDER BY salary, employee_id) আর ROWS ব্যবহার করুন।
মানের অফসেটসহ RANGE: "শেষ ৩০ দিন"
ফ্রেম যখন মানের একটা পরিসর, যেমন সময়, তখন RANGE-ই সবচেয়ে কাজের। প্রতিটা অর্ডারের জন্য গুনুন, তার তারিখসহ আগের ৩০ দিনে কয়টা অর্ডার হয়েছে, প্রতিটা ডায়ালেক্টে:
SELECT order_id,
order_date,
COUNT(*) OVER (ORDER BY julianday(order_date)
RANGE BETWEEN 30 PRECEDING AND CURRENT ROW) AS orders_last_30d
FROM orders
WHERE order_date < '2026-04-01'
ORDER BY order_date;+----------+------------+-----------------+
| order_id | order_date | orders_last_30d |
+----------+------------+-----------------+
| 1 | 2026-01-12 | 1 |
| 2 | 2026-01-20 | 2 |
| 3 | 2026-02-03 | 3 |
| 4 | 2026-02-14 | 3 |
| 5 | 2026-02-25 | 3 |
| 6 | 2026-03-08 | 3 |
| 7 | 2026-03-15 | 4 |
| 8 | 2026-03-28 | 3 |
+----------+------------+-----------------+-- PostgreSQL: তারিখ দিয়ে সরাসরি সাজানো যায়; অফসেটটা একটা INTERVAL
SELECT order_id,
order_date,
COUNT(*) OVER (ORDER BY order_date
RANGE BETWEEN INTERVAL '30 days' PRECEDING AND CURRENT ROW) AS orders_last_30d
FROM orders
WHERE order_date < '2026-04-01'
ORDER BY order_date;+----------+------------+-----------------+
| order_id | order_date | orders_last_30d |
+----------+------------+-----------------+
| 1 | 2026-01-12 | 1 |
| 2 | 2026-01-20 | 2 |
| 3 | 2026-02-03 | 3 |
| 4 | 2026-02-14 | 3 |
| 5 | 2026-02-25 | 3 |
| 6 | 2026-03-08 | 3 |
| 7 | 2026-03-15 | 4 |
| 8 | 2026-03-28 | 3 |
+----------+------------+-----------------+-- MySQL: একই ধারণা, MySQL-এর INTERVAL লেখার ধরনে
SELECT order_id,
order_date,
COUNT(*) OVER (ORDER BY order_date
RANGE BETWEEN INTERVAL 30 DAY PRECEDING AND CURRENT ROW) AS orders_last_30d
FROM orders
WHERE order_date < '2026-04-01'
ORDER BY order_date;তিনটাই একই ৮টা সারি দেয়। SQLite তারিখ রাখে টেক্সট হিসেবে, তাই SQLite-এর কোয়েরি সাজায় julianday(order_date) দিয়ে, যা দিনের হিসাবে একটা সংখ্যা। ROWS BETWEEN 2 PRECEDING-এর সাথে পার্থক্য হলো, কোনো দিনে অর্ডার না থাকলে বা কোনো দিনে কয়েকটা থাকলেও এই ফ্রেম ঠিক থাকে।
FIRST_VALUE, LAST_VALUE আর ডিফল্ট ফ্রেমের ফাঁদ
FIRST_VALUE(x) আর LAST_VALUE(x) ফ্রেমের প্রথম আর শেষ সারি থেকে x দেয় (NTH_VALUE(x, n) দেয় n-তম সারি থেকে)। ডিফল্ট ফ্রেম যেহেতু বর্তমান সারিতেই থামে, LAST_VALUE কোনো এরর ছাড়াই বর্তমান সারিটাই ফেরত দেয়:
SELECT department,
name,
salary,
FIRST_VALUE(name) OVER (PARTITION BY department ORDER BY salary DESC, name) AS top_earner,
LAST_VALUE(name) OVER (PARTITION BY department ORDER BY salary DESC, name) AS wrong_lowest,
LAST_VALUE(name) OVER (PARTITION BY department ORDER BY salary DESC, name
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS lowest
FROM employees
WHERE department IN ('Data', 'Engineering')
ORDER BY department, salary DESC, name;+-------------+--------------+--------+--------------+--------------+-------------+
| department | name | salary | top_earner | wrong_lowest | lowest |
+-------------+--------------+--------+--------------+--------------+-------------+
| Data | Lamia Karim | 160000 | Lamia Karim | Lamia Karim | Shuvo Roy |
| Data | Priya Sen | 110000 | Lamia Karim | Priya Sen | Shuvo Roy |
| Data | Shuvo Roy | 95000 | Lamia Karim | Shuvo Roy | Shuvo Roy |
| Engineering | Hasan Mahmud | 180000 | Hasan Mahmud | Hasan Mahmud | Zahid Hasan |
| Engineering | Nusrat Jahan | 120000 | Hasan Mahmud | Nusrat Jahan | Zahid Hasan |
| Engineering | Zahid Hasan | 120000 | Hasan Mahmud | Zahid Hasan | Zahid Hasan |
+-------------+--------------+--------+--------------+--------------+-------------+wrong_lowest আসলে প্রতিটা সারির নিজের নাম: ফ্রেম বর্তমান সারিতে শেষ হয়েছে, তাই বর্তমান সারিটাই ছিল শেষ সারি। (name টাই-ব্রেকার না থাকলে ফ্রেম শেষ হতো শেষ পিয়ারে, ফলে Nusrat-এর সারিতে Zahid দেখাতে পারত — আরও বিভ্রান্তিকর।) LAST_VALUE বা NTH_VALUE ব্যবহার করলেই পুরো ফ্রেম লিখে দিন — অথবা উল্টো ক্রমে FIRST_VALUE নিন, যাতে কোনো ফ্রেমই লাগে না।
উইন্ডোর নাম একবার: WINDOW ক্লজ
কয়েকটা ফাংশন একই উইন্ডো ব্যবহার করলে HAVING-এর পরে (ORDER BY-এর আগে) উইন্ডোটার একটা নাম দিন আর সেই নাম দিয়ে ব্যবহার করুন। উইন্ডো ফাংশন যেখানে চলে সেখানেই এটা চলে: SQLite 3.25+, MySQL 8 আর PostgreSQL:
SELECT o.customer_id,
o.order_id,
o.order_date,
SUM(oi.quantity * oi.unit_price) AS order_value,
ROW_NUMBER() OVER w AS order_no,
SUM(SUM(oi.quantity * oi.unit_price)) OVER w AS spend_to_date,
ROUND(AVG(SUM(oi.quantity * oi.unit_price)) OVER w, 0) AS avg_order_to_date
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id
WHERE o.status <> 'cancelled' AND o.customer_id IN (1, 2)
GROUP BY o.customer_id, o.order_id, o.order_date
WINDOW w AS (PARTITION BY o.customer_id ORDER BY o.order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
ORDER BY o.customer_id, o.order_date;+-------------+----------+------------+-------------+----------+---------------+-------------------+
| customer_id | order_id | order_date | order_value | order_no | spend_to_date | avg_order_to_date |
+-------------+----------+------------+-------------+----------+---------------+-------------------+
| 1 | 1 | 2026-01-12 | 2100 | 1 | 2100 | 2100.0 |
| 1 | 3 | 2026-02-03 | 3000 | 2 | 5100 | 2550.0 |
| 1 | 10 | 2026-04-19 | 18500 | 3 | 23600 | 7867.0 |
| 2 | 2 | 2026-01-20 | 4500 | 1 | 4500 | 4500.0 |
| 2 | 6 | 2026-03-08 | 15000 | 2 | 19500 | 9750.0 |
| 2 | 12 | 2026-05-21 | 3000 | 3 | 22500 | 7500.0 |
+-------------+----------+------------+-------------+----------+---------------+-------------------+"এই গ্রাহক কি আবার অর্ডার করবে?" — এমন মডেলের ফিচার টেবিল ঠিক এই আকারেরই হয়: প্রতিটা সারি শুধু অতীত আর বর্তমান জানে, ভবিষ্যৎ কখনো নয়। CURRENT ROW-এ শেষ হওয়া ফ্রেম আপনা থেকেই টার্গেট লিকেজ (target leakage) ঠেকায়; বিষয়টা বিস্তারিত আছে SQL ও pandas দিয়ে ML-এর উপযোগী ডেটা তৈরি পাতায়।
বাস্তবে কোথায় দেখবেন
- ড্যাশবোর্ড: মাস-থেকে-মাস পরিবর্তন (
LAG), বছরের শুরু থেকে এখন পর্যন্ত (রানিংSUM), রেভিনিউর ভাগ (SUM() OVER ())। - টাইম-সিরিজ আর ফোরকাস্টিং: ল্যাগ ফিচার (
LAG(sales, 7)মানে "গত সপ্তাহের একই দিন") আর রোলিং গড় ফোরকাস্টিং মডেলের সাধারণ ইনপুট। - প্রোডাক্ট আর LLM অ্যানালিটিক্স: একজন ইউজারের দুই সেশনের মাঝের সময়, প্রতিটা API key-তে এই মাসে এখন পর্যন্ত কত টোকেন খরচ হলো, রিগ্রেশন ধরতে ইভ্যালুয়েশন স্কোরের ৭ দিনের রোলিং গড়।
সাধারণ ভুল
- উইন্ডোর ফলাফল দিয়ে
WHERE-এ ফিল্টার।WHERE change < 0চলবে না: উইন্ডো হিসাব হয়WHERE-এর পরে। কোয়েরিটা একটা CTE-তে মুড়ে বাইরে ফিল্টার করুন:WITH t AS (SELECT …, revenue - LAG(revenue) OVER (ORDER BY month) AS change FROM monthly) SELECT * FROM t WHERE change < 0; OVER-এORDER BYভুলে যাওয়া।SUM(revenue) OVER ()প্রতিটা সারিতে গ্র্যান্ড টোটাল দেয়, রানিং টোটাল নয়। লিখুনOVER (ORDER BY month)।- ডিফল্ট ফ্রেমে
LAST_VALUE। এটা বর্তমান সারিই ফেরত দেয়।ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWINGযোগ করুন। - কোনো সময়কাল বাদ পড়া ডেটায় সারি গুনে মুভিং অ্যাভারেজ।
ROWS BETWEEN 2 PRECEDINGমানে "দুটো সারি", "দুই মাস" নয়। আগে ক্যালেন্ডার পূরণ করুন, অথবা তারিখের ওপরRANGEনিন। - শতাংশে পূর্ণসংখ্যার ভাগ।
100 * revenue / totalদশমিক কেটে ফেলতে পারে; লিখুন100.0 * revenue / total, তারপরROUNDকরুন।
নিজে চেষ্টা করুন
- সহজ: গ্রাহক ১ আর ৩-এর বাতিল-না-হওয়া প্রতিটা অর্ডারের পাশে সেই গ্রাহকের পরের অর্ডারের তারিখ দেখান (শেষটার জন্য
NULL)। - মাঝারি: পেমেন্টগুলো
paid_onক্রমে দেখান (টাই হলেpayment_id), সাথেamount-এর রানিং টোটাল আর পাওয়া মোট টাকার মধ্যে প্রতিটা পেমেন্টের শতাংশ, ১ দশমিক ঘর পর্যন্ত। - কঠিন: প্রতিটা ডিপার্টমেন্টের প্রত্যেক কর্মীর বেতন, ডিপার্টমেন্টের সর্বোচ্চ বেতন আর দুটোর পার্থক্য দেখান, সাথে প্রতিটা সারির পাশে ডিপার্টমেন্টের গড় বেতন। একটাই নাম দেওয়া উইন্ডো ব্যবহার করুন।
উত্তর
-- সহজ
SELECT customer_id, order_id, order_date,
LEAD(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) AS next_order
FROM orders
WHERE status <> 'cancelled' AND customer_id IN (1, 3)
ORDER BY customer_id, order_date;-- মাঝারি
SELECT payment_id, paid_on, amount,
SUM(amount) OVER (ORDER BY paid_on, payment_id) AS running_total,
ROUND(100.0 * amount / SUM(amount) OVER (), 1) AS pct_of_all
FROM payments
ORDER BY paid_on, payment_id;-- কঠিন
SELECT department, name, salary,
MAX(salary) OVER d AS dept_max,
MAX(salary) OVER d - salary AS gap_to_top,
ROUND(AVG(salary) OVER d, 0) AS dept_avg
FROM employees
WINDOW d AS (PARTITION BY department)
ORDER BY department, salary DESC, name;কঠিন উত্তরের উইন্ডোতে কোনো ORDER BY নেই, তাই তার ফ্রেম পুরো পার্টিশন, আর MAX ও AVG ডিপার্টমেন্টের সব কর্মীকে দেখে।
সারসংক্ষেপ
LAG/LEADঅবস্থান ধরে অন্য সারি পড়ে; এদেরORDER BYদিন, আর মনে রাখুন এরা সারি গোনে, সময় নয়।OVER-সহ অ্যাগ্রিগেট দেয় রানিং টোটাল (ORDER BY), গ্র্যান্ড টোটাল (OVER ()) আর মোটের শতাংশ — কোনো সাবকোয়েরি ছাড়াই।- ফাংশন কী দেখবে তা ঠিক করে ফ্রেম।
ORDER BYথাকলে ডিফল্ট হলোRANGE … CURRENT ROW, যা পিয়ারদেরও ধরে আরLAST_VALUE-কে ভুল উত্তর দেওয়ায়। ROWSসারি গোনে,RANGEমান মাপে ("শেষ ৩০ দিন"-এর জন্য দারুণ),GROUPSপিয়ার গ্রুপ গোনে (MySQL-এ নেই)।- কয়েকবার লাগে এমন উইন্ডোর নাম দিন
WINDOW w AS (…)দিয়ে; শুধু অতীতের দিকে তাকানো ফ্রেম দিয়ে লিকেজমুক্ত ফিচার বানানো যায়।
এরপর: অ্যানালিটিক্স প্যাটার্ন: গ্রোথ, কোহর্ট, রিটেনশন আর গ্যাপ পাতায় CTE আর এই উইন্ডো ফাংশনগুলো মিলিয়ে ক্লাসিক বিশ্লেষণের কোয়েরি লেখা হবে: দ্বিতীয় সর্বোচ্চ মান, মাস-থেকে-মাস গ্রোথ, কোহর্ট রিটেনশন টেবিল আর গ্যাপস-অ্যান্ড-আইল্যান্ডস দিয়ে টানা ধারা খোঁজা।