Siz mağazaya on məhsul almağa gedirsiniz. On dəfə gedib hər dəfə bir məhsul ala bilərsiniz, ya da siyahı tutub hamısını bir dəfəyə ala bilərsiniz. Keçən dərsdəki resolver-lər bazaya «bir məhsul-bir gediş» qaydası ilə gedir: hər post üçün şərhlər barədə ayrıca sorğu. Bu, məşhur N+1 problemidir. Bu dərsdə onu Prisma jurnalında görəcək və DataLoader ilə həll edəcəksiniz — bir «takt»ın sorğularını birinə yığan kiçik kitabxana.

N+1 necə yaranır

{ posts { title comments { text } } } sorğusuna baxaq. Query.posts bir SQL sorğusu icra edib N post qaytarır. Sonra hər post üçün Post.comments çağırılır və hər çağırış öz WHERE "postId" = … sorğusunu göndərir. Cəmi 1 + N sorğu — adı da buradandır. 10 post olanda 11 sorğu, 100 post olanda 101 sorğu olur.

N+1: 10 post üçün 11 sorğu, DataLoader ilə isə 2 sorğu
N+1: 10 post üçün 11 sorğu, DataLoader ilə isə 2 sorğu

Bu iki səbəbdən pisdir. Hər sorğu şəbəkə üzərindən bazaya gedişdir, bağlantı hovuzu isə məhduddur. Üstəlik səviyyələr bir-birini vurur: hər şərhin müəllifi də varsa, sorğu sayı 1 + N + N·M olacaq. Müştəri isə cəmi bir sorğu göndərib.

N+1-i Prisma jurnalında görmək

Klient log: ['query'] ilə yaradılıb (keçən dərs). Sorğunu Sandbox-da icra edin və serverin konsoluna baxın (çıxış qısaldılıb):

text
prisma:query SELECT ... FROM "public"."Post" ... ORDER BY ... DESC
prisma:query SELECT ... FROM "public"."Comment" WHERE ... "postId" = $1 ...
prisma:query SELECT ... FROM "public"."Comment" WHERE ... "postId" = $1 ...
prisma:query SELECT ... FROM "public"."Comment" WHERE ... "postId" = $1 ...
... 7 more queries, one for each remaining post

Post-a sorğu birdir, Comment-ə isə hər post üçün bir sorğu var. Jurnalda sorğuları saymağa öyrəşin: N+1-i tapmağın ən asan yolu budur.

💡 Prisma bir taktda edilən findUnique çağırışlarını özü IN ilə bir sorğuya birləşdirə bilir. Ona görə findUniqueOrThrow ilə yazılmış Post.author jurnalda Post.comments-dən yaxşı görünə bilər: orada findMany var, onu Prisma birləşdirmir. Buna güvənmək olmaz — bu üsul yalnız findUnique üçün işləyir. DataLoader isə istənilən mənbə (başqa REST API, Redis, servis) üçün yararlıdır və eyni zamanda nəticələri sorğu müddətində keşləyir.

İdeya: açarları yığıb bir dəfə soruşmaq

Hamısını bir gedişdə almaq üçün siyahı lazımdır. GraphQL Post.comments-in 10 resolver-ini cavab gözləmədən ardıcıl çağırır. Belə sinxron kodun hamısı Node.js hadisə dövrünün (event loop) bir taktına (tick) — gözləmə fasilələri arasındakı bir iş hissəsinə — sığır. Deməli, resolver bazaya dərhal getməyə bilər, «mənə 3-cü postun şərhləri lazımdır» deyə növbəyə yazar. Taktın sonunda növbədə bütün 10 açar olacaq — və bir WHERE "postId" IN (…) sorğusu göndərib, hər resolver-ə öz hissəsini paylamaq olar. Bu ideyanın oyuncaq versiyası: