Go + Postgres prestandaoptimeringsguide för AI-genererade API:er: poola anslutningar, granska EXPLAIN, indexera smart, paginera med cursors och forma JSON effektivt.

AI-genererade API:er kan kännas snabba i tidiga tester. Du träffar ett endpoint några gånger, datasetet är litet och förfrågningarna kommer en i taget. Sedan kommer verklig trafik: blandade endpoints, burstig load, kallare caches och fler rader än väntat. Samma kod kan börja kännas slumpmässigt långsam även om inget faktiskt gick sönder.
Långsamhet visar sig ofta på några sätt: latensspikar (de flesta förfrågningar är fina, några tar 5× till 50× längre), timeouts (en liten procent misslyckas) eller CPU som går varm (Postgres CPU för queries eller Go-CPU för JSON, goroutines, loggning och retries).
Ett vanligt scenario är en list-endpoint med ett flexibelt sökfilter som returnerar ett stort JSON-svar. I en testdatabas skannar den några tusen rader och avslutar snabbt. I produktion skannar den några miljoner rader, sorterar dem och applicerar först sedan en LIMIT. API:et “fungerar” fortfarande, men p95-latensen exploderar och några förfrågningar time-outar under burstar.
För att separera databas-långsamhet från app-långsamhet, håll mental modellen enkel.
Om databasen är långsam spenderar din Go-handler mest tid på att vänta på queryn. Du kan också se många requests fastna “in flight” medan Go-CPU ser normal ut.
Om appen är långsam avslutas queryn snabbt, men tiden förloras efter queryn: bygga stora svarobjekt, marshala JSON, köra extra queries per rad eller göra för mycket arbete per request. Go-CPU stiger, minnet ökar och latensen växer med svarsstorleken.
”Tillräckligt bra” prestanda före lansering är inte perfektion. För många CRUD-endpoints, sikta på stabil p95-latens (inte bara genomsnitt), förutsägbarhet under burstar och inga timeouts vid förväntad peak. Målet är enkelt: inga överraskande långsamma requests när data och trafik växer, och tydliga signaler när något drar iväg.
Innan du optimerar något, bestäm vad “bra” betyder för din API. Utan en baseline är det lätt att spendera timmar på inställningar och fortfarande inte veta om du förbättrat eller bara flyttat flaskhalsen.
Tre siffror brukar berätta större delen av historien:
p95 är “den dåliga dagen”-metrisken. Om p95 är hög men genomsnittet är okej så gör en liten uppsättning requests för mycket arbete, blir blockerad på locks eller triggar långsamma planer.
Gör långsamma queries synliga tidigt. I Postgres, slå på slow query-logging med en låg tröskel för pre-launch-testning (till exempel 100–200 ms), och logga hela statement så du kan kopiera det till en SQL-klient. Håll detta temporärt — att logga alla långsamma queries i produktion blir snabbt brusigt.
Testa sedan med realistiska förfrågningar, inte bara en enkel “hello world”-rutt. Ett litet urval räcker om det matchar vad användarna faktiskt gör: en list-anrop med filter och sortering, en detaljsida med några joins, ett create eller update med validering, och en sökstil-query med delmatchningar.
Om du genererar endpoints från en specifikation (till exempel med ett vibe-coding-verktyg som Koder.ai), kör samma handfull requests upprepade gånger med konsekventa inputs. Det gör ändringar som index, pagineringsjusteringar och query-omskrivningar enklare att mäta.
Slutligen, välj ett mål du kan säga högt. Exempel: “De flesta requests håller sig under 200 ms p95 vid 50 samtidiga användare, och fel under 0.5%.” Exakta siffror beror på din produkt, men ett tydligt mål förhindrar ändlöst pill.
En anslutningspool håller ett begränsat antal öppna DB-anslutningar och återanvänder dem. Utan pool kan varje request öppna en ny connection, och Postgres slösar tid och minne på att hantera sessioner istället för att köra queries.
Målet är att hålla Postgres upptagen med användbart arbete, inte context-switching mellan för många connections. Detta är ofta den första meningsfulla vinsten, särskilt för AI-genererade API:er som tyst kan bli pratiga endpoints.
I Go brukar du ställa max open connections, max idle connections och connection lifetime. En säker startpunkt för många små API:er är en liten multipel av dina CPU-kärnor (ofta 5 till 20 totala connections), med ett liknande antal som hålls idle, och återvinna connections periodiskt (till exempel var 30–60:e minut).
Om du kör flera API-instanser, kom ihåg att poolen multipliceras. En pool på 20 connections över 10 instanser blir 200 connections mot Postgres, vilket ofta är hur team oväntat stöter på connection-gränser.
Poolproblem känns annorlunda än långsam SQL.
Om poolen är för liten väntar requests innan de ens når Postgres. Latensen spikar, men databas-CPU och query-tider kan se normala ut.
Om poolen är för stor ser Postgres överbelastat ut: många aktiva sessions, minnespress och ojämn latens över endpoints.
Ett snabbt sätt att skilja dem åt är att mäta DB-anrop i två delar: tid som spenderas på att vänta på en connection vs tid i själva queryn. Om mest tid är “waiting” är poolen flaskhalsen. Om mest tid är “in query”, fokusera på SQL och index.
Några snabba kontroller:
max_connections du ligger.Om du använder pgxpool får du en Postgres-fokuserad pool med tydliga stats och bra defaults för Postgres-beteende. Om du använder database/sql får du ett standardinterface som funkar över flera databaser, men du måste vara explicit om pool-inställningar och driver-beteende.
En praktisk regel: om du är helhjärtat på Postgres och vill ha direkt kontroll är pgxpool ofta enklare. Om du förlitar dig på bibliotek som förväntar sig database/sql, håll dig till det, ställ in poolen explicit och mät väntetider.
Exempel: en endpoint som listar orders kanske kör på 20 ms, men vid 100 samtidiga användare hoppar den till 2 s. Om loggar visar 1.9 s väntan på en connection hjälper inte query-tuning förrän pool och totala Postgres-connections är dimensionerade rätt.
När en endpoint känns långsam, kontrollera vad Postgres faktiskt gör. En snabb titt på EXPLAIN pekar ofta på lösningen inom några minuter.
Kör detta på exakt den SQL din API skickar:
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, status, created_at
FROM orders
WHERE user_id = $1 AND status = $2
ORDER BY created_at DESC
LIMIT 50;
Några rader är viktigast. Titta på toppnoden (vad Postgres valde) och totalerna i botten (hur lång tid det tog). Jämför sedan estimerat vs faktiskt antal rader. Stora gap betyder oftast att planern gissade fel.
Om du ser Index Scan eller Index Only Scan använder Postgres ett index, vilket brukar vara bra. Bitmap Heap Scan kan vara okej för medelstora träffar. Seq Scan betyder att hela tabellen lästes, vilket bara är okej när tabellen är liten eller nästan varje rad matchar.
Vanliga röda flaggor:
ORDER BY)Långsamma planer kommer ofta från några mönster:
WHERE + ORDER BY-mönster (t.ex. (user_id, status, created_at))WHERE (t.ex. WHERE lower(email) = $1), vilket kan tvinga fram scans om du inte lägger till ett matchande uttrycksindexOm planen ser konstig ut och skattningarna är långt ifrån sanningen är statistik ofta föråldrad. Kör ANALYZE (eller låt autovacuum göra jobbet) så Postgres lär sig nuvarande radsantal och värdefördelningar. Detta är viktigt efter stora importer eller när nya endpoints börjar skriva mycket data snabbt.
Index hjälper bara när de matchar hur din API frågar data. Om du bygger dem från gissningar får du långsammare skrivningar, större lagring och liten eller ingen förbättring.
Ett praktiskt sätt att tänka: ett index är en genväg för en specifik fråga. Om din API ställer en annan fråga ignorerar Postgres genvägen.
Om en endpoint filtrerar på account_id och sorterar på created_at DESC, slår ett enda kompositindex ofta två separata index. Det hjälper Postgres att hitta rätt rader och returnera dem i rätt ordning med mindre arbete.
Tumregler som ofta håller:
Exempel: om din API har GET /orders?status=paid och alltid visar nyast först, är ett index som (status, created_at DESC) en bra match. Om de flesta queries också filtrerar på kund, kan (customer_id, status, created_at) vara bättre, men bara om det speglar hur endpointen faktiskt körs i produktion.
Om större delen av trafiken träffar en smal skiva av rader kan ett partial index vara billigare och snabbare. Till exempel, om din app mest läser aktiva poster, indexera bara WHERE active = true så håller indexet sig mindre och mer sannolikt i minnet.
För att bekräfta att ett index hjälper, gör snabba kontroller:
EXPLAIN (eller EXPLAIN ANALYZE i säker miljö) och leta efter en index-scan som matchar din query.Ta bort oanvända index försiktigt. Kolla användningsstatistik (t.ex. om ett index blivit skannat). Droppa ett i taget under låg risk och ha en återställningsplan. Oanvända index är inte ofarliga — de saktar ner inserts och updates vid varje skrivning.
Paginering är ofta där ett snabbt API börjar kännas långsamt, även när databasen är frisk. Behandla paginering som ett query-designproblem, inte en UI-detalj.
LIMIT/OFFSET ser enkelt ut, men djupare sidor kostar mer. Postgres måste fortfarande gå förbi (och ofta sortera) de rader du hoppar över. Sida 1 kan röra några dussin rader. Sida 500 kan tvinga databasen att skanna och kasta tiotusentals för att returnera 20 resultat.
Det kan också skapa ostadiga resultat när rader läggs till eller tas bort mellan requests. Användare kan se dubbletter eller saknade objekt eftersom betydelsen av “rad 10 000” ändras när tabellen förändras.
Keyset-paginering frågar en annan fråga: “Ge mig nästa 20 rader efter den sista raden jag såg.” Det håller databasen på ett litet, konsekvent snitt.
En enkel version använder inkrementellt id:
SELECT id, created_at, title
FROM posts
WHERE id > $1
ORDER BY id
LIMIT 20;
Din API returnerar en next_cursor som är sista id i sidan. Nästa request använder det värdet som $1.
För tidsbaserad sortering, använd en stabil ordning och bryt oavgjort. created_at ensam räcker inte om två rader har samma tidsstämpel. Använd en sammansatt cursor:
WHERE (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC
LIMIT 20;
Några regler för att undvika dubbletter och saknade rader:
ORDER BY (vanligtvis id).created_at och id tillsammans).En överraskande vanlig anledning till att ett API känns långsamt är inte databasen. Det är svaret. Stor JSON tar längre tid att bygga, längre tid att skicka och längre tid för klienter att parsa. Den snabbaste vinsten är ofta att returnera mindre.
Börja med din SELECT. Om en endpoint bara behöver id, name och status, be om de kolumnerna och inget mer. SELECT * blir tyst tyngre över tid när tabeller får långa texter, JSON-blobs och audit-kolumner.
En annan vanlig flaskhals är N+1 i svarssammansättning: du hämtar en lista med 50 items och kör sedan 50 extra queries för att fästa relaterad data. Det kan klara tester men kollapsa under verklig trafik. Föredra en enda query som returnerar vad du behöver (med försiktiga joins), eller två queries där den andra batchar efter IDs.
Några sätt att hålla payloads mindre utan att bryta klienter:
include=-flagga (eller fields=) så list-svar förblir lätta och detaljsvar ber om tillägg.Båda kan vara snabba. Välj efter vad du optimerar för.
Postgres JSON-funktioner (jsonb_build_object, json_agg) är användbara när du vill ha färre round trips och förutsägbara former från en query. Formning i Go är användbar när du behöver betingad logik, återanvända structs eller hålla SQL enklare att underhålla. Om din JSON-byggnad i SQL blir svår att läsa blir den också svår att optimera.
En bra regel: låt Postgres filtrera, sortera och aggregera. Låt sedan Go hantera slutlig presentation.
Om du genererar API:er snabbt (till exempel med Koder.ai), hjälper include-flaggor tidigt att undvika endpoints som växer i vikt över tid. Det ger också ett säkert sätt att lägga till fält utan att göra varje svar tyngre.
Du behöver inte ett enormt testlabb för att hitta de flesta prestandaproblem. Ett kort, upprepningsbart pass blottlägger problemen som blir till driftstopp när trafiken kommer, speciellt när startpunkten är genererad kod som du tänker släppa.
Innan du ändrar något, skriv ner en liten baseline:
Börja smått, ändra en sak i taget och testa efter varje ändring.
Kör ett 10–15 minuters loadtest som ser ut som verklig användning. Slå mot samma endpoints som dina första användare kommer att träffa (login, list-sidor, search, create). Sortera sedan rutter efter p95-latency och total tid.
Kontrollera connection-press innan du tunar SQL. En pool som är för stor överväldigar Postgres. En pool som är för liten skapar långa väntetider. Leta efter stigande väntetid för att få en connection och spikes i connection-counts. Justera pool och idle-gränser först och kör samma load igen.
EXPLAIN de långsammaste queriesna och fixa största röda flaggan. Vanliga bovar är fulla tabellskanningar på stora tabeller, sorteringar på stora resultatuppsättningar och joins som exploderar radantalet. Välj den enskilt värsta queryn och gör den tråkig.
Lägg till eller justera ett index och testa igen. Index hjälper när de matchar ditt WHERE och ORDER BY. Lägg inte till fem samtidigt. Om din långsamma endpoint är “lista orders per user_id sorterat på created_at”, kan ett kompositindex på (user_id, created_at) vara skillnaden mellan omedelbart och plågsamt.
Trimma svar och paginering, och testa igen. Om en endpoint returnerar 50 rader med stora JSON-blobs betalar databas, nätverk och klient alla priset. Returnera endast fälten UI behöver och föredra paginering som inte blir långsammare när tabellen växer.
Håll en enkel förändringslogg: vad som ändrades, varför och vad som rörde sig i p95. Om en förändring inte förbättrar din baseline, återställ den och gå vidare.
De flesta prestandaproblem i Go-API:er på Postgres är självförvållade. Den goda nyheten är att ett fåtal kontroller fångar många av dem innan verklig trafik kommer.
En klassisk fälla är att behandla pool-storlek som en hastighetsknapp. Att sätta den “så hög som möjligt” gör ofta allt långsammare. Postgres spenderar mer tid på att hantera sessioner, minne och locks, och din app börjar time-outa i vågor. En mindre, stabil pool med förutsägbar konkurrent brukar vinna.
Ett annat vanligt misstag är “indexa allt”. Extra index kan hjälpa reads, men de saktar också skrivningar och kan ändra query-planer på överraskande sätt. Om din API ofta insertar eller uppdaterar innebär varje extra index mer arbete. Mät före och efter, och kontrollera planer efter att ha lagt till ett index.
Pagineringstekniks-skuld smyger in tyst. Offset-paginering ser bra ut tidigt men p95 stiger över tid eftersom databasen måste gå förbi fler och fler rader.
JSON-payload-storlek är en annan dold kostnad. Kompression hjälper bandbredd men tar inte bort kostnaden för att bygga, allokera och parsa stora objekt. Trimma fält, undvik djup inbäddning och returnera endast vad skärmen behöver.
Om du bara tittar på genomsnittlig responstid missar du var användarsmärtan börjar. p95 (och ibland p99) är där pool-saturation, lock waits och långsamma planer visar sig först.
En snabb pre-launch-självkontroll:
EXPLAIN igen efter att ha lagt till index eller ändrat filter.Innan verkliga användare kommer vill du ha bevis på att din API förblir förutsägbar under belastning. Målet är inte perfekta siffror. Det är att fånga de få problem som orsakar timeouts, spikar eller en databas som slutar ta emot arbete.
Kör kontroller i en staging-miljö som liknar produktion (liknande DB-storlek, samma index, samma pool-inställningar): mät p95-latency per nyckel-endpoint under load, fånga dina topp långsamma queries efter total tid, övervaka pool-wait-time, kör EXPLAIN (ANALYZE, BUFFERS) på den värsta queryn för att bekräfta att den använder det index du väntat dig, och sanity-checka payload-storlekar på dina mest trafikerade rutter.
Gör sedan en worst-case-körning som efterliknar hur produkter går sönder: be om en djup sida, applicera det bredaste filtret och kör med en kallstart (restart av API och slå samma request först). Om djup paginering blir långsammare varje sida, byt till cursor-baserad paginering före lansering.
Skriv ner dina defaults så teamet gör konsekventa val senare: pool-gränser och timeouts, pagineringsregler (max page size, om offset är tillåtet, cursor-format), query-regler (välj bara nödvändiga kolumner, undvik SELECT *, sätt gränser för dyra filter), och loggregler (slow query-tröskel, hur länge samples behålls, hur endpoints märks).
Om du bygger och exporterar Go + Postgres-tjänster med Koder.ai hjälper ett kort planeringspass före deployment att hålla filter, paginering och svarformer avsiktliga. När du börjar tunna index och query-former gör snapshots och rollback det lättare att ångra en “fix” som hjälper en endpoint men skadar andra. Om du vill ha ett enda ställe för att iterera på det arbetsflödet är Koder.ai på koder.ai designat för att generera och förfina de tjänsterna via chat, och sedan exportera koden när du är redo.
Börja med att separera DB-väntetid från app-arbetstid.
Lägg in enkel timing runt “väntan på connection” och “query-exekvering” för att se vilken sida som dominerar.
Använd ett litet, upprepningsbart baseline:
Välj ett tydligt mål, till exempel “p95 under 200 ms vid 50 samtidiga användare, fel under 0.5%”. Ändra bara en sak åt gången och testa om samma mix av requests.
Aktivera slow query-logging med en låg tröskel under pre-launch-testning (till exempel 100–200 ms) och logga hela statement så du kan kopiera det till en SQL-klient.
Håll det temporärt:
När du hittat de värsta bärare, byt till sampling eller höj tröskeln.
Ett praktiskt default är en liten multipel av CPU-kärnorna per API-instans, ofta 5–20 max open connections, med ungefär samma antal för max idle, och återvinn connections var 30–60 minuter.
Två vanliga fel:
Kom ihåg att poolar multipliceras över instanser (20 connections × 10 instanser = 200 connections).
Tidssätt DB-anrop i två delar:
Om majoriteten av tiden är pool-wait, justera pool-storlek, timeouts och antal instanser. Om mest tid är query-exekvering, fokusera på EXPLAIN och index.
Kontrollera också att du alltid stänger rows så att connections snabbt återlämnas till poolen.
Kör EXPLAIN (ANALYZE, BUFFERS) på exakt den SQL din API skickar och titta efter:
Index bör matcha vad endpointen faktiskt gör: filter + sorteringsordning.
Bra grundregel:
WHERE + ORDER BY-mönster.Använd partial index när största delen av trafiken träffar en förutsägbar delmängd rader.
Mönster där det lönar sig:
active = trueEn partial index som ... WHERE active = true blir mindre, mer sannolik att ligga i minnet och minskar skrivkostnaden jämfört med att indexera allt.
Bekräfta med att Postgres faktiskt använder den för dina högtrafikerade queries.
LIMIT/OFFSET blir långsammare på djupa sidor eftersom Postgres fortfarande måste passera (och ofta sortera) de rader du hoppar över. Sida 500 kan vara avsevärt dyrare än sida 1.
Föredra keyset (cursor) pagination:
Ofta ja för listendpoints. Det snabbaste svaret är det du inte skickar.
Pragmatiska vinster:
SELECT *).include= eller fields= så klienter väljer tunga fält.ORDER BY)Åtgärda den största varningssignalen först; tunna inte allt på en gång.
Exempel: om du filtrerar på user_id och visar nyast först, är (user_id, created_at DESC) ofta det som krävs för stabil p95.
EXPLAINid).ORDER BY mellan requests.(created_at, id) eller liknande i en cursor.Detta håller kostnaden per sida ungefär konstant även när tabellen växer.
Du minskar ofta Go-CPU, minnestryck och tail-latency genom att krympa payloads.