چطور یه کوئری ساده، فاکتور ۱۳۴ دلاری از Cloudflare برام ساخت
خلاصهٔ کاملتر
نویسنده در فوریه ۲۰۲۶ یه سایت به اسم whatmedicaidpays.com راه انداخت که دادههای Medicaid آمریکا رو نمایش میده. استک پروژه: SvelteKit روی Cloudflare Workers، دیتابیس D1 (SQLite ابری کلودفلر) و Drizzle به عنوان ORM. ماهها فاکتور چند دلاری بود تا اینکه در آوریل یه فاکتور ۱۳۴ دلاری اومد که ۹۵٪ش فقط از «row reads» بود.
D1 به جای تعداد کوئری، بر اساس تعداد ردیفهایی که SQLite اسکن میکنه هزینه میگیره — چه اون ردیفها برگردونده بشن، چه نه. جدول اصلی (reimbursement) با ۷۶۵ هزار ردیف هیچ ایندکسی نداشت. چهار کوئری داغ مسئول ۹۳٪ کل row reads بودن؛ از جمله select max(year) from reimbursement که در هر page load اجرا میشد و هر بار کل جدول رو اسکن میکرد.
مشکل اینجا بدتر میشه: تابع getNavData() روی هر صفحه از طریق +layout.svelte صدا زده میشد و خودش دو تابع دیگه رو صدا میزد که هر کدوم یه full table scan بودن. یعنی هر بازدیدکننده، قبل از لود دادهی اصلی صفحه، دو بار کل جدول رو اسکن میکرد.
راهحل اول: ایندکسهای composite با Drizzle. چهار ایندکس ترکیبی روی ستونهایی که کوئریهای پرمصرف بر اساسشون فیلتر و گروهبندی میکردن اضافه شد:
(table) => [
index('reimbursement_year_idx').on(table.year),
index('reimbursement_year_hcpcs_idx').on(table.year, table.hcpcsCodeId),
index('reimbursement_year_state_idx').on(table.year, table.stateId),
index('reimbursement_state_hcpcs_idx').on(table.stateId, table.hcpcsCodeId)
]این ایندکسها الگوهای join و group-by که اپ واقعاً استفاده میکنه رو پوشش میدن.
راهحل دوم: دستور ANALYZE. بعد از ساخت ایندکسها، query planner هنوز از بعضیشون استفاده نمیکرد چون آمار جدول قدیمی بود. دستور ANALYZE به SQLite میگه آمار توزیع دادهها رو بهروز کنه تا planner بتونه تصمیم بهتری بگیره. این یه مشکل رایج در SQLiteه که اغلب نادیده گرفته میشه.
راهحل سوم: کش KV برای دادههای navigation. چون دادههای nav (مثل آخرین سال و لیست کدهای پرتکرار) به ندرت تغییر میکنن، این دادهها در Cloudflare KV با TTL یه ساعته کش شدن. این کار row reads مربوط به layout رو عملاً به صفر رسوند. بعداً این pattern تعمیم داده شد تا همهی routeهای پرخواندن سایت رو پوشش بده.
نکات کلیدی:
- D1 و SQLite بر اساس ردیفهای اسکنشده هزینه میگیرن، نه تعداد کوئری — بدون ایندکس، هر کوئری یه full table scanه
- ایندکسهای composite باید با ستونهایی که واقعاً در WHERE و GROUP BY استفاده میشن همراستا باشن
- بعد از ساخت ایندکس روی SQLite، حتماً
ANALYZEبزن تا query planner از ایندکسها استفاده کنه - دادههای پرتکرار و کمتغییر (مثل navigation) رو حتماً کش کن — حتی TTL یه ساعته تفاوت چشمگیری ایجاد میکنه
- ابزار
wrangler d1 insightsبرای پیدا کردن کوئریهای پرهزینه خیلی مفیده




