WIUNIX ACADEMY
پیداکردن کوئریهای پرهزینه در PostgreSQL و خواندن درست plan
صفحهٔ 2 از 3؛ مراحل قبلی و پیشنیازها را پیش از ادامه مرور کن.
آموزش متناسب با محیط تو
کجا اجرا میکنی؟
سیستمعامل و نسخه را انتخاب کن تا مراحل، دستورها و راهحلها برای همان محیط نمایش داده شوند.
آموزش متناسب با مقادیر تو
تنظیمات این آموزش
مقادیر نمونه را برای محیط خودت تنظیم کن؛ متن و دستورهای مرتبط با هم تغییر میکنند. رمز، کلید خصوصی و توکن وارد نکن. مقادیر فقط در همین زبانه نگهداری میشوند و به سرور یا لینک اشتراکگذاری فرستاده نمیشوند. پیش از اجرا، DNS، گواهیها و سیاست شبکهٔ واقعی را بررسی کن.
آیپی، دامنه یا پورت مثالها با محیط تو فرق دارد؟ دکمهٔ زیر را باز کن، مقادیر را تغییر بده و «اعمال در آموزش» را بزن؛ سپس دستورهای بهروزشده را کپی کن.
تنظیم مقادیر برای محیط من3 مقدار قابلتغییر
قدم بعدی
اختلافِ قابلاعتماد را حساب کنید
اگر آمار سراسری reset شده باشد، کمکردن نمونهٔ قبل از بعد معتبر نیست. رکوردی که زمان آغاز آمارش عوض شده نیز کنار گذاشته میشود. افزایش dealloc هشدار میدهد که بخشی از رکوردها از مجموعه خارج شدهاند و این بازه ممکن است همهٔ کار را نشان ندهد.
این پرسوجو فقط رکوردهای مشترک دو نمونه را مقایسه میکند؛ کوئری تازهای که پس از نمونهٔ اول ظاهر شده در نتیجه نیست و باید جدا دیده شود. اختلاف منفی هم نشانهٔ اجرای سریعتر نیست. چنین دادهای را بهعنوان مبنای تصمیم کنار میگذاریم.
همان نشست باز psql برای پایش
SELECT b.captured_at,clock_timestamp() AS observed_at,i.stats_reset=b.stats_reset AS same_reset,i.dealloc-b.dealloc AS evicted_entries FROM public.pg_stat_statements_info i CROSS JOIN academy_info_before b;
SELECT s.queryid,s.userid,s.toplevel,s.calls-b.calls AS interval_calls,round((s.total_exec_time-b.total_exec_time)::numeric,2) AS interval_ms,round(((s.total_exec_time-b.total_exec_time)/NULLIF(s.calls-b.calls,0))::numeric,2) AS ms_per_call,s.rows-b.rows AS interval_rows,s.shared_blks_read-b.shared_blks_read AS blocks_read,s.temp_blks_written-b.temp_blks_written AS temp_blocks_written
FROM public.pg_stat_statements s JOIN academy_stat_before b USING(dbid,userid,queryid,toplevel) CROSS JOIN academy_info_before ib CROSS JOIN public.pg_stat_statements_info ii
WHERE s.stats_since=b.stats_since AND ii.stats_reset=ib.stats_reset AND s.calls>b.calls AND s.total_exec_time>=b.total_exec_time
ORDER BY s.total_exec_time-b.total_exec_time DESC LIMIT 15;فهرست هزینهٔ همین بازه آماده است و محدودیت reset، خروج رکورد و کوئریهای تازه روشن است.
از عددها فرضیه بسازید، نه نسخهٔ فوری ایندکس
تکرار زیاد همراه با زمان کل بالا میتواند به کار تکراری یا رفتوبرگشت زیاد اشاره کند. هزینهٔ زیاد هر اجرا به پارامترهایی شبیه درخواستهای واقعی برنامه و plan نیاز دارد. ستون rows تعداد ردیف خروجی یا تغییریافته را میشمارد، نه همهٔ ردیفهایی که درون گرههای plan خوانده شدهاند.
خواندن بلوک از دید PostgreSQL ممکن است در نهایت از cache سیستمعامل تأمین شود؛ این عدد را تأخیر مستقیم دیسک ندانید. نوشتن بلوک موقت میتواند نشانهٔ نوشتن دادهٔ موقت روی دیسک باشد، ولی افزایش سراسری work_mem در عملیات همزمان مصرف را چند برابر میکند. همزمانی و تفاوت پارامترها را در بازتولید حفظ کنید.
قدم بعدی
برای آزمایش plan، اتصال دوم را باز کنید
در ترمینال دیگری به دیتابیس دورریختنی وصل شوید و مقصد را بررسی کنید. همهٔ پنلهای باقیماندهٔ SQL در همین نشست دوم اجرا میشوند. جدول، داده و شیء آمار این بخش موقتاند و پس از بستهشدن نشست از بین میروند.
تعداد ردیف، زمان مجاز و فضای موقت را پیش از اجرا محدود نگه دارید. این داده برای نشاندادن یک رابطهٔ معلوم بین ستونها ساخته میشود؛ آزمون کارایی محیط عملیاتی یا بازسازی کامل دادههای برنامه نیست.
کلاینت پایگاهدادهٔ آزمایشیِ قابل حذف
/usr/lib/postgresql/18/bin/psql -X -v ON_ERROR_STOP=1 'service=academy_plan_lab'اتصال دوم فقط به دیتابیس مخصوص آزمایش وصل است.
قدم بعدی
دادهای بسازید که رابطهٔ ستونهایش را میدانید
در این مثال وضعیت هر tenant از روی همان شناسه تعیین میشود؛ دو شرط مستقل نیستند. اگر برنامهریز آنها را مستقل فرض کند، ممکن است تعداد ردیفهای مطابق هر دو شرط را کمتر از واقع تخمین بزند.
برآورد و مقدار واقعی را در گرهٔ scan مقایسه کنید؛ گرهٔ بالاییِ count در هر حال یک ردیف نتیجه میدهد. وجود sequential scan بهتنهایی خطا نیست. مهم این است که تعداد ردیفها، تعداد تکرار گره و کار لازم با واقعیت داده سازگار باشد.
همان نشست psql در پایگاهدادهٔ آزمایشی
SET statement_timeout='30s';
CREATE TEMP TABLE academy_plan_probe AS SELECT i AS id,(i%100)::integer AS tenant_id,CASE WHEN i%100<5 THEN 'pending' ELSE 'done' END::text AS status FROM generate_series(1,100000) AS g(i);
ANALYZE academy_plan_probe;
EXPLAIN (ANALYZE,BUFFERS,TIMING OFF,SETTINGS) SELECT count(*) FROM academy_plan_probe WHERE tenant_id=1 AND status='pending';در plan آزمایشی، تخمین و تعداد واقعی ردیفهای گرهٔ scan را میبینید.
قدم بعدی
رابطهٔ ستونها را در آمار ثبت و دوباره مقایسه کنید
آمار dependency را روی همان جدول موقت بسازید، ANALYZE را تکرار کنید و همان پرسوجو را بدون تغییر داده دوباره ببینید. این بار برنامهریز اطلاعاتی دربارهٔ وابستگی دو ستون دارد.
بهبود تخمین لزوماً زمان اجرا را کمتر نمیکند. ساخت و جمعآوری آمار روی جدول واقعی هزینه دارد و باید با مسئول همان دیتابیس بررسی شود. این مثال موقت، توصیهٔ خودکار برای تغییر آمار همهٔ جدولها نیست.
همان نشست psql در پایگاهدادهٔ آزمایشی
CREATE STATISTICS pg_temp.academy_plan_dependency(dependencies) ON tenant_id,status FROM pg_temp.academy_plan_probe;
ANALYZE academy_plan_probe;
EXPLAIN (ANALYZE,BUFFERS,TIMING OFF,SETTINGS) SELECT count(*) FROM pg_temp.academy_plan_probe WHERE tenant_id=1 AND status='pending';تخمین جدید را با اجرای واقعی روی همان داده مقایسه کردهاید؛ جدول اصلی یا ایندکس production تغییر نکرده است.
EXPLAIN ANALYZE واقعاً کوئری را اجرا میکند
EXPLAIN معمولی برآورد را نشان میدهد، اما افزودن ANALYZE باعث اجرای statement و اضافهشدن هزینهٔ اندازهگیری میشود. دستور نوشتن یا تابع دارای اثر بیرونی را بدون بررسی در آن قرار ندهید؛ rollback هم همهٔ اثرهای بیرونی و افزایش sequence را برنمیگرداند.
برای مسئلهٔ واقعی، یک کوئری خواندنی متناسب با بار واقعی برنامه را با پارامتر درست و timeout در staging بررسی کنید. تعداد تکرار گره، خطای تخمین ردیف، کار روی بلوک و فایل موقت را با هم بخوانید. اجرای دوم با cache گرم بهتنهایی شاهد بهبود پایدار نیست.