এই পর্বে যা শিখবেন
- Stored procedure আর function কী, কেন ব্যবহার হয়, আর কেন অনেক দল এড়িয়ে চলে — দুই দিকের যুক্তি
- PL/pgSQL-এর গঠন: DECLARE, BEGIN…END, IF/ELSIF, LOOP, RAISE EXCEPTION, RETURN QUERY; SQL-language function আর table-valued function
- IMMUTABLE, STABLE আর VOLATILE — planner কীভাবে এগুলো ব্যবহার করে; Python থেকে SQLite-এ নিজের function বসানো
- PostgreSQL PROCEDURE ও CALL, MySQL-এর DELIMITER, IN/OUT/INOUT আর HANDLER; place_order আর transfer_money-র সম্পূর্ণ, atomic রূপ
- Dynamic SQL-এ injection ঠেকানো, SECURITY DEFINER-এর ঝুঁকি, procedure টেস্ট করা আর plan cache
১ · ডেটাবেসের ভেতরে রাখা কোড — procedure আর function কী
এতদিন আপনি SQL লিখেছেন একটি একটি স্টেটমেন্ট করে: একটি SELECT, একটি UPDATE. কিন্তু বাস্তবের একটি কাজ প্রায়ই কয়েকটি স্টেটমেন্টের দল। দোকানে একটি অর্ডার নেওয়া মানে: স্টক আছে কি না দেখা, orders-এ একটি সারি, order_items-এ একটি লাইন, products-এ স্টক কমানো — আর এর যেকোনো ধাপ ব্যর্থ হলে সবকিছু বাতিল। এই পুরো দলটিকে একটি নাম দিয়ে ডেটাবেসের ভেতরেই রেখে দেওয়া যায়: place_order(customer, product, qty). এটিই stored procedure — ডেটাবেসে সংরক্ষিত, নাম ধরে ডাকা যায় এমন কোড।
ব্যাংকের কাউন্টারের কথা ভাবুন। আপনি ক্যাশিয়ারের ভল্টে ঢুকে নিজে টাকা গুনে নেন না, খাতায় নিজে লেখেন না। আপনি একটি নির্দিষ্ট ফর্ম পূরণ করেন — “জমা” বা “উত্তোলন” — আর বাকি সব ধাপ ব্যাংকের ভেতরের লোক নিয়ম মেনে করেন। bKash অ্যাপের “Send Money” বোতামও একই: আপনি শুধু নম্বর আর টাকার অঙ্ক দেন, ভেতরে কোন খাতায় কী লেখা হলো, আপনি দেখেন না। Stored procedure ডেটাবেসের সেই “ফর্ম”: বাইরের লোক নির্দিষ্ট কাজটি চাইতে পারে, ভেতরের টেবিলে সরাসরি হাত দিতে পারে না।
দুই রকম জিনিস আছে, আর পার্থক্যটি পরীক্ষায় প্রায়ই আসে:
Function (user-defined function)
ইনপুট নেয়, একটি মান ফেরত দেয় — একটি সংখ্যা, লেখা, বা পুরো একটি টেবিল। কোয়েরির ভেতরে ব্যবহার হয়: SELECT name, customer_tier(customer_id) FROM customers. SQL-এর নিজের upper() বা round()-এর মতোই, শুধু আপনার লেখা।
Procedure (stored procedure)
একটি কাজ করে — ডেটা বদলায়, কয়েক ধাপ চালায়। আলাদা স্টেটমেন্টে ডাকা হয়: CALL transfer_money(1, 2, 500). PostgreSQL-এ procedure নিজের ভেতরে COMMIT করতে পারে, function পারে না।
এই কোড লেখা হয় DBMS-এর নিজস্ব procedural ভাষায় — যেখানে SQL-এর সাথে আছে ভ্যারিয়েবল, IF, লুপ আর ত্রুটি তোলার ব্যবস্থা। PostgreSQL-এ PL/pgSQL, Oracle-এ PL/SQL, SQL Server-এ T-SQL, আর MySQL-এ SQL standard-এর SQL/PSM ঘেঁষা একটি ভাষা। নাম আর খুঁটিনাটি আলাদা, ধারণা একই।
এই পাতার লাইভ ইঞ্জিন SQLite-এ stored procedure নেই। CREATE PROCEDURE বা SQL দিয়ে CREATE FUNCTION — কোনোটিই SQLite চেনে না; লিখলে syntax error. কারণটি নকশায়: SQLite একটি embedded database, অ্যাপের একই প্রোগ্রামের ভেতরে চলে। সেখানে “নেটওয়ার্ক বাঁচাতে কোড ডেটাবেসে পাঠাও” যুক্তিটি খাটে না, আর নিজের function দরকার হলে অ্যাপের ভাষাতেই (C, Python…) লিখে SQLite-এ বসানো যায় — অংশ ৫-এ দেখবেন। তাই এই পর্বের PostgreSQL আর MySQL কোডগুলো চালানো যায় না, তবে সাবধানে যাচাই করা সঠিক কোড; লাইভ এডিটরে চলবে সেই কোডের ভেতরের SQL, আর transaction, view ও trigger দিয়ে তৈরি বিকল্প।
সংক্ষিপ্ত ইতিহাস। ১৯৮০-এর দশকের শেষে Sybase SQL Server stored procedure-কে জনপ্রিয় করে; তার T-SQL আজও Microsoft SQL Server-এ বেঁচে আছে। Oracle-এর PL/SQL প্রায় একই সময়ে আসে, আর Oracle 7 (১৯৯২) থেকে ডেটাবেসে সংরক্ষিত procedure পুরোদমে চালু হয়। ১৯৯৬ সালে ISO SQL standard-এ যোগ হয় SQL/PSM (Persistent Stored Modules) অংশ। PostgreSQL-এর PL/pgSQL আসে ১৯৯০-এর দশকের শেষ দিকে, আর আলাদা CREATE PROCEDURE আসে অনেক পরে — PostgreSQL 11-এ (২০১৮)। MySQL stored procedure পায় ৫.০ সংস্করণে (২০০৫), view আর trigger-এর সাথে একসাথে।
লাইভ SQL চলে দোকানের ডেটাবেসে (db-shop): customers (customer_id, name, city, phone, joined_on — ৮ জন), categories (৪টি শ্রেণি), products (product_id, name, category_id, price, stock — ১০টি পণ্য; stock-এ CHECK (stock >= 0)), orders (order_id, customer_id, order_date, status — ১০টি অর্ডার) আর order_items (order_id, product_id, quantity, unit_price — ১৬টি লাইন)। বিশেষ করে মনে রাখুন: Smart Watch (110)-এর দাম 3500, স্টক মাত্র 5. দুটি এডিটর নিজের ছোট wallet টেবিল বানিয়ে নেয়।
২ · কেন ডেটাবেসে কোড রাখবেন — round trip, নিয়ম আর অধিকার
প্রথম কারণ গতি, আরও নির্দিষ্টভাবে round trip. অ্যাপ সার্ভার আর ডেটাবেস সার্ভার সাধারণত আলাদা যন্ত্র; প্রতিটি স্টেটমেন্ট পাঠানো আর উত্তর ফেরত পাওয়া মানে নেটওয়ার্কে একবার যাওয়া-আসা। একই ডেটা সেন্টারে এটি সাধারণত এক মিলিসেকেন্ডের কম থেকে কয়েক মিলিসেকেন্ড; ভিন্ন শহরের সার্ভারে আরও বেশি। আর যতক্ষণ অ্যাপ নেটওয়ার্কে অপেক্ষা করছে, ততক্ষণ transaction খোলা, lock ধরা। নিচের ডায়াগ্রামে একটি অর্ডার দুইভাবে দেখুন:
| round trip এ পর্যন্ত | |
| lock ধরা অবস্থায় নেটওয়ার্ক-অপেক্ষা |
- পথ ক, trip ১. অ্যাপ transaction শুরু করে। এখনো কিছু বদলায়নি, কিন্তু একটি যাওয়া-আসা খরচ হয়ে গেল।
- Trip ২: স্টক পড়া আর lock.
FOR UPDATEসারিটিতে lock বসায়, যাতে অন্য কেউ একই সময়ে শেষ ঘড়িটি বেচতে না পারে। উত্তর আসে 5; সিদ্ধান্ত নেয় অ্যাপ, নেটওয়ার্কের ওপারে। - Trip ৩–৪: দুটি INSERT. প্রতিটি আলাদা যাওয়া-আসা। পুরো সময়টি Smart Watch-এর সারি lock — অন্য ক্রেতারা অপেক্ষা করছেন।
- Trip ৫–৬: UPDATE আর COMMIT. মোট ছয়টি round trip; lock ধরা ছিল চারটি trip জুড়ে। প্রতিটি trip ১ মিলিসেকেন্ড ধরলে শুধু অপেক্ষাতেই কয়েক মিলিসেকেন্ড — ঈদের সেলে হাজার ক্রেতা একই পণ্যে এলে এই অপেক্ষাই লাইন তৈরি করে।
- পথ খ: একটি ডাক। অ্যাপ শুধু function-টি ডাকে। স্টক যাচাই, দুটি INSERT আর UPDATE চলে ডেটাবেসের ভেতরে, নেটওয়ার্ক ছাড়া।
- এক trip, ছোট lock. উত্তর আসে নতুন order_id 1011. lock ধরা ছিল শুধু ডেটাবেসের ভেতরের কাজের সময়টুকু। তবে মনে রাখুন: অ্যাপ আর ডেটাবেস একই যন্ত্রে, বা SQLite-এর মতো একই প্রোগ্রামে থাকলে এই পার্থক্য প্রায় মিলিয়ে যায়।
দ্বিতীয় কারণ: নিয়ম এক জায়গায়। একটি দোকানের ডেটাবেসে লেখে ওয়েবসাইট, মোবাইল অ্যাপ, অ্যাডমিন প্যানেল আর রাতের আমদানি-স্ক্রিপ্ট — হয়তো চারটি ভিন্ন ভাষায়। “স্টক কমাও, শূন্যের নিচে নয়, অর্ডারের দাম products থেকে নাও” — এই ব্যবসার নিয়ম চার জায়গায় চারবার লিখলে কোনো একদিন একটিতে ভুল থাকবেই। procedure-এ লিখলে সবাই একই কোড ডাকে।
তৃতীয় কারণ: নিরাপত্তা। ডেটাবেসে অধিকার দেওয়া যায় শুধু procedure চালানোর — EXECUTE privilege — টেবিলে সরাসরি UPDATE-এর অধিকার না দিয়ে। তখন অ্যাপের অ্যাকাউন্ট চুরি হলেও আক্রমণকারী UPDATE products SET price = 1 চালাতে পারে না; সে শুধু place_order ডাকতে পারে, যা নিজের নিয়ম মানে। এটি least privilege নীতির প্রয়োগ (পর্ব ৫১)।
procedure-এর ভেতরে যে কোয়েরি থাকে, সেটি সাধারণ SQL-ই। যেমন একটি function customer_tier(id) গ্রাহককে gold/silver/regular শ্রেণি দেবে তাঁর মোট কেনাকাটা দেখে। তার ভেতরের হিসাবটি এখানেই চালিয়ে দেখুন — বাতিল অর্ডার বাদ দিয়ে প্রত্যেক গ্রাহকের মোট:
-- customer_tier() function ভেতরে ঠিক এই হিসাবটি করত
SELECT c.customer_id, c.name,
SUM(oi.quantity * oi.unit_price) AS total_spent
FROM customers c
JOIN orders o ON o.customer_id = c.customer_id
JOIN order_items oi ON oi.order_id = o.order_id
WHERE o.status <> 'cancelled'
GROUP BY c.customer_id, c.name
ORDER BY total_spent DESC;ছয়টি সারি: 1 | Rahim Uddin | 37180, 4 | Nusrat Jahan | 5050, 2 | Karima Begum | 5000, 6 | Farhana Akter | 3500, 7 | Imran Hossain | 1040, 8 | Mitu Rani | 550. Rahim-এর হিসাব: অর্ডার 1001 (15000 + 2400), 1003 (1260 + 520) আর 1009 (14500 + 3500) = 37180. Sabbir নেই, কারণ তাঁর একমাত্র অর্ডার বাতিল; Tanvir নেই, কারণ তাঁর কোনো অর্ডারই নেই — INNER JOIN দুজনকেই বাদ দিয়েছে। একটি function বানালে এই ফাঁকটি ভাবতে হয়: অর্ডারহীন গ্রাহকের জন্য NULL ফেরত দেবে, না 0? অংশ ৪-এর function COALESCE(…, 0) দিয়ে 0 বেছে নিয়েছে।
৩ · কেন অনেক দল এড়িয়ে চলে — ভারসাম্যপূর্ণ হিসাব
উপরের যুক্তিগুলো শক্ত, তবু আজ অনেক সফল দল ইচ্ছে করেই ভারী logic ডেটাবেসে রাখে না। তাদের কারণগুলোও সমান বাস্তব:
| বিষয় | procedure-এর পক্ষে | বিপক্ষে বা সতর্কতা |
|---|---|---|
| গতি | কম round trip, ডেটার পাশেই হিসাব, কম সময় lock | অ্যাপ আর DB কাছাকাছি থাকলে লাভ সামান্য; ভালো লেখা একটি set-based কোয়েরিও একটি round trip |
| সামঞ্জস্য | সব অ্যাপ একই নিয়ম মানে | নিয়ম দুই জায়গায় ছড়িয়ে যায় — কিছু অ্যাপে, কিছু DB-তে; কোথায় কী আছে খুঁজে পাওয়া কঠিন |
| নিরাপত্তা | টেবিলে অধিকার না দিয়ে শুধু EXECUTE | dynamic SQL ভুলভাবে লিখলে procedure-ই injection-এর দরজা (অংশ ৯) |
| টেস্ট আর debug | pgTAP-এর মতো টুল আছে | অ্যাপের ভাষার মতো সমৃদ্ধ টেস্ট, mock আর debugger সাধারণত নেই |
| Version আর deploy | migration ফাইলে রাখা যায় | কোড আর schema একসাথে deploy করতে হয়; পুরোনো-নতুন অ্যাপ একসাথে চললে দুই সংস্করণের procedure লাগতে পারে |
| DBMS বদল | — | PL/pgSQL, T-SQL, PL/SQL একে অপরের সাথে মেলে না — vendor lock-in |
| বাড়ানো (scaling) | — | অ্যাপ সার্ভার দশটি বাড়ানো সহজ; প্রধান ডেটাবেস সার্ভার সাধারণত একটি, তার CPU সবচেয়ে দামি |
বাস্তবে বেশিরভাগ দল মাঝামাঝি পথ নেয়: ডেটার নিজের সত্যতা — NOT NULL, CHECK, foreign key, UNIQUE, আর “স্টক শূন্যের নিচে নয়” — থাকে ডেটাবেসে, কারণ কোনো অ্যাপ এগুলো এড়াতে পারে না। কাজের প্রবাহ — SMS পাঠানো, পেমেন্ট গেটওয়ে ডাকা, ছাড় হিসাবের জটিল নিয়ম — থাকে অ্যাপে। আর procedure ব্যবহার হয় বেছে বেছে: যেখানে অনেক সারি নিয়ে ভারী কাজ (মাসিক সুদ, পুরোনো ডেটা সরানো), যেখানে কঠোর অধিকার-নিয়ন্ত্রণ দরকার, বা যেখানে round trip সত্যিই সমস্যা। ব্যাংক আর সরকারি বড় সিস্টেমে, যেখানে Oracle বা SQL Server বহু বছর ধরে চলছে, procedure-এর ব্যবহার এখনো ব্যাপক; নতুন ওয়েব কোম্পানিগুলোতে কম।
৪ · PL/pgSQL-এর গঠন — CREATE FUNCTION থেকে RETURN QUERY
PostgreSQL-এ একটি function-এর কাঠামো প্রায় সবসময় একই। নিচের customer_tier অংশ ২-এর হিসাবটি নিয়ে গ্রাহকের শ্রেণি ফেরত দেয়। প্রতিটি অংশ চিনে নিন:
CREATE OR REPLACE FUNCTION customer_tier(p_customer int)
RETURNS text -- কী ফেরত দেবে
LANGUAGE plpgsql
STABLE -- টেবিল পড়ে, বদলায় না (অংশ ৫)
AS $$
DECLARE
v_total numeric := 0; -- ভ্যারিয়েবল, প্রাথমিক মানসহ
BEGIN
SELECT COALESCE(SUM(oi.quantity * oi.unit_price), 0)
INTO v_total -- ফল ভ্যারিয়েবলে
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
WHERE o.customer_id = p_customer AND o.status <> 'cancelled';
IF v_total >= 20000 THEN
RETURN 'gold';
ELSIF v_total >= 5000 THEN
RETURN 'silver';
ELSE
RETURN 'regular';
END IF;
END;
$$;
SELECT name, customer_tier(customer_id) AS tier FROM customers;CREATE OR REPLACE— না থাকলে বানানো, থাকলে বদলানো। তবে প্যারামিটারের টাইপ বদলালে PostgreSQL এটিকে আলাদা function ধরে (একই নামে একাধিক function থাকতে পারে — overloading), আর ফেরত-টাইপ বদলাতে আগে DROP লাগে।$$ … $$— dollar quoting: পুরো শরীরটি একটি লেখা (string) হিসেবে PostgreSQL-এ যায়।'…'ব্যবহার করলে ভেতরের প্রতিটি'gold'-কে''gold''লিখতে হতো।DECLARE— ভ্যারিয়েবল ঘোষণা; মান না দিলে শুরুতে NULL. প্যারামিটারের নামেp_, ভ্যারিয়েবলেv_— এই রীতি কলামের নামের সাথে সংঘর্ষ ঠেকায়।BEGIN … END— শরীর। সাবধান: এটি transaction-এর BEGIN নয়, শুধু একটি block. function চলে ডাকার স্টেটমেন্টের transaction-এর ভেতরেই।SELECT … INTO— কোয়েরির প্রথম সারি ভ্যারিয়েবলে। কোনো সারি না এলে ভ্যারিয়েবল NULL হয় আরFOUNDহয় false — অংশ ৭-এ এটিই কাজে লাগবে।IF / ELSIF / ELSE— খেয়াল করুন বানান: ELSIF (MySQL-এ ELSEIF).
db-shop-এর ডেটায় এই কোয়েরির ফল হতো: Rahim gold (37180), Nusrat আর Karima silver (5050 আর 5000 — ঠিক 5000-ও silver, কারণ শর্ত >=), বাকি সবাই regular — Sabbir আর Tanvir-ও, কারণ COALESCE তাঁদের 0 দিয়েছে।
লুপ, ত্রুটি আর বার্তা
PL/pgSQL-এ লুপ তিন রকম: LOOP … EXIT WHEN … END LOOP, WHILE, আর সবচেয়ে বেশি ব্যবহৃত FOR r IN SELECT … LOOP — যা কোয়েরির প্রতিটি সারি একে একে দেয়। RAISE NOTICE শুধু একটি বার্তা দেখায়; RAISE EXCEPTION ত্রুটি তোলে, function থামায়, আর ডাকার স্টেটমেন্টের সব বদল বাতিল করে। % চিহ্নের জায়গায় পরের মানগুলো বসে। নিচে একটি নামহীন block (DO), যা কোনো function না বানিয়েই PL/pgSQL চালায়:
DO $$
DECLARE
r record;
BEGIN
FOR r IN SELECT product_id, name, stock FROM products WHERE stock < 10 LOOP
RAISE NOTICE 'restock % (%): only % left', r.product_id, r.name, r.stock;
END LOOP;
IF NOT EXISTS (SELECT 1 FROM categories WHERE name = 'Books') THEN
RAISE EXCEPTION 'category Books is missing'
USING ERRCODE = 'P0002', HINT = 'run the seed migration first';
END IF;
END;
$$;SQL-language function আর table-valued function
সব function-এ PL/pgSQL লাগে না। শুধু একটি হিসাব হলে LANGUAGE sql যথেষ্ট — সহজ, আর PostgreSQL প্রায়ই এমন function-কে কোয়েরির ভেতরে সরাসরি বসিয়ে (inline) দেয়, তাই ডাকার আলাদা খরচ থাকে না। আর function যদি একটি সংখ্যার বদলে পুরো টেবিল ফেরত দেয় — table-valued function — তাকে FROM-এ টেবিলের মতো ব্যবহার করা যায়:
-- SQL-language function (PostgreSQL 14+ এর SQL-standard শরীর)
CREATE OR REPLACE FUNCTION line_total(qty int, price numeric)
RETURNS numeric
LANGUAGE sql IMMUTABLE
RETURN qty * price; -- পুরোনো রূপ: AS $$ SELECT qty * price $$
-- table-valued function: একজন গ্রাহকের অর্ডারগুলো
-- (ধরে নিচ্ছি orders.status-এর টাইপ text)
CREATE OR REPLACE FUNCTION customer_orders(p_customer int)
RETURNS TABLE (order_id int, status text, total numeric)
LANGUAGE plpgsql STABLE
AS $$
BEGIN
RETURN QUERY
SELECT o.order_id, o.status, SUM(oi.quantity * oi.unit_price)
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
WHERE o.customer_id = p_customer
GROUP BY o.order_id, o.status;
END;
$$;
SELECT * FROM customer_orders(1) ORDER BY order_id;একটি সূক্ষ্ম ফাঁদ: RETURNS TABLE-এর কলামগুলো (order_id, status, total) PL/pgSQL-এর ভেতরে ভ্যারিয়েবলও। তাই কোয়েরিতে o.order_id না লিখে শুধু order_id লিখলে PostgreSQL বলে column reference "order_id" is ambiguous. সবসময় table alias দিয়ে কলাম লিখুন। আর RETURN QUERY-এর কলামের টাইপ হুবহু মিলতে হয় — status varchar হলে o.status::text লিখতে হতো।
SQLite-এ table-valued function লেখা যায় না, কিন্তু সবচেয়ে কাছের বিকল্প আছে: একটি view (পর্ব ২৪), আর ব্যবহারের সময় WHERE দিয়ে “প্যারামিটার”। ফলাফল একই, শুধু প্যারামিটারটি বাইরে থেকে আসে:
CREATE VIEW order_lines AS
SELECT o.order_id, o.customer_id, o.status, p.name AS product,
oi.quantity, oi.quantity * oi.unit_price AS line_total
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p ON p.product_id = oi.product_id;
-- PostgreSQL-এ হতো: SELECT * FROM customer_orders(1);
SELECT order_id, status, product, quantity, line_total
FROM order_lines WHERE customer_id = 1
ORDER BY order_id, product;প্রথমে “✓ CREATE VIEW”, তারপর Rahim (1)-এর ছয়টি লাইন: 1001 | delivered | Headphone | 2 | 2400, 1001 | delivered | Mobile Phone | 1 | 15000, 1003 | shipped | Mustard Oil 1L | 2 | 520, 1003 | shipped | Rice 5kg | 3 | 1260, 1009 | pending | Mobile Phone | 1 | 14500, 1009 | pending | Smart Watch | 1 | 3500. view-টি ডেটাবেসে থাকে, তাই সব অ্যাপ একই সংজ্ঞা ব্যবহার করে — function-এর “নিয়ম এক জায়গায়” লাভের অনেকটাই। পার্থক্য: view-এর ভেতরে IF, লুপ বা ত্রুটি তোলা নেই, আর প্যারামিটার শুধু WHERE দিয়ে বাইরে থেকে। SQLite-এ WHERE customer_id = 1 view-এর ভেতরে ঢুকে মূল কোয়েরিতে মিশে যায়, তাই পুরো view আগে হিসাব হয় না।
৫ · Volatility — IMMUTABLE, STABLE, VOLATILE আর host-ভাষার function
PostgreSQL-এ প্রতিটি function-এর একটি volatility লেবেল থাকে — এটি planner-কে দেওয়া একটি প্রতিশ্রুতি, function-টি কতটা “স্থির”:
| লেবেল | প্রতিশ্রুতি | উদাহরণ |
|---|---|---|
IMMUTABLE | একই ইনপুটে চিরকাল একই ফল; কোনো টেবিল পড়ে না | lower(), line_total(qty, price) |
STABLE | একটি স্টেটমেন্টের ভেতরে একই ইনপুটে একই ফল; টেবিল পড়তে পারে, বদলায় না | now(), customer_tier() |
VOLATILE (ডিফল্ট) | কোনো প্রতিশ্রুতি নেই; প্রতিবার আলাদা হতে পারে, ডেটা বদলাতেও পারে | random(), nextval(), place_order() |
লেবেলটি শুধু কাগুজে নয় — planner এটি দেখে সিদ্ধান্ত নেয়, কোয়েরি কত দ্রুত চলবে, এমনকি ফল সঠিক হবে কি না। ধরুন orders-এ এক লাখ সারি, order_date-এ index আছে, আর একটি function cutoff_date() ফেরত দেয় আজ থেকে ৩০ দিন আগের তারিখ:
| লেবেল | |
| function ডাক | |
| index ব্যবহার | |
| ফল সঠিক? |
- প্রশ্ন। একই কোয়েরি, শুধু function-এর লেবেল বদলাব। দেখার বিষয়: function কতবার চলে, আর order_date-এর index ব্যবহার হয় কি না।
- VOLATILE. planner ধরে নেয়, প্রতিটি সারিতে ফল আলাদা হতে পারে, তাই index-এ একটি নির্দিষ্ট মান খোঁজা সম্ভব নয়। ফল: পুরো টেবিল পড়া, আর প্রায় এক লাখবার function ডাক। ফল সঠিক, কিন্তু ধীর।
- STABLE. স্টেটমেন্টের ভেতরে ফল একটিই, তাই executor scan শুরুর আগে একবার function চালিয়ে index-এ খোঁজে। এখানে এটিই সঠিক লেবেল, কারণ function current_date পড়ে, আর current_date একটি transaction-এর ভেতরে স্থির।
- IMMUTABLE — মিথ্যা প্রতিশ্রুতি। planner কোয়েরি সাজানোর সময়েই function চালিয়ে তারিখটি plan-এ বসিয়ে দেয়। সাধারণ একবারের কোয়েরিতে ক্ষতি চোখে পড়ে না, কিন্তু PL/pgSQL বা prepared statement plan জমা রাখে — সেখানে পরের দিনগুলোতেও পুরোনো তারিখ। ভুল লেবেলের ফল চুপচাপ ভুল ডেটা।
- নিয়ম। শুধু ইনপুট থেকে হিসাব হলে IMMUTABLE; টেবিল, সময় বা সেটিং পড়লে STABLE; ডেটা বদলালে বা random হলে VOLATILE. expression index-এ শুধু IMMUTABLE function চলে, কারণ index-এ জমা মান কখনো বদলানো যায় না। নিশ্চিত না হলে ডিফল্ট VOLATILE রাখুন।
Host-ভাষা থেকে function — SQLite-এর পথ
SQLite-এ SQL দিয়ে function লেখা না গেলেও অ্যাপের ভাষায় লেখা function SQLite-এ বসানো যায় — C-তে sqlite3_create_function(), Python-এ create_function(). তারপর সেটি SQL-এর ভেতরে অন্য যেকোনো function-এর মতো চলে। নিচের function ফোন নম্বর এক রূপে আনে (+880 1711-000001 → 01711000001). deterministic=True হলো PostgreSQL-এর IMMUTABLE-এর SQLite-রূপ:
import sqlite3
def norm_phone(p): # '+880 1711-000001' -> '01711000001'
if p is None: # SQL NULL arrives as None
return None
digits = "".join(ch for ch in p if ch.isdigit())
return digits[2:] if digits.startswith("880") else digits
con = sqlite3.connect(":memory:")
con.create_function("norm_phone", 1, norm_phone, deterministic=True)
con.execute("CREATE TABLE customers (name TEXT, phone TEXT)")
con.executemany("INSERT INTO customers VALUES (?, ?)", [
("Rahim", "+880 1711-000001"), ("Karima", "01811000002"), ("Tanvir", None)])
for row in con.execute("SELECT name, norm_phone(phone) FROM customers"):
print(row)
con.execute("CREATE INDEX customers_phone_idx ON customers (norm_phone(phone))")
print("index on norm_phone(phone): ok")
con.create_function("lucky", 0, lambda: 7) # not marked deterministic
try:
con.execute("CREATE INDEX bad_idx ON customers (lucky())")
except sqlite3.OperationalError as e:
print("error:", e)আউটপুট পাঁচ লাইন: ('Rahim', '01711000001'), ('Karima', '01811000002'), ('Tanvir', None), index on norm_phone(phone): ok আর error: non-deterministic functions prohibited in index expressions. SQL-এর NULL Python-এ আসে None হয়ে, তাই function-কে নিজেই NULL সামলাতে হয়েছে। deterministic চিহ্ন থাকায় SQLite function-টি index-এ নিতে রাজি; চিহ্নহীন lucky()-কে নেয়নি — ঠিক PostgreSQL-এর IMMUTABLE নিয়মের মতো। একটি বাস্তব সতর্কতা: function-টি থাকে শুধু এই সংযোগে। ডেটাবেসটি ফাইলে রাখা হলে অন্য প্রোগ্রাম (যেমন sqlite3 কমান্ড-লাইন) সেটি খুলে টেবিলে লিখতে গেলে index-এর জন্য function খুঁজবে আর পাবে না: no such function: norm_phone.
৬ · PROCEDURE আর CALL — PostgreSQL আর MySQL
PostgreSQL 11-এর আগে সবকিছুই function ছিল — ডেটা বদলানোর কাজও SELECT f() দিয়ে ডাকা হতো। ১১ থেকে এসেছে আসল CREATE PROCEDURE, যা ডাকা হয় CALL দিয়ে। মূল পার্থক্যগুলো:
| বিষয় | FUNCTION | PROCEDURE (11+) |
|---|---|---|
| ডাকা হয় | কোয়েরির ভেতরে: SELECT f(…), WHERE-এ, FROM-এ | আলাদা স্টেটমেন্টে: CALL p(…) |
| ফেরত মান | বাধ্যতামূলক (void-ও একটি টাইপ) | নেই; ফল পাঠাতে INOUT, আর ১৪ থেকে OUT প্যারামিটার |
| নিজের ভেতরে COMMIT/ROLLBACK | না — ডাকার স্টেটমেন্টের transaction-এ চলে | হ্যাঁ, যদি CALL বাইরের কোনো transaction block-এর ভেতরে না থাকে |
procedure-এর ভেতরে COMMIT কেন দরকার? একটি বড় কাজ টুকরো টুকরো করতে। ধরুন audit_log টেবিল থেকে দুই বছরের পুরোনো কয়েক কোটি সারি মুছতে হবে। একটি DELETE-এ করলে বিশাল একটি transaction — দীর্ঘ lock, বিশাল log, আর মাঝপথে ব্যর্থ হলে সব ফেরত। তার বদলে প্রতি ব্যাচ শেষে COMMIT:
CREATE OR REPLACE PROCEDURE purge_audit_log(p_before date, p_batch int DEFAULT 10000)
LANGUAGE plpgsql
AS $$
DECLARE
v_deleted int;
BEGIN
LOOP
DELETE FROM audit_log
WHERE log_id IN (SELECT log_id FROM audit_log
WHERE logged_at < p_before
LIMIT p_batch);
GET DIAGNOSTICS v_deleted = ROW_COUNT; -- এই DELETE কতটি সারি মুছল
COMMIT; -- প্রতিটি ব্যাচ আলাদা transaction
EXIT WHEN v_deleted = 0;
END LOOP;
END;
$$;
CALL purge_audit_log('2024-01-01'); -- autocommit-এ: চলবে
-- BEGIN; CALL purge_audit_log(...); -- ত্রুটি: invalid transaction terminationMySQL-এর procedure — DELIMITER, IN/OUT/INOUT আর HANDLER
MySQL-এ প্রথম যে জিনিসটি অদ্ভুত লাগে, তা হলো DELIMITER. mysql কমান্ড-লাইন client প্রতিটি ; দেখে ভাবে স্টেটমেন্ট শেষ, আর সার্ভারে পাঠিয়ে দেয় — অথচ procedure-এর শরীরের ভেতরেই অনেক ;. তাই কিছুক্ষণের জন্য শেষ-চিহ্ন বদলে // করা হয়। এটি client-এর কৌশল, SQL-এর অংশ নয়; অ্যাপের driver দিয়ে CREATE PROCEDURE পাঠালে DELIMITER লাগে না।
প্যারামিটারের তিন mode: IN (ডিফল্ট — ভেতরে আসে), OUT (বাইরে যায়, শুরুতে NULL), INOUT (দুই দিকেই)। ত্রুটি সামলাতে DECLARE … HANDLER: EXIT HANDLER ত্রুটি হলে নির্দিষ্ট কাজ করে block থেকে বেরিয়ে যায়, CONTINUE HANDLER কাজ করে পরের লাইন থেকে চালিয়ে যায়। নিজে ত্রুটি তুলতে SIGNAL SQLSTATE '45000' — 45000 হলো “ব্যবহারকারীর তৈরি ত্রুটি”-র প্রচলিত SQLSTATE.
DELIMITER //
CREATE PROCEDURE customer_stats(IN p_customer INT,
OUT p_orders INT, OUT p_spent DECIMAL(12,2))
BEGIN
SELECT COUNT(DISTINCT o.order_id), COALESCE(SUM(oi.quantity * oi.unit_price), 0)
INTO p_orders, p_spent
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
WHERE o.customer_id = p_customer AND o.status <> 'cancelled';
END //
DELIMITER ;
CALL customer_stats(1, @n, @total); -- OUT মান যায় session variable-এ
SELECT @n, @total; -- 3, 37180.00৭ · পূর্ণ উদাহরণ — place_order: স্টক যাচাই থেকে atomic বদল
এবার এই পর্বের মূল উদাহরণ। নিয়মগুলো: (১) পরিমাণ ধনাত্মক; (২) পণ্যটি থাকতে হবে; (৩) স্টক যথেষ্ট না হলে ত্রুটি, কিছুই বদলাবে না; (৪) একটি নতুন অর্ডার, একটি অর্ডার-লাইন (দাম products থেকে), আর স্টক কমানো — সব একসাথে, নয়তো একটিও না; (৫) একসাথে দুজন ক্রেতা শেষ ঘড়িটি কিনতে এলে একজনই পাবেন। দুটি DBMS-এ একই কাজ (ধরে নিচ্ছি orders.order_id স্বয়ংক্রিয় — PostgreSQL-এ IDENTITY, MySQL-এ AUTO_INCREMENT):
CREATE OR REPLACE FUNCTION place_order(p_customer int, p_product int, p_qty int)
RETURNS int
LANGUAGE plpgsql
AS $$
DECLARE
v_stock int;
v_price numeric(10,2);
v_order_id int;
BEGIN
IF p_qty IS NULL OR p_qty <= 0 THEN
RAISE EXCEPTION 'quantity must be positive, got %', p_qty;
END IF;
-- সারিটি lock করে পড়া: একই পণ্যের অন্য অর্ডার এখানে অপেক্ষা করবে
SELECT stock, price INTO v_stock, v_price
FROM products WHERE product_id = p_product
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'product % not found', p_product;
ELSIF v_stock < p_qty THEN
RAISE EXCEPTION 'insufficient stock for product %: have %, need %',
p_product, v_stock, p_qty;
END IF;
INSERT INTO orders (customer_id, order_date, status)
VALUES (p_customer, current_date, 'pending')
RETURNING order_id INTO v_order_id; -- গ্রাহক না থাকলে FK নিজেই ত্রুটি দেবে
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES (v_order_id, p_product, p_qty, v_price);
UPDATE products SET stock = stock - p_qty WHERE product_id = p_product;
RETURN v_order_id;
END;
$$;
SELECT place_order(1, 110, 2); -- 1011DELIMITER //
CREATE PROCEDURE place_order(IN p_customer INT, IN p_product INT,
IN p_qty INT, OUT p_order_id INT)
BEGIN
DECLARE v_stock INT;
DECLARE v_price DECIMAL(10,2);
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK; -- যা হয়েছে সব ফেরাও
RESIGNAL; -- মূল ত্রুটিটি ডাকার জনের কাছে পাঠাও
END;
IF p_qty IS NULL OR p_qty <= 0 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'quantity must be positive';
END IF;
START TRANSACTION;
SELECT stock, price INTO v_stock, v_price
FROM products WHERE product_id = p_product
FOR UPDATE; -- সারি না পেলে v_stock থাকে NULL
IF v_stock IS NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'product not found';
ELSEIF v_stock < p_qty THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'insufficient stock';
END IF;
INSERT INTO orders (customer_id, order_date, status)
VALUES (p_customer, CURDATE(), 'pending');
SET p_order_id = LAST_INSERT_ID();
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES (p_order_id, p_product, p_qty, v_price);
UPDATE products SET stock = stock - p_qty WHERE product_id = p_product;
COMMIT;
END //
DELIMITER ;
CALL place_order(1, 110, 2, @new_id);
SELECT @new_id; -- 1011দুটির পার্থক্যগুলো শিক্ষণীয়। PostgreSQL-এর function নিজে transaction শুরু করে না — ডাকার স্টেটমেন্টের transaction-এ চলে, আর RAISE EXCEPTION হলে পুরো স্টেটমেন্ট (সাথে function-এর সব বদল) বাতিল হয়। MySQL-এর procedure নিজেই START TRANSACTION আর COMMIT করছে, আর ত্রুটি হলে HANDLER ROLLBACK করে। এর একটি পার্শ্বপ্রতিক্রিয়া আছে: MySQL-এ START TRANSACTION ডাকার জনের আগে থেকে খোলা transaction-কে নীরবে commit করে দেয় (implicit commit). তাই কোন স্তর transaction-এর মালিক — অ্যাপ না procedure — দলে আগে থেকে ঠিক করে নিতে হয়। নিচের ডায়াগ্রামে PostgreSQL সংস্করণটি দুবার চলছে: প্রথমে সফল, তারপর ব্যর্থ:
1FUNCTION place_order(p_customer, p_product, p_qty)2BEGIN3 SELECT stock, price INTO v_stock, v_price4 FROM products WHERE ... FOR UPDATE;5 IF NOT FOUND THEN RAISE EXCEPTION ...;6 ELSIF v_stock < p_qty THEN RAISE EXCEPTION ...;7 END IF;8 INSERT INTO orders ... RETURNING order_id INTO v_order_id;9 INSERT INTO order_items VALUES (v_order_id, ...);10 UPDATE products SET stock = stock - p_qty ...;11 RETURN v_order_id;12END;
| ডাক | |
| v_stock | |
| v_order_id | |
| টেবিলে stock (110) | |
| সারির lock |
- ডাক ১: Rahim (1) চান ২টি Smart Watch. function শুরু; DECLARE-এর ভ্যারিয়েবলগুলো NULL. টেবিলে স্টক 5.
- লাইন ৩–৪: lock করে পড়া। v_stock = 5, v_price = 3500.
FOR UPDATEএখন সারিটি ধরে রেখেছে — এই মুহূর্তে অন্য কেউ একই পণ্যের অর্ডার দিলে তাঁর SELECT এখানে অপেক্ষা করবে। - লাইন ৫–৬: যাচাই। সারি পাওয়া গেছে (FOUND সত্য), আর 5 < 2 মিথ্যা — কোনো ত্রুটি নেই, function এগিয়ে চলে।
- লাইন ৮: অর্ডার। orders-এ নতুন সারি;
RETURNING … INTOনতুন id 1011 ভ্যারিয়েবলে এনে দিল। - লাইন ৯: অর্ডার-লাইন। (1011, 110, 2, 3500) — দাম অ্যাপ থেকে নয়, products থেকে; তাই কেউ অ্যাপে দাম বদলে পাঠাতে পারবে না।
- লাইন ১০: স্টক কমানো। 5 − 2 = 3. তিনটি বদলই এখনো একটি অসমাপ্ত transaction-এর অংশ।
- লাইন ১১: ফেরত 1011. ডাকার স্টেটমেন্ট শেষ, transaction commit — তিনটি বদল একসাথে স্থায়ী হলো, আর lock ছাড়া হলো।
- ডাক ২: Mitu (8) চান ১০টি। আবার lock করে পড়া: এবার v_stock = 3, কারণ ডাক ১ স্টক কমিয়েছে।
- লাইন ৬: 3 < 10 সত্য — RAISE EXCEPTION. function সেখানেই থামে; লাইন ৮–১১ কখনো চলে না। ত্রুটি ডাকার স্টেটমেন্টে পৌঁছায়, transaction rollback, lock ছাড়া। কোনো অর্ধেক অর্ডার, ভুল স্টক বা অনাথ লাইন থাকে না।
- শেষ অবস্থা। একটি নতুন অর্ডার (1011), স্টক 3, আর Mitu-র অ্যাপ পেয়েছে স্পষ্ট ত্রুটি-বার্তা, যা দেখে সে বলতে পারে “মাত্র ৩টি আছে”। ব্যর্থ পথটিও নকশার অংশ — শুধু সফল পথ নয়।
SQLite-এ একই কাজ — BEGIN…COMMIT-এর ভেতরে
SQLite-এ procedure নেই, তাই এই স্টেটমেন্টগুলো অ্যাপ পাঠায়, একটি transaction-এ। প্রথমে সফল পথ — Rahim-এর ২টি Smart Watch:
BEGIN;
SELECT stock, price FROM products WHERE product_id = 110; -- অ্যাপ যাচাই করে
INSERT INTO orders (customer_id, order_date, status)
VALUES (1, '2025-10-20', 'pending');
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
SELECT last_insert_rowid(), 110, 2, price FROM products WHERE product_id = 110;
UPDATE products SET stock = stock - 2 WHERE product_id = 110;
COMMIT;
SELECT o.order_id, o.status, oi.quantity, oi.unit_price, p.stock
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p ON p.product_id = oi.product_id
WHERE o.order_id = (SELECT MAX(order_id) FROM orders);প্রথম SELECT দেয় 5 | 3500 — অ্যাপ এখানে দেখে 5 ≥ 2. তারপর দুটি INSERT আর UPDATE-এ “১টি সারি বদলেছে”, আর শেষ SELECT: 1011 | pending | 2 | 3500 | 3. last_insert_rowid() হলো PostgreSQL-এর RETURNING-এর কাজ: আগের INSERT-এর নতুন id (1011). আর দাম বসেছে INSERT … SELECT দিয়ে, সরাসরি products থেকে। একটি পার্থক্য খেয়াল করুন: SQLite-এ সারি-ভিত্তিক FOR UPDATE নেই। SQLite পুরো ডেটাবেসে একসাথে একজন লেখককেই অনুমতি দেয়, তাই BEGIN IMMEDIATE দিয়ে শুরু করলে প্রথম থেকেই লেখার অধিকার নেওয়া যায় — পর্ব ৪৬-এ বিস্তারিত।
এবার ব্যর্থ পথ। procedure যে কাজটি IF … RAISE দিয়ে করে, অ্যাপ-পক্ষে তার একটি নিরাপদ রূপ হলো শর্তসহ UPDATE: WHERE stock >= 10 — যাচাই আর বদল একই স্টেটমেন্টে, তাই মাঝখানে কেউ ঢুকতে পারে না। কতটি সারি বদলাল, দেখে অ্যাপ সিদ্ধান্ত নেয়:
BEGIN;
INSERT INTO orders (customer_id, order_date, status)
VALUES (8, '2025-10-20', 'pending');
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
SELECT last_insert_rowid(), 110, 10, price FROM products WHERE product_id = 110;
UPDATE products SET stock = stock - 10
WHERE product_id = 110 AND stock >= 10; -- স্টক থাকলেই কমাও
-- "০টি সারি বদলেছে" দেখে অ্যাপ সব ফেরায়:
ROLLBACK;
SELECT (SELECT COUNT(*) FROM orders) AS orders,
(SELECT COUNT(*) FROM order_items) AS lines,
(SELECT stock FROM products WHERE product_id = 110) AS watch_stock;দুটি INSERT সফল (“১টি সারি বদলেছে”), কিন্তু UPDATE-এ “০টি সারি বদলেছে” — 5 ≥ 10 মিথ্যা, তাই কোনো সারি শর্ত মানেনি। এটিই ব্যর্থতার সংকেত। ROLLBACK-এর পর শেষ SELECT: 10 | 16 | 5 — নতুন অর্ডার আর লাইন দুটিই উধাও, স্টক অক্ষত। procedure-এর RAISE EXCEPTION আর অ্যাপের “সারি-সংখ্যা দেখে ROLLBACK” — একই নীতির দুই রূপ। যে অ্যাপ সারি-সংখ্যা দেখে না, সে COMMIT করে দিত একটি অর্ডার, যার পণ্য গুদামে নেই।
শেষ প্রতিরক্ষা থাকে ডেটাবেসের নিজের নিয়মে। db-shop-এর products-এ আছে CHECK (stock >= 0); শর্ত ছাড়া UPDATE products SET stock = stock - 10 WHERE product_id = 110 চালালে পান CHECK constraint failed: stock >= 0. আর procedure-এর মতো নিজের ত্রুটি-বার্তা চাইলে SQLite-এ আছে trigger (পর্ব ২৯-এ বিস্তারিত) — RAISE(ABORT, '…') দিয়ে:
CREATE TRIGGER order_items_stock_guard
BEFORE INSERT ON order_items
WHEN (SELECT stock FROM products
WHERE product_id = NEW.product_id) < NEW.quantity
BEGIN
SELECT RAISE(ABORT, 'insufficient stock');
END;
INSERT INTO orders VALUES (1011, 8, '2025-10-20', 'pending');
INSERT INTO order_items VALUES (1011, 110, 10, 3500);“✓ CREATE TRIGGER”, orders-এর INSERT-এ “১টি সারি বদলেছে”, তারপর ত্রুটি (স্টেটমেন্ট ৩): insufficient stock — trigger দেখেছে 5 < 10. কিন্তু এখানে একটি শিক্ষা লুকিয়ে আছে: BEGIN নেই, তাই প্রতিটি স্টেটমেন্ট নিজেই একটি transaction. orders-এর সারি 1011 আগেই স্থায়ী হয়ে গেছে — এখন একটি লাইনহীন অনাথ অর্ডার। trigger একটি স্টেটমেন্ট পাহারা দেয়; কয়েকটি স্টেটমেন্টকে এক করে transaction. এডিটরে INSERT দুটির আগে BEGIN; যোগ করুন — ত্রুটির পর অ্যাপ ROLLBACK করলে অনাথ অর্ডারও থাকত না।
৮ · transfer_money, cursor আর ত্রুটি সামলানো
দ্বিতীয় ক্লাসিক উদাহরণ: একটি wallet থেকে আরেকটিতে টাকা পাঠানো, bKash-এর “Send Money”-র মতো। নিয়ম: প্রেরকের ব্যালান্স যথেষ্ট হতে হবে, প্রেরক আর প্রাপক আলাদা, অঙ্ক ধনাত্মক, আর ডেবিট-ক্রেডিট দুটিই হবে, নয়তো একটিও না। এখানে একটি নতুন বিপদ আছে — deadlock: A→B আর B→A দুটি লেনদেন একসাথে চললে প্রথমটি A-র সারি lock করে B-র জন্য অপেক্ষা করে, দ্বিতীয়টি B lock করে A-র জন্য। প্রতিকার: দুটি সারি সবসময় একই ক্রমে lock করা (ছোট id আগে)।
CREATE OR REPLACE PROCEDURE transfer_money(p_from int, p_to int, p_amount numeric)
LANGUAGE plpgsql
AS $$
BEGIN
IF p_amount IS NULL OR p_amount <= 0 OR p_from = p_to THEN
RAISE EXCEPTION 'invalid transfer';
END IF;
-- দুটি সারি একই ক্রমে lock: deadlock এড়াতে
PERFORM 1 FROM wallets WHERE wallet_id IN (p_from, p_to)
ORDER BY wallet_id FOR UPDATE;
UPDATE wallets SET balance = balance - p_amount
WHERE wallet_id = p_from AND balance >= p_amount;
IF NOT FOUND THEN -- শেষ UPDATE কোনো সারি বদলায়নি
RAISE EXCEPTION 'insufficient balance or no wallet %', p_from;
END IF;
UPDATE wallets SET balance = balance + p_amount WHERE wallet_id = p_to;
IF NOT FOUND THEN
RAISE EXCEPTION 'wallet % not found', p_to; -- ডেবিটও ফিরে যাবে
END IF;
INSERT INTO transfers (from_id, to_id, amount) VALUES (p_from, p_to, p_amount);
END;
$$;
CALL transfer_money(1, 2, 500);এখানে procedure-এ COMMIT নেই, ইচ্ছে করেই: CALL-এর transaction-এর ভেতরেই সব চলে, আর যেকোনো RAISE পুরো CALL ফেরায় — দ্বিতীয় UPDATE ব্যর্থ হলে প্রথমটিও। এবার SQLite-এ। এডিটরের ছোট schema: wallets (wallet_id, owner, balance — CHECK (balance >= 0); Rahim 2000, Karima 300, Sabbir 0) আর transfers (transfer_id, from_id, to_id, amount). প্রথমে Rahim থেকে Karima-কে ৫০০ টাকা:
-- transfer_money(1, 2, 500)
BEGIN;
UPDATE wallets SET balance = balance - 500 WHERE wallet_id = 1;
UPDATE wallets SET balance = balance + 500 WHERE wallet_id = 2;
INSERT INTO transfers (from_id, to_id, amount) VALUES (1, 2, 500);
COMMIT;
SELECT * FROM wallets;
SELECT * FROM transfers;
SELECT SUM(balance) AS total_money FROM wallets;তিনটি বদলেই “১টি সারি বদলেছে”। wallets: 1 | Rahim | 1500, 2 | Karima | 800, 3 | Sabbir | 0; transfers: 1 | 1 | 2 | 500; আর মোট 2300 — শুরুতেও ছিল 2000 + 300 + 0 = 2300. টাকা এক জায়গা থেকে আরেক জায়গায় গেছে, তৈরি বা ধ্বংস হয়নি। প্রতিটি লেনদেনের পরে এই “মোট অপরিবর্তিত” যাচাইটি পেমেন্ট সিস্টেমের সবচেয়ে মৌলিক পরীক্ষা।
এবার একটি বিপজ্জনক ভুল, যা AI-এর লেখা কোডেও প্রায়ই দেখা যায়। ডেবিটে শর্ত আছে (balance >= 5000), কিন্তু কেউ দেখছে না সেটি আসলে কোনো সারি বদলাল কি না — PL/pgSQL-এর IF NOT FOUND-এর কাজটি বাদ পড়েছে:
-- ভুল: ডেবিট শর্তসহ, কিন্তু ফল যাচাই ছাড়াই ক্রেডিট
BEGIN;
UPDATE wallets SET balance = balance - 5000
WHERE wallet_id = 1 AND balance >= 5000;
UPDATE wallets SET balance = balance + 5000 WHERE wallet_id = 3;
INSERT INTO transfers (from_id, to_id, amount) VALUES (1, 3, 5000);
COMMIT;
SELECT * FROM wallets;
SELECT SUM(balance) AS total_money FROM wallets;কোনো ত্রুটি নেই — সেটিই ভয়ের। ডেবিটে “০টি সারি বদলেছে” (2000 ≥ 5000 মিথ্যা), কিন্তু ক্রেডিটে “১টি সারি বদলেছে”। wallets: 1 | Rahim | 2000, 2 | Karima | 300, 3 | Sabbir | 5000; মোট 7300 — শূন্য থেকে ৫০০০ টাকা তৈরি হলো! CHECK এখানে বাঁচায়নি, কারণ কোনো ব্যালান্স ঋণাত্মক হয়নি। শিক্ষা: শর্তসহ UPDATE ব্যবহার করলে প্রতিবার বদলানো সারির সংখ্যা যাচাই করতেই হবে — PL/pgSQL-এ IF NOT FOUND বা GET DIAGNOSTICS, MySQL-এ ROW_COUNT(), অ্যাপে driver-এর rowcount.
বিকল্প নিরাপদ পথ: ডেবিটে কোনো শর্তই না দিয়ে CHECK-কে পাহারাদার বানানো। ব্যালান্স ঋণাত্মক হওয়ার মুহূর্তে ডেটাবেস নিজেই স্টেটমেন্টটি থামায়:
BEGIN;
UPDATE wallets SET balance = balance - 5000 WHERE wallet_id = 1;
UPDATE wallets SET balance = balance + 5000 WHERE wallet_id = 3;
COMMIT;“✓ BEGIN”, তারপর ত্রুটি (স্টেটমেন্ট ২): CHECK constraint failed: balance >= 0. ক্রেডিট আর COMMIT কখনো চলেনি — এডিটর প্রথম ত্রুটিতে থামে, যেমন একটি procedure RAISE-এ থামে। বাস্তব অ্যাপে এখানে ROLLBACK পাঠাতে হয়, কারণ SQLite-এ ব্যর্থ স্টেটমেন্টের পরেও transaction খোলা থাকে। CHECK কাজ করে কোন অ্যাপ বা কোন procedure লিখছে তা নির্বিশেষে — তাই নিয়মের সবচেয়ে ভরসাযোগ্য জায়গা constraint, আর procedure-এর যাচাই তার ওপর বাড়তি স্তর, যা ভালো ত্রুটি-বার্তা দেয়।
Cursor — সারি ধরে ধরে, আর কখন দরকার নেই
Cursor একটি কোয়েরির ফলের ওপর আঙুল রেখে একটি একটি সারি পড়ার ব্যবস্থা। PL/pgSQL-এর FOR r IN SELECT … LOOP ভেতরে ভেতরে একটি cursor-ই। MySQL-এ পুরো রূপটি লিখতে হয় — DECLARE, OPEN, FETCH, আর সারি ফুরোলে ধরার জন্য একটি NOT FOUND HANDLER:
DECLARE done INT DEFAULT 0;
DECLARE v_id INT;
DECLARE cur CURSOR FOR SELECT product_id FROM products WHERE stock < 10;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN cur;
read_loop: LOOP
FETCH cur INTO v_id;
IF done THEN LEAVE read_loop; END IF;
UPDATE products SET stock = stock + 20 WHERE product_id = v_id;
END LOOP;
CLOSE cur;কিন্তু এই এগারো লাইনের কাজ আসলে একটি স্টেটমেন্টের। SQL set-ভিত্তিক ভাষা — “এই শর্তের সব সারি” একবারে বলাই তার শক্তি:
-- cursor-এর লুপের বদলে: স্টক ১০-এর কম এমন সব পণ্যে ২০টি করে যোগ
UPDATE products SET stock = stock + 20 WHERE stock < 10;
SELECT product_id, name, stock FROM products
WHERE product_id IN (103, 107, 110);“✓ UPDATE — ৩টি সারি বদলেছে”, আর ফল: 103 | Power Bank | 20 (ছিল 0), 107 | Saree | 28 (ছিল 8), 110 | Smart Watch | 25 (ছিল 5). cursor-এর লুপ প্রতিটি সারির জন্য আলাদা UPDATE চালায় — এক লাখ সারিতে এক লাখ স্টেটমেন্ট; set-ভিত্তিক UPDATE একটি, আর optimizer পুরো কাজটির জন্য একটি পরিকল্পনা করতে পারে। cursor দরকার তখনই, যখন প্রতিটি সারির জন্য সত্যিই আলাদা কিছু করতে হয় — যেমন প্রতি সারির পর বাইরের কোনো function ডাকা, বা অংশ ৬-এর মতো ব্যাচে ব্যাচে COMMIT.
Procedure-এর ভেতরে ত্রুটি ধরা
PL/pgSQL-এ BEGIN … EXCEPTION WHEN … END দিয়ে ত্রুটি ধরে সামলানো যায়। block-টি ভেতরে ভেতরে একটি savepoint: ত্রুটি হলে শুধু সেই block-এর বদলগুলো ফেরে, বাকি transaction চলতে থাকে।
BEGIN
INSERT INTO customers (name, city, phone, joined_on)
VALUES (p_name, p_city, p_phone, current_date);
EXCEPTION
WHEN unique_violation THEN -- phone UNIQUE: আগেই নিবন্ধিত
RAISE NOTICE 'phone % already registered', p_phone;
-- এখানে যা ঠিক মনে হয়: বিদ্যমান গ্রাহক খুঁজে নেওয়া, বা আবার RAISE
END;দুটি সতর্কতা। প্রথমত, savepoint-এর খরচ আছে; লক্ষ সারির লুপের ভেতরে EXCEPTION block রাখলে কাজ লক্ষণীয়ভাবে ধীর হয়। দ্বিতীয়ত, ত্রুটি গিলে ফেলা (WHEN OTHERS THEN NULL) সবচেয়ে খারাপ অভ্যাস — ব্যর্থতা লুকিয়ে যায়, আর ডেটা নীরবে ভুল হয়। যে ত্রুটি আপনি নির্দিষ্টভাবে সামলাতে জানেন, শুধু সেটিই ধরুন।
৯ · নিরাপত্তা, টেস্ট আর পারফরম্যান্স
Dynamic SQL-এ SQL injection
একটি প্রচলিত ভুল ধারণা: “stored procedure ব্যবহার করলে SQL injection হয় না।” সত্য শুধু এটুকু: procedure-এর প্যারামিটার যখন মান হিসেবে ব্যবহার হয় (WHERE product_id = p_product), তখন তা কখনো SQL কোড হয় না। কিন্তু procedure-এর ভেতরে লেখা জোড়া দিয়ে কোয়েরি বানালে — dynamic SQL — ব্যবহারকারীর লেখা আবার কোড হয়ে যেতে পারে। নিরাপদ পথ দুটি: মান পাঠান USING দিয়ে, আর টেবিল বা কলামের নাম (যা placeholder হতে পারে না) বসান format()-এর %I দিয়ে, যা নামটিকে ঠিকমতো উদ্ধৃত করে:
-- বিপজ্জনক: p_status সরাসরি লেখায় জোড়া হচ্ছে
-- EXECUTE 'SELECT count(*) FROM ' || p_table || ' WHERE status = ''' || p_status || '''';
-- নিরাপদ: নাম %I দিয়ে, মান USING দিয়ে ($1 = প্রথম মান)
EXECUTE format('SELECT count(*) FROM %I WHERE status = $1', p_table)
INTO v_count
USING p_status;
-- আরও নিরাপদ: কোন টেবিল চলবে, তার তালিকা আগে থেকে ঠিক করা
IF p_table NOT IN ('orders', 'archived_orders') THEN
RAISE EXCEPTION 'table not allowed';
END IF;%L একটি মানকে লেখা হিসেবে উদ্ধৃত করে, তবে মানের জন্য USING ভালো — কোড আর ডেটা পুরোপুরি আলাদা থাকে। MySQL-এ সমতুল্য: PREPARE stmt FROM @sql; EXECUTE stmt USING @status;, আর SQL Server-এ প্যারামিটারসহ sp_executesql. মূল নীতিটি পর্ব ৪৯ আর ৫১-এর মতোই: ব্যবহারকারীর দেওয়া কোনো মান কখনো SQL-এর লেখায় জোড়া দেবেন না।
SECURITY DEFINER — কার অধিকারে চলবে
PostgreSQL-এ function সাধারণত চলে ডাকার জনের অধিকারে (SECURITY INVOKER). SECURITY DEFINER লিখলে চলে function-এর মালিকের অধিকারে — এভাবেই “টেবিলে অধিকার নেই, কিন্তু procedure চালাতে পারে” ব্যবস্থাটি তৈরি হয়। এটি শক্তিশালী, তাই বিপজ্জনকও: function-এর ভেতরের কোনো ফাঁক মানে মালিকের পূর্ণ অধিকার আক্রমণকারীর হাতে। PostgreSQL-এর ডকুমেন্টেশনের পরামর্শগুলো:
CREATE OR REPLACE FUNCTION place_order(p_customer int, p_product int, p_qty int)
RETURNS int
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = shop, pg_temp -- নিজের schema ঠিক করা, pg_temp শেষে
AS $$ ... $$;
REVOKE EXECUTE ON FUNCTION place_order(int, int, int) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION place_order(int, int, int) TO web_app;search_path ঠিক না করলে আক্রমণকারী নিজের schema-য় products নামে একটি টেবিল বা একই নামের একটি operator বানিয়ে function-কে সেটি ব্যবহার করাতে পারে। আর PostgreSQL-এ নতুন function চালানোর অধিকার ডিফল্টভাবে PUBLIC — সবার — থাকে, তাই REVOKE লাইনটি ভুলবেন না।
টেস্ট আর version
procedure-ও কোড, তাই তারও টেস্ট লাগে। PostgreSQL-এর জন্য pgTAP একটি জনপ্রিয় unit-test framework: টেস্টগুলো নিজেরাই SQL, একটি transaction-এ চলে আর শেষে ROLLBACK হয়, তাই ডেটাবেস নোংরা হয় না। SQL Server-এ tSQLt, Oracle-এ utPLSQL একই কাজ করে। একটি ভালো টেস্ট-তালিকায় থাকে সফল পথ, প্রতিটি ব্যর্থ পথ (স্টক কম, পণ্য নেই, পরিমাণ শূন্য), আর অপরিবর্তনীয় নিয়ম (মোট টাকা অপরিবর্তিত)। আর procedure-এর প্রতিটি সংস্করণ রাখুন migration ফাইলে (পর্ব ২৩, ৫০), git-এ — কখনো সরাসরি production সার্ভারে হাতে বদলাবেন না।
পারফরম্যান্স আর plan cache
PL/pgSQL একটি function-এর প্রতিটি SQL স্টেটমেন্টের plan প্রথমবার চলার সময় বানায়, আর সেই সংযোগে (session) জমা রাখে; পরের ডাকগুলোতে আবার parse আর plan করতে হয় না। প্রথম কয়েকবার (বর্তমানে পাঁচবার) প্যারামিটারের আসল মান দেখে আলাদা plan বানানো হয়; তারপর একটি সাধারণ (generic) plan যথেষ্ট ভালো হলে সেটিতে চলে যায় — plan_cache_mode সেটিং দিয়ে এটি নিয়ন্ত্রণ করা যায়। SQL Server-ও procedure-এর plan জমা রাখে, আর MySQL প্রতিটি সংযোগে stored program-এর একটি cache রাখে। লাভ স্পষ্ট, তবে ফাঁদও আছে: প্রথম প্যারামিটারের জন্য ভালো plan পরের অন্য রকম প্যারামিটারের জন্য খারাপ হতে পারে — ভাবনার প্রশ্ন ৫-এ।
আরও গভীরে: PL/pgSQL-এর প্রতিটি expression — এমনকি v_stock < p_qty — আসলে PostgreSQL-এর executor দিয়ে একটি ছোট SELECT হিসেবে মূল্যায়িত হয়, আর SQL স্টেটমেন্টগুলো যায় Server Programming Interface (SPI) দিয়ে। তাই PL/pgSQL-এ হাজার হাজারবার ঘোরা সাধারণ গণনার লুপ C বা Python-এর চেয়ে অনেক ধীর; এর শক্তি SQL চালানোয়, গণিতে নয়। আর প্রতিটি EXCEPTION block একটি subtransaction খোলে, যা কিছু লিখলে নিজস্ব transaction id পায়। একটি transaction-এ এমন ৬৪-র বেশি subtransaction id জমলে প্রতিটি সংযোগের জন্য রাখা ছোট cache উপচে পড়ে, আর তখন অন্য সংযোগগুলোর snapshot তৈরি ধীর হয়ে যেতে পারে — একটি লুপ গোটা সার্ভারকে ধীর করে দেওয়ার পরিচিত কারণ।
১০ · AI-এর যুগে procedure লেখা
“একটি place_order procedure লিখে দাও” — AI কয়েক সেকেন্ডে চল্লিশ লাইন দেয়, আর তা সাধারণত চলেও। কিন্তু procedure-এর ভুলগুলো সফল পথে দেখা যায় না; দেখা যায় ব্যর্থ পথে, একসাথে দুটি অনুরোধ এলে, বা অন্য DBMS-এ। AI-এর সাধারণ ভুলগুলো:
| AI-এর ভুল | ফল | কীভাবে ধরবেন |
|---|---|---|
SQLite-এর জন্য CREATE PROCEDURE লিখেছে | syntax error | প্রম্পটে DBMS আর সংস্করণ বলুন |
MySQL আর PostgreSQL মিশিয়েছে (ELSEIF বনাম ELSIF, DELIMITER, $$) | তৈরিই হয় না | একটি খালি test ডেটাবেসে আগে CREATE চালান |
| স্টক পড়ে যাচাই করেছে, lock ছাড়া | একসাথে দুই অর্ডারে শেষ পণ্য দুবার বিক্রি | FOR UPDATE বা শর্তসহ UPDATE খুঁজুন |
| শর্তসহ UPDATE-এর পরে সারি-সংখ্যা দেখেনি | অংশ ৮-এর মতো টাকা তৈরি | প্রতিটি UPDATE-এর পরে IF NOT FOUND / ROW_COUNT আছে কি না |
WHEN OTHERS THEN NULL বা ত্রুটি গিলে ফেলা HANDLER | ব্যর্থতা লুকায়, অর্ধেক কাজ থেকে যায় | প্রতিটি HANDLER-এ ROLLBACK আর RESIGNAL আছে কি না |
dynamic SQL-এ || দিয়ে মান জোড়া | SQL injection | USING আর %I খুঁজুন |
| ভুল volatility (টেবিল পড়ে, অথচ IMMUTABLE) | পুরোনো বা ভুল ফল | function কী কী পড়ে, তালিকা করুন |
| নেই এমন কলাম বা টেবিলের নাম | CREATE সফল, প্রথম ডাকে ত্রুটি (PL/pgSQL ভেতরের SQL পরে যাচাই করে) | সব পথ অন্তত একবার চালিয়ে দেখুন |
AI-কে কোড চাওয়ার আগে নিয়ম, schema আর ব্যর্থ পথ বলে দিন — আর টেস্টও চেয়ে নিন:
AI-এর লেখা procedure চালানোর আগে — চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- Stored procedure আর function-এর পার্থক্য? — function মান ফেরত দেয় আর কোয়েরির ভেতরে চলে; procedure CALL দিয়ে চলে, মান ফেরত দেয় না (OUT প্যারামিটার ছাড়া), PostgreSQL-এ নিজে COMMIT করতে পারে।
- IN, OUT আর INOUT প্যারামিটার কী? — IN শুধু ভেতরে আসে, OUT শুধু বাইরে যায়, INOUT দুই দিকে।
- Stored procedure কি SQL injection পুরোপুরি ঠেকায়? — না; ভেতরে dynamic SQL-এ মান জোড়া দিলে injection সম্ভব। USING বা parameter দিয়ে মান পাঠাতে হয়।
- IMMUTABLE আর STABLE-এর পার্থক্য? — IMMUTABLE চিরকাল একই ইনপুটে একই ফল দেয়; STABLE শুধু একটি স্টেটমেন্টের ভেতরে, আর টেবিল পড়তে পারে।
- Procedure ব্যবহারের দুটি সুবিধা আর দুটি অসুবিধা? — সুবিধা: কম round trip, EXECUTE-ভিত্তিক নিরাপত্তা; অসুবিধা: vendor lock-in, টেস্ট আর version রাখা কঠিন।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১SQLite পৃথিবীর সবচেয়ে বেশি ব্যবহৃত ডেটাবেসগুলোর একটি, অথচ stored procedure কখনো যোগ করেনি। এটি কি ঘাটতি, না সচেতন নকশা?
উত্তর খুঁজতে আগে জিজ্ঞেস করুন, stored procedure কোন সমস্যার সমাধান। অংশ ২-এর প্রধান যুক্তি ছিল round trip: অ্যাপ আর ডেটাবেস আলাদা যন্ত্রে, মাঝে নেটওয়ার্ক, তাই কাজ ডেটার কাছে পাঠালে সময় বাঁচে। SQLite-এ এই নেটওয়ার্কই নেই। SQLite একটি library, যা অ্যাপের নিজের প্রোগ্রামের ভেতরে চলে; একটি “কোয়েরি পাঠানো” মানে একটি সাধারণ function ডাক, এক যন্ত্র থেকে আরেক যন্ত্রে যাওয়া নয়। ফলে অ্যাপের ভাষায় লেখা একটি লুপ আর ডেটাবেসের ভেতরের একটি লুপ — দুটির খরচ প্রায় সমান।
দ্বিতীয় যুক্তি ছিল “অনেক অ্যাপ, এক নিয়ম” আর EXECUTE-ভিত্তিক নিরাপত্তা। SQLite-এর সাধারণ ব্যবহারে — মোবাইল অ্যাপ, ব্রাউজার, ডেস্কটপ সফটওয়্যার — একটি ডেটাবেস ফাইল সাধারণত একটি অ্যাপই ব্যবহার করে, আর SQLite-এ ব্যবহারকারী, GRANT বা অধিকারের ধারণাই নেই; ফাইল খুলতে পারলেই সব পড়া যায়। তাই procedure দিয়ে অধিকার সীমিত করার প্রশ্ন ওঠে না।
তবু নিজের function-এর দরকার পড়ে, আর SQLite সেটি দিয়েছে অন্য পথে: sqlite3_create_function() দিয়ে অ্যাপের ভাষার যেকোনো function SQL-এ বসানো যায়, aggregate আর window function-ও; virtual table দিয়ে এমনকি table-valued function-ও। এতে একটি নতুন প্রোগ্রামিং ভাষা ডেটাবেসের ভেতরে বানাতে, চালাতে আর রক্ষণাবেক্ষণ করতে হয়নি — যা SQLite-এর “ছোট, দ্রুত, নির্ভরযোগ্য” লক্ষ্যের সাথে মেলে। দাম হলো অংশ ৫-এর সতর্কতা: function থাকে অ্যাপে, ডেটাবেস ফাইলে নয়; অন্য প্রোগ্রাম ফাইলটি খুললে সেই function পায় না। trigger অবশ্য SQLite-এ আছে, কারণ ডেটা বদলের নিয়ম ফাইলের সাথেই থাকা দরকার।
মূল উপলব্ধি: একটি ফিচারের মূল্য নির্ভর করে পরিবেশের ওপর। নেটওয়ার্ক আর বহু-ব্যবহারকারী না থাকলে stored procedure-এর প্রধান দুটি সুবিধাই প্রায় শূন্য — তাই SQLite-এর সিদ্ধান্তটি ঘাটতি নয়, তার ব্যবহারের ধরনের সাথে মানানসই নকশা।
প্র ০২১৯৯০-এর দশকে ব্যবসার নিয়ম রাখা হতো ডেটাবেসের procedure-এ, ২০০০-এর দশকের ওয়েব framework-গুলো সেটি অ্যাপে সরিয়ে আনে, আর এখন কিছু প্ল্যাটফর্ম আবার ডেটাবেস function-কে সরাসরি API বানাচ্ছে। এই দোলাচলের কারণ কী?
১৯৯০-এর দশকের client-server যুগে প্রতিটি অফিসের কম্পিউটারে একটি “মোটা” ডেস্কটপ অ্যাপ (Visual Basic, PowerBuilder, Delphi-তে লেখা) সরাসরি Oracle বা Sybase সার্ভারে যুক্ত হতো। নিয়ম যদি অ্যাপে থাকত, প্রতিটি বদলের পর শত শত কম্পিউটারে নতুন সংস্করণ বসাতে হতো, আর ধীর অফিস-নেটওয়ার্কে প্রতিটি round trip দামি ছিল। procedure-এ নিয়ম রাখা মানে এক জায়গায় বদল, সবার জন্য কার্যকর — যৌক্তিক সিদ্ধান্ত।
ওয়েবের যুগে ছবি বদলাল। ব্যবহারকারী আর ডেটাবেসের মাঝে এল অ্যাপ সার্ভার, যা দলের নিজের হাতে, দিনে কয়েকবার deploy করা যায়। Java, Ruby, Python-এ টেস্ট, debugger, code review আর library-র বিশাল ব্যবস্থা তৈরি হলো, যা PL/SQL বা T-SQL-এ ছিল দুর্বল। Ruby on Rails-এর মতো framework ডেটাবেসকে মূলত “ডেটা রাখার জায়গা” হিসেবে দেখার দর্শন জনপ্রিয় করে, আর ORM দিয়ে একই কোড একাধিক DBMS-এ চালানো সহজ হলো। অ্যাপ সার্ভার বাড়ানো সস্তা, ডেটাবেস সার্ভার বাড়ানো কঠিন — তাই CPU-র কাজ অ্যাপে সরানো scaling-এর দিক থেকেও যুক্তিসংগত ছিল।
সাম্প্রতিক বছরগুলোতে PostgREST-এর মতো টুল আর তার ওপর তৈরি কিছু প্ল্যাটফর্ম PostgreSQL-এর schema, view আর function-কে সরাসরি HTTP API হিসেবে খুলে দেয়; সেখানে function আবার ব্যবসার নিয়মের ঘর, আর নিরাপত্তা আসে ডেটাবেসের role ও row-level security থেকে। কারণ — ছোট দলের জন্য আলাদা অ্যাপ সার্ভার লেখা আর চালানোর খরচ বাঁচে, আর PostgreSQL-এর function, টেস্ট-টুল ও migration ব্যবস্থা আগের চেয়ে অনেক পরিণত।
দোলাচলটি তাই ফ্যাশন নয়; প্রতিবার বদলেছে প্রেক্ষাপট — deploy কত সহজ, নেটওয়ার্ক কত ধীর, কোন স্তরে ভালো টুল আছে, আর কোন যন্ত্র বাড়ানো সস্তা। আপনার সিস্টেমের প্রশ্নও একই: কতগুলো অ্যাপ এই ডেটা লেখে? দল কোন ভাষায় সবচেয়ে ভালো টেস্ট লেখে? ডেটাবেসের CPU কি আগেই চাপে আছে?
মূল উপলব্ধি: “logic কোথায় থাকবে” প্রশ্নের চিরস্থায়ী উত্তর নেই — আছে প্রেক্ষাপটের হিসাব। তবে ডেটার সত্যতার নিয়ম (constraint) প্রায় সব যুগেই ডেটাবেসে থাকার যোগ্য।
প্র ০৩place_order-এ FOR UPDATE বাদ দিলে কী ঘটতে পারে? আর FOR UPDATE-এর বদলে শর্তসহ UPDATE (WHERE stock >= qty) কি সমান নিরাপদ?
FOR UPDATE ছাড়া ক্রমটি হয় “পড়ো, ভাবো, লেখো” — check-then-act. স্টক 1, আর দুজন ক্রেতা প্রায় একই মুহূর্তে শেষ ঘড়িটি চাইলেন। PostgreSQL-এর ডিফল্ট READ COMMITTED isolation-এ দুটি function-ই SELECT চালিয়ে দেখল stock = 1, দুটিই যাচাই পার হলো, দুটিই অর্ডার বানাল। এরপর দুটি UPDATE: প্রথমটি stock = 0 করল; দ্বিতীয়টি প্রথমটির commit-এর জন্য অপেক্ষা করে নতুন মান 0 দেখে তা থেকে 1 বাদ দিতে গেল — CHECK থাকায় ত্রুটি। CHECK না থাকলে stock = −1, আর একটি ঘড়ি দুজনের কাছে বিক্রি। এটি পর্ব ৪৭-এ দেখবেন এমন একটি anomaly-র পরিবার: যাচাইয়ের সময় যা সত্য ছিল, লেখার সময় আর সত্য নেই।
FOR UPDATE সমস্যাটি গোড়াতেই থামায়: প্রথম function সারিটি lock করে পড়ে, দ্বিতীয়টির SELECT … FOR UPDATE অপেক্ষা করে। প্রথমটি commit করলে দ্বিতীয়টি সর্বশেষ মান (0) পায়, আর যাচাইয়ে ব্যর্থ হয়ে স্পষ্ট বার্তা দেয়।
শর্তসহ UPDATE (UPDATE … SET stock = stock - 1 WHERE product_id = 110 AND stock >= 1)-ও নিরাপদ, কারণ যাচাই আর বদল একই স্টেটমেন্টে। PostgreSQL-এ দ্বিতীয় UPDATE প্রথমটির commit-এর জন্য অপেক্ষা করে, তারপর সারির নতুন সংস্করণে WHERE শর্তটি আবার যাচাই করে; 0 ≥ 1 মিথ্যা, তাই কোনো সারি বদলায় না। তবে দুটি শর্ত: (১) এরপর সারি-সংখ্যা দেখতেই হবে (অংশ ৮-এর টাকা-তৈরির ভুল), আর (২) অর্ডারের INSERT-গুলো UPDATE-এর পরে রাখা ভালো, বা পুরোটা একটি transaction-এ যাতে ব্যর্থ হলে ফেরানো যায়। এই কৌশলে lock ধরা থাকে কম সময়, কারণ আলাদা SELECT নেই।
আরেকটি বিকল্প SERIALIZABLE isolation, যেখানে DBMS নিজেই এমন সংঘর্ষ ধরে একটি transaction বাতিল করে, আর অ্যাপকে আবার চেষ্টা করতে হয়। আর সব কৌশলের নিচে শেষ জাল হিসেবে থাকা উচিত CHECK (stock >= 0).
মূল উপলব্ধি: concurrency-তে “যাচাই” আর “বদল”-এর মাঝের ফাঁকই বিপদ। সেই ফাঁক বন্ধ করুন lock দিয়ে, বা দুটিকে এক স্টেটমেন্টে মিলিয়ে — আর নিচে constraint রাখুন।
প্র ০৪“আমাদের সব কোয়েরি stored procedure-এ, তাই SQL injection অসম্ভব” — একজন ডেভেলপারের এই দাবি কতটা সত্য? নিরাপত্তা আসলে কোথা থেকে আসে?
দাবিটি আংশিক সত্য, আর আংশিক সত্যই সবচেয়ে বিপজ্জনক। procedure-এর প্যারামিটার যখন সরাসরি SQL-এ মান হিসেবে বসে — WHERE phone = p_phone — তখন ডেটাবেস প্যারামিটারটিকে সবসময় ডেটা হিসেবে দেখে; কেউ p_phone-এ যেকোনো উদ্ধৃতি-চিহ্ন বা SQL শব্দ পাঠালেও তা শুধু একটি অদ্ভুত ফোন নম্বর হয়ে থাকে। এখানে procedure prepared statement-এর মতোই নিরাপদ।
ফাঁক তৈরি হয় তিন জায়গায়। প্রথমত, procedure-এর ভেতরে dynamic SQL: সাজানোর কলাম, ফিল্টারের টেবিল বা খোঁজার শর্ত লেখা জোড়া দিয়ে বানালে ব্যবহারকারীর লেখা আবার কোড হয়ে যায়। সমাধান অংশ ৯-এ: মান USING দিয়ে, নাম %I/quote_ident দিয়ে, আর সম্ভব হলে অনুমোদিত নামের একটি তালিকা। দ্বিতীয়ত, অ্যাপ যদি procedure-কেই লেখা জোড়া দিয়ে ডাকে — "CALL find_customer('" + name + "')" — তাহলে injection ঘটে procedure-এ পৌঁছানোর আগেই। ডাকার সময়ও placeholder লাগে। তৃতীয়ত, SECURITY DEFINER function-এ ঠিক না করা search_path: এখানে কেউ কোড পাঠায় না, বরং ভুল টেবিল বা operator ব্যবহার করিয়ে মালিকের অধিকার কাজে লাগায়।
তাহলে নিরাপত্তা আসলে আসে কয়েকটি স্তর থেকে: (১) কোড আর ডেটা সবসময় আলাদা — প্রতিটি স্তরে parameter; (২) least privilege — অ্যাপের অ্যাকাউন্ট শুধু যা দরকার তা-ই পারে, যেমন শুধু EXECUTE; (৩) অনুমোদিত মানের তালিকা যেখানে parameter সম্ভব নয়; (৪) code review আর স্বয়ংক্রিয় স্ক্যান। procedure দ্বিতীয় স্তরটিকে শক্তিশালী করে — চুরি হওয়া অ্যাকাউন্ট দিয়েও পুরো টেবিল মোছা যায় না — কিন্তু প্রথম স্তরের বিকল্প নয়।
মূল উপলব্ধি: SQL injection একটি অভ্যাসের ব্যর্থতা — লেখা জোড়া দিয়ে কোড বানানো — কোনো প্রযুক্তির নয়। procedure সেই অভ্যাস থেকে রক্ষা করে না; parameter আর least privilege করে।
প্র ০৫একটি procedure সাধারণত দ্রুত চলে, কিন্তু মাঝে মাঝে হঠাৎ শতগুণ ধীর — অথচ কোড বা ডেটা বদলায়নি। plan cache এর পেছনে কীভাবে থাকতে পারে?
ধরুন orders_of_city(p_city) procedure. ডেটায় ঢাকার অর্ডার লক্ষাধিক, আর সিলেটের কয়েকশো। সিলেটের জন্য সেরা plan হলো city-র index দিয়ে কয়েকটি সারি খোঁজা; ঢাকার জন্য পুরো টেবিল একবারে পড়া দ্রুত, কারণ index দিয়ে লক্ষবার আলাদা আলাদা খোঁজা ধীর। একই কোয়েরি, প্যারামিটার অনুযায়ী দুটি ভিন্ন সেরা plan — একে বলে data skew.
SQL Server procedure-এর plan প্রথম ডাকের প্যারামিটার দেখে বানায় আর জমা রাখে — parameter sniffing. সার্ভার restart বা cache খালির পর প্রথম ডাকটি সিলেটের হলে index-ভিত্তিক plan জমা হলো; এরপর ঢাকার প্রতিটি ডাকও সেই plan-এ চলে আর ধীর হয়। কোনো কোড বদলায়নি, শুধু কে আগে ডেকেছিল তা বদলেছে — তাই সমস্যাটি এলোমেলো আর রহস্যময় মনে হয়। প্রচলিত প্রতিকার: OPTION (RECOMPILE) দিয়ে প্রতিবার নতুন plan (CPU-র দাম দিয়ে), OPTIMIZE FOR দিয়ে একটি প্রতিনিধি মানের জন্য plan, বা কোয়েরিকে দুই ভাগ করা। SQL Server 2022 একই কোয়েরির জন্য একাধিক plan রাখার একটি স্বয়ংক্রিয় ব্যবস্থাও এনেছে।
PostgreSQL ভিন্ন কৌশল নেয়: প্রথম পাঁচবার প্রতিটি ডাকে আসল মান দেখে custom plan, তারপর generic plan-এর আনুমানিক খরচ custom plan-গুলোর গড়ের চেয়ে খুব বেশি না হলে generic-এ চলে যায়। এটি sniffing-এর অনেকটা এড়ায়, কিন্তু উল্টো ঝুঁকি আছে — generic plan সব মানের জন্য মাঝারি, কোনোটির জন্য সেরা নয়। PostgreSQL 12 থেকে plan_cache_mode দিয়ে force_custom_plan বা force_generic_plan বেছে নেওয়া যায়। ভুল volatility লেবেলও (অংশ ৫) একই পরিবারের সমস্যা: জমা plan-এ একটি পুরোনো মান স্থির হয়ে যায়।
রোগনির্ণয়ের অভ্যাস: ধীর ডাকের সময় আসল plan দেখুন (PostgreSQL-এ auto_explain, SQL Server-এ Query Store), আর একই কোয়েরি ভিন্ন প্যারামিটারে EXPLAIN করে তুলনা করুন।
মূল উপলব্ধি: plan cache সময় বাঁচায় এই অনুমানে যে, “পরের ডাকও আগের মতো”। ডেটা অসমান হলে অনুমানটি ভাঙে — তখন cache-ই ধীরতার কারণ।
অনুশীলন
একটি কোয়েরি লেখা, একটি হাতে ফল বের করা, একটি function লেখা আর একটি নকশার সিদ্ধান্ত।
-
কোয়েরি লিখুন: একটি procedure
cancel_order(id)-এর কাজ SQLite-এ স্টেটমেন্ট হিসেবে লিখুন: অর্ডারটি 'pending' হলে তার প্রতিটি লাইনের পরিমাণ স্টকে ফেরত দিন আর status 'cancelled' করুন — একটি transaction-এ; pending না হলে কিছুই বদলাবে না। অর্ডার 1006 (Smart Watch × 1, pending) দিয়ে উপরের যেকোনো db-shop এডিটরে চালিয়ে দেখুন। 1001 (delivered) দিলে কী হবে?BEGIN; UPDATE products SET stock = stock + (SELECT oi.quantity FROM order_items oi WHERE oi.order_id = 1006 AND oi.product_id = products.product_id) WHERE product_id IN (SELECT product_id FROM order_items WHERE order_id = 1006) AND (SELECT status FROM orders WHERE order_id = 1006) = 'pending'; UPDATE orders SET status = 'cancelled' WHERE order_id = 1006 AND status = 'pending'; COMMIT;1006-এ: দুটি UPDATE-এই “১টি সারি বদলেছে”; Smart Watch-এর স্টক 5 থেকে 6, অর্ডার cancelled. ক্রম জরুরি — স্টক আগে, status পরে; উল্টো করলে প্রথম UPDATE-এর পরে status আর pending নেই, স্টক ফেরতই হতো না। 1001 দিলে দুটি UPDATE-এই “০টি সারি বদলেছে” — delivered অর্ডার বাতিল হয় না। PL/pgSQL-এ এখানে
IF NOT FOUND THEN RAISE EXCEPTION 'order not pending'দিয়ে স্পষ্ট ত্রুটি দেওয়া ভালো। -
ফল আগে বলুন (trace): wallets: A = 1000, B = 200. একটি অ্যাপ প্রতিটি transfer এভাবে করে:
BEGIN; UPDATE … balance - x WHERE id = A AND balance >= x; UPDATE … balance + x WHERE id = B; COMMIT;— সারি-সংখ্যা না দেখে। পরপর দুবার transfer(A → B, 700) চললে শেষে A, B আর মোট কত? সারি-সংখ্যা দেখে RAISE করে এমন PL/pgSQL procedure হলে কত?অ্যাপের কোডে: প্রথমবার 1000 ≥ 700, তাই A = 300, B = 900, মোট 1200. দ্বিতীয়বার 300 ≥ 700 মিথ্যা — ডেবিট “০টি সারি”, কিন্তু ক্রেডিট চলে: B = 1600. শেষে A = 300, B = 1600, মোট 1900 — ৭০০ টাকা শূন্য থেকে তৈরি।
procedure-এ: প্রথমবার একই (A = 300, B = 900). দ্বিতীয়বার ডেবিটের পরে
IF NOT FOUNDসত্য, RAISE EXCEPTION, পুরো CALL ফেরত। শেষে A = 300, B = 900, মোট 1200 অপরিবর্তিত। -
Function লিখুন (PL/pgSQL):
stock_status(p_product int) RETURNS text— স্টক 0 হলে 'out of stock', ১০-এর কম হলে 'low', নইলে 'ok'; পণ্য না থাকলে RAISE EXCEPTION. কোন volatility লেবেল দেবেন, কেন? db-shop-এ 103, 107 আর 104-এর ফল কী হবে?CREATE OR REPLACE FUNCTION stock_status(p_product int) RETURNS text LANGUAGE plpgsql STABLE AS $$ DECLARE v_stock int; BEGIN SELECT stock INTO v_stock FROM products WHERE product_id = p_product; IF NOT FOUND THEN RAISE EXCEPTION 'product % not found', p_product; ELSIF v_stock = 0 THEN RETURN 'out of stock'; ELSIF v_stock < 10 THEN RETURN 'low'; END IF; RETURN 'ok'; END; $$;লেবেল STABLE: function টেবিল পড়ে কিন্তু বদলায় না; একটি স্টেটমেন্টের ভেতরে ফল একই। IMMUTABLE ভুল হতো, কারণ স্টক বদলায়। ফল: 103 (স্টক 0) → 'out of stock', 107 (8) → 'low', 104 (25) → 'ok'. ক্রমটি খেয়াল করুন: 0-এর যাচাই আগে, নইলে 0 < 10 হিসেবে 'low' আসত।
-
সিদ্ধান্ত নিন: নিচের প্রতিটি কাজ কোথায় রাখবেন — ডেটাবেসের constraint, stored procedure, নাকি অ্যাপ? এক লাইনের যুক্তি দিন। (ক) অর্ডারের পর গ্রাহককে SMS পাঠানো; (খ) “স্টক কখনো শূন্যের নিচে নয়”; (গ) মাসের শেষে এক কোটি সঞ্চয় হিসাবে সুদ যোগ; (ঘ) ব্যবহারকারীর password hash করা।
- (ক) অ্যাপে। SMS বাইরের সেবা, rollback করা যায় না; transaction-এর ভেতর থেকে পাঠালে পরে rollback হলেও SMS চলে যেত। commit-এর পরে অ্যাপ (বা একটি queue) পাঠাবে।
- (খ) Constraint-এ (
CHECK (stock >= 0)) — কোনো অ্যাপ, procedure বা হাতে লেখা UPDATE এটি এড়াতে পারে না; procedure-এর যাচাই শুধু ভালো বার্তার জন্য বাড়তি স্তর। - (গ) ডেটাবেসে — একটি set-based UPDATE, বা ব্যাচে COMMIT করা procedure (অংশ ৬)। কোটি সারি অ্যাপে টেনে এনে হিসাব করে ফেরত পাঠানো মানে বিশাল নেটওয়ার্ক-খরচ।
- (ঘ) অ্যাপে — bcrypt বা Argon2-এর মতো পরীক্ষিত library দিয়ে। আসল password ডেটাবেসে পাঠালে তা কোয়েরি log-এ থেকে যেতে পারে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি atomic place_order procedure-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- পরিমাণ ধনাত্মক কি না যাচাই
- পণ্যের সারি FOR UPDATE দিয়ে lock করে স্টক আর দাম পড়া
- পণ্য না থাকলে বা স্টক কম হলে RAISE EXCEPTION
- orders-এ নতুন সারি, নতুন order_id ফেরত নেওয়া
- order_items-এ লাইন, দাম products থেকে
- স্টক কমানো, তারপর commit
SQLite-এ CREATE PROCEDURE place_order(...) ... চালালে কী হবে?
- procedure তৈরি হবে, CALL দিয়ে চালানো যাবে
- সতর্কবার্তা দিয়ে উপেক্ষা করবে
- syntax error, কারণ SQLite-এ stored procedure নেই
- procedure-টি স্বয়ংক্রিয়ভাবে trigger হয়ে যাবে
PostgreSQL-এ FUNCTION আর PROCEDURE-এর একটি সঠিক পার্থক্য কোনটি?
- শুধু function PL/pgSQL-এ লেখা যায়
- procedure CALL দিয়ে চলে আর নিজের ভেতরে COMMIT করতে পারে; function পারে না
- function কোনো ডেটা বদলাতে পারে না
- procedure SELECT-এর ভেতরে ব্যবহার করা যায়, function যায় না
একটি PostgreSQL function শুধু customers টেবিল পড়ে একজন গ্রাহকের শহর ফেরত দেয়, কিছু বদলায় না। কোন volatility লেবেল সবচেয়ে উপযুক্ত?
- IMMUTABLE
- VOLATILE, কারণ এটিই সবসময় সঠিক
- DETERMINISTIC
- STABLE
wallets টেবিলে balance-এ কোনো CHECK নেই। একটি transaction-এ প্রথমে UPDATE wallets SET balance = balance - 500 WHERE id = 1 AND balance >= 500 (wallet 1-এর ব্যালান্স 200), তারপর UPDATE wallets SET balance = balance + 500 WHERE id = 2, তারপর COMMIT. ফল কী?
- wallet 1 অপরিবর্তিত, wallet 2-তে ৫০০ বাড়ল — মোট টাকা ৫০০ বেড়ে গেল
- প্রথম UPDATE-এ ত্রুটি, সব বাতিল
- wallet 1 হলো −300, wallet 2-তে ৫০০ বাড়ল
- দুটিই বাদ, কারণ DBMS বোঝে লেনদেনটি অসম্পূর্ণ
একটি PL/pgSQL function-এ টেবিলের নাম p_table আর একটি মান p_status ব্যবহারকারী থেকে আসে। কোন dynamic SQL নিরাপদ?
- EXECUTE 'SELECT count(*) FROM ' || p_table || ' WHERE status = ''' || p_status || '''';
- EXECUTE format('SELECT count(*) FROM %s WHERE status = %s', p_table, p_status);
- EXECUTE format('SELECT count(*) FROM %I WHERE status = $1', p_table) USING p_status;
- EXECUTE 'SELECT count(*) FROM "' || p_table || '" WHERE status = ''' || p_status || '''';
MySQL-এর mysql কমান্ড-লাইন client-এ procedure বানানোর আগে DELIMITER // লেখা হয় কেন?
- সার্ভারকে জানাতে যে এটি একটি procedure
- শরীরের ভেতরের ; দেখে client যেন স্টেটমেন্টটি মাঝপথে না পাঠায়
- // দিয়ে মন্তব্য লেখার জন্য
- procedure-কে transaction-এর ভেতরে রাখতে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Function মান বা টেবিল ফেরত দেয় আর কোয়েরির ভেতরে চলে; procedure CALL দিয়ে চলে, PostgreSQL-এ নিজে COMMIT করতে পারে। SQLite-এ দুটির কোনোটিই SQL দিয়ে লেখা যায় না — function আসে অ্যাপের ভাষা থেকে।
- লাভ: কম round trip, নিয়ম এক জায়গায়, EXECUTE-ভিত্তিক নিরাপত্তা। দাম: টেস্ট ও version কঠিন, vendor lock-in, ডেটাবেস সার্ভারে বাড়তি চাপ।
- PL/pgSQL:
CREATE OR REPLACE FUNCTION … RETURNS … LANGUAGE plpgsql AS $$ DECLARE … BEGIN … END $$; ত্রুটি তোলেRAISE EXCEPTION, টেবিল ফেরত দেয়RETURN QUERY. - Volatility একটি প্রতিশ্রুতি: শুধু ইনপুট → IMMUTABLE, টেবিল বা সময় পড়ে → STABLE, বদলায় → VOLATILE. ভুল প্রতিশ্রুতি মানে ভুল ফল।
- place_order আর transfer_money: lock বা শর্তসহ UPDATE, প্রতিটি UPDATE-এর পরে সারি-সংখ্যা যাচাই, ব্যর্থ হলে সব ফেরত, নিচে CHECK constraint; dynamic SQL-এ মান যায় শুধু USING দিয়ে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৯ · Trigger — নিয়ম মেনে স্বয়ংক্রিয় কাজপরবর্তী পর্বINSERT, UPDATE বা DELETE-এ DBMS নিজে যে কোড চালায়।
- পর্ব ২৪ · View আর materialized viewফিরে দেখুনtable-valued function-এর সবচেয়ে কাছের SQLite-বিকল্প।
- ডেটাবেস (DBMS) ও SQL · কোর্স হোম৬৭টি পর্বপুরো কোর্সের রোডম্যাপ — ১০টি মডিউল।
- Python · SQLite ডেটাবেসহাতে-কলমেPython থেকে SQLite: transaction, placeholder আর নিজের function.
- Java · JDBC দিয়ে ডেটাবেসপাশের ট্র্যাকঅ্যাপ থেকে ডেটাবেস ডাকা — prepared statement আর transaction.