এই পর্বে যা শিখবেন
- View কী,
CREATE VIEWদিয়ে বানানো, আর টেবিলের মতো তার ওপর কোয়েরি চালানো - View কেন: সরলতা, নিরাপত্তা, পুরোনো অ্যাপের সাথে মিল আর এক রকম ব্যবসার সংজ্ঞা
- Optimizer কীভাবে view খুলে মূল টেবিলের কোয়েরি বানায়, আর view-এর ওপর view, বদল ও মোছার নির্ভরতা
- কোন view-তে লেখা যায়,
WITH CHECK OPTION, আর SQLite-এ INSTEAD OF trigger - Materialized view, refresh, পুরোনো ফলের ঝুঁকি, SQLite-এ এর অনুকরণ, আর view দিয়ে অধিকার (GRANT) দেওয়া
১ · View কী — নাম দেওয়া একটি প্রশ্ন
একটি মুদি দোকানের মূল খাতায় সব লেনদেন লেখা থাকে: কে কী নিলেন, কত দাম, কবে, নগদ না বাকি। মাসের শেষে মালিক চান “বাকির তালিকা”। হিসাবরক্ষক প্রতিবার মূল খাতা দেখে তালিকাটি নতুন করে বানান — আলাদা খাতায় কপি করে রাখেন না। তালিকাটির একটি নাম আছে, একটি নিয়ম আছে (“যাঁদের বাকি শূন্যের বেশি”), কিন্তু নিজস্ব কোনো ডেটা নেই। আজ কেউ বাকি শোধ করলে আজকের তালিকায় তাঁর নাম আর আসবে না।
SQL-এ ঠিক এই জিনিসটি হলো view. View হলো একটি SELECT কোয়েরি, যাকে একটি নাম দিয়ে ডেটাবেসে রেখে দেওয়া হয়েছে। তারপর সেই নাম টেবিলের মতোই FROM-এ লেখা যায়। এজন্য view-কে বলে virtual table (ভার্চুয়াল টেবিল): দেখতে টেবিল, কিন্তু ভেতরে শুধু একটি প্রশ্ন। যে আসল টেবিলগুলো থেকে view ডেটা নেয়, তাদের বলে base table (মূল টেবিল)।
পর্ব ৩-এ three-schema architecture-এর external schema দেখেছিলেন: প্রতিটি ব্যবহারকারী ডেটাবেসের নিজের অংশটুকু নিজের মতো করে দেখেন। SQL-এ external schema লেখার প্রধান হাতিয়ারই view. সেখানে গুদামের জন্য দাম-লুকানো একটি view বানিয়েছিলেন। এই পর্বে view-কে পুরোপুরি চিনবেন।
CREATE VIEW view_name AS
SELECT ...; -- যেকোনো বৈধ SELECT: JOIN, GROUP BY, subquery সব চলে
SELECT * FROM view_name WHERE ...; -- টেবিলের মতো ব্যবহার
DROP VIEW view_name; -- শুধু সংজ্ঞা মোছে, base table-এর ডেটা অক্ষতএই পর্বের লাইভ SQL চলে আগের পর্বগুলোর দুটি নমুনা ডেটাবেসে। db-shop একটি দোকান: customers (customer_id, name, city, phone, joined_on — ৮ জন; Tanvir-এর phone নেই, Imran-এর city নেই), categories, products (product_id, name, category_id, price, stock — ১০টি পণ্য), orders (order_id, customer_id, order_date, status — ১০টি অর্ডার, status হলো pending, shipped, delivered বা cancelled) আর order_items (order_id, product_id, quantity, unit_price). db-uni একটি বিশ্ববিদ্যালয়: students (student_id, name, dept_id, batch, cgpa), courses (course_id, title, dept_id, credits, instructor_id), enrollments (student_id, course_id, term, grade) আর departments, instructors. প্রতিবার চালালে ডেটাবেস নতুন করে তৈরি হয়, তাই প্রতিটি এডিটরে দরকারি view আবার বানানো হয়েছে।
প্রথম view-টি এই পর্ব জুড়ে কাজে লাগবে। দোকানের প্রায় প্রতিটি রিপোর্টে একই প্রশ্ন লাগে: কোন অর্ডার, কোন গ্রাহকের, কবে, কী অবস্থায়, আর মোট কত টাকা? উত্তরের জন্য তিনটি টেবিল জুড়তে হয় আর GROUP BY করতে হয় (পর্ব ১৮–১৯). একবার order_summary নামে লিখে রাখলে আর কাউকে সেটি আবার লিখতে হবে না:
-- প্রতিটি অর্ডারের সারাংশ: একবার লিখে রাখা
CREATE VIEW order_summary AS
SELECT o.order_id, c.name AS customer, o.order_date, o.status,
SUM(i.quantity * i.unit_price) AS total
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
JOIN order_items i ON i.order_id = o.order_id
GROUP BY o.order_id, c.name, o.order_date, o.status;
SELECT * FROM order_summary;
-- এখন টেবিলের মতোই ছাঁকা আর সাজানো
SELECT customer, total FROM order_summary
WHERE status = 'delivered' AND total > 3000
ORDER BY total DESC;CREATE VIEW শুধু ✓ দেখায় — কোনো সারি তৈরি হয়নি, শুধু সংজ্ঞাটি system catalog-এ (SQLite-এ sqlite_schema) লেখা হয়েছে। দ্বিতীয় স্টেটমেন্টের ফলে ১০টি সারি, যেমন 1001 | Rahim Uddin | 2025-09-01 | delivered | 17400, 1004 | Sabbir Hasan | 2025-09-12 | cancelled | 4500, 1009 | Rahim Uddin | 2025-10-08 | pending | 18000. তৃতীয় ফলে তিনটি সারি: Rahim Uddin | 17400, Nusrat Jahan | 5050, Karima Begum | 3250. যিনি তৃতীয় কোয়েরিটি লিখলেন, তাঁকে JOIN বা GROUP BY জানতেই হয়নি। view-এর customer আর total কলামের নাম এসেছে সংজ্ঞার alias থেকে।
ট্রেনের টিকিটের অ্যাপে “আমার যাত্রা” পাতাটি ভাবুন। পাতাটি রেলওয়ের বিশাল বুকিং-ডেটাবেস থেকে শুধু আপনার টিকিটগুলো দেখায়। পাতাটির আলাদা কোনো ডেটা নেই; টিকিট বাতিল করলে পরের বার খুললেই সেটি আর দেখাবে না। View ঠিক এমন একটি জানালা — জানালা দিয়ে দেখা ঘরটিই আসল, জানালা শুধু ঠিক করে কতটুকু দেখা যাবে।
২ · View কেন — চারটি বাস্তব কারণ
🧩 সরলতা
তিন টেবিলের JOIN আর GROUP BY একবার লিখে নাম দেওয়া। নতুন সহকর্মী বা রিপোর্ট-টুল শুধু order_summary চেনে।
🔒 নিরাপত্তা
কিছু কলাম (ফোন নম্বর) বা কিছু সারি (শুধু ঢাকার গ্রাহক) লুকানো। ব্যবহারকারীকে মূল টেবিলের বদলে শুধু view পড়ার অধিকার দেওয়া হয়।
🔁 পুরোনো অ্যাপের সাথে মিল
টেবিল ভাগ বা নাম বদলালেও পুরোনো নামে একটি view রেখে দিলে পুরোনো কোড ভাঙে না — পর্ব ৩-এর logical data independence.
📏 এক সংজ্ঞা
“আয় মানে বাতিল অর্ডার বাদে” — নিয়মটি একটি view-তে থাকলে হিসাব শাখা আর বিক্রয় দলের রিপোর্টে আলাদা সংখ্যা আসে না।
নিরাপত্তা: কলাম আর সারি লুকানো
দোকানের হিসাব শাখা গ্রাহকের নাম আর শহর দেখবে, কিন্তু ফোন নম্বর নয় — ফোন নম্বর ব্যক্তিগত তথ্য, আর যার কাজে লাগে না তার কাছে না থাকাই নিরাপদ। ঢাকা শাখার কর্মী শুধু ঢাকার গ্রাহক দেখবেন। প্রথম view কলাম লুকায় (SELECT তালিকা দিয়ে), দ্বিতীয়টি সারিও লুকায় (WHERE দিয়ে):
-- হিসাব শাখার জন্য: ফোন নম্বর ছাড়া
CREATE VIEW public_customers AS
SELECT customer_id, name, city, joined_on FROM customers;
-- ঢাকা শাখার জন্য: শুধু ঢাকার সারি
CREATE VIEW dhaka_customers AS
SELECT customer_id, name, city FROM customers WHERE city = 'Dhaka';
SELECT * FROM public_customers WHERE customer_id <= 3;
SELECT * FROM dhaka_customers;
-- view-তে phone নেই
SELECT name, phone FROM public_customers;প্রথম ফলে তিনটি সারি, চারটি কলাম — যেমন 1 | Rahim Uddin | Dhaka | 2025-01-15; phone কলামটির কোনো চিহ্নই নেই। দ্বিতীয় ফলে ঢাকার তিনজন: 1 | Rahim Uddin, 3 | Sabbir Hasan, 8 | Mitu Rani. শেষ স্টেটমেন্টে ত্রুটি: no such column: phone. view-এর ব্যবহারকারীর কাছে phone নামে কিছু নেই। তবে মনে রাখুন, এই এডিটরে আপনি নিজে customers টেবিলও পড়তে পারেন — view তখনই সত্যিকারের পাহারা হয়, যখন ব্যবহারকারীকে শুধু view-এর অধিকার দেওয়া হয় আর base table-এর নয়। অধিকারের কথা অংশ ৯-এ।
সরলতা: বিশ্ববিদ্যালয়ের transcript
বিশ্ববিদ্যালয়ের রেজিস্টার অফিসে প্রতিদিন একই প্রশ্ন: এই শিক্ষার্থী কোন কোর্সে কী গ্রেড পেলেন, মোট কত credit নিলেন? তিনটি টেবিলের JOIN একবার transcript নামে লিখে রাখলে প্রতিটি প্রশ্ন একটি সরল SELECT হয়ে যায়:
CREATE VIEW transcript AS
SELECT s.student_id, s.name, e.course_id, c.title, c.credits, e.term, e.grade
FROM students s
JOIN enrollments e ON e.student_id = s.student_id
JOIN courses c ON c.course_id = e.course_id;
SELECT name, course_id, grade FROM transcript WHERE student_id = 2101;
SELECT name, SUM(credits) AS credits_taken FROM transcript
GROUP BY student_id, name ORDER BY credits_taken DESC;প্রথম ফলে Arif Hossain-এর তিনটি কোর্স: CSE201 | A, CSE301 | A-, CSE302 | A. দ্বিতীয় ফলে ৭টি সারি: Dilruba Khatun 9, Arif Hossain 7.5, Bristy Saha 6, Gulshan Ara 6, Chayon Roy 3, Emon Mia 3, Fahim Karim 3. Habib Ullah নেই, কারণ তাঁর কোনো enrollment নেই আর view-টি inner JOIN দিয়ে বানানো। এটিই view-এর একটি লুকানো বিপদ: যিনি শুধু transcript দেখেন, তিনি জানেন না ভেতরে inner JOIN আছে। “সব শিক্ষার্থীর credit” চাইলে view-টি তাঁকে নীরবে একজন কম দেবে। ভালো view-এর নাম আর বর্ণনা তাই তার অর্থ স্পষ্ট বলে। আরেকটি সূক্ষ্মতা: Dilruba-র CSE301-এর গ্রেড এখনো NULL, তবু তার ৩ credit যোগ হয়েছে — “নেওয়া” আর “শেষ করা” credit আলাদা প্রশ্ন।
৩ · ভেতরে কী হয় — view expansion
View-এর ওপর কোয়েরি চালালে DBMS কি আগে view-এর পুরো ফল বানিয়ে তারপর ছাঁকে? সাধারণত না। Optimizer কোয়েরির লেখায় view-এর নামের জায়গায় তার সংজ্ঞাটি বসিয়ে দেয়, যেন আপনি FROM-এ একটি derived table (পর্ব ২১) লিখেছেন। তারপর সম্ভব হলে সেই subquery-কে বাইরের কোয়েরির সাথে মিশিয়ে ফেলে, যাতে শেষে শুধু base table-এর ওপর একটি সাধারণ কোয়েরি থাকে। এই প্রক্রিয়ার নাম view expansion (view merging বা inlining নামেও পরিচিত)। নিচে ঢাকা শাখার একটি প্রশ্ন ধাপে ধাপে দেখুন: “ঢাকার কোন গ্রাহকের অর্ডার এখন পথে (shipped)?”
| FROM-এ যা আছে | |
| WHERE-এর শর্ত | |
| view-এর ফল আলাদা বানানো? | |
| ফল |
- পাঠকের কোয়েরি। লেখায়
dhaka_customersএকটি টেবিলের মতো বসে আছে। parser নামটি চেনে না — এটি টেবিল না view, জানতে হবে। - Catalog-এ খোঁজা।
sqlite_schema-এ নামটির type হলো view, আর সাথে লেখা আছে তার SELECT. View-এর জন্য catalog-এ শুধু এই লেখাটিই রাখা থাকে, কোনো সারি নয়। - নামের জায়গায় সংজ্ঞা।
dhaka_customers d-এর জায়গায় বসে(SELECT … WHERE city = 'Dhaka') AS d. কোয়েরিটি এখন একটি derived table-ওয়ালা সাধারণ কোয়েরি; অর্থ একটুও বদলায়নি। - মিশিয়ে ফেলা (flattening). ভেতরের SELECT-এ GROUP BY, DISTINCT বা LIMIT নেই, তাই optimizer subquery-টি খুলে ফেলে: FROM-এ সরাসরি customers, আর view-এর শর্ত
city = 'Dhaka'যোগ হয় বাইরের WHERE-এ, AND দিয়ে। view আর নেই — আছে শুধু base table. - Plan. Optimizer এখন সাধারণ কোয়েরির মতোই plan বানায়: orders পড়ে, প্রতিটি সারির গ্রাহক primary key দিয়ে খোঁজে। নিচের এডিটরে দেখবেন, হাতে লেখা কোয়েরির plan-ও হুবহু এক।
- ফল: দুটি সারি। shipped অর্ডার দুটি — 1003 (Rahim) আর 1008 (Mitu), দুজনই ঢাকার। view-এর পুরো ফল (ঢাকার তিনজন) কখনো আলাদা করে বানানো হয়নি।
নিজে যাচাই করুন। এই এডিটরে dhaka_customers আর অংশ ১-এর order_summary আগে থেকেই বানানো আছে (নমুনা ডেটার বাক্সের শেষে দেখতে পাবেন)। EXPLAIN QUERY PLAN (পর্ব ১৭-এ প্রথম দেখেছেন) দেখায় SQLite কোয়েরিটি কীভাবে চালাবে:
-- view-এর ওপর কোয়েরি
EXPLAIN QUERY PLAN
SELECT d.name, o.order_id FROM dhaka_customers d
JOIN orders o ON o.customer_id = d.customer_id
WHERE o.status = 'shipped';
-- একই প্রশ্ন, হাতে view খুলে লেখা
EXPLAIN QUERY PLAN
SELECT c.name, o.order_id FROM customers c
JOIN orders o ON o.customer_id = c.customer_id
WHERE c.city = 'Dhaka' AND o.status = 'shipped';
-- GROUP BY-ওয়ালা view: মেশানো যায় না, কিন্তু শর্ত ভেতরে ঠেলা যায়
EXPLAIN QUERY PLAN
SELECT * FROM order_summary WHERE order_id = 1005;প্রথম দুটি plan-এর detail কলাম একই: SCAN o আর SEARCH customers USING INTEGER PRIMARY KEY (rowid=?) (দ্বিতীয়টিতে টেবিলের নাম alias c দিয়ে লেখা)। View থাকা বা না থাকায় কাজের কোনো পার্থক্য নেই। তৃতীয় plan আলাদা: CO-ROUTINE order_summary, তার নিচে SEARCH o USING INTEGER PRIMARY KEY (rowid=?), SEARCH c …, SEARCH i USING INDEX sqlite_autoindex_order_items_1 (order_id=?), আর শেষে SCAN order_summary. GROUP BY-ওয়ালা view-কে বাইরের কোয়েরির সাথে মেশানো যায় না, তাই SQLite সেটিকে আলাদা একটি co-routine হিসেবে চালায়। কিন্তু order_id = 1005 শর্তটি view-এর ভেতরে ঠেলে দেওয়া হয়েছে — তাই ১০টি অর্ডারের মোট না বানিয়ে শুধু 1005-এর সারিগুলো primary key দিয়ে খোঁজা হয়েছে। এই কৌশলের নাম predicate pushdown.
আরও গভীরে: কোন view মেশানো যাবে, তার নিয়ম প্রতিটি DBMS-এ আলাদা আর বেশ সূক্ষ্ম। SQLite-এর ডকুমেন্টেশনে “query flattening”-এর দুই ডজনেরও বেশি শর্ত তালিকা করা আছে — যেমন aggregate-ওয়ালা subquery-কে SQLite কখনো মেশায় না। PostgreSQL-এ view আসলে একটি rewrite rule (_RETURN নামের ON SELECT rule) হিসেবে রাখা হয়; parser-এর পরে rewriter view-এর নামকে subquery-তে বদলায়, তারপর planner “subquery pull-up” করে। MySQL দুই পথের কথা স্পষ্ট বলে: ALGORITHM = MERGE (মিশিয়ে ফেলা) আর ALGORITHM = TEMPTABLE (আগে ফল অস্থায়ী টেবিলে, তারপর কোয়েরি) — আর TEMPTABLE view-তে লেখা যায় না।
৪ · View-এর ওপর view, বদলানো আর মোছা
View যেহেতু টেবিলের মতো আচরণ করে, একটি view-এর ওপর আরেকটি view-ও বানানো যায়। দোকানের মালিক প্রতি মাসের আয় দেখতে চান। ব্যবসার নিয়ম: বাতিল অর্ডার আয় নয়। নিয়মটি রিপোর্ট-লেখকের মাথায় না রেখে view-এর ভেতরে রাখুন, তাহলে সবাই একই সংখ্যা পাবেন। এই এডিটরে order_summary আগে থেকে বানানো:
-- view-এর ওপর view: মাসভিত্তিক আয় (বাতিল অর্ডার আয় নয়)
CREATE VIEW monthly_revenue AS
SELECT substr(order_date, 1, 7) AS month,
COUNT(*) AS n_orders, SUM(total) AS revenue
FROM order_summary
WHERE status <> 'cancelled'
GROUP BY substr(order_date, 1, 7);
SELECT * FROM monthly_revenue;
-- একই view থেকে আরেকটি রিপোর্ট: সেরা তিন গ্রাহক
SELECT customer, SUM(total) AS spent FROM order_summary
WHERE status <> 'cancelled'
GROUP BY customer ORDER BY spent DESC LIMIT 3;প্রথম ফল দুটি সারি: 2025-09 | 5 | 29480 আর 2025-10 | 4 | 22840. সেপ্টেম্বরে ৬টি অর্ডার ছিল, কিন্তু 1004 (Sabbir, 4500 টাকা, cancelled) বাদ গেছে: 17400 + 1750 + 1780 + 5050 + 3500 = 29480. substr(order_date, 1, 7) ISO তারিখের প্রথম ৭টি অক্ষর, অর্থাৎ বছর-মাস (পর্ব ১৭)। দ্বিতীয় ফল: Rahim Uddin | 37180, Nusrat Jahan | 5050, Karima Begum | 5000. খেয়াল করুন, দ্বিতীয় কোয়েরিতে status <> 'cancelled' আবার হাতে লিখতে হয়েছে — নিয়মটি monthly_revenue-তে আছে, order_summary-তে নেই। কেউ লিখতে ভুলে গেলে Sabbir-এর বাতিল অর্ডারও “আয়” হয়ে যাবে। তাই অনেক দল একটি আলাদা valid_orders view বানায়, যেখানে নিয়মটি একবারই লেখা থাকে।
View বদলানো: CREATE OR REPLACE
নিয়ম বদলাতে পারে: হিসাব শাখা বলল, আয় গুনবে শুধু delivered অর্ডার, কারণ pending অর্ডার বাতিলও হতে পারে। PostgreSQL, MySQL আর Oracle-এ CREATE OR REPLACE VIEW এক স্টেটমেন্টে সংজ্ঞা বদলায় (SQL Server-এ CREATE OR ALTER VIEW). SQLite-এ এটি নেই; সেখানে DROP VIEW করে আবার CREATE VIEW করতে হয়। আর যে view অন্য view-এর ভিত্তি, সেটি মুছলে কী হয়? এই এডিটরে order_summary আর monthly_revenue দুটিই আগে থেকে বানানো:
SELECT type, name FROM sqlite_schema WHERE type = 'view';
-- নতুন নিয়ম: শুধু delivered অর্ডার আয়
DROP VIEW monthly_revenue;
CREATE VIEW monthly_revenue AS
SELECT substr(order_date, 1, 7) AS month, COUNT(*) AS n_orders,
SUM(total) AS revenue
FROM order_summary WHERE status = 'delivered'
GROUP BY substr(order_date, 1, 7);
SELECT * FROM monthly_revenue;
-- ভিত্তির view-টি মুছলে?
DROP VIEW order_summary;
SELECT * FROM monthly_revenue;প্রথম ফলে দুটি view: view | order_summary আর view | monthly_revenue. নতুন সংজ্ঞার ফল: 2025-09 | 3 | 24200 আর 2025-10 | 2 | 4290 — shipped আর pending অর্ডার এখন বাদ, তাই অক্টোবরের আয় 22840 থেকে নেমে 4290 (1007-এর 3250 আর 1010-এর 1040). একই ডেটা, একই প্রশ্ন, শুধু সংজ্ঞা বদলে সংখ্যা পাঁচ ভাগের এক ভাগ — “আয়” শব্দের সংজ্ঞা কত গুরুত্বপূর্ণ, এখানেই দেখা যায়। তারপর DROP VIEW order_summary কোনো আপত্তি ছাড়াই সফল! কিন্তু শেষ স্টেটমেন্টে ত্রুটি: no such table: main.order_summary. SQLite view-এর সংজ্ঞা শুধু লেখা হিসেবে রাখে, কে কার ওপর নির্ভর করে তার হিসাব রাখে না; ভাঙা view ধরা পড়ে ব্যবহারের সময়।
PostgreSQL এখানে কড়া। এটি view বানানোর সময়েই প্রতিটি টেবিল আর কলাম যাচাই করে, আর নির্ভরতার হিসাব রাখে (pg_depend catalog-এ)। তাই যে জিনিসের ওপর কোনো view নির্ভর করে, সেটি সরাসরি মোছা বা বদলানো যায় না:
DROP VIEW order_summary;
-- ERROR: cannot drop view order_summary because other objects depend on it
-- DETAIL: view monthly_revenue depends on view order_summary
-- HINT: Use DROP ... CASCADE to drop the dependent objects too.
DROP VIEW order_summary CASCADE; -- monthly_revenue-ও মুছে যাবে!
ALTER TABLE orders ALTER COLUMN status TYPE varchar(20);
-- ERROR: cannot alter type of a column used by a view or rule
-- CREATE OR REPLACE: পুরোনো কলামগুলো একই নাম, ক্রম আর টাইপে রাখতে হবে;
-- শুধু শেষে নতুন কলাম যোগ করা যায়
CREATE OR REPLACE VIEW monthly_revenue AS
SELECT substr(order_date, 1, 7) AS month, COUNT(*) AS n_orders,
SUM(total) AS revenue
FROM order_summary WHERE status = 'delivered'
GROUP BY 1;পর্ব ২৩-এ দেখেছিলেন, SQLite-ও view-এ ব্যবহৃত কলাম DROP COLUMN করতে দেয় না — কারণ ALTER চলার সময় SQLite পুরো schema নতুন করে parse করে দেখে। কিন্তু view মোছায় সে এই পরীক্ষা করে না। আরেকটি পার্থক্য: SQLite CREATE VIEW-এর সময় কলামের নামও যাচাই করে না — ভুল কলামের view তৈরি হয়ে যায়, ত্রুটি আসে প্রথম ব্যবহারে। আর PostgreSQL view-এর SELECT * তৈরির মুহূর্তেই কলামের তালিকায় বদলে রাখে, তাই পরে base table-এ নতুন কলাম যোগ হলে view-তে আসে না; SQLite প্রতিবার লেখাটি নতুন করে পড়ে, তাই আসে। দুই আচরণ জানা থাকলে migration-এর সময় চমক কম হয়।
View-এর ওপর view-এর ওপর view… বড় প্রতিষ্ঠানে প্রায়ই দেখা যায়, একটি রিপোর্ট পাঁচ-ছয় স্তরের view-এর ওপর দাঁড়িয়ে। প্রতিটি স্তর আলাদা মানুষ, আলাদা সময়ে লিখেছেন। তখন তিনটি সমস্যা: (১) কোনো স্তরে একটি অপ্রয়োজনীয় JOIN বা DISTINCT থাকলে তা প্রতিটি কোয়েরিতে চলে, আর কেউ দেখতে পায় না; (২) GROUP BY-ওয়ালা স্তর মেশানো যায় না, তাই optimizer-এর হাত বাঁধা পড়ে; (৩) নিচের একটি view বদলালে ওপরের কোনটি ভাঙবে, বোঝা কঠিন। নিয়ম: দুই-তিন স্তরের বেশি নয়, আর প্রতিটি view-এর অর্থ নামেই স্পষ্ট।
৫ · View-তে লেখা — updatable view আর WITH CHECK OPTION
View পড়া সহজ। কিন্তু view-তে INSERT, UPDATE বা DELETE করলে কী হবে? view-এর নিজের কোনো সারি নেই, তাই পরিবর্তনটি কোনো base table-এ পৌঁছাতে হবে। প্রশ্ন হলো, DBMS কি নিশ্চিতভাবে বুঝতে পারে কোন টেবিলের কোন সারি বদলাতে হবে? যেমন monthly_revenue-এ “সেপ্টেম্বরের আয় 30000 করো” — কোন অর্ডারের কোন লাইনের দাম কত বাড়বে? এর কোনো একক উত্তর নেই। তাই শুধু সরল view-তেই লেখা যায়। এমন view-কে বলে updatable view.
| নিজে থেকে লেখা যায় (সাধারণ নিয়ম) | লেখা যায় না |
|---|---|
| FROM-এ ঠিক একটি base table (বা আরেকটি updatable view) | একাধিক টেবিলের JOIN (MySQL, Oracle আর SQL Server কিছু ক্ষেত্রে একটি টেবিলের কলাম বদলাতে দেয়) |
| প্রতিটি সারি base table-এর ঠিক একটি সারি থেকে আসে | GROUP BY, HAVING, aggregate (SUM, COUNT…), DISTINCT |
সাধারণ কলাম-রেফারেন্স (name, city) | হিসাব করা কলাম (price * 1.15) — এগুলো পড়া যায়, বদলানো যায় না |
| WHERE-এ যেকোনো শর্ত | UNION / INTERSECT / EXCEPT, LIMIT/OFFSET, window function |
PostgreSQL ৯.৩ (২০১৩) থেকে এমন সরল view নিজে থেকেই updatable; MySQL অনেক আগে থেকে। public_customers আর dhaka_customers দুটিই এই নিয়মে পড়ে। PostgreSQL-এ UPDATE dhaka_customers SET city = 'Khulna' WHERE customer_id = 3 চালালে DBMS এটিকে লিখে নেয় UPDATE customers SET city = 'Khulna' WHERE customer_id = 3 AND city = 'Dhaka' — view-এর WHERE নিজে থেকে যোগ হয়, তাই view-এর বাইরের সারি (যেমন Khulna-র Tanvir) এই view দিয়ে বদলানো যায় না। কিন্তু একটি অদ্ভুত ব্যাপার ঘটে: সারিটি বদলানোর পর সেটি নিজেই view থেকে উধাও হয়ে যায়! এটি আটকাতে আছে WITH CHECK OPTION. ধাপে ধাপে দেখুন:
| CHECK OPTION | |
| Sabbir-এর city (base table) | |
| view-এ সারি | |
| ফল |
- শুরু। view-এ ঢাকার তিনজন গ্রাহক। এখানে কোনো CHECK OPTION নেই — PostgreSQL এই সরল view নিজে থেকেই updatable মনে করে।
- view দিয়ে UPDATE. DBMS view-এর WHERE যোগ করে কোয়েরিটি base table-এর UPDATE-এ বদলায়। Sabbir-এর সারি view-এর ভেতরে ছিল, তাই বদলায়: city এখন Khulna.
- সারি উধাও। নতুন city আর view-এর শর্ত মানে না, তাই পরের SELECT-এ Sabbir নেই। ঢাকা শাখার কর্মী ভাববেন তিনি একজন গ্রাহক মুছে ফেলেছেন। একইভাবে এই view দিয়ে Sylhet-এর একজনকে INSERT করা যেত, যাঁকে view কখনো দেখাবে না।
- WITH CHECK OPTION. এখন DBMS প্রতিটি নতুন বা বদলানো সারি view-এর শর্ত দিয়ে পরীক্ষা করে। (3, Sabbir, Khulna) শর্ত মানে না, তাই পুরো স্টেটমেন্ট ত্রুটি দিয়ে বাতিল, কিছুই বদলায় না।
- শর্ত মানলে চলে। নাম বদলানো সারির city এখনো Dhaka, তাই পরীক্ষায় পাস। CHECK OPTION শুধু view-এর WHERE-এর শর্ত দেখে — view-কে একটি “এই সীমার ভেতরে থাকো” নিয়মে পরিণত করে।
CREATE VIEW dhaka_customers AS
SELECT customer_id, name, city FROM customers
WHERE city = 'Dhaka'
WITH CHECK OPTION; -- standard-এ ডিফল্ট CASCADED
UPDATE dhaka_customers SET city = 'Khulna' WHERE customer_id = 3;
-- PostgreSQL: ERROR: new row violates check option for view "dhaka_customers"
-- MySQL: ERROR 1369 (HY000): CHECK OPTION failed 'shop.dhaka_customers'
-- view-এর ওপর view: LOCAL = শুধু নিজের শর্ত, CASCADED = নিচের সব view-এর শর্তও
CREATE VIEW dhaka_new AS
SELECT * FROM dhaka_customers WHERE customer_id > 5
WITH LOCAL CHECK OPTION;SQLite-এ এসব কিছুই নেই: SQLite-এর প্রতিটি view শুধু পড়ার জন্য, সে যত সরলই হোক, আর WITH CHECK OPTION লিখলে syntax error আসে। দেখুন:
-- public_customers আগে থেকে বানানো (শুধু এক টেবিল, কোনো GROUP BY নেই)
UPDATE public_customers SET city = 'Gazipur' WHERE customer_id = 7;ত্রুটি: cannot modify public_customers because it is a view. PostgreSQL বা MySQL-এ এই একই UPDATE কোনো সমস্যা ছাড়াই customers-এ Imran-এর city বসিয়ে দিত। SQLite-এর ডিজাইন সরল রাখার সিদ্ধান্ত: view-তে লেখার নিয়ম সে নিজে অনুমান করে না, আপনাকে স্পষ্ট করে বলে দিতে হয় — পরের অংশের trigger দিয়ে।
৬ · INSTEAD OF trigger — view-তে লেখার নিজের নিয়ম
Trigger হলো ডেটাবেসে রাখা কিছু SQL, যা কোনো ঘটনায় (INSERT, UPDATE, DELETE) DBMS নিজে চালায় — পর্ব ২৯-এ বিস্তারিত দেখবেন। INSTEAD OF trigger একটি বিশেষ ধরন, যা SQLite আর PostgreSQL-এ শুধু view-এ বসে (SQL Server-এ টেবিলেও বসানো যায়)। view-এ কেউ INSERT করলে DBMS সেই INSERT চালায় না; তার বদলে (instead of) trigger-এর ভেতরের SQL চালায়। ভেতরে NEW.column মানে যে সারিটি লেখার চেষ্টা হচ্ছে, তার মান (UPDATE আর DELETE-এ পুরোনো মান OLD.column). এতে view-তে লেখার মানে আপনি নিজে ঠিক করে দেন — এমনকি জটিল view-তেও।
CREATE VIEW public_customers AS
SELECT customer_id, name, city, joined_on FROM customers;
-- view-এ INSERT এলে: customers-এ লেখো, phone ফাঁকা রাখো
CREATE TRIGGER public_customers_ins
INSTEAD OF INSERT ON public_customers
BEGIN
INSERT INTO customers (customer_id, name, city, phone, joined_on)
VALUES (NEW.customer_id, NEW.name, NEW.city, NULL, NEW.joined_on);
END;
INSERT INTO public_customers VALUES (9, 'Sumaiya Islam', 'Barishal', '2025-10-20');
SELECT * FROM customers WHERE customer_id >= 8;শেষ SELECT-এ দুটি সারি: 8 | Mitu Rani | Dhaka | 01411000008 | 2025-08-12 আর 9 | Sumaiya Islam | Barishal | NULL | 2025-10-20. হিসাব শাখা ফোন নম্বর না দেখেও নতুন গ্রাহক যোগ করতে পারল। একটি মজার জিনিস দেখুন: INSERT-এর লাইনে লেখা “০টি সারি বদলেছে”, অথচ customers-এ একটি সারি এসেছে! SQLite-এর নিয়ম হলো, যে পরিবর্তন INSTEAD OF trigger আটকে নিজে করে, সেটি স্টেটমেন্টের “বদলানো সারির সংখ্যা”-য় গোনা হয় না। অ্যাপের কোড যদি “০টি সারি মানে ব্যর্থ” ধরে নেয়, এখানে ভুল বুঝবে — তাই এমন ক্ষেত্রে ফল SELECT দিয়ে যাচাই করুন।
একই কৌশলে INSTEAD OF UPDATE OF city ON public_customers লিখে শুধু city বদলানোর অনুমতি দেওয়া যায় (অনুশীলন ৪-এ লিখবেন), আর INSTEAD OF DELETE দিয়ে “মুছে ফেলা” মানে আসলে soft delete — একটি deleted_at কলামে সময় লেখা — বানানো যায়। PostgreSQL, SQL Server আর Oracle-এও INSTEAD OF trigger আছে; জটিল (JOIN বা GROUP BY-ওয়ালা) view-কে লেখার উপযোগী করতে সেখানেও এটিই পথ। MySQL-এ INSTEAD OF trigger নেই।
৭ · Materialized view — ফল জমিয়ে রাখা
সাধারণ view প্রতিবার নতুন করে হিসাব হয়। দোকানে ১০টি অর্ডারে তাতে সমস্যা নেই। কিন্তু একটি বড় ই-কমার্স সাইটের কয়েক কোটি অর্ডার-লাইনের ওপর “পণ্যভিত্তিক মোট বিক্রি” প্রতিবার হিসাব করতে কয়েক মিনিট লাগতে পারে — আর dashboard-টি দিনে হাজারবার খোলা হয়। সমাধান: হিসাবটি একবার করে ফল ডিস্কে রেখে দেওয়া, তারপর সবাই সেই জমানো ফল পড়বে। এটিই materialized view: একটি view, যার ফল আসল টেবিলের মতো জমা থাকে।
বোর্ড পরীক্ষার ফল প্রকাশের দিন বোর্ড প্রতিটি স্কুলের পাসের হারের একটি তালিকা ছাপায়। লাখো শিক্ষার্থীর খাতা থেকে প্রতিবার কেউ সেটি নতুন করে হিসাব করে না — একবার হিসাব করে ছাপানো তালিকাই সবাই পড়ে। পড়া খুব দ্রুত। কিন্তু পুনর্নিরীক্ষণে কারও ফল বদলালে ছাপানো তালিকা আর সঠিক থাকে না, যতক্ষণ না নতুন তালিকা ছাপা হয়। Materialized view-এর সুবিধা আর ঝুঁকি দুটিই এই ছবিতে আছে।
জমানো ফল base table বদলালে নিজে থেকে বদলায় না। তাকে নতুন করে হিসাব করানোকে বলে refresh, আর refresh-এর আগের পুরোনো ফলকে বলে stale (বাসি) ডেটা। প্রধান DBMS-গুলোতে চেহারাটা এমন:
CREATE MATERIALIZED VIEW mv_product_sales AS
SELECT p.product_id, p.name, SUM(i.quantity) AS units_sold
FROM products p JOIN order_items i ON i.product_id = p.product_id
GROUP BY p.product_id, p.name; -- এখনই চলে, ফল জমা হয়
SELECT * FROM mv_product_sales; -- জমানো ফল পড়া, কোনো JOIN চলে না
REFRESH MATERIALIZED VIEW mv_product_sales; -- পুরো ফল নতুন করে; চলার সময় পড়াও আটকে থাকে
CREATE UNIQUE INDEX ON mv_product_sales (product_id);
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_product_sales; -- পড়া চালু রেখেPostgreSQL ৯.৩ (২০১৩) থেকে materialized view আছে, ৯.৪ থেকে CONCURRENTLY. সাধারণ REFRESH চলার সময় view-টি পুরো আটকে রাখে (ACCESS EXCLUSIVE lock), তাই dashboard অপেক্ষা করে। CONCURRENTLY নতুন ফল আলাদা হিসাব করে, পুরোনোর সাথে তুলনা করে শুধু পার্থক্যটুকু বসায় — পড়া চলতে থাকে, কিন্তু কাজটি ধীর, আর এর জন্য materialized view-এ অন্তত একটি UNIQUE index লাগে। PostgreSQL নিজে থেকে কখনো refresh করে না; সাধারণত একটি নির্ধারিত কাজ (cron বা pg_cron) প্রতি ঘণ্টায় বা রাতে চালায়।
-- base table-এর প্রতিটি পরিবর্তন একটি log-এ লেখা হবে
CREATE MATERIALIZED VIEW LOG ON order_items
WITH ROWID, SEQUENCE (product_id, quantity) INCLUDING NEW VALUES;
CREATE MATERIALIZED VIEW mv_product_sales
BUILD IMMEDIATE
REFRESH FAST ON COMMIT -- প্রতিটি COMMIT-এ শুধু পরিবর্তনটুকু বসাও
ENABLE QUERY REWRITE -- optimizer নিজে এটি ব্যবহার করতে পারে
AS SELECT product_id, SUM(quantity) AS units_sold,
COUNT(quantity) AS cnt_qty, COUNT(*) AS cnt
FROM order_items GROUP BY product_id;Oracle-এ materialized view বহু পুরোনো (আগে “snapshot” নামে পরিচিত ছিল)। Fast refresh পুরো ফল নতুন করে না বানিয়ে log থেকে শুধু পরিবর্তিত সারি নিয়ে জমানো ফল হালনাগাদ করে — একে বলে incremental maintenance. SUM-এর পাশে COUNT কলাম কেন? কোনো পণ্যের শেষ লাইনটি মুছে গেলে COUNT শূন্য হয়, আর তখন সারিটিই মুছতে হবে — শুধু SUM দেখে সেটি বোঝা যায় না। Query rewrite আরও চমকপ্রদ: কেউ সরাসরি order_items-এ GROUP BY লিখলেও optimizer চুপচাপ materialized view থেকে উত্তর দিতে পারে।
CREATE VIEW dbo.product_sales WITH SCHEMABINDING AS
SELECT product_id, SUM(quantity) AS units_sold, COUNT_BIG(*) AS cnt
FROM dbo.order_items GROUP BY product_id;
-- unique clustered index বসালেই ফল জমা হয় (indexed view)
CREATE UNIQUE CLUSTERED INDEX ix_product_sales ON dbo.product_sales (product_id);SQL Server-এ নাম indexed view. এটি প্রতিটি INSERT/UPDATE/DELETE-এর সাথে একই transaction-এ হালনাগাদ হয়, তাই কখনো stale হয় না — দাম দিতে হয় প্রতিটি লেখায় বাড়তি কাজ দিয়ে। MySQL-এ কোনো materialized view নেই। সেখানে সাধারণ টেবিল বানিয়ে নির্ধারিত সময়ে (EVENT scheduler বা বাইরের cron দিয়ে) নতুন করে ভরা হয়, অথবা trigger দিয়ে হালনাগাদ রাখা হয় — ঠিক যেটি পরের অংশে SQLite-এ করবেন।
৮ · SQLite-এ materialized view-এর অনুকরণ আর stale ফল
SQLite-এ materialized view নেই, কিন্তু ধারণাটি সহজেই বানানো যায়। পর্ব ১৩-এর CREATE TABLE AS SELECT একটি কোয়েরির ফল দিয়ে আসল টেবিল বানায় — এটিই “materialize” করা। Refresh মানে পুরোনো সারি মুছে একই কোয়েরি দিয়ে আবার ভরা। নিচে একটি নতুন বিক্রির পর জমানো ফল আর আসল ডেটা কীভাবে আলাদা হয়ে যায়, দেখুন:
-- materialize: ফল একটি আসল টেবিলে
CREATE TABLE mv_product_sales AS
SELECT p.product_id, p.name, SUM(i.quantity) AS units_sold
FROM products p JOIN order_items i ON i.product_id = p.product_id
GROUP BY p.product_id, p.name;
-- নতুন বিক্রি: অর্ডার 1008-এ আরও ৩টি Headphone
INSERT INTO order_items VALUES (1008, 102, 3, 1200);
SELECT units_sold AS stored FROM mv_product_sales WHERE product_id = 102;
SELECT SUM(quantity) AS live FROM order_items WHERE product_id = 102;
-- refresh: মুছে, নতুন করে ভরা
DELETE FROM mv_product_sales;
INSERT INTO mv_product_sales
SELECT p.product_id, p.name, SUM(i.quantity)
FROM products p JOIN order_items i ON i.product_id = p.product_id
GROUP BY p.product_id, p.name;
SELECT units_sold AS after_refresh FROM mv_product_sales WHERE product_id = 102;stored = 3, কিন্তু live = 6. জমানো টেবিল এখনো পুরোনো হিসাব দিচ্ছে: অর্ডার 1001-এর ২টি আর 1007-এর ১টি; নতুন ৩টি সে জানে না। এটিই stale ফল। DELETE-এ “৯টি সারি বদলেছে” (যে ৯টি পণ্য অন্তত একবার বিক্রি হয়েছে; Power Bank কখনো বিক্রি হয়নি, তাই তার সারিই নেই), INSERT-এও ৯টি, আর refresh-এর পর after_refresh = 6. বাস্তবে DELETE আর INSERT একটি transaction-এর ভেতরে (BEGIN … COMMIT, পর্ব ৪৪) চালাতে হয়, নইলে মাঝের মুহূর্তে কেউ পড়লে একটি খালি টেবিল দেখবে।
সাধারণ view আর materialized view-এর ফল সময়ের সাথে কীভাবে আলাদা হয়, একটি দিনের ঘটনাক্রমে দেখুন (Headphone-এর মোট বিক্রি):
| সাধারণ view | |
| materialized view | |
| পার্থক্য |
- ১০:০০ — refresh. জমানো ফল সদ্য হিসাব হয়েছে: Headphone ৩টি (1001-এ ২, 1007-এ ১)। এই মুহূর্তে view আর materialized view একই কথা বলে।
- ১০:০৫ — নতুন বিক্রি। অর্ডার 1008-এ ৩টি Headphone যোগ হলো। সাধারণ view পরের পড়াতেই ৬ দেখায়, কারণ সে প্রতিবার order_items পড়ে। materialized view এখনো ৩ — সে order_items-এর দিকে তাকায়ই না।
- ১০:২০ — আরেকটি বিক্রি। পার্থক্য বাড়ছে: আসল ৭, জমানো ৩. যে ম্যানেজার এই dashboard দেখে স্টক অর্ডার করছেন, তিনি ভুল সংখ্যা দেখছেন।
- ১০:৩০ — আবার refresh. পুরো কোয়েরি আবার চলে, ফল ৭. দুটি আবার সমান — পরের পরিবর্তন পর্যন্ত। refresh-এর ব্যবধান যত বেশি, ফল তত পুরোনো হতে পারে; যত কম, refresh-এর খরচ তত বেশিবার।
- বিকল্প — trigger. প্রতিটি INSERT-এর সাথে একটি trigger জমানো সংখ্যায় যোগ করলে ফল কখনো stale হয় না (SQL Server-এর indexed view-এর মতো)। দাম সরে যায় লেখার দিকে: প্রতিটি বিক্রিতে একটি বাড়তি লেখা, আর একই পণ্যের ওপর অনেক লেখা একসাথে এলে তারা একই সারির জন্য অপেক্ষা করে।
শেষ ধাপের trigger-পদ্ধতিটি এবার চালিয়ে দেখুন। জমানো টেবিল product_sales-এ product_id primary key; trigger পর্ব ১৪-এর upsert (ON CONFLICT … DO UPDATE) দিয়ে নতুন পণ্য হলে সারি বানায়, পুরোনো হলে যোগ করে:
CREATE TABLE product_sales (product_id INTEGER PRIMARY KEY, units_sold INTEGER NOT NULL);
INSERT INTO product_sales
SELECT product_id, SUM(quantity) FROM order_items GROUP BY product_id;
-- প্রতিটি নতুন লাইনের সাথে জমানো ফল হালনাগাদ
CREATE TRIGGER sales_on_insert AFTER INSERT ON order_items
BEGIN
INSERT INTO product_sales VALUES (NEW.product_id, NEW.quantity)
ON CONFLICT (product_id) DO UPDATE SET units_sold = units_sold + NEW.quantity;
END;
INSERT INTO order_items VALUES (1008, 102, 3, 1200); -- পুরোনো পণ্য
INSERT INTO order_items VALUES (1006, 103, 2, 1800); -- আগে কখনো বিক্রি হয়নি
SELECT * FROM product_sales WHERE product_id IN (102, 103);ফল: 102 | 6 আর 103 | 2. Headphone-এর সারি আগে থেকেই ছিল, তাই ON CONFLICT শাখা 3 + 3 = 6 বসিয়েছে। Power Bank (103)-এর কোনো সারি ছিল না, তাই সাধারণ INSERT শাখা নতুন সারি বানিয়েছে। কোনো refresh লাগেনি। কিন্তু এই trigger শুধু INSERT সামলায় — কেউ order_items-এর quantity UPDATE করলে বা লাইন DELETE করলে জমানো ফল ভুল হয়ে যাবে। পূর্ণ সমাধানে তিনটি trigger লাগে (INSERT, UPDATE, DELETE), আর MIN/MAX-এর মতো হিসাবে DELETE সামলানো আরও কঠিন — সর্বোচ্চ মানটি মুছে গেলে পরের সর্বোচ্চটি খুঁজতে আবার base table পড়তে হয়।
| বিষয় | সাধারণ view | materialized view |
|---|---|---|
| কী জমা থাকে | শুধু SELECT-এর লেখা | SELECT-এর পুরো ফল (ডিস্কে) |
| ফল কতটা নতুন | সবসময় সর্বশেষ | শেষ refresh-এর সময়ের (trigger বা ON COMMIT না থাকলে) |
| পড়ার খরচ | প্রতিবার পুরো কোয়েরি | একটি টেবিল পড়া; নিজের index-ও রাখা যায় |
| লেখার খরচ | কিছু না | refresh-এর খরচ, বা প্রতিটি লেখায় বাড়তি কাজ |
| জায়গা | প্রায় শূন্য | ফলের আকারের সমান |
| কখন | সরলতা, নিরাপত্তা, এক সংজ্ঞা | ভারী aggregate, dashboard, রিপোর্ট — যেখানে কিছুটা পুরোনো ফল চলে |
৯ · পারফরম্যান্সের ভুল ধারণা আর view দিয়ে অধিকার
“View দিলে কোয়েরি দ্রুত হয়” — না
অংশ ৩-এ দেখেছেন, সাধারণ view-এর কোয়েরি খুলে base table-এর কোয়েরি হয়ে যায়। তাই view নিজে থেকে কোনো কোয়েরিকে দ্রুত করে না — কোনো ফল আগে থেকে হিসাব করা নেই, কোনো index তৈরি হয়নি। ধীর কোয়েরিকে view-এ মুড়লে সেটি সমান ধীরই থাকে। উল্টো দিকে, view সাধারণত ধীরও করে না — যদি না view-এর ভেতরে এমন কিছু থাকে যা আপনার প্রশ্নের দরকার নেই। যেমন order_summary থেকে শুধু order_id আর status চাইলেও view-টি customers আর order_items-এর JOIN আর GROUP BY চালাবে। গতি চাইলে আসল হাতিয়ার index (পর্ব ৪০–৪১) আর materialized view; কোনটি কাজ করছে, তা বলবে query plan.
GRANT: শুধু view-এর অধিকার
অংশ ২-এর নিরাপত্তা আসল হয় privilege (অধিকার) দিয়ে। সার্ভার-DBMS-এ প্রতিটি ব্যবহারকারী বা role শুধু সেই টেবিল আর view পড়তে বা লিখতে পারে, যার অধিকার তাকে GRANT দিয়ে দেওয়া হয়েছে। কৌশলটি হলো: হিসাব শাখার role-কে customers টেবিলে কোনো অধিকার না দিয়ে শুধু public_customers view-এ SELECT দেওয়া। SQLite একটি ফাইল-ভিত্তিক embedded ডেটাবেস — ফাইলটি যে খুলতে পারে, সে সব পড়তে পারে — তাই সেখানে GRANT নেই; উদাহরণটি PostgreSQL-এর:
CREATE ROLE accounts_office LOGIN;
GRANT SELECT ON public_customers TO accounts_office; -- শুধু view
-- customers টেবিলে কোনো GRANT নেই
-- accounts_office হিসেবে লগইন করে:
SELECT name, city FROM public_customers; -- চলে
SELECT name, phone FROM customers;
-- ERROR: permission denied for table customers
-- পর্ব ৫১-এর ঝলক: সারির স্তরে নিয়ম (row-level security)
ALTER TABLE customers ENABLE ROW LEVEL SECURITY;
CREATE POLICY dhaka_branch_only ON customers
FOR SELECT TO dhaka_branch USING (city = 'Dhaka');view কীভাবে customers পড়ে, যখন ব্যবহারকারীর নিজের সেই অধিকার নেই? কারণ PostgreSQL-এ view ডিফল্টভাবে base table পড়ে view-এর মালিকের অধিকারে (MySQL-এ একই ধারণার নাম SQL SECURITY DEFINER). PostgreSQL ১৫ থেকে WITH (security_invoker = true) দিয়ে উল্টোটা — যিনি পড়ছেন তাঁর অধিকারে — বেছে নেওয়া যায়। কলাম লুকানোর আরেকটি পথ কলামভিত্তিক GRANT: GRANT SELECT (customer_id, name, city) ON customers TO …. আর প্রতিটি শাখার জন্য আলাদা view না বানিয়ে একটি টেবিলেই সারিভিত্তিক নিয়ম বসানো যায় — row-level security, পর্ব ৫১-এ বিস্তারিত। মূল নীতিটি least privilege: যার যতটুকু কাজে লাগে, ঠিক ততটুকুই।
View-এর ধারণা relational ডেটাবেসের শুরু থেকেই। IBM-এর System R প্রকল্পে (১৯৭০-এর দশক) view ছিল দুই কাজের হাতিয়ার: ব্যবহারকারীকে সরল ছবি দেখানো, আর অধিকার নিয়ন্ত্রণ। ১৯৭৬ সালে Patricia Griffiths আর Bradford Wade ACM TODS-এ System R-এর অধিকার-ব্যবস্থা বর্ণনা করেন, যেখানে GRANT আর REVOKE-এর সাথে view দিয়ে টেবিলের অংশবিশেষ দেখানোর কৌশল ছিল — আজকের SQL-এর GRANT প্রায় সেই রূপেই টিকে আছে। প্রথম SQL standard (১৯৮৬)-এই CREATE VIEW আর WITH CHECK OPTION অন্তর্ভুক্ত ছিল। Materialized view-এর গবেষণা জোর পায় ১৯৮০-এর দশকের মাঝামাঝি, যখন জমানো ফল দক্ষভাবে হালনাগাদ রাখার পদ্ধতি নিয়ে কাজ শুরু হয়; ১৯৯০-এর দশকে data warehouse-এর (পর্ব ৫৫) উত্থানের সাথে এটি বাণিজ্যিক DBMS-গুলোর প্রধান বৈশিষ্ট্য হয়ে ওঠে।
১০ · AI-এর যুগে view
AI-কে “একটি রিপোর্টের view বানিয়ে দাও” বললে সে দ্রুত একটি সুন্দর CREATE VIEW লেখে, আর সেটি চলেও। কিন্তু view-এর ভুল একবার ঢুকলে প্রতিটি রিপোর্টে ছড়িয়ে পড়ে, কারণ সবাই সেটিই ব্যবহার করে। যা প্রায়ই দেখা যায়:
| AI-এর ভুল | ফল | কীভাবে ধরবেন |
|---|---|---|
| view-এ inner JOIN, যেখানে LEFT JOIN দরকার | যাঁর অর্ডার বা enrollment নেই, তিনি নীরবে বাদ | view আর base table-এর সারি-সংখ্যা মেলান |
| JOIN-এর পরে SUM, এক-থেকে-অনেক সম্পর্কে | টাকার হিসাব দ্বিগুণ-তিনগুণ | একটি অর্ডারের মোট হাতে হিসাব করে মেলান |
| ব্যবসার নিয়ম ধরে নেওয়া (বাতিল অর্ডারও আয়) | রিপোর্টের সংখ্যা ভুল | “আয়”, “সক্রিয় গ্রাহক”-এর সংজ্ঞা লিখিতভাবে দিন |
| “view দিলে দ্রুত হবে” দাবি | ধীর কোয়েরি ধীরই থাকে | EXPLAIN দিয়ে আগে-পরে তুলনা |
| SQLite-এ WITH CHECK OPTION বা CREATE OR REPLACE VIEW, MySQL-এ MATERIALIZED VIEW | syntax error | DBMS-এর নাম আর সংস্করণ প্রম্পটে বলুন |
| নিরাপত্তার view, কিন্তু base table-এর অধিকার খোলা | লুকানো কলাম সরাসরি পড়া যায় | সেই role হিসেবে লগইন করে base table পড়ার চেষ্টা করুন |
AI-কে view চাইলে schema, ব্যবসার নিয়ম আর DBMS — তিনটিই দিন, আর যাচাইয়ের উপায়ও চেয়ে নিন:
AI-এর লেখা view হাতে পেলে — যাচাইয়ের চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- View আর টেবিলের পার্থক্য? — টেবিল ডেটা রাখে; সাধারণ view শুধু একটি সংরক্ষিত SELECT, যা প্রতিবার base table থেকে হিসাব হয়।
- View আর materialized view-এর পার্থক্য? — materialized view ফল ডিস্কে জমিয়ে রাখে; পড়া দ্রুত, কিন্তু refresh না হলে ফল পুরোনো।
- সব view-তে কি UPDATE করা যায়? — না; সাধারণত এক টেবিলের, GROUP BY/aggregate/DISTINCT ছাড়া view-তে যায়; বাকিগুলোয় INSTEAD OF trigger লাগে।
- WITH CHECK OPTION কী করে? — view দিয়ে এমন INSERT/UPDATE আটকায়, যার ফলে সারিটি view-এর শর্তের বাইরে চলে যেত।
- View কি কোয়েরি দ্রুত করে? — সাধারণ view নয়; optimizer সেটি খুলে base table-এর কোয়েরি বানায়। দ্রুত করে index বা materialized view.
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১কেন সব view-তে লেখা যায় না? “view update problem” বলতে গবেষকেরা কী বোঝান, আর INSTEAD OF trigger এই সমস্যার কী সমাধান দেয়?
View হলো base table-এর ওপর একটি ফাংশন: টেবিলের অবস্থা দিলে view-এর ফল পাওয়া যায়। view-তে লেখা মানে উল্টো দিকে যাওয়া — view-এর নতুন অবস্থা দিয়ে base table-এর নতুন অবস্থা খোঁজা। আর উল্টো ফাংশন প্রায়ই অস্পষ্ট। monthly_revenue-এ “সেপ্টেম্বরের আয় 29480 থেকে 30000 করো” বললে 520 টাকা কোন অর্ডারে, কোন লাইনে যাবে? একটি লাইনের দাম বাড়ানো, নতুন লাইন যোগ, কোনো বাতিল অর্ডারকে delivered করা — সবই view-কে 30000 দেখাবে, কিন্তু ডেটাবেসের অবস্থা সম্পূর্ণ আলাদা হবে। DBMS-এর পক্ষে এর একটিকে “সঠিক” বলার কোনো উপায় নেই।
JOIN-ওয়ালা view-তেও সমস্যা। order_summary-এর একটি সারি মুছলে কী মুছবে — অর্ডার, তার লাইনগুলো, নাকি গ্রাহক? গ্রাহক মুছলে তাঁর অন্য অর্ডারগুলোও view থেকে উধাও হবে, যা কেউ চায়নি। এমন অনাকাঙ্ক্ষিত পার্শ্বপ্রতিক্রিয়াকে গবেষণায় বলে side effect.
১৯৮০-এর দশকের শুরুতে এটি একটি গুরুত্বপূর্ণ গবেষণার বিষয় ছিল। François Bancilhon আর Nicolas Spyratos ১৯৮১ সালে “constant complement” ধারণা দেন: একটি view-update তখনই নির্দিষ্টভাবে অনুবাদযোগ্য, যখন view-এর বাইরের অংশ (complement) অপরিবর্তিত রেখে সেটি করা যায়। Umeshwar Dayal আর Philip Bernstein ১৯৮২ সালে ACM TODS-এ দেখান, কোন শর্তে view-এর একটি update-কে base table-এর update-এ সঠিকভাবে, পার্শ্বপ্রতিক্রিয়া ছাড়া অনুবাদ করা যায়। এই কাজগুলোর ব্যবহারিক ফল হলো আজকের রক্ষণশীল নিয়ম: এক টেবিল, প্রতিটি view-সারি একটি base-সারি থেকে, aggregate নেই — কারণ তখন অনুবাদ নিশ্চিতভাবে এক।
INSTEAD OF trigger সমস্যাটির একটি ব্যবহারিক সমাধান: DBMS অনুমান করে না, অনুবাদের দায়িত্ব নেন ডিজাইনার। আপনি জানেন “public_customers-এ নতুন গ্রাহক মানে customers-এ phone ফাঁকা রেখে একটি সারি” — এই জ্ঞান trigger-এ লিখে দেন। দায়িত্বও আপনার: trigger ভুল লিখলে view-তে লেখা সফল দেখাবে, কিন্তু ডেটা ভুল জায়গায় যাবে।
মূল উপলব্ধি: view পড়া একমুখী হিসাব, তাই সবসময় সম্ভব; view-তে লেখা উল্টো হিসাব, যা শুধু সরল ক্ষেত্রে এক উত্তর দেয়। বাকি ক্ষেত্রে মানুষকেই নিয়ম লিখে দিতে হয়।
প্র ০২একটি ই-কমার্স কোম্পানির dashboard ধীর। একজন বললেন materialized view বানাতে, আরেকজন বললেন Redis-এর মতো cache-এ ফল রাখতে, তৃতীয়জন বললেন orders টেবিলে একটি total কলাম যোগ করতে। তিনটির মধ্যে মিল আর পার্থক্য কী, আর কীভাবে বাছবেন?
তিনটিই আসলে একই কৌশল: হিসাবের ফল আগে থেকে জমিয়ে রাখা, যাতে পড়ার সময় কাজ কম হয়। তিনটিরই একই মৌলিক দাম: base ডেটা বদলালে জমানো ফল পুরোনো হতে পারে, আর তাকে হালনাগাদ রাখার দায়িত্ব কারও না কারও। পার্থক্য শুধু সেই দায়িত্ব কোথায় থাকে।
Materialized view-এ দায়িত্ব ডেটাবেসের। সংজ্ঞাটি SQL-এ লেখা, refresh এক লাইনের কমান্ড, আর ফল ডেটাবেসের ভেতরেই — অন্য টেবিলের সাথে JOIN করা যায়, index বসানো যায়। Oracle-এ fast refresh আর query rewrite থাকলে অ্যাপের কোডও বদলাতে হয় না। দুর্বলতা: PostgreSQL-এ refresh পুরো কোয়েরি আবার চালায়, তাই বড় ফলে refresh নিজেই ভারী।
Cache (Redis, পর্ব ৫৮) ডেটাবেসের বাইরে, অ্যাপের দায়িত্বে। পড়া অত্যন্ত দ্রুত (মেমরি থেকে), আর ডেটাবেসের ওপর চাপ কমে। কিন্তু কখন cache মুছতে হবে (invalidation), সেটি অ্যাপের প্রতিটি লেখার জায়গায় মনে রাখতে হয়। প্রোগ্রামিং জগতে একটি পুরোনো রসিকতা আছে: কম্পিউটার বিজ্ঞানের দুটি কঠিন কাজের একটি হলো cache invalidation.
Denormalized কলাম (orders.total, পর্ব ৩৮) টেবিলের ভেতরেই, সাধারণত trigger বা অ্যাপের কোড দিয়ে হালনাগাদ। একটি সারির জন্য দ্রুত, কিন্তু order_items আর orders.total অমিল হওয়ার সুযোগ তৈরি হয় — normalization ঠিক যে সমস্যা দূর করতে চায়।
বাছাইয়ের প্রশ্নগুলো: ফল কতটা পুরোনো চলে? মাসিক বিক্রির রিপোর্ট এক ঘণ্টা পুরোনো হলে কেউ টের পায় না — materialized view-এর আদর্শ ক্ষেত্র। গ্রাহকের নিজের অর্ডারের মোট এক সেকেন্ডও ভুল দেখানো চলে না — সেখানে সাধারণ view বা লেনদেনের সাথে হালনাগাদ হওয়া কলাম। কোনটি বেশি ঘটে, পড়া না লেখা? পড়া হাজারগুণ বেশি হলে জমানোর লাভ বেশি। আর কে রক্ষণাবেক্ষণ করবেন? একটি ছোট দলের পক্ষে ডেটাবেসের ভেতরের সমাধান সাধারণত নিরাপদ।
মূল উপলব্ধি: প্রতিটি জমানো ফল একটি চুক্তি — “এতটুকু পুরোনো চলবে”। আগে সেই চুক্তিটি লিখুন, তারপর হাতিয়ার বাছুন।
প্র ০৩নিরাপত্তার জন্য view যথেষ্ট কি? PostgreSQL-এ “security_barrier” নামের একটি view-বিকল্প কেন আনতে হয়েছিল?
View দিয়ে সারি লুকানো একটি সূক্ষ্ম ফাঁক তৈরি করতে পারে, আর সেটি এসেছে ঠিক অংশ ৩-এর view expansion থেকে। ধরুন dhaka_customers view-এর মালিক DBA, আর ঢাকা শাখার কর্মীকে শুধু এই view পড়ার অধিকার দেওয়া হয়েছে। কর্মী নিজে একটি ফাংশন লিখলেন, যা প্রতিটি প্রাপ্ত সারির মান একটি বার্তায় ছাপে আর TRUE ফেরত দেয়, তারপর লিখলেন SELECT * FROM dhaka_customers WHERE my_func(name, city).
View expansion-এর পর কোয়েরিটি হয়ে যায় WHERE city = 'Dhaka' AND my_func(name, city). AND-এর দুই শর্তের কোনটি আগে চলবে, optimizer খরচ দেখে ঠিক করে। ফাংশনটিকে “সস্তা” ঘোষণা করা থাকলে সেটি আগে চলতে পারে — তখন city-র শর্ত পরীক্ষার আগেই প্রতিটি সারি, Khulna-র Tanvir-সহ, ফাংশনের হাতে যায়, আর বার্তায় ছাপা হয়ে যায়। view-এর ফলে Tanvir আসেননি, কিন্তু তাঁর তথ্য ফাঁস হয়ে গেছে। এই ধরনের view-কে বলা হয় “leaky view”.
PostgreSQL ৯.২ (২০১২) এর সমাধান আনে: CREATE VIEW … WITH (security_barrier). এমন view-এর শর্ত সবসময় বাইরের কোয়েরির ব্যবহারকারী-লেখা ফাংশনের আগে চলে; শুধু “leakproof” চিহ্নিত নিরাপদ ফাংশন (যেমন সাধারণ তুলনা) ভেতরে ঠেলা যায়। দাম: optimizer-এর স্বাধীনতা কমে, তাই কোয়েরি ধীর হতে পারে। নিরাপত্তা আর গতির এই টানাপোড়েন ডেটাবেস ডিজাইনে বারবার ফিরে আসে।
এখান থেকে আরও বড় শিক্ষা: view একটি সুবিধাজনক নিরাপত্তা-স্তর, কিন্তু একমাত্র স্তর হওয়া উচিত নয়। সারিভিত্তিক নিয়মের জন্য PostgreSQL ৯.৫ থেকে row-level security আছে, যেখানে নিয়মটি টেবিলের সাথে বাঁধা, তাই কোনো view এড়িয়ে যাওয়ার পথ থাকে না। এর সাথে লাগে least privilege, অডিট লগ, আর সংবেদনশীল কলামের encryption — পর্ব ৫১-এর বিষয়।
মূল উপলব্ধি: optimizer-এর কাজ দ্রুত উত্তর দেওয়া, গোপনীয়তা রক্ষা নয়। নিরাপত্তার view-এ তাই স্পষ্ট করে বলতে হয়: “আমার শর্ত আগে”।
প্র ০৪দোকানের customers টেবিলটি ভেঙে customers আর customer_contacts (phone, email) — দুটি টেবিল বানাতে হবে, কিন্তু পুরোনো অ্যাপের শতাধিক কোয়েরি এখনো customers.phone পড়ে। view কীভাবে এই পরিবর্তনকে নিরাপদ করতে পারে, আর তত্ত্বে এর নাম কী?
এটি পর্ব ৩-এর logical data independence-এর একেবারে বাস্তব রূপ: conceptual schema বদলাবে, কিন্তু external schema — অ্যাপ যেভাবে ডেটা দেখে — একই থাকবে। ১৯৭৫ সালের ANSI/SPARC প্রতিবেদনের তিন-স্তরের নকশার মূল প্রতিশ্রুতিই এটি, আর SQL-এ সেটি রক্ষার প্রধান হাতিয়ার view.
একটি নিরাপদ পরিকল্পনা: (১) নতুন টেবিল customers_core(customer_id, name, city, joined_on) আর customer_contacts(customer_id, phone, email) বানিয়ে ডেটা কপি। (২) পুরোনো customers টেবিলের নাম বদলে সরিয়ে রাখা, আর একই নামে একটি view: CREATE VIEW customers AS SELECT c.customer_id, c.name, c.city, k.phone, c.joined_on FROM customers_core c LEFT JOIN customer_contacts k ON k.customer_id = c.customer_id. LEFT JOIN জরুরি — Tanvir-এর মতো যাঁর phone নেই, তিনি যেন বাদ না পড়েন। (৩) পুরোনো অ্যাপ লেখেও, তাই INSTEAD OF INSERT/UPDATE trigger, যা একটি লেখাকে দুই টেবিলে ভাগ করে দেয়। (৪) অ্যাপের কোড ধীরে ধীরে নতুন টেবিলে সরানো, আর সবাই সরে গেলে view মুছে ফেলা।
এটি পর্ব ২৩-এর expand–contract কৌশলেরই একটি রূপ: view পুরোনো আর নতুন জগতের মাঝে একটি সেতু। তবে সেতুরও দাম আছে। আগে যে কোয়েরি একটি টেবিল পড়ত, এখন একটি JOIN চালায়। orders.customer_id-এর foreign key আগে customers টেবিলের দিকে নির্দেশ করত; কিন্তু কোনো foreign key view-এর দিকে নির্দেশ করতে পারে না — foreign key শুধু base table-এ বসে, তাই সেটি নতুন customers_core টেবিলে সরাতে হবে। আর trigger-এর ভেতরের যুক্তি ভুল হলে দুই টেবিল অমিল হবে। তাই এমন সেতু সাময়িক রাখাই ভালো, স্থায়ী নয়।
বড় প্রতিষ্ঠানে আরেকটি প্রচলিত রূপ: অ্যাপগুলো কখনো সরাসরি টেবিল পড়ে না, শুধু একটি আলাদা schema-র view পড়ে (যেমন api schema). ভেতরের টেবিল ইচ্ছামতো বদলানো যায়, শুধু view-এর চুক্তি ঠিক রাখতে হয়।
মূল উপলব্ধি: view হলো ডেটাবেসের “interface” — বাইরের জগৎ যা দেখে। interface স্থির রাখলে ভেতরের নকশা স্বাধীনভাবে বদলানো যায়।
প্র ০৫Materialized view পুরোটা আবার না বানিয়ে শুধু পরিবর্তনটুকু বসানো (incremental maintenance) কেন SUM আর COUNT-এর জন্য সহজ, কিন্তু MIN, MAX বা COUNT(DISTINCT)-এর জন্য কঠিন?
Incremental maintenance-এর মূল প্রশ্ন: base table-এ একটি সারি যোগ বা মোছা হলে শুধু সেই সারি আর পুরোনো জমানো ফল দেখে নতুন ফল বের করা যায় কি? SUM আর COUNT-এর উত্তর হ্যাঁ, দুই দিকেই। নতুন লাইনে quantity 3 এলে units_sold = units_sold + 3; একটি লাইন মুছলে units_sold − 3. পুরোনো যোগফল আর পরিবর্তনটুকুই যথেষ্ট; base table আর পড়তে হয় না। গণিতের ভাষায় SUM আর COUNT-এর বিপরীত ক্রিয়া (বিয়োগ) আছে। AVG সরাসরি নয়, কিন্তু SUM আর COUNT আলাদা রেখে AVG = SUM / COUNT বের করা যায় — এজন্যও Oracle-এর fast refresh SUM-এর পাশে COUNT কলাম চায়।
MAX-এর ক্ষেত্রে যোগ সহজ: নতুন মান বর্তমান MAX-এর চেয়ে বড় হলে সেটিই নতুন MAX. কিন্তু মোছা কঠিন। ধরুন Headphone-এর সবচেয়ে বড় একক বিক্রি ছিল 5, আর সেই লাইনটিই মুছে গেল। নতুন MAX কত? জমানো ফলে শুধু “5” লেখা — পরের বড়টি 3 না 4, জানার উপায় নেই। base table-এর সেই পণ্যের সব লাইন আবার পড়তেই হবে। COUNT(DISTINCT customer_id)-এও একই সমস্যা: একটি লাইন মুছলে গ্রাহকটি কি এই পণ্যের আর কোনো লাইনে আছেন? জানতে হলে হয় base table পড়তে হয়, নয়তো প্রতিটি মানের কয়টি কপি আছে তার আলাদা হিসাব রাখতে হয়।
এই প্রশ্নগুলোই materialized view গবেষণার কেন্দ্রে। ১৯৮৬ সালে José Blakeley, Per-Åke Larson আর Frank Tompa SIGMOD-এ জমানো view দক্ষভাবে হালনাগাদের একটি প্রভাবশালী পদ্ধতি দেন। ১৯৯০-এর দশকে Ashish Gupta, Inderpal Singh Mumick প্রমুখ duplicate গোনার (counting) কৌশল আর পুনরাবৃত্ত view-এর জন্য পদ্ধতি নিয়ে কাজ করেন, আর এই বিষয়ে তাঁদের সম্পাদিত বই ও সমীক্ষা বহুল পঠিত। ফলস্বরূপ বাণিজ্যিক DBMS-গুলো fast refresh-এর জন্য নির্দিষ্ট শর্ত চায়: কোন aggregate চলবে, কোন কলাম রাখতে হবে, কোন log লাগবে।
বাস্তবের শিক্ষা: materialized view-এর সংজ্ঞা লেখার সময়ই ভাবুন সেটি incremental-ভাবে হালনাগাদযোগ্য কি না। SUM, COUNT আর GROUP BY-এ থাকুন; MIN/MAX লাগলে মোছার সময় কী হবে, তার পরিকল্পনা রাখুন। PostgreSQL-এর মূল সংস্করণে incremental refresh নেই, তবে pg_ivm-এর মতো extension এই কাজ করে।
মূল উপলব্ধি: যে হিসাবের “উল্টো” আছে, তাকে টুকরো টুকরো হালনাগাদ করা যায়; যার নেই, তার জন্য base ডেটায় ফিরে যেতেই হয়।
অনুশীলন
উপরের যেকোনো db-shop এডিটরে কোড বদলে নিজের উত্তর চালিয়ে দেখতে পারেন।
-
View লিখুন: db-shop-এ গুদামের জন্য
low_stockনামে একটি view বানান: যেসব পণ্যের stock ১০-এর কম, তাদের নাম, stock আর শ্রেণির নাম (categories.name). তারপর view থেকে stock অনুযায়ী সাজিয়ে পড়ুন। এই view কি PostgreSQL-এ updatable?CREATE VIEW low_stock AS SELECT p.name, p.stock, c.name AS category FROM products p JOIN categories c ON c.category_id = p.category_id WHERE p.stock < 10; SELECT * FROM low_stock ORDER BY stock;ফল তিনটি সারি:
Power Bank | 0 | Electronics,Smart Watch | 5 | Electronics,Saree | 8 | Clothing. দুই টেবিলের JOIN থাকায় PostgreSQL এটিকে নিজে থেকে updatable মনে করবে না; stock বদলাতে হবে products-এ সরাসরি, অথবা INSTEAD OF UPDATE trigger লিখে। products.category_id NULL হলে inner JOIN সেই পণ্য বাদ দিত — শ্রেণিহীন পণ্যের stock কমলেও গুদাম জানত না। নিরাপদ হতে LEFT JOIN লিখুন। -
ফল আগে বলুন (trace): PostgreSQL-এ
CREATE VIEW premium AS SELECT product_id, name, price FROM products WHERE price > 2000;— db-shop-এর দামগুলো 15000, 1200, 1800, 650, 550, 2200, 4500, 420, 260, 3500. ক্রমানুসারে বলুন: (ক)SELECT COUNT(*) FROM premium(খ)UPDATE premium SET price = 500 WHERE name = 'Saree'কয়টি সারি বদলায়, আর তারপর (ক)-এর ফল কত? (গ)UPDATE premium SET price = 600 WHERE name = 'Headphone'কয়টি সারি বদলায়? (ঘ) view-টিWITH CHECK OPTIONদিয়ে বানানো হলে (খ)-তে কী হতো?(ক) ৪ — 15000, 2200, 4500, 3500.
(খ) ১টি সারি বদলায়; Saree-র দাম এখন 500, তাই সে view থেকে উধাও — COUNT এখন ৩.
(গ) ০টি সারি। DBMS লিখে নেয়
… WHERE name = 'Headphone' AND price > 2000; Headphone-এর দাম 1200, view-এর বাইরে, তাই view দিয়ে তাকে ছোঁয়াই যায় না। কোনো ত্রুটি নেই, শুধু UPDATE 0.(ঘ) ত্রুটি:
new row violates check option for view "premium"— নতুন দাম 500 শর্ত মানে না। কিছুই বদলায় না, COUNT ৪-ই থাকে। -
Stale ফল হিসাব করুন: সকাল ৯টায় একটি materialized view
mv_status(status, n)refresh হলো — db-shop-এর orders থেকে প্রতিটি status-এ কয়টি অর্ডার। তারপর: ৯:১০-এ অর্ডার 1006 pending থেকে shipped হলো; ৯:২০-এ নতুন অর্ডার 1011 এল (pending); ৯:৩০-এ কেউSELECT * FROM mv_statusচালালেন। তিনি কী দেখবেন, আর আসল সংখ্যা কত?৯টার হিসাব (তিনি এটিই দেখবেন):
cancelled 1,delivered 5,pending 2(1006, 1009),shipped 2(1003, 1008) — মোট ১০.আসল অবস্থা ৯:৩০-এ: cancelled 1, delivered 5, pending 2 (1009, 1011), shipped 3 (1003, 1008, 1006) — মোট ১১. pending-এর সংখ্যা কাকতালীয়ভাবে একই (একটি গেল, একটি এল), কিন্তু অর্ডারগুলো আলাদা! শুধু সংখ্যা মিলিয়ে “ফল ঠিক আছে” বলা যায় না। shipped আর মোট সংখ্যা স্পষ্টভাবে পুরোনো।
-
Trigger লিখুন: SQLite-এ
public_customersview দিয়ে শুধু city বদলানোর অনুমতি দিন — একটি INSTEAD OF UPDATE trigger লিখে। তারপর Imran (customer_id 7)-এর city 'Gazipur' করে customers টেবিলে যাচাই করুন। তাঁর phone কি অক্ষত থাকবে?CREATE VIEW public_customers AS SELECT customer_id, name, city, joined_on FROM customers; CREATE TRIGGER public_customers_upd INSTEAD OF UPDATE OF city ON public_customers BEGIN UPDATE customers SET city = NEW.city WHERE customer_id = OLD.customer_id; END; UPDATE public_customers SET city = 'Gazipur' WHERE customer_id = 7; SELECT customer_id, name, city, phone FROM customers WHERE customer_id = 7;ফল:
7 | Imran Hossain | Gazipur | 01311000007— phone অক্ষত, কারণ trigger শুধু city বসায়।UPDATE OF cityথাকায় কেউ view দিয়ে name বদলাতে চাইলে trigger চলবে না; তখন SQLite আবার “cannot modify … because it is a view” ত্রুটি দেবে — ঠিক যা চাই। আগের মতোই UPDATE-এর লাইনে “০টি সারি বদলেছে” দেখাবে, কারণ পরিবর্তনটি trigger করেছে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি সাধারণ (materialized নয়) view-এর ওপর SELECT চালালে DBMS কোন ক্রমে কাজ করে, সাজান (▲▼ চেপে সরান)
- কোয়েরি parse করে FROM-এর নামটি খুঁজে পাওয়া
- System catalog-এ দেখা যে নামটি একটি view, আর তার SELECT-টি পড়া
- view-এর নামের জায়গায় তার SELECT-টি subquery হিসেবে বসানো
- সম্ভব হলে subquery মিশিয়ে ফেলা আর শর্ত ভেতরে ঠেলে plan বানানো
- base table থেকে সারি পড়ে ফল তৈরি করা
একটি সাধারণ view v তৈরি হলো SELECT * FROM products WHERE price > 1000 দিয়ে। তারপর products-এ ১০০টি নতুন দামি পণ্য যোগ হলো। SELECT COUNT(*) FROM v কী দেখাবে?
- view তৈরির সময়ের সংখ্যা, কারণ ফল তখনই জমা হয়েছিল
- নতুন ১০০টিসহ সর্বশেষ সংখ্যা
- REFRESH না করা পর্যন্ত ত্রুটি
- view অবৈধ হয়ে যাবে, আবার বানাতে হবে
নিচের কোন view সাধারণ নিয়মে (PostgreSQL/MySQL-এ) নিজে থেকে updatable?
SELECT customer_id, SUM(total) FROM orders GROUP BY customer_idSELECT DISTINCT city FROM customersSELECT name FROM customers UNION SELECT name FROM suppliersSELECT customer_id, name, city FROM customers WHERE city = 'Dhaka'
view dhaka_customers সংজ্ঞায় WHERE city = 'Dhaka' WITH CHECK OPTION আছে। UPDATE dhaka_customers SET city = 'Sylhet' WHERE customer_id = 1 চালালে কী হবে (customer 1-এর city এখন Dhaka)?
- ত্রুটি দেবে, কিছুই বদলাবে না
- সারি বদলাবে আর view থেকে উধাও হবে
- পুরোনো সারি থাকবে, Sylhet-এর একটি নতুন সারি তৈরি হবে
- ০টি সারি বদলাবে, কোনো ত্রুটি ছাড়া
SQLite-এ একটি view-তে INSERT করার উপায় কোনটি?
- view-টি
WITH CHECK OPTIONদিয়ে বানানো - view-টি এক টেবিলের আর GROUP BY ছাড়া হলেই সরাসরি INSERT চলে
- view-এর ওপর একটি
INSTEAD OF INSERTtrigger বানানো CREATE OR REPLACE VIEWদিয়ে আবার বানানো
একজন ডেভেলপার বললেন, “ধীর রিপোর্ট-কোয়েরিটিকে একটি সাধারণ view-তে রাখলে দ্রুত চলবে, কারণ DBMS view-এর plan মনে রাখে।” এই দাবি সম্পর্কে কোনটি সঠিক?
- ঠিক, কারণ view-এর ফল প্রথমবার চালানোর পর জমা থাকে
- ভুল; view expansion-এর পর কোয়েরিটি সাধারণত হাতে লেখা কোয়েরির সমান কাজই করে
- ঠিক, কারণ CREATE VIEW নিজে থেকে index তৈরি করে
- ভুল; view সবসময় কোয়েরিকে অনেক ধীর করে
হিসাব শাখা গ্রাহকের নাম আর শহর দেখবে কিন্তু ফোন নম্বর নয়। PostgreSQL-এ সবচেয়ে নিরাপদ ব্যবস্থা কোনটি?
- phone ছাড়া একটি view বানানো, আর customers আর view দুটিতেই SELECT অধিকার দেওয়া
- অ্যাপের কোডে phone কলামটি না দেখানো
- phone ছাড়া একটি view বানিয়ে শুধু সেই view-এ GRANT SELECT, customers টেবিলে কোনো অধিকার নয়
- phone ছাড়া একটি materialized view বানানো
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- View হলো নাম দেওয়া একটি সংরক্ষিত SELECT; টেবিলের মতো পড়া যায়, কিন্তু সাধারণ view নিজে ডেটা রাখে না — প্রতিবার base table থেকে হিসাব হয়।
- View-এর চার কাজ: সরলতা, নিরাপত্তা (কলাম/সারি লুকানো + শুধু view-এ GRANT), পুরোনো অ্যাপের সাথে মিল, আর এক রকম ব্যবসার সংজ্ঞা।
- Optimizer view expansion করে, তাই view নিজে কোয়েরি দ্রুত করে না; গতির জন্য index বা materialized view, প্রমাণের জন্য EXPLAIN.
- এক টেবিলের সরল view updatable (PostgreSQL/MySQL); WITH CHECK OPTION শর্ত ভাঙা লেখা আটকায়। SQLite-এর view শুধু পড়ার জন্য — লিখতে INSTEAD OF trigger.
- Materialized view ফল জমায়: পড়া দ্রুত, কিন্তু refresh পর্যন্ত stale. MySQL/SQLite-এ টেবিল + refresh বা trigger দিয়ে অনুকরণ।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৫ · CTE আর recursive queryপরবর্তী পর্বএকটি কোয়েরির ভেতরে নাম দেওয়া অংশ, আর গাছের মতো ডেটায় পুনরাবৃত্ত খোঁজ।
- ডেটাবেস (DBMS) ও SQL · কোর্স হোম৬৭টি পর্বপুরো কোর্সের রোডম্যাপ — ১০টি মডিউল।
- Python · SQLite ডেটাবেসহাতে-কলমেনিজের Python অ্যাপ থেকে view বানানো আর পড়া।
- Python · SQLAlchemy ORMপাশের ট্র্যাকORM থেকে view-কে শুধু-পড়ার টেবিল হিসেবে ব্যবহার।
- Java · Spring Data JPAপাশের ট্র্যাকJava অ্যাপে রিপোর্টের জন্য view আর ডেটাবেস।