Go + Postgres পারফরম্যান্স টিউনিং: API-এর জন্য একটি কার্যকর প্লেবুক
Go + Postgres পারফরম্যান্স টিউনিং প্লেবুক: কানেকশন পুল, কুয়েরি প্ল্যান দেখুন, স্মার্ট ইনডেক্সিং, নিরাপদ প্যাজিনেশন, এবং দ্রুত JSON আকার দিন।

Go APIs-এ Postgres-এ "ধীর" কেমন দেখায়
AI-জেনারেটেড API গুলো প্রাথমিক পরীক্ষায় দ্রুত মনে হতে পারে। আপনি একটি এন্ডপয়েন্ট কয়েকবার হিট করেন, ডেটাসেট ছোট থাকে, এবং অনুরোধগুলো একবারে আসে। তারপর বাস্তব ট্রাফিক আসে: মিশ্র এন্ডপয়েন্ট, ঝটিপটে লোড, কোল্ড ক্যাশ, এবং প্রত্যাশার চেয়ে বেশি সারি। একই কোড আকস্মিকভাবে ধীর অনুভূত হতে পারে যদিও আসলে কিছু ভাঙেনি।
ধীরতা সাধারণত কিছু উপায়ে ظاهر হয়: ল্যাটেন্সি স্পাইক (অধিকাংশ অনুরোধ ঠিক আছে, কিন্তু কিছু 5x থেকে 50x বেশি সময় নেয়), টাইমআউট (ছোট শতাংশ বিফল হয়), অথবা CPU উচ্চ (Postgres-এ কুয়েরি কাজ থেকে CPU, অথবা Go-তে JSON, goroutines, লগিং, ও রিট্রাই থেকে)।
একটি সাধারণ দৃশ্য হল একটি লিস্ট এন্ডপয়েন্ট যেখানে একটি ফ্লেক্সিবল সার্চ ফিল্টার বড় JSON রেসপন্স দেয়। টেস্ট ডাটাবেসে এটি কয়েক হাজার সারি স্ক্যান করে দ্রুত শেষ হয়। প্রোডাকশনে এটি কয়েক মিলিয়ন সারি স্ক্যান করে, সেগুলোকে সাজায়, এবং তারপর LIMIT প্রয়োগ করে। API এখনও "কাজ করে", কিন্তু p95 ল্যাটেন্সি বিস্ফোরিত হয় এবং ঝটপট লোডে কয়েকটি অনুরোধ টাইমআউট করে।
ডেটাবেস ধীর নাকি অ্যাপ ধীর তা আলাদা করতে মানসিক মডেল সহজ রাখুন।
যদি ডেটাবেস ধীর হয়, আপনার Go হ্যান্ডলার বেশিরভাগ সময় কুয়েরির জন্য অপেক্ষা করে কাটায়। আপনি দেখতে পেতে পারেন অনেক অনুরোধ "in flight" আটকে আছে যখন Go CPU স্বাভাবিক থাকে।
যদি অ্যাপ ধীর হয়, কুয়েরি দ্রুত শেষ হয়, কিন্তু কুয়েরির পরে সময় চলে যায়: বড় রেসপন্স অবজেক্ট তৈরি করা, JSON মার্শাল করা, প্রতিটি সারির জন্য অতিরিক্ত কুয়েরি চালানো, বা অনুরোধের প্রতি বেশি কাজ করা। Go CPU বাড়ে, মেমরি বাড়ে, এবং রেসপন্স সাইজ বাড়লেই ল্যাটেন্সি বাড়ে।
প্রারম্ভিক স্তরে "ভালভাবে কাজ করা" সর্বোচ্চ নয়। অনেক CRUD এন্ডপয়েন্টের জন্য লক্ষ্য রাখুন স্থিতিশীল p95 ল্যাটেন্সি (শুধু গড় নয়), ঝটপট লোডে পূর্বানুমেয় আচরণ, এবং আপনার প্রত্যাশিত পিকে টাইমে কোন টাইমআউট না হওয়া। লক্ষ্য সরল: ডেটা ও ট্রাফিক বাড়লে হঠাৎ ধীর অনুরোধ না হওয়া, এবং যখন কিছু ড্রিফট করে তখন স্পষ্ট সিগন্যাল থাকা।
প্রথমে বেসলাইন: কয়েকটি সংখ্যাই যে বিষয়গুলো মেপে দেয়
কিছুই টিউন করার আগে সিদ্ধান্ত নিন "ভাল" এর মানে কি। বেসলাইন না রাখলে ঘণ্টার পর ঘণ্টা সেটিংস বদলাতে সময় চলে যায় এবং আপনি জানবেন না উন্নতি হয়েছে কি না বা কণ্ঠস্বরটি অন্য কোথায় ঘুরে গেছে।
তিনটি সংখ্যা সাধারণত গল্পের বেশিরভাগ বলে দেয়:
- p95 অনুরোধ ল্যাটেন্সি (গড় নয়)
- ত্রুটির হার (HTTP 5xx, টাইমআউট, বাতিল করা অনুরোধ)
- প্রতি অনুরোধ DB সময় (প্রতিটি অনুরোধ Postgres-এ কতক্ষণ অপেক্ষা করে)
p95 হলো "খারাপ দিনের" মেট্রিক। যদি p95 বেশি কিন্তু গড় ঠিক থাকে, ছোট কিছু অনুরোধ অতিরিক্ত কাজ করছে, লক-এ ব্লক হচ্ছে, বা ধীর প্ল্যান ট্রিগার করছে।
স্লো কুয়েরিগুলোকে শুরুতেই দৃশ্যমান করুন। Postgres-এ, প্র-লঞ্চ টেস্টিংয়ের জন্য লো থ্রেশহোল্ড দিয়ে স্লো কুয়েরি লগিং সক্রিয় করুন (উদাহরণস্বরূপ 100–200 মি.সেক)। পুরো স্টেটমেন্ট লগ করুন যাতে আপনি সেটি SQL ক্লায়েন্টে কপি করে চালাতে পারেন। এটা অস্থায়ী রাখুন — প্রোডাকশনে প্রতিটি স্লো কুয়েরি লগ করলে দ্রুত গোলমাল হয়।
পরের ধাপে, বাস্তব-সদৃশ অনুরোধ দিয়ে পরীক্ষা করুন, কেবল একটি "hello world" রুট নয়। একটি ছোট সেট যথেষ্ট যদি সেটা ব্যবহারকারীরা কীভাবে ব্যবহার করবে তার সাথে মেলে: ফিল্টার ও সর্টিং সহ একটি লিস্ট কল, কয়েকটি join সহ একটি ডিটেইল পেজ, ভ্যালিডেশনসহ একটি create বা update, এবং পার্শিয়াল মেসেজ সহ একটি সার্চ-স্টাইল কুয়েরি।
যদি আপনি স্পেস থেকে এন্ডপয়েন্ট জেনারেট করেন (উদাহরণস্বরূপ Koder.ai-র মতো একটি টুল দিয়ে), একই ছোট অনুরোধগুলো ধারাবাহিক ইনপুট নিয়ে বারংবার চালান। এতে ইনডেক্স, প্যাজিনেশন টুইক, এবং কুয়েরি রিরাইট মতো পরিবর্তনগুলো মাপা সহজ হয়।
অবশেষে, এমন একটি লক্ষ্য বাছুন যা আপনি জোরে জোরে বলতে পারেন। উদাহরণ: "অধিকাংশ অনুরোধ 50 concurrent ব্যবহারকারীর ক্ষেত্রে p95 এ 200 মি.সেকের নিচে থাকবে এবং ত্রুটি 0.5% নিচে থাকবে।" সঠিক সংখ্যা আপনার প্রোডাক্টের ওপর নির্ভর করে, কিন্তু একটি পরিষ্কার লক্ষ্য অনবর্থ tinkering আটকায়।
কানেকশন পুলিং যা Postgres কে স্থিতিশীল রাখে
কানেকশন পুল খোলা ডাটাবেস কানেকশনগুলো সীমিত করে এবং সেগুলো পুনঃব্যবহার করে। পুল না থাকলে প্রতিটি অনুরোধ হয়তো একটি নতুন কানেকশন খুলবে, এবং Postgres সেশন ম্যানেজ করতে সময় ও মেমরি নষ্ট করে, কুয়েরি চালাতে নয়।
লক্ষ্য হল Postgres-কে দরকারী কাজ করতে রাখা, অনেক কানেকশনের মধ্যে কনটেক্সট-সুইচে না রাখা। এটা প্রায়শই প্রথম বাস্তব জয়, বিশেষত AI-জেনারেটেড API গুলো যে নীরবে চ্যাটি এন্ডপয়েন্টে পরিণত হতে পারে।
সহজ স্টার্টিং সেটিংস
Go তে সাধারণত আপনি max open connections, max idle connections, এবং connection lifetime টিউন করেন। অনেক ছোট API-র জন্য একটি নিরাপদ স্টার্টিং পয়েন্ট হল আপনার CPU কোরের ছোট একটি গুণক (সাধারণত মোট 5 থেকে 20 কানেকশন), সমপ্রায় আইডল রাখার সংখ্যা, এবং নিয়মিত কানেকশন রিপ্লেস করা (উদাহরণস্বরূপ প্রতি 30–60 মিনিটে)।
যদি আপনি একাধিক API ইনস্ট্যান্স চালান, মন রাখুন পুল গুনিত হয়। 10 ইনস্ট্যান্সে পুল 20 থাকলে তা মিলিয়ে 200 কানেকশন হবে যা Postgres-এ আঘাত করে; তাই টিমগুলো অনাকাঙ্ক্ষিতভাবে কানেকশন সীমায় পড়ে যায়।
কিভাবে জানবেন পুল সমস্যা হচ্ছে
পুল সমস্যা ধীর SQL থেকে আলাদা অনুভূত হয়।
যদি পুল ছোট হয়, অনুরোধ Postgres-এ পৌঁছানোর আগেই অপেক্ষা করবে। ল্যাটেন্সি স্পাইক দেখাবেন, কিন্তু ডাটাবেস CPU ও কুয়েরি সময় ঠিক থাকতে পারে।
যদি পুল অনেক বড় হয়, Postgres ওভারলোডড মনে হবে: অনেক সক্রিয় সেশন, মেমরি প্রেসার, এবং এন্ডপয়েন্টগুলোর মধ্যে অসম ল্যাটেন্সি।
দুইটি ভাগে DB কল টাইম করে দ্রুত আলাদা করা যায়: কানেকশনের জন্য অপেক্ষা করা সময় বনাম কুয়েরি এক্সিকিউশনের সময়। যদি বেশিরভাগ সময় "ওয়েটিং" হয়, পুলই বোতলগল। যদি বেশিরভাগ সময় "কুয়েরিতে" হয়, SQL ও ইনডেক্সে ফোকাস করুন।
কিছু দ্রুত চেক:
- পুল স্ট্যাটস লগ করুন (open, in-use, idle) এবং দেখুন in-use কি max-এ আটকে আছে।
- কানেকশন অর্জনে timeout যোগ করুন যাতে স্টেজিং-এ ওয়েট দ্রুত ব্যর্থ হয়ে যায়।
- Postgres-এ active connections মনিটর করুন এবং আপনি
max_connections-এর কতটা কাছাকাছি আছেন তা দেখুন। - নিশ্চিত করুন প্রতিটি অনুরোধ rows বন্ধ করে এবং কানেকশন দ্রুত মুক্ত করে।
- আপনি যে অ্যাপ ইনস্ট্যান্স সংখ্যা চালাবেন সেই একই সংখ্যায় লোড টেস্ট করুন।
pgx pool vs database/sql
যদি আপনি pgxpool ব্যবহার করেন, আপনি Postgres-ফার্স্ট পুল পাবেন স্পষ্ট স্ট্যাটস এবং Postgres আচরণে ভাল ডিফল্ট। যদি আপনি database/sql ব্যবহার করেন, আপনি একটি স্ট্যান্ডার্ড ইন্টারফেস পাবেন যা ডাটাবেস জুড়ে কাজ করে, কিন্তু পুল সেটিংস ও ড্রাইভার আচরণ সম্পর্কে স্পষ্ট হতে হবে।
একটি ব্যবহারিক নিয়ম: আপনি যদি সম্পূর্ণভাবে Postgres-এ আছেন এবং সরাসরি কন্ট্রোল চান, pgxpool প্রায়ই সহজ। আপনি যদি লাইব্রেরিগুলোর উপর নির্ভর করেন যা database/sql আশা করে, তাহলে সেটির সাথেই থাকুন, পুল স্পষ্টভাবে সেট করুন, এবং ওয়েটগুলো পরিমাপ করুন।
উদাহরণ: একটি এন্ডপয়েন্ট যে 20 মি.সেক চলে, কিন্তু 100 concurrent ব্যবহারকারীর সময় 2 সেকেন্ডে উঠে যায়। যদি লগ দেখায় 1.9 সেকেন্ড কানেকশনের জন্য অপেক্ষা করা হয়েছে, তাহলে কুয়েরি টিউনিং সাহায্য করবে না যতক্ষণ না পুল ও মোট Postgres কানেকশন সঠিকভাবে সেট করা হয়।
কুয়েরি প্ল্যানিং: EXPLAIN আউটপুট দ্রুত পড়া
যখন একটি এন্ডপয়েন্ট ধীর মনে হয়, দেখুন Postgres আসলেই কী করছে। EXPLAIN দ্রুত পড়লে প্রায়ই মিনিটের মধ্যে সমাধানের দিকে ইঙ্গিত করে।
আপনার API যেই সঠিক SQL পাঠায় সেটির উপর এটা চালান:
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, status, created_at
FROM orders
WHERE user_id = $1 AND status = $2
ORDER BY created_at DESC
LIMIT 50;
কয়েকটা লাইন সবচেয়ে বেশি গুরুত্বপূর্ণ। উপরের নোড দেখুন (Postgres কি চয়ন করেছে) এবং নিচের মোটস (কতক্ষণ লেগেছে)। তারপর estimated vs actual rows তুলনা করুন। বড় ফাঁক সাধারণত পরিকল্পনার ভুল অনুমান বোঝায়।
কী লাইনগুলি সাধারণত মানে দেয়
যদি আপনি দেখেন Index Scan বা Index Only Scan, Postgres ইনডেক্স ব্যবহার করছে, যা সাধারণত ভালো। Bitmap Heap Scan মাঝারি আকারের ম্যাচের জন্য ঠিক আছে। Seq Scan মানে পুরো টেবিল পড়া হয়েছে, যা ছোট টেবিলের ক্ষেত্রে বা প্রায় প্রতিটি সারি মেলে এমন ক্ষেত্রে ঠিক আছে।
সাধারণ রেড ফ্ল্যাগ:
- বড় টেবিলে Seq Scan
- Estimated rows বনাম actual rows অনেক দূরে (উদাহরণ: 10 অনুমান করা হয়েছে কিন্তু 10,000 বাস্তবে)
- Sort বেশিরভাগ সময় নিচ্ছে (প্রায়ই
ORDER BYএর সাথে) - "Filter:" অনেক সারি পরে বাদ দিচ্ছে
- BUFFERS-এ high shared read blocks (অনেক ডেটা পড়া)
প্ল্যান কেন ভুল হয় (এবং সহজ ফিক্স)
ধীর প্ল্যান সাধারণত কিছু প্যাটার্ন থেকে আসে:
- আপনার
WHERE+ORDER BYপ্যাটার্নের জন্য ইনডেক্স নেই (উদাহরণ:(user_id, status, created_at)) - টাইপ mismatch (উদাহরণ: UUID কলামকে text প্যারামিটার সঙ্গে তুলনা), যা ইনডেক্স ব্যবহারে বাধা দেয়
WHERE-এ ফাংশন আছে (উদাহরণ:WHERE lower(email) = $1), যা ইনডেক্স ব্যবহার বন্ধ করতে পারে যদি আপনি সমন্বিত expression index না করেন
যদি প্ল্যান অদ্ভুত দেখায় এবং অনুমান অনেক ভুল, স্ট্যাটস পুরনো হতে পারে। ANALYZE চালান (অথবা autovacuum-কে ধরতে দিন) যাতে Postgres বর্তমান সারি-গণনা ও ভ্যালু ডিস্ট্রিবিউশন শিখে। বড় ইমপোর্টের পর বা নতুন এন্ডপয়েন্ট দ্রুত লিখা শুরু করলে এটা গুরুত্বপূর্ণ।
আপনি যেসব কুয়েরি আসলে চালান সেই অনুযায়ী ইনডেক্সিং
ইনডেক্স কেবল তখনই সাহায্য করে যখন সেগুলো আপনার API কিভাবে ডেটা কোয়েরি করে তার সাথে মেলে। অনুমান করে ইনডেক্স বানালে আপনি শুধু লেখার সময় ধীরতা, স্টোরেজ বাড়া, এবং সামান্য উপকার পাবেন।
প্রায়োগিকভাবে ভাবার উপায়: একটি ইনডেক্স হল একটি নির্দিষ্ট প্রশ্নের জন্য শর্টকাট। যদি আপনার API অন্য প্রশ্ন করে, Postgres সেই শর্টকাট উপেক্ষা করে।
ফিল্টার + sort অর্ডার দেখে ইনডেক্স বানান
যদি একটি এন্ডপয়েন্ট account_id দিয়ে ফিল্টার করে এবং created_at DESC দিয়ে সর্ট করে, একটি কম্পোজিট ইনডেক্স সাধারণত আলাদা দুটি ইনডেক্সকে হারায়। এটি Postgres-কে সঠিক সারি খুঁজে পেতে এবং সেগুলোকে সঠিক ক্রমে কম কাজ করে ফিরিয়ে দিতে সাহায্য করে।
নিয়ম-কথ্যগুলি যা সাধারণত কাজ করে:
- সবচেয়ে বেশি ফিল্টার করা কলামগুলো ইনডেক্স করুন, তারপর আপনি যে কলামে সর্ট করবেন সেটি যোগ করুন।
- কম্পোজিট ইনডেক্স ছোট রাখুন। দুই কলাম সাধারণ; তিনটা কখনো কখনো ঠিক; বেশি হলে সাধারণত সন্দেহজনক।
- সবচেয়ে সিলেকটিভ ফিল্টার প্রথমে রাখুন (যেটা ফলাফল সবচেয়ে বেশি সংকীর্ণ করে)।
- এমন আলাদা ইনডেক্স এড়ান যা একটি ভাল কম্পোজিট ইনডেক্স দ্বারা পুরোপুরি ঢেকে যায়।
- কয়েকটি "শায়ে ব্যবহার করা হতে পারে" ইনডেক্সের চেয়ে একটি ভালো নির্বাচিত ইনডেক্স পছন্দ করুন।
উদাহরণ: যদি আপনার API-তে GET /orders?status=paid থাকে এবং এটি সর্বদা নবীনতম দেখায়, (status, created_at DESC) ধরনের ইনডেক্স ভাল ফিট হবে। যদি বেশিরভাগ কুয়েরিতেই customer দিয়ে ফিল্টার করা হয়, তবে (customer_id, status, created_at) ভাল হতে পারে, কিন্তু কেবল যদি সেটিই প্রোডাকশনে এন্ডপয়েন্ট বাস্তবে চালায়।
সাধারণ ফিল্টারের জন্য partial index
যদি অধিকাংশ ট্রাফিক একটি সংকীর্ণ সারি-সেট লক্ষ্য করে, partial index সস্তা এবং দ্রুত হতে পারে। উদাহরণস্বরূপ, যদি আপনার অ্যাপ বেশিরভাগ সময় active রেকর্ড পড়ে, WHERE active = true কেবল ইনডেক্স করা হলে ইনডেক্স ছোট থাকে এবং মেমরিতে থাকার সম্ভাবনা বেশি।
ইনডেক্স সাহায্য করছে কিনা যাচাই করতে দ্রুত চেক করুন:
EXPLAINচালান (অথবা নিরাপদ পরিবেশেEXPLAIN ANALYZE) এবং দেখুন যে ইনডেক্স স্ক্যান আপনার কুয়েরির সাথে মেলে কি না।- ইনডেক্স থাকলে ও না থাকলে টাইমিং ও পড়া সারি তুলনা করুন।
- "Rows Removed by Filter" যদি বেশি থাকে, তা প্রায়ই মানে যে ইনডেক্স আপনার ফিল্টার মেলে না।
অব্যবহৃত ইনডেক্স সাবধানে সরান। ব্যবহার স্ট্যাটস দেখুন (উদাহরণ: ইনডেক্স স্ক্যান হয়েছে কিনা)। একবারে একটি করে ড্রপ করুন কম-ঝুঁকির সময়ে, এবং রোলব্যাক প্ল্যান রাখুন। অপ্রয়োগী ইনডেক্স নিরব নয় — এগুলো প্রতিটি লেখা অপারেশনে ইনসার্ট ও আপডেটে ধীরতা যোগ করে।
প্যাজিনেশন প্যাটার্ন যা সময়ের সাথে ধীর হয় না
প্যাজিনেশন প্রায়শই যেখানে একটি দ্রুত API ধীর অনুভূত হতে শুরু করে, এমনকি ডাটাবেস স্বাভাবিক থাকলেও। প্যাজিনেশনকে UI-র বিবরণ না করে একটি কুয়েরি ডিজাইন সমস্যা ভাবুন।
কেন LIMIT/OFFSET ধীরে হয়
LIMIT/OFFSET সহজ দেখায়, কিন্তু গভীর পেজগুলো সাধারণত বেশি খরচ করে। Postgres এখনও সেই সারিগুলোর পাশে হাঁটতে হয় (আর প্রায়ই সেগুলোকে সাজাতে হয়) যেগুলো আপনি স্কিপ করেছেন। পেজ 1 হয়তো কয়েক ডজন সারি স্পর্শ করে; পেজ 500 হয়তো ডাটাবেসকে দশ হাজারেরও বেশি সারি স্ক্যান ও বাতিল করতে পারে কেবল 20 ফলাফল রিটার্ন করার জন্য।
এটি অনিয়ন্ত্রিত ফলাফলও তৈরি করতে পারে যখন মাঝে মধ্যে সারি ইনসার্ট বা ডিলিট হয়। ব্যবহারকারীরা ডুপ্লিকেট দেখতে পারে বা আইটেম মিস করতে পারে কারণ "row 10,000"-এর মান টেবিল পরিবর্তনের সাথে বদলে যায়।
Keyset pagination (cursor) একটি "last seen" উদাহরণ
Keyset pagination একটি আলাদা প্রশ্ন করে: "আমাকে শেষ দেখা সারির পরবর্তী 20টি সারি দিন।" এতে ডাটাবেস একটি ছোট, ধারাবাহিক স্লাইসে কাজ করে।
একটি সহজ ভার্সন একটি বাড়তে থাকা id ব্যবহার করে:
SELECT id, created_at, title
FROM posts
WHERE id > $1
ORDER BY id
LIMIT 20;
আপনার API next_cursor হিসেবে পেজে থাকা শেষ id রিটার্ন করে। পরের অনুরোধ সেই মানটি $1 হিসেবে ব্যবহার করে।
টাইম-ভিত্তিক সোর্টিংয়ের জন্য একটি স্থির অর্ডার এবং টাই-ব্রেকার ব্যবহার করুন। শুধুমাত্র created_at যথেষ্ট নয় যদি দুইটি সারির টাইমস্ট্যাম্প একই হয়। একটি যৌগিক কার্
dসর ব্যবহার করুন:
WHERE (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC
LIMIT 20;
কিছু নিয়ম ডুপ্লিকেট ও মিস হওয়া প্রতিরোধ করে:
ORDER BY-এ সর্বদা একটি ইউনিক টাই-ব্রেকার অন্তর্ভুক্ত করুন (সাধারণতid)।- প্রতিটি অনুরোধে একই সোর্ট ব্যবহার রাখুন।
- কার্সার ক্লায়েন্টদের কাছে অপ্যাক করুন (একসাথে
created_atওidএনকোড করুন)। - যদি ব্যবহারকারীরা ফিল্টার করতে পারে, প্রতিটি পেজে একই ফিল্টার অন্তর্ভুক্ত করুন।
- সম্ভব হলে পরিবর্তনশীল ক্ষেত্রগুলোর (status, score) চাইতে অপরিবর্তনীয় সোর্ট ফিল্ড (created time) ব্যবহার করুন।
JSON আকার দেয়ার কৌশল: দ্রুত রেসপন্স ও ছোট পে-লোড
অপ্রত্যাশিতভাবে সাধারণ একটি কারণ যে একটি API ধীর লাগে তা ডাটাবেস নয়—এটি রেসপন্স। বড় JSON তৈরি করতে বেশি সময় লাগে, পাঠাতে বেশি সময় লাগে, এবং ক্লায়েন্টদের পার্স করতে বেশি সময় লাগে। দ্রুততম জয় প্রায়শই কম রিটার্ন করা।
শুরু করুন আপনার SELECT থেকে। যদি একটি এন্ডপয়েন্ট কেবল id, name, এবং status দরকার, সেগুলো কলাম চেয়ে নিন এবং অন্য কিছু নেবেন না। SELECT * কল্পনায় ধীরে ধীরে ভারী হয়ে যায় কারণ টেবিলে বড় টেক্সট, JSON ব্লব, এবং অডিট কলাম বাড়ে।
আরেকটি সাধারণ ধীরতা হলো N+1 প্যাটার্ন: আপনি 50 আইটেমের লিস্ট ফেচ করেন, তারপর সম্পর্কিত ডেটা জোড়তে 50টি আলাদা কুয়েরি চালান। এটা পরীক্ষায় পাস করতে পারে, কিন্তু বাস্তব ট্রাফিকের নিচে ভেঙে পড়ে। একক কুয়েরি পছন্দ করুন যা আপনি প্রয়োজন তা রিটার্ন করে (সাবধানতার সাথে join), অথবা ID গুলো ব্যাচ করে দ্বিতীয় কুয়েরি চালান।
কিছু উপায় পে-লোড ছোট রাখার:
include=ফ্ল্যাগ (অথবাfields=মাস্ক) ব্যবহার করুন যাতে লিস্ট রেসপন্স লীন থাকে এবং ডিটেইল রেসপন্স অপ্ট-ইন করে অতিরিক্তগুলো।- নেস্টেড অ্যারেগুলো ক্যাপ করুন (উদাহরণ: কেবল সর্বশেষ 10 ইভেন্ট) এবং সম্পূর্ণ ইতিহাসের জন্য আলাদা এন্ডপয়েন্ট দিন।
- ক্লায়েন্টরা যদি কেবল কয়েকটি কী চায়, র কাঁচা অভ্যন্তরীণ JSON কলাম ফেরত দেবেন না।
- দীর্ঘ লেবেল বারবার ব্যবহারের বদলে সংক্ষিপ্ত কোড ব্যবহার করুন।
Postgres-এ JSON বানাবেন না কি Go-তে?
উভয়ই দ্রুত হতে পারে। আপনি কী জন্য অপ্টিমাইজ করছেন তার ওপর ভিত্তি করে নির্বাচন করুন।
Postgres JSON ফাংশন (jsonb_build_object, json_agg) কম রাউন্ড-ট্রিপ এবং এক কুয়েরি থেকে predictable শেপ চাইলে উপকারী। Go-তে শেপিং ভাল যখন আপনাকে শর্তাধীন লজিক, struct পুনঃব্যবহার, বা SQL সহজ রাখা দরকার। যদি আপনার JSON-বিল্ডিং SQL পড়তে কঠিন হয়ে যায়, তা টিউন করাও কঠিন হয়ে যায়।
ভাল নিয়ম: Postgres-কে filter, sort, এবং aggregate করতে দিন। তারপর Go-কে ফাইনাল প্রেজেন্টেশন হ্যান্ডেল করতে দিন।
আপনি যদি দ্রুত API জেনারেট করেন (উদাহরণ: Koder.ai দিয়ে), শুরুতেই include ফ্ল্যাগ যোগ করলে এন্ডপয়েন্টগুলো সময়ের সাথে ফেটে যাওয়া এড়ায়। এটি ক্ষেত্র যোগ করার নিরাপদ উপায় দেয় যাতে প্রতিটি রেসপন্স ভারী না হয়।
প্রথম ইউজারদের আগে ধাপে ধাপে টিউনিং পাস
আপনার বড় টেস্ট ল্য্যাবের প্রয়োজন নেই ব্যতিক্রমীভাবে বেশিরভাগ পারফরম্যান্স ইস্যু ধরতে। একটি ছোট, পুনরাবৃত্তি যোগ্য পাস সেই সমস্যাগুলো প্রকাশ করে যা ট্রাফিক আসলে আউটেজে রূপ নেবে, বিশেষত যদি শুরুটা জেনারেটেড কোড থেকে হয় যা আপনি চালাতে যাচ্ছেন।
কিছুই বদলানোর আগে একটি ছোট বেসলাইন নোট করে রাখুন:
- আপনার ব্যস্ততম এন্ডপয়েন্টগুলোর p95 এবং p99 ল্যাটেন্সি
- ত্রুটির হার ও টাইমআউট
- ডাটাবেস CPU ও active connections
- মোট সময় অনুযায়ী সর্বাধিক ধীরে চলা 5 কুয়েরি (শুধু একটিই নয়)
টিউনিং পাস
ছোট থেকে শুরু করুন, একবারে এক জিনিস পরিবর্তন করুন, এবং প্রতিটি পরিবর্তনের পরে আবার টেস্ট করুন।
-
10–15 মিনিটের একটি লোড টেস্ট চালান যা বাস্তব ব্যবহারের মত দেখায়। আপনার প্রথম ইউজাররা যে এন্ডপয়েন্টগুলো হিট করবে সেগুলো হিট করুন (লগইন, লিস্ট পেজ, সার্চ, ক্রিয়েট)। তারপর রুটগুলোকে p95 ল্যাটেন্সি ও মোট সময় অনুযায়ী সাজান।
-
SQL টিউন করার আগে কানেকশন প্রেসার চেক করুন। খুব বড় পুল Postgres-কে ওভারওয়েল্ম করে। খুব ছোট পুল লম্বা অপেক্ষা তৈরি করে। ওয়েট টাইম বাড়ছে কি না দেখুন এবং কানেকশন কাউন্ট স্পাইক করছে কি না। প্রথমে পুল ও idle সীমা ঠিক করুন, তারপর একই লোড আবার চালান।
-
সর্বোচ্চ ধীর কুয়েরিগুলো
EXPLAINকরুন এবং সবচেয়ে বড় রেড ফ্ল্যাগ ফিক্স করুন। সাধারণ অপরাধীরা বড় টেবিলে ফুল টেবিল স্ক্যান, বড় ফলাফল সেটে সোর্ট, এবং এমন join যা সারি সংখ্যা বিস্ফোটিত করে। একটিই সবচেয়ে খারাপ কুয়েরি নিয়ে শুরু করুন এবং সেটাকে রুটিন করে তুলুন। -
একটি ইনডেক্স যোগ করুন বা সমন্বয় করুন, তারপর আবার টেস্ট করুন। ইনডেক্স সাহায্য করে যখন সেগুলো আপনার
WHEREএবংORDER BY-এর সাথে মেলে। একসাথে পাঁচটা যোগ করবেন না। যদি আপনার ধীর এন্ডপয়েন্ট হয় "user_id ধরে অর্ডার লিস্ট করা, created_at অনুযায়ী", একটি কম্পোজিট ইনডেক্স(user_id, created_at)তফাৎ আনতে পারে। -
রেসপন্স ও প্যাজিনেশন টাইট করুন, তারপর আবার টেস্ট করুন। যদি একটি এন্ডপয়েন্ট বড় JSON ব্লব সহ 50 সারি রিটার্ন করে, আপনার ডাটাবেস, নেটওয়ার্ক, এবং ক্লায়েন্ট সবেরই মূল্য চুকাতে হয়। UI যা চায় তা মাত্রাই রিটার্ন করুন, এবং টেবিল বাড়ার সঙ্গে ধীরে না পড়া এমন প্যাজিনেশন প্যাটার্ন ব্যবহার করুন।
একটি সহজ চেঞ্জলগ রাখুন: কী পরিবর্তন হলো, কেন, এবং p95-এ কী পরিবর্তন ঘটলো। যদি কোনো পরিবর্তন আপনার বেসলাইন উন্নত না করে, রিভার্ট করুন এবং সামনে যান।
সাধারণ ভুল ও ফাঁদগুলি যা এড়ানো জরুরি
বেশিরভাগ পারফরম্যান্স সমস্যা Go API ও Postgres-এ স্ব-সৃষ্ট। ভালো খবর হল কয়েকটা চেক প্রায়শই বড়গুলো ধরতে পারে প্রোডাকশনে যাওয়ার আগে।
একটি ক্লাসিক ফাঁদ হল পুল সাইজকে একটি স্পিড নবের মতো দেখা। এটাকে "যত বড় সম্ভব" করে সেট করলে প্রায়ই সবকিছু ধীর হয়ে যায়। Postgres সেশনগুলো জগলিং করতে, মেমরি ও লক নিয়ে বেশি সময় খরচ করে, এবং আপনার অ্যাপ ঢেউয়ের মতো টাইমআউট শুরু করে। একটি ছোট, স্থিতিশীল পুল সাধারণত জিতবে।
আরেকটি সাধারণ ভুল হল "সবকিছুতে ইনডেক্স" করা। অতিরিক্ত ইনডেক্স পড়ার ক্ষেত্রে সাহায্য করতে পারে, কিন্তু লেখাকে ধীর করে এবং আকস্মিকভাবে কুয়েরি প্ল্যান পরিবর্তন করতে পারে। আপনার API যদি ঘন ঘন ইনসার্ট বা আপডেট করে, প্রতিটি অতিরিক্ত ইনডেক্স কাজ বাড়ায়। যোগ করার আগে মাপুন এবং ইনডেক্স যোগ করার পরে প্ল্যানগুলো পুনরায় চেক করুন।
প্যাজিনেশন ঋণ ছদ্মবেশে ঢুকে পড়ে। অফসেট প্যাজিনেশন শুরুতে ঠিক দেখায়, তারপর p95 সময়ের সাথে বাড়ে কারণ ডাটাবেসকে স্কিপ করা সারিগুলোর ওপর দিয়ে যেতে হয়।
JSON পে-লোড সাইজ আরেকটি লুকানো কর। কমপ্রেশন ব্যান্ডউইথ কমায়, কিন্তু বড় অবজেক্ট তৈরি, অ্যালোকেট এবং পার্স করার খরচ মুছে দেয় না। ফিল্ড কাটা, গভীর নেস্টিং এড়ানো, এবং কেবল স্ক্রিন যা চায় তা রিটার্ন করুন।
আপনি যদি শুধুই গড় রেসপন্স টাইম দেখেন, আপনি বাস্তব ব্যবহারকারীর কষ্ট কোথায় শুরু হচ্ছে তা মিস করবেন। p95 (এবং কখনো কখনো p99) হচ্ছে যেখানে পুল স্যাচুরেশন, লক ওয়েট, এবং ধীর প্ল্যান প্রথমে দেখা দেয়।
একটি দ্রুত প্রি-লঞ্চ স্ব-মূল্যায়ন:
- ছোট লোড টেস্ট চলাকালীন পুল ওয়েট টাইম এবং Postgres কানেকশন কাউন্ট দেখুন।
- একই এন্ডপয়েন্টের গড় বনাম p95 তুলনা করুন।
- টেবিল 10x বড় হলে প্যাজিনেশন degrade করে কি না যাচাই করুন।
- লিস্ট এন্ডপয়েন্টগুলোর রেসপন্স সাইজ (বাইট) পরিদর্শন করুন।
- ইনডেক্স যোগ করা বা ফিল্টার পরিবর্তনের পরে
EXPLAINপুনরায় চালান।
লঞ্চের আগে দ্রুত চেকলিস্ট ও পরবর্তী ধাপ
বাস্তব ব্যবহারকারীরা আসার আগে আপনি চাইবেন আপনার API চাপের অধীনে পূর্বানুমেয় থাকে—প্রমাণ থাকুক যে এটা এমনভাবে থাকবে। লক্ষ্য নিখুঁত সংখ্যা নয়; লক্ষ্য হল সেই কয়েকটি সমস্যা ধরা যা টাইমআউট, স্পাইক বা ডাটাবেস যে নতুন কাজ গ্রহণ বন্ধ করে দেয় সেসব সৃষ্টি করে।
একটি staging পরিবেশে পরীক্ষা করুন যা প্রোডাকশনের মতো (সমান DB সাইজ রেঞ্জ, একই ইনডেক্স, একই পুল সেটিংস): গুরুত্বপূর্ণ এন্ডপয়েন্টে লোডে p95 ল্যাটেন্সি মাপুন, সময় অনুযায়ী আপনার শীর্ষ ধীর কুয়েরিগুলো ক্যাপচার করুন, সবচেয়ে খারাপ কুয়েরি EXPLAIN (ANALYZE, BUFFERS) চালিয়ে নিশ্চিত করুন এটি আপনার প্রত্যাশিত ইনডেক্স ব্যবহার করছে, এবং আপনার ব্যস্ততম রুটগুলোর পে-লোড সাইজ স্যানিটি-চেক করুন।
তারপর একটি worst-case র্যান করুন যা পণ্য ভাঙার মত করে: একটি গভীর পেজ অনুরোধ করুন, সবচেয়ে বিস্তৃত ফিল্টার প্রয়োগ করুন, এবং একটি cold start-এ (API রিস্টার্ট করে একই রিকোয়েস্ট প্রথমে হিট করা) চেষ্টা করুন। যদি গভীর প্যাজিনেশন প্রতিটি পেজে ধীর হয়, লঞ্চের আগে কার্সর-ভিত্তিক প্যাজিনেশন চালু করুন।
আপনার ডিফল্ট লেখা রাখুন যাতে দল পরবর্তীতে ধারাবাহিক পছন্দ করে: পুল লিমিট ও টাইমআউট, প্যাজিনেশন নিয়ম (সর্বোচ্চ পেজ সাইজ, অফসেট অনুমোদিত কি না, কার্সর ফরম্যাট), কুয়েরি নিয়ম (প্রয়োজনীয় কলামই সিলেক্ট করা, SELECT * এড়ানো, ব্যয়বহুল ফিল্টার কপিং), এবং লগিং নিয়ম (স্লো কুয়েরি থ্রেশহোল্ড, নমুনা কতদিন রাখবেন, কিভাবে এন্ডপয়েন্ট লেবেল করবেন)।
আপনি যদি Koder.ai দিয়ে Go + Postgres সার্ভিস নির্মাণ ও এক্সপোর্ট করেন, ডিপ্লয়মেন্টের আগে একটি ছোট পরিকল্পনা পাস ফিল্টার, প্যাজিনেশন, এবং রেসপন্স শেপগুলো ইন্টেনশনাল রাখে। একবার আপনি ইনডেক্স ও কুয়েরি শেপ টিউন করা শুরু করলে, স্ন্যাপশট ও রোলব্যাক থাকলে এমন একটি "ফিক্স"কে দ্রুত উল্টিয়ে ফেলা সহজ হয় যা একটি এন্ডপয়েন্টকে সাহায্য করে কিন্তু অন্যগুলোর কাছে ক্ষতিকর। যদি আপনি সেই ওয়ার্কফ্লোতে এক জায়গায় ইটারেট করতে চান, Koder.ai on koder.ai চ্যাটের মাধ্যমে সেসব সার্ভিস জেনারেট ও পরিমার্জন করার জন্য ডিজাইন করা হয়েছে, তারপর আপনি প্রস্তুত হলে সোর্স এক্সপোর্ট করতে পারেন।
সাধারণ প্রশ্ন
How do I quickly tell if my Go API is slow because of Postgres or because of my code?
Start by separating DB wait time from app work time.
- If the database is slow, the handler mostly waits on the query. Go CPU often stays normal while requests pile up “in flight.”
- If the app is slow, queries return fast but time is spent building objects, doing extra per-row queries, marshaling large JSON, or logging. Go CPU and memory usually rise with response size.
Add simple timing around “wait for connection” and “query execution” to see which side dominates.
What metrics should I track first before tuning anything?
Use a small baseline you can repeat:
- p95 latency per key endpoint (not average)
- error rate (5xx, timeouts, cancellations)
- DB time per request (time spent waiting on Postgres)
Pick a clear target like “p95 under 200 ms at 50 concurrent users, errors under 0.5%.” Then only change one thing at a time and re-test the same request mix.
Should I turn on Postgres slow query logging, and what threshold is practical?
Enable slow query logging with a low threshold in pre-launch testing (for example 100–200 ms) and log the full statement so you can copy it into a SQL client.
Keep it temporary:
- It gets noisy fast in production.
- It can add overhead if you log too much.
Once you’ve found the worst offenders, switch to sampling or raise the threshold.
What are good starting connection pool settings for a Go API on Postgres?
A practical default is a small multiple of CPU cores per API instance, often 5–20 max open connections, with similar max idle connections, and recycle connections every 30–60 minutes.
Two common failure modes:
- Pool too small: requests wait to get a connection even though Postgres query time looks fine.
- Pool too big: Postgres gets overloaded with many active sessions and latency becomes uneven.
Remember pools multiply across instances (20 connections × 10 instances = 200 connections).
How can I confirm the connection pool is the bottleneck (not SQL)?
Time DB calls in two parts:
- Time waiting for a connection (pool wait)
- Time executing the query (Postgres work)
If most time is pool wait, adjust pool sizing, timeouts, and instance counts. If most time is query execution, focus on EXPLAIN and indexes.
Also confirm you always close rows promptly so connections return to the pool.
What should I look for first in EXPLAIN when an endpoint is slow?
Run EXPLAIN (ANALYZE, BUFFERS) on the exact SQL your API sends and look for:
- Seq Scan on a large table
- Huge gap between estimated rows vs actual rows
- Sort dominating time (often with
ORDER BY) - “Rows Removed by Filter” being very high
- Lots of shared read blocks in BUFFERS (heavy reads)
Fix the biggest red flag first; don’t tune everything at once.
How do I choose the right index for a list endpoint with filters and sorting?
Indexes should match what the endpoint actually does: filters + sort order.
Good default approach:
- Build a composite index for your common
WHERE+ORDER BYpattern. - Keep it small (2 columns often, 3 sometimes).
- Put the most selective filter first, then the sort column.
Example: if you filter by user_id and sort by newest, an index like (user_id, created_at DESC) is often the difference between stable p95 and spikes.
When is a partial index worth it in Postgres?
Use a partial index when most traffic hits a predictable subset of rows.
Example pattern:
- Many reads only for
active = true - Few queries for inactive rows
A partial index like ... WHERE active = true stays smaller, is more likely to fit in memory, and reduces write overhead versus indexing everything.
Confirm with EXPLAIN that Postgres actually uses it for your high-traffic queries.
Why does LIMIT/OFFSET pagination get slower over time, and what should I use instead?
LIMIT/OFFSET gets slower on deep pages because Postgres still has to walk past (and often sort) the skipped rows. Page 500 can be dramatically more expensive than page 1.
Prefer keyset (cursor) pagination:
- Use a stable sort plus a unique tie-breaker (often
id). - Keep
ORDER BYidentical across requests. - Encode
(created_at, id)or similar into a cursor.
This keeps each page cost roughly constant as tables grow.
My DB queries are fast, but responses are still slow—should I trim JSON payloads?
Usually yes for list endpoints. The fastest response is the one you don’t send.
Practical wins:
- Select only needed columns (avoid
SELECT *). - Add
include=orfields=so clients opt into heavy fields. - Cap nested arrays (e.g., latest 10 items) and fetch the rest via a dedicated endpoint.
- Avoid N+1 patterns (50 rows + 50 extra queries). Use joins or batch queries.
You’ll often reduce Go CPU, memory pressure, and tail latency just by shrinking payloads.