অধ্যায় 1 · অ্যাডভান্সড কোয়েরি
CTE আর রিকার্সিভ কোয়েরি
- পৃষ্ঠা 1 / 22
- 16 মিনিট পড়া
বিগিনার টিউটোরিয়ালের শেষে ক্যাটাগরি রিপোর্টটা কাজ করেছিল ঠিকই, কিন্তু পড়া কঠিন ছিল: একটা LEFT JOIN-এর ভেতরে সাবকোয়েরি, ROUND-এর ভেতরে আরেকটা, আর "বিক্রি মানে delivered বা shipped অর্ডার" নিয়মটা দুবার লেখা। কমন টেবিল এক্সপ্রেশন (common table expression, সংক্ষেপে CTE) এই সমস্যার সমাধান। WITH name AS (…) দিয়ে আপনি কোয়েরির প্রতিটা ধাপকে একটা নাম দেন, ছোট একটা প্রোগ্রামের মতো ধাপগুলো ওপর থেকে নিচে লেখেন, আর প্রতিটা নাম এমনভাবে ব্যবহার করেন যেন সেটা একটা টেবিল।
আসল ডেটার কাজে এমন ধাপে ধাপে লেখা কোয়েরিই বেশি: কাঁচা ইভেন্ট থেকে ML ফিচার বানানো, ট্রেনিংয়ের আগে টেবিল পরিষ্কার করা, মূল্যায়ন রিপোর্টের সংখ্যা তৈরি করা। dbt-র মতো টুল মূলত CTE-ভিত্তিক SELECT দিয়ে ভরা কয়েকটা ফোল্ডার। CTE-র আরেকটা ক্ষমতাও আছে: রিকার্সিভ (recursive) CTE নতুন সারি তৈরি করতে পারে (ক্যালেন্ডার, সংখ্যার ক্রম), আর অর্গ চার্ট, ক্যাটাগরির ট্রি বা চ্যাট মেসেজের থ্রেডের মতো হায়ারার্কির (hierarchy) একেবারে নিচ পর্যন্ত নামতে পারে।
যা শিখবেন
WITHদিয়ে CTE লেখা, কয়েকটা CTE পরপর জোড়া, আর একটা CTE দুবার ব্যবহার করা।- কখন CTE, কখন সাবকোয়েরি, টেম্পোরারি টেবিল বা ভিউ।
- রিকার্সিভ CTE কীভাবে কাজ করে: অ্যাঙ্কর, রিকার্সিভ ধাপ, থামার শর্ত।
- SQLite, MySQL আর PostgreSQL-এ সংখ্যার ক্রম আর ক্যালেন্ডার তৈরি করা, আর অর্গ চার্টের সব স্তর বের করা।
WITH: একটা ধাপকে নাম দিন
ব্যবসার নিয়মটা একবার লিখুন, sold নামের একটা CTE হিসেবে, তারপর সেটাকে কোয়েরি করুন:
WITH sold AS (
SELECT o.order_id, o.customer_id, o.order_date, oi.product_id,
oi.quantity * oi.unit_price AS amount
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id
WHERE o.status IN ('delivered', 'shipped')
)
SELECT strftime('%Y-%m', order_date) AS month,
COUNT(DISTINCT order_id) AS orders,
SUM(amount) AS revenue
FROM sold
GROUP BY month
ORDER BY month;+---------+--------+---------+
| month | orders | revenue |
+---------+--------+---------+
| 2026-01 | 2 | 6600 |
| 2026-02 | 2 | 7000 |
| 2026-03 | 3 | 21400 |
| 2026-04 | 2 | 23450 |
| 2026-05 | 2 | 8500 |
| 2026-06 | 1 | 3100 |
+---------+--------+---------+যা খেয়াল করবেন:
- CTE একটা স্টেটমেন্টেরই অংশ:
WITH … AS (…)-এর ঠিক পরেই মূলSELECT, মাঝখানে কোনো সেমিকোলন নেই। - মূল কোয়েরিতে
soldএকটা টেবিলের মতোই কাজ করে; তার কলাম হলো CTE-তে বাছাই করা কলামগুলো। কোথাও কিছু জমা থাকে না;soldথাকে শুধু এই স্টেটমেন্ট চলার সময়টুকু। - ফলাফল প্রজেক্ট: BitByte Shop-এর বিক্রয় বিশ্লেষণ রিপোর্ট পাতার মাসভিত্তিক কোয়েরির সাথে হুবহু এক। CTE কোয়েরিকে পড়তে সহজ করে, অর্থ বদলায় না।
পরপর কয়েকটা CTE
একটাই WITH, তার নিচে CTE-গুলো কমা দিয়ে আলাদা। প্রতিটা CTE তার ওপরেরগুলো ব্যবহার করতে পারে। আবার সেই ক্যাটাগরি রিপোর্ট, এবার সহজে পড়া যায় এমন তিনটা ধাপে:
WITH sold AS (
SELECT oi.product_id, oi.quantity * oi.unit_price AS amount
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id
WHERE o.status IN ('delivered', 'shipped')
),
by_category AS (
SELECT COALESCE(c.name, 'Uncategorised') AS category,
SUM(s.amount) AS revenue
FROM sold AS s
JOIN products AS p ON p.product_id = s.product_id
LEFT JOIN categories AS c ON c.category_id = p.category_id
GROUP BY COALESCE(c.name, 'Uncategorised')
),
total AS (
SELECT SUM(revenue) AS revenue FROM by_category
)
SELECT b.category,
b.revenue,
ROUND(100.0 * b.revenue / t.revenue, 1) AS pct
FROM by_category AS b
CROSS JOIN total AS t
ORDER BY b.revenue DESC, b.category;+-------------+---------+------+
| category | revenue | pct |
+-------------+---------+------+
| Electronics | 31550 | 45.0 |
| Courses | 21000 | 30.0 |
| Accessories | 8900 | 12.7 |
| Books | 8600 | 12.3 |
+-------------+---------+------+ওপর থেকে নিচে পড়ুন: বিক্রি হওয়া আইটেম, তারপর প্রতি ক্যাটাগরির রেভিনিউ, তারপর মোট, তারপর শেষ টেবিল। প্রতিটা ধাপ আলাদাভাবে পরীক্ষা করা যায়: শেষের SELECT-এর জায়গায় SELECT * FROM by_category লিখে চালান। এরর পড়া আর কোয়েরি ডিবাগ করা পাতার পদ্ধতিটা (টুকরো টুকরো চালানো) এখানে কোয়েরির গঠনের মধ্যেই আছে। Furniture নেই, কারণ ধাপগুলো শুরু হয়েছে বিক্রি হওয়া আইটেম থেকে; প্রতিটা ক্যাটাগরি দেখাতে হলে প্রজেক্টের মতো categories থেকে শুরু করে বাকিগুলো LEFT JOIN করুন।
একটা CTE দুবার ব্যবহার
সাবকোয়েরি যতবার দরকার ততবার পুরোটা লিখতে হয়; CTE-কে নাম ধরে যতবার খুশি ব্যবহার করা যায়। কোন গ্রাহকেরা গড় গ্রাহকের চেয়ে বেশি খরচ করেন?
WITH customer_revenue AS (
SELECT o.customer_id, 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 IN ('delivered', 'shipped')
GROUP BY o.customer_id
)
SELECT c.name, cr.revenue,
(SELECT ROUND(AVG(revenue), 2) FROM customer_revenue) AS avg_revenue
FROM customer_revenue AS cr
JOIN customers AS c ON c.customer_id = cr.customer_id
WHERE cr.revenue > (SELECT AVG(revenue) FROM customer_revenue)
ORDER BY cr.revenue DESC, c.customer_id;+--------------+---------+-------------+
| name | revenue | avg_revenue |
+--------------+---------+-------------+
| Nadia Rahman | 23600 | 11675.0 |
| Tanvir Ahmed | 22500 | 11675.0 |
+--------------+---------+-------------+একই কোয়েরি কোনো বদল ছাড়াই MySQL 8 আর PostgreSQL-এ চলে (MySQL 5.7 বা তার আগের সংস্করণে CTE একেবারেই নেই):
-- MySQL
WITH customer_revenue AS (
SELECT o.customer_id, 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 IN ('delivered', 'shipped')
GROUP BY o.customer_id
)
SELECT COUNT(*) AS customers, MAX(revenue) AS top_revenue
FROM customer_revenue;+-----------+-------------+
| customers | top_revenue |
+-----------+-------------+
| 6 | 23600 |
+-----------+-------------+CTE, সাবকোয়েরি, টেম্পোরারি টেবিল, নাকি ভিউ?
চারটা দিয়েই মাঝপথের একটা ফলাফলকে নাম দেওয়া বা আবার ব্যবহার করা যায়। পার্থক্য হলো ফলাফলটা কতক্ষণ টেকে আর কে দেখতে পায়:
| টুল | কতক্ষণ টেকে | কীসের জন্য ভালো |
|---|---|---|
| সাবকোয়েরি | যেখানে লেখা, শুধু সেখানে | ছোট, একবারের ধাপ (WHERE price > (SELECT AVG(price) …)) |
| CTE | একটা স্টেটমেন্ট | কয়েক ধাপের কোয়েরি, দুবার লাগে এমন ধাপ, রিকার্শন |
| টেম্পোরারি টেবিল | আপনার কানেকশন (বন্ধ হলে মুছে যায়) | ধীর একটা ধাপ, যা কয়েকটা স্টেটমেন্টে লাগে; মাঝের ফলাফলে ইনডেক্স যোগ করা |
| ভিউ | স্থায়ী, DROP না করা পর্যন্ত | এমন সংজ্ঞা, যা অনেকে আর অনেক কোয়েরি একসাথে ব্যবহার করে (পুরো টিমের জন্য একটা "sold items" ভিউ) |
টেম্পোরারি টেবিল সত্যিই সারি সংরক্ষণ করে, তাই পরের স্টেটমেন্টগুলো সেগুলো ব্যবহার করতে পারে:
CREATE TEMP TABLE sold_items AS
SELECT oi.order_id, oi.product_id, oi.quantity * oi.unit_price AS amount
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id
WHERE o.status IN ('delivered', 'shipped');
SELECT COUNT(*) AS item_rows, SUM(amount) AS revenue FROM sold_items;
SELECT COUNT(DISTINCT order_id) AS orders FROM sold_items;+-----------+---------+
| item_rows | revenue |
+-----------+---------+
| 18 | 70050 |
+-----------+---------+
+--------+
| orders |
+--------+
| 12 |
+--------+সমস্যা একটাই: টেম্পোরারি টেবিল একটা কপি। বানানোর পর কোনো অর্ডারের status বদলালে কপিটা আর হালনাগাদ থাকে না। CTE বা ভিউ সবসময় বর্তমান ডেটা পড়ে। (ভিউ নিয়ে এই টিউটোরিয়ালে পরে আলাদা পাতা আছে: ভিউ, স্টোরড প্রসিডিওর, ফাংশন আর ট্রিগার।)
পারফরম্যান্স: CTE কোয়েরি পড়া সহজ করার উপায়, ক্যাশ নয়। PostgreSQL 12+ একবার ব্যবহৃত CTE-কে সাধারণত মূল কোয়েরির ভেতরে মিশিয়ে দেয়, ঠিক সাবকোয়েরির মতো;
WITH x AS MATERIALIZED (…)লিখে একবারই হিসাব করতে বাধ্য করা যায়। SQLite 3.35+ একইMATERIALIZED/NOT MATERIALIZEDনির্দেশ মানে; MySQL মেশাবে নাকি আলাদা করে হিসাব করবে, তা নিজেই ঠিক করে। CTE কিছু দ্রুত বা ধীর করে — এমন ধরে নেওয়ার আগে মেপে দেখুন (দেখুন EXPLAIN আর কোয়েরি অপ্টিমাইজেশন)।
রিকার্সিভ CTE
রিকার্সিভ CTE নিজের ভেতরে নিজেকেই ব্যবহার করে। এর দুটো অংশ, UNION ALL দিয়ে জোড়া: একটা অ্যাঙ্কর (anchor), যা প্রথম সারিগুলো দেয়, আর একটা রিকার্সিভ ধাপ, যা আগের রাউন্ডে তৈরি সারি থেকে নতুন সারি বানায়। কোনো রাউন্ডে নতুন সারি না এলে কোয়েরি থেমে যায়।
WITH RECURSIVE n(x) AS (
SELECT 1 -- অ্যাঙ্কর: রাউন্ড 1 দেয় x = 1
UNION ALL
SELECT x + 1 FROM n -- ধাপ: প্রতি রাউন্ডে আগের রাউন্ডের সারির সাথে 1 যোগ
WHERE x < 5 -- থামা: এটা false হলে নতুন সারি নেই, শেষ
)
round 1: 1 round 2: 2 round 3: 3 round 4: 4 round 5: 5 round 6: (none)WITH RECURSIVE n(x) AS (
SELECT 1
UNION ALL
SELECT x + 1 FROM n WHERE x < 5
)
SELECT x, x * x AS square FROM n;+---+--------+
| x | square |
+---+--------+
| 1 | 1 |
| 2 | 4 |
| 3 | 9 |
| 4 | 16 |
| 5 | 25 |
+---+--------+n(x) লিখলে CTE-র কলামের নাম ভেতরে AS দিয়ে না দিয়ে হেডারেই দেওয়া হয়। MySQL আর PostgreSQL-এ RECURSIVE বাধ্যতামূলক; SQLite এটা ছাড়াও কোয়েরি চালায়, তবু সবসময় লিখুন। তালিকার একটা CTE রিকার্সিভ হলেও RECURSIVE একবারই লেখা হয়, WITH-এর ঠিক পরে; বাকি CTE-গুলো সাধারণ থাকতে পারে।
একটা ক্যালেন্ডার: যেদিন কোনো অর্ডার নেই
অর্ডারের তারিখে GROUP BY করলে শুধু সেই দিনগুলো আসে, যেদিন অর্ডার আছে। দৈনিক অর্ডারের চার্ট তখন কিছু না জানিয়েই খালি দিনগুলো বাদ দেবে, আর দোকানকে আসলের চেয়ে ব্যস্ত দেখাবে। আগে প্রতিটা দিন তৈরি করুন, তারপর অর্ডারগুলো LEFT JOIN করুন:
WITH RECURSIVE days(day) AS (
SELECT '2026-05-28'
UNION ALL
SELECT date(day, '+1 day') FROM days WHERE day < '2026-06-10'
)
SELECT d.day, COUNT(o.order_id) AS orders
FROM days AS d
LEFT JOIN orders AS o ON o.order_date = d.day
GROUP BY d.day
ORDER BY d.day;+------------+--------+
| day | orders |
+------------+--------+
| 2026-05-28 | 0 |
| 2026-05-29 | 0 |
| 2026-05-30 | 0 |
| 2026-05-31 | 0 |
| 2026-06-01 | 0 |
| 2026-06-02 | 1 |
| 2026-06-03 | 0 |
| 2026-06-04 | 0 |
| 2026-06-05 | 0 |
| 2026-06-06 | 0 |
| 2026-06-07 | 0 |
| 2026-06-08 | 0 |
| 2026-06-09 | 1 |
| 2026-06-10 | 0 |
+------------+--------+এই ১৪ দিনের ১২ দিনেই কোনো অর্ডার নেই, আর এখন ফলাফল সেটা স্পষ্ট দেখাচ্ছে। তারিখের এমন পূর্ণ তালিকা (date spine) দিয়ে ফাঁক ভরানোই প্রচলিত নিয়ম, টাইম-সিরিজ চার্ট বা ফোরকাস্টিং মডেলে দেওয়ার আগে, কারণ এরা সাধারণত প্রতিদিনের জন্য একটা সারি আশা করে। তারিখের হিসাব ডায়ালেক্টভেদে আলাদা, তাই ডেটাবেস বদলালে এই অংশটাই বদলায়:
-- MySQL
WITH RECURSIVE days AS (
SELECT CAST('2026-06-01' AS DATE) AS day
UNION ALL
SELECT day + INTERVAL 1 DAY FROM days WHERE day < '2026-06-03'
)
SELECT day FROM days ORDER BY day;+------------+
| day |
+------------+
| 2026-06-01 |
| 2026-06-02 |
| 2026-06-03 |
+------------+-- PostgreSQL
SELECT CAST(d AS DATE) AS day
FROM generate_series(DATE '2026-06-01', DATE '2026-06-03', INTERVAL '1 day') AS g(d)
ORDER BY day;+------------+
| day |
+------------+
| 2026-06-01 |
| 2026-06-02 |
| 2026-06-03 |
+------------+সংখ্যা বা তারিখের সারির জন্য PostgreSQL-এ রিকার্শন খুব কমই লাগে: generate_series সরাসরি কাজটা করে দেয়। রিকার্সিভ রূপটাও সেখানে চলে।
অর্গ চার্টের সব স্তর
প্রত্যেক কর্মীর একটা manager_id আছে, যা আরেকজন কর্মীকে দেখায়। সেলফ জয়েন (দেখুন LEFT, RIGHT, FULL, CROSS আর সেলফ জয়েন) এক স্তর পেরোয়; রিকার্সিভ CTE সবগুলো স্তর। শুরু করুন যাঁর কোনো ম্যানেজার নেই তাঁকে দিয়ে, তারপর বারবার তাঁদের যোগ করুন, যাঁদের ম্যানেজার আগে থেকেই ফলাফলে আছেন:
WITH RECURSIVE chart AS (
SELECT employee_id, name, 1 AS level, name AS path
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT e.employee_id, e.name, c.level + 1, c.path || ' > ' || e.name
FROM employees AS e
JOIN chart AS c ON e.manager_id = c.employee_id
)
SELECT level, name, path
FROM chart
ORDER BY path;+-------+-----------------+-----------------------------------------------+
| level | name | path |
+-------+-----------------+-----------------------------------------------+
| 1 | Ayesha Siddiqua | Ayesha Siddiqua |
| 2 | Hasan Mahmud | Ayesha Siddiqua > Hasan Mahmud |
| 3 | Nusrat Jahan | Ayesha Siddiqua > Hasan Mahmud > Nusrat Jahan |
| 3 | Zahid Hasan | Ayesha Siddiqua > Hasan Mahmud > Zahid Hasan |
| 2 | Lamia Karim | Ayesha Siddiqua > Lamia Karim |
| 3 | Priya Sen | Ayesha Siddiqua > Lamia Karim > Priya Sen |
| 3 | Shuvo Roy | Ayesha Siddiqua > Lamia Karim > Shuvo Roy |
+-------+-----------------+-----------------------------------------------+path দিয়ে সাজালে প্রত্যেকে নিজের ম্যানেজারের ঠিক নিচে বসেন, তাই ফলাফলটা একটা ট্রি-র মতো পড়া যায়। একই প্যাটার্নে পুরোটা বের করা যায় ক্যাটাগরির ট্রি, বিল অব ম্যাটেরিয়ালস, বা এমন চ্যাট থ্রেডের, যেখানে প্রতিটা মেসেজের একটা parent_id আছে (LLM চ্যাট অ্যাপে শাখা-প্রশাখাওয়ালা কথোপকথন রাখার এটা একটা প্রচলিত উপায়)।
সার্ভারে প্রতিটা কলামের টাইপ ঠিক করে দেয় অ্যাঙ্কর। এখানে অ্যাঙ্করে path হয় VARCHAR(100), কিন্তু রিকার্সিভ ধাপে || দেয় সীমাহীন দৈর্ঘ্যের টেক্সট, আর PostgreSQL এই অমিল মানে না:
-- PostgreSQL
WITH RECURSIVE chart AS (
SELECT employee_id, name, 1 AS level, name AS path
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT e.employee_id, e.name, c.level + 1, c.path || ' > ' || e.name
FROM employees AS e
JOIN chart AS c ON e.manager_id = c.employee_id
)
SELECT level, name, path FROM chart ORDER BY path;Error: recursive query "chart" column 4 has type character varying(100) in non-recursive term but type character varying overallসমাধান হলো অ্যাঙ্করের কলামকে এমন টাইপ দেওয়া, যা প্রতিটা রাউন্ডের জন্য যথেষ্ট চওড়া: PostgreSQL-এ CAST(name AS TEXT), MySQL-এ CAST(name AS CHAR(500)) (আর সেখানে ||-কেও CONCAT করতে হবে):
-- PostgreSQL
WITH RECURSIVE chart AS (
SELECT employee_id, name, 1 AS level, CAST(name AS TEXT) AS path
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT e.employee_id, e.name, c.level + 1, c.path || ' > ' || e.name
FROM employees AS e
JOIN chart AS c ON e.manager_id = c.employee_id
)
SELECT level, path FROM chart WHERE level = 3 ORDER BY path;+-------+-----------------------------------------------+
| level | path |
+-------+-----------------------------------------------+
| 3 | Ayesha Siddiqua > Hasan Mahmud > Nusrat Jahan |
| 3 | Ayesha Siddiqua > Hasan Mahmud > Zahid Hasan |
| 3 | Ayesha Siddiqua > Lamia Karim > Priya Sen |
| 3 | Ayesha Siddiqua > Lamia Karim > Shuvo Roy |
+-------+-----------------------------------------------+-- MySQL
WITH RECURSIVE chart AS (
SELECT employee_id, name, 1 AS level, CAST(name AS CHAR(500)) AS path
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT e.employee_id, e.name, c.level + 1, CONCAT(c.path, ' > ', e.name)
FROM employees AS e
JOIN chart AS c ON e.manager_id = c.employee_id
)
SELECT level, path FROM chart WHERE level = 3 ORDER BY path;+-------+-----------------------------------------------+
| level | path |
+-------+-----------------------------------------------+
| 3 | Ayesha Siddiqua > Hasan Mahmud > Nusrat Jahan |
| 3 | Ayesha Siddiqua > Hasan Mahmud > Zahid Hasan |
| 3 | Ayesha Siddiqua > Lamia Karim > Priya Sen |
| 3 | Ayesha Siddiqua > Lamia Karim > Shuvo Roy |
+-------+-----------------------------------------------+সাধারণ ভুল
দুবার WITH লেখা
একটাই WITH, তারপর কমা দিয়ে আলাদা CTE। দ্বিতীয় WITH সিনট্যাক্স এরর:
WITH a AS (SELECT 1 AS x),
WITH b AS (SELECT 2 AS y)
SELECT * FROM a, b;Error: near "b": syntax errorসমাধান: WITH a AS (…), b AS (…) SELECT …।
পরের স্টেটমেন্টে CTE ব্যবহার
CTE থাকে শুধু নিজের স্টেটমেন্টের ভেতরে। সেমিকোলনের পর সেটা আর নেই:
WITH sold AS (SELECT * FROM orders WHERE status = 'delivered')
SELECT COUNT(*) AS delivered FROM sold;
SELECT COUNT(*) FROM sold;+-----------+
| delivered |
+-----------+
| 11 |
+-----------+
Error: no such table: soldসমাধান: WITH আবার লিখুন, অথবা কয়েকটা স্টেটমেন্টে ফলাফল লাগলে টেম্পোরারি টেবিল বা ভিউ ব্যবহার করুন।
যে রিকার্শন কখনো থামে না
থামার শর্ত না থাকলে রিকার্সিভ ধাপ প্রতিবারই নতুন সারি পায়। MySQL ১,০০০ রাউন্ডের পর থামিয়ে দেয় (cte_max_recursion_depth সেটিং, যা একটা সেশনের জন্য বাড়ানো যায়); PostgreSQL আর SQLite-এ এমন কোনো সীমা নেই, কোয়েরি চলতেই থাকে, যতক্ষণ না আপনি বাতিল করেন, টাইমআউট থামায়, বা মেমরি বা ডিস্ক ফুরায়:
-- MySQL
WITH RECURSIVE n AS (
SELECT 1 AS x
UNION ALL
SELECT x + 1 FROM n
)
SELECT COUNT(*) FROM n;Error: ERROR 3636: Recursive query aborted after 1001 iterations. Try increasing @@cte_max_recursion_depth to a larger value.সমাধান: রিকার্সিভ ধাপে সবসময় এমন একটা WHERE দিন, যা একসময় false হবে (WHERE x < 5, WHERE level < 10)। ডেটায় চক্র থাকলে (A, B-র ম্যানেজার; B, A-র ম্যানেজার) সঠিক জয়েনেও লুপ চলতেই থাকে, তাই লেভেলের একটা সীমা দিয়ে রাখা সহজ একটা সুরক্ষা।
পরের রাউন্ডের জন্য ছোট টেক্সট কলাম
MySQL প্রতিটা কলামের টাইপ অ্যাঙ্কর থেকেই ঠিক করে ফেলে। ফাঁকা স্ট্রিং মানে শূন্য দৈর্ঘ্যের টেক্সট, তাই তার ওপর ইনডেন্ট বানাতে গেলে দ্বিতীয় রাউন্ডেই এরর আসে:
-- MySQL
WITH RECURSIVE chart AS (
SELECT employee_id, name, '' AS indent
FROM employees WHERE manager_id IS NULL
UNION ALL
SELECT e.employee_id, e.name, CONCAT(c.indent, '-- ')
FROM employees AS e JOIN chart AS c ON e.manager_id = c.employee_id
)
SELECT CONCAT(indent, name) AS org FROM chart;Error: ERROR 1406: Data too long for column 'indent' at row 1সমাধান: CAST('' AS CHAR(100)) AS indent। ওপরের PostgreSQL-এর CAST(name AS TEXT)-এর মতোই ধারণা।
নিজে চেষ্টা করুন
- সহজ:
low_stockনামে একটা CTE লিখুন, যাতে থাকবে যেসব প্রোডাক্টের স্টক ১০-এর কম (NULL স্টক বাদ), তারপর ক্যাটাগরির নামসহ সেগুলোর তালিকা দেখান। - মাঝারি: দুটো CTE দিয়ে বিক্রি হওয়া অর্ডার থেকে সবচেয়ে বেশি রেভিনিউর মাস বের করুন: আগে প্রতি মাসের রেভিনিউ, তারপর সর্বোচ্চটা, তারপর যে মাসে সেটা।
- কঠিন: প্রত্যেক কর্মীর জন্য গুনুন, যেকোনো স্তরে কতজন তাঁর অধীনে কাজ করেন (সরাসরি আর পরোক্ষ)। ইঙ্গিত: রিকার্সিভ CTE-তে নিচে নামার সময় শুরুর ম্যানেজারের id একটা কলামে ধরে রাখুন।
উত্তর
-- 1. কম স্টক
WITH low_stock AS (
SELECT product_id, name, category_id, stock
FROM products
WHERE stock < 10
)
SELECT l.name, l.stock, c.name AS category
FROM low_stock AS l
LEFT JOIN categories AS c ON c.category_id = l.category_id
ORDER BY l.stock, l.product_id;
-- 2. সেরা মাস
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 IN ('delivered', 'shipped')
GROUP BY month
),
best AS (
SELECT MAX(revenue) AS revenue FROM monthly
)
SELECT m.month, m.revenue
FROM monthly AS m
JOIN best AS b ON b.revenue = m.revenue;
-- 3. প্রত্যেক ম্যানেজারের অধীনে সবাই
WITH RECURSIVE under AS (
SELECT manager_id AS boss_id, employee_id
FROM employees
WHERE manager_id IS NOT NULL
UNION ALL
SELECT u.boss_id, e.employee_id
FROM under AS u
JOIN employees AS e ON e.manager_id = u.employee_id
)
SELECT b.name, COUNT(u.employee_id) AS people_under
FROM employees AS b
LEFT JOIN under AS u ON u.boss_id = b.employee_id
GROUP BY b.employee_id, b.name
ORDER BY people_under DESC, b.employee_id;সারসংক্ষেপ
WITH name AS (…)কোয়েরির একটা ধাপকে নাম দেয়; একটাWITH-এর নিচে কমা দিয়ে ধাপগুলো জুড়ুন, আর যতবার দরকার প্রতিটা নাম ধরে ব্যবহার করুন।- CTE টেকে শুধু একটা স্টেটমেন্ট জুড়ে। কয়েকটা স্টেটমেন্টে সংরক্ষিত সারি লাগলে টেম্পোরারি টেবিল, ভাগ করে নেওয়া স্থায়ী সংজ্ঞার জন্য ভিউ।
- CTE-র কাজ কোয়েরি পড়া সহজ করা; মিশিয়ে দেবে নাকি আলাদা হিসাব করবে, তা অপ্টিমাইজার ঠিক করে।
- রিকার্সিভ CTE = অ্যাঙ্কর
UNION ALLএমন একটা ধাপ, যা CTE-কেই ব্যবহার করে, সাথে একটাWHERE, যা একসময় থামায়। - সংখ্যার ক্রম, ফাঁক ভরানো ক্যালেন্ডার আর হায়ারার্কির জন্য রিকার্শন; সার্ভারে অ্যাঙ্করের কলাম যথেষ্ট চওড়া টাইপে cast করুন, আর MySQL-এর
CONCATও PostgreSQL-এরgenerate_seriesমনে রাখুন।
এরপর: উইন্ডো ফাংশন ১: PARTITION BY, ROW_NUMBER ও RANK পাতায় শিখবেন সম্পর্কিত সারিগুলোর ওপর হিসাব করতে, সারিগুলোকে এক সারিতে না গুটিয়েই। এসব কোয়েরি প্রায়ই এই পাতার মতো একটা CTE-র ওপর লেখা হয়।