[{"data":1,"prerenderedAt":259},["ShallowReactive",2],{"navigation":3,"/blog/postgresql-vs-mongodb-crm-reporting":26,"/blog/postgresql-vs-mongodb-crm-reporting-surround":255},[4],{"title":5,"path":6,"stem":7,"children":8,"page":25},"Blog","/blog","blog",[9,13,17,21],{"title":10,"path":11,"stem":12},"GitHub Actions CI/CD for Laravel: A Zero-Downtime Deployment Setup","/blog/github-actions-laravel-zero-downtime-deployment","blog/github-actions-laravel-zero-downtime-deployment",{"title":14,"path":15,"stem":16},"Integrating LLMs into Laravel Using Python Microservices","/blog/integrating-llms-into-laravel-python-microservices","blog/integrating-llms-into-laravel-python-microservices",{"title":18,"path":19,"stem":20},"How I Cut Report Generation from 30s to 2s in Laravel","/blog/laravel-report-generation-30s-to-2s","blog/laravel-report-generation-30s-to-2s",{"title":22,"path":23,"stem":24},"Why I Chose PostgreSQL Over MongoDB for a CRM Reporting Engine","/blog/postgresql-vs-mongodb-crm-reporting","blog/postgresql-vs-mongodb-crm-reporting",false,{"id":27,"title":22,"author":28,"body":32,"date":246,"description":247,"extension":248,"image":249,"meta":250,"minRead":107,"navigation":251,"path":23,"seo":252,"sitemap":253,"stem":24,"__hash__":254},"blog/blog/postgresql-vs-mongodb-crm-reporting.md",{"name":29,"avatar":30},"Vincent Ndegwa",{"src":31,"alt":29},"/personal/profile.jpeg",{"type":33,"value":34,"toc":240},"minimark",[35,39,42,55,58,63,66,69,123,126,130,133,148,151,155,158,169,172,218,222,233,236],[36,37,38],"p",{},"When teams build CRMs, database choice usually starts with opinions and trends. I prefer to start with the kinds of questions the business will ask six months later.",[36,40,41],{},"In this case, those questions looked like this:",[43,44,45,49,52],"ul",{},[46,47,48],"li",{},"Which sales rep closed the highest value deals this quarter?",[46,50,51],{},"Which lead source has the best conversion rate by region?",[46,53,54],{},"Which pipeline stage is slowing down revenue?",[36,56,57],{},"These are relationship heavy queries. That shaped the decision.",[59,60,62],"h2",{"id":61},"why-postgresql-won","Why PostgreSQL Won",[36,64,65],{},"MongoDB is excellent for document heavy workloads. A CRM reporting engine is not one of them. Most critical reports involve joins, strict consistency, and aggregate functions.",[36,67,68],{},"In PostgreSQL, the query intent stays clear:",[70,71,76],"pre",{"className":72,"code":73,"language":74,"meta":75,"style":75},"language-sql shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","SELECT rep_id, SUM(d.value) AS total\nFROM leads l\nJOIN deals d ON d.lead_id = l.id\nWHERE l.created_at >= date_trunc('quarter', now())\nGROUP BY rep_id\nORDER BY total DESC\nLIMIT 10;\n","sql","",[77,78,79,87,93,99,105,111,117],"code",{"__ignoreMap":75},[80,81,84],"span",{"class":82,"line":83},"line",1,[80,85,86],{},"SELECT rep_id, SUM(d.value) AS total\n",[80,88,90],{"class":82,"line":89},2,[80,91,92],{},"FROM leads l\n",[80,94,96],{"class":82,"line":95},3,[80,97,98],{},"JOIN deals d ON d.lead_id = l.id\n",[80,100,102],{"class":82,"line":101},4,[80,103,104],{},"WHERE l.created_at >= date_trunc('quarter', now())\n",[80,106,108],{"class":82,"line":107},5,[80,109,110],{},"GROUP BY rep_id\n",[80,112,114],{"class":82,"line":113},6,[80,115,116],{},"ORDER BY total DESC\n",[80,118,120],{"class":82,"line":119},7,[80,121,122],{},"LIMIT 10;\n",[36,124,125],{},"This is easy to review, tune, and explain to another engineer during handover.",[59,127,129],{"id":128},"flexibility-without-chaos","Flexibility Without Chaos",[36,131,132],{},"One argument for document databases is custom fields. PostgreSQL already handles this well with JSONB.",[70,134,136],{"className":72,"code":135,"language":74,"meta":75,"style":75},"ALTER TABLE contacts ADD COLUMN metadata JSONB;\nCREATE INDEX idx_contacts_metadata ON contacts USING GIN (metadata);\n",[77,137,138,143],{"__ignoreMap":75},[80,139,140],{"class":82,"line":83},[80,141,142],{},"ALTER TABLE contacts ADD COLUMN metadata JSONB;\n",[80,144,145],{"class":82,"line":89},[80,146,147],{},"CREATE INDEX idx_contacts_metadata ON contacts USING GIN (metadata);\n",[36,149,150],{},"You keep a normalized core schema and still support tenant specific fields safely.",[59,152,154],{"id":153},"reporting-features-that-matter-in-production","Reporting Features That Matter in Production",[36,156,157],{},"PostgreSQL gave us three practical wins:",[43,159,160,163,166],{},[46,161,162],{},"window functions for rolling conversion trends",[46,164,165],{},"materialized views for expensive dashboards",[46,167,168],{},"full text search with tsvector for notes and lead history",[36,170,171],{},"Example rolling metric:",[70,173,175],{"className":72,"code":174,"language":74,"meta":75,"style":75},"SELECT rep_id,\n       day,\n       SUM(conversions) OVER (\n         PARTITION BY rep_id\n         ORDER BY day\n         ROWS BETWEEN 29 PRECEDING AND CURRENT ROW\n       ) AS rolling_30_day_conversions\nFROM rep_daily_metrics;\n",[77,176,177,182,187,192,197,202,207,212],{"__ignoreMap":75},[80,178,179],{"class":82,"line":83},[80,180,181],{},"SELECT rep_id,\n",[80,183,184],{"class":82,"line":89},[80,185,186],{},"       day,\n",[80,188,189],{"class":82,"line":95},[80,190,191],{},"       SUM(conversions) OVER (\n",[80,193,194],{"class":82,"line":101},[80,195,196],{},"         PARTITION BY rep_id\n",[80,198,199],{"class":82,"line":107},[80,200,201],{},"         ORDER BY day\n",[80,203,204],{"class":82,"line":113},[80,205,206],{},"         ROWS BETWEEN 29 PRECEDING AND CURRENT ROW\n",[80,208,209],{"class":82,"line":119},[80,210,211],{},"       ) AS rolling_30_day_conversions\n",[80,213,215],{"class":82,"line":214},8,[80,216,217],{},"FROM rep_daily_metrics;\n",[59,219,221],{"id":220},"what-changed-after-the-migration","What Changed After the Migration",[43,223,224,227,230],{},[46,225,226],{},"report generation became faster and more predictable",[46,228,229],{},"query debugging got easier for the whole team",[46,231,232],{},"schema evolution became deliberate instead of ad hoc",[36,234,235],{},"The bigger lesson: choose storage based on query shape and reliability needs, not hype. For Laravel, Django, and FastAPI backends that power analytics heavy products, PostgreSQL is often the better default.",[237,238,239],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":75,"searchDepth":89,"depth":89,"links":241},[242,243,244,245],{"id":61,"depth":89,"text":62},{"id":128,"depth":89,"text":129},{"id":153,"depth":89,"text":154},{"id":220,"depth":89,"text":221},"2025-03-15","Exploring how strategic database choices impact query complexity, reporting accuracy, and long-term maintainability — and why relational won for this use case.","md","https://images.unsplash.com/photo-1544383835-bda2bc66a55d?w=1260&auto=format&fit=crop&q=80",{},true,{"title":22,"description":247},{"loc":23},"lbJSE6iryG0UqsR9px3IB2zGqZ_GJVYMdcR-vUIwegw",[256,258],{"title":18,"path":19,"stem":20,"description":257,"children":-1},"A practical walkthrough of how Redis caching, Laravel Queues, and careful query optimisation transformed a sluggish ERP reporting module into a near-instant one.",null,1791651066261]