chore: Improve database performance by adding indexes (#1593)

This commit is contained in:
Matti Nannt
2023-11-08 07:11:43 +01:00
committed by GitHub
parent 10ab71b20f
commit ca21c9cea7
9 changed files with 163 additions and 103 deletions

View File

@@ -7,7 +7,7 @@ import type { AttributeClass } from "@prisma/client";
import { useForm } from "react-hook-form";
import { ArchiveBoxArrowDownIcon, ArchiveBoxXMarkIcon } from "@heroicons/react/24/solid";
import { useRouter } from "next/navigation";
import { updatetAttributeClass } from "@formbricks/lib/attributeClass/service";
import { updateAttributeClass } from "@formbricks/lib/attributeClass/service";
import { useState } from "react";
interface AttributeSettingsTabProps {
@@ -25,7 +25,7 @@ export default function AttributeSettingsTab({ attributeClass, setOpen }: Attrib
const onSubmit = async (data) => {
setisAttributeBeingSubmitted(true);
setOpen(false);
await updatetAttributeClass(attributeClass.id, data);
await updateAttributeClass(attributeClass.id, data);
router.refresh();
setisAttributeBeingSubmitted(false);
};
@@ -33,7 +33,7 @@ export default function AttributeSettingsTab({ attributeClass, setOpen }: Attrib
const handleArchiveToggle = async () => {
setisAttributeBeingSubmitted(true);
const data = { archived: !attributeClass.archived };
await updatetAttributeClass(attributeClass.id, data);
await updateAttributeClass(attributeClass.id, data);
setisAttributeBeingSubmitted(false);
};

View File

@@ -4,7 +4,7 @@ import { NextResponse } from "next/server";
import {
deleteAttributeClass,
getAttributeClass,
updatetAttributeClass,
updateAttributeClass,
} from "@formbricks/lib/attributeClass/service";
import { TAttributeClass, ZAttributeClassUpdateInput } from "@formbricks/types/attributeClasses";
import { transformErrorToDetails } from "@/app/lib/api/validator";
@@ -82,7 +82,7 @@ export async function PUT(
transformErrorToDetails(inputValidation.error)
);
}
const updatedAttributeClass = await updatetAttributeClass(params.attributeClassId, inputValidation.data);
const updatedAttributeClass = await updateAttributeClass(params.attributeClassId, inputValidation.data);
if (updatedAttributeClass) {
return responses.successResponse(updatedAttributeClass);
}

View File

@@ -0,0 +1,71 @@
-- CreateIndex
CREATE INDEX "Account_userId_idx" ON "Account"("userId");
-- CreateIndex
CREATE INDEX "ApiKey_environmentId_idx" ON "ApiKey"("environmentId");
-- CreateIndex
CREATE INDEX "AttributeClass_environmentId_idx" ON "AttributeClass"("environmentId");
-- CreateIndex
CREATE INDEX "Display_surveyId_idx" ON "Display"("surveyId");
-- CreateIndex
CREATE INDEX "Display_personId_idx" ON "Display"("personId");
-- CreateIndex
CREATE INDEX "Environment_productId_idx" ON "Environment"("productId");
-- CreateIndex
CREATE INDEX "Integration_environmentId_idx" ON "Integration"("environmentId");
-- CreateIndex
CREATE INDEX "Invite_teamId_idx" ON "Invite"("teamId");
-- CreateIndex
CREATE INDEX "Membership_userId_idx" ON "Membership"("userId");
-- CreateIndex
CREATE INDEX "Membership_teamId_idx" ON "Membership"("teamId");
-- CreateIndex
CREATE INDEX "Person_environmentId_idx" ON "Person"("environmentId");
-- CreateIndex
CREATE INDEX "Product_teamId_idx" ON "Product"("teamId");
-- CreateIndex
CREATE INDEX "Response_surveyId_created_at_idx" ON "Response"("surveyId", "created_at");
-- CreateIndex
CREATE INDEX "Response_surveyId_idx" ON "Response"("surveyId");
-- CreateIndex
CREATE INDEX "ResponseNote_responseId_idx" ON "ResponseNote"("responseId");
-- CreateIndex
CREATE INDEX "Survey_environmentId_idx" ON "Survey"("environmentId");
-- CreateIndex
CREATE INDEX "SurveyAttributeFilter_surveyId_idx" ON "SurveyAttributeFilter"("surveyId");
-- CreateIndex
CREATE INDEX "SurveyAttributeFilter_attributeClassId_idx" ON "SurveyAttributeFilter"("attributeClassId");
-- CreateIndex
CREATE INDEX "SurveyTrigger_surveyId_idx" ON "SurveyTrigger"("surveyId");
-- CreateIndex
CREATE INDEX "Tag_environmentId_idx" ON "Tag"("environmentId");
-- CreateIndex
CREATE INDEX "TagsOnResponses_responseId_idx" ON "TagsOnResponses"("responseId");
-- CreateIndex
CREATE INDEX "User_email_idx" ON "User"("email");
-- CreateIndex
CREATE INDEX "Webhook_environmentId_idx" ON "Webhook"("environmentId");
-- RenameIndex
ALTER INDEX "email_teamId_unique" RENAME TO "Invite_email_teamId_idx";

View File

@@ -47,6 +47,8 @@ model Webhook {
environmentId String
triggers PipelineTriggers[]
surveyIds String[]
@@index([environmentId])
}
model Attribute {
@@ -82,6 +84,7 @@ model AttributeClass {
attributeFilters SurveyAttributeFilter[]
@@unique([name, environmentId])
@@index([environmentId])
}
model Person {
@@ -94,6 +97,8 @@ model Person {
sessions Session[]
attributes Attribute[]
displays Display[]
@@index([environmentId])
}
model Response {
@@ -120,6 +125,8 @@ model Response {
singleUseId String?
@@unique([surveyId, singleUseId])
@@index([surveyId, createdAt]) // to determine monthly response count
@@index([surveyId])
}
model ResponseNote {
@@ -133,6 +140,8 @@ model ResponseNote {
text String
isResolved Boolean @default(false)
isEdited Boolean @default(false)
@@index([responseId])
}
model Tag {
@@ -145,6 +154,7 @@ model Tag {
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
@@unique([environmentId, name])
@@index([environmentId])
}
model TagsOnResponses {
@@ -154,6 +164,7 @@ model TagsOnResponses {
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([responseId, tagId])
@@index([responseId])
}
enum SurveyStatus {
@@ -178,6 +189,9 @@ model Display {
personId String?
responseId String? @unique
status DisplayStatus?
@@index([surveyId])
@@index([personId])
}
model SurveyTrigger {
@@ -190,6 +204,7 @@ model SurveyTrigger {
eventClassId String
@@unique([surveyId, eventClassId])
@@index([surveyId])
}
enum SurveyAttributeFilterCondition {
@@ -209,6 +224,8 @@ model SurveyAttributeFilter {
value String
@@unique([surveyId, attributeClassId])
@@index([surveyId])
@@index([attributeClassId])
}
enum SurveyType {
@@ -272,6 +289,8 @@ model Survey {
/// [SurveyVerifyEmail]
verifyEmail Json?
pin String?
@@index([environmentId])
}
model Event {
@@ -341,6 +360,7 @@ model Integration {
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
@@unique([type, environmentId])
@@index([environmentId])
}
model Environment {
@@ -359,6 +379,8 @@ model Environment {
webhooks Webhook[]
tags Tag[]
integration Integration[]
@@index([productId])
}
enum WidgetPlacement {
@@ -386,6 +408,7 @@ model Product {
darkOverlay Boolean @default(false)
@@unique([teamId, name])
@@index([teamId])
}
model Team {
@@ -398,8 +421,7 @@ model Team {
/// @zod.custom(imports.ZTeamBilling)
/// [TeamBilling]
billing Json @default("{\"stripeCustomerId\": null, \"features\": {\"inAppSurvey\": {\"status\": \"inactive\", \"unlimited\": false}, \"linkSurvey\": {\"status\": \"inactive\", \"unlimited\": false}, \"userTargeting\": {\"status\": \"inactive\", \"unlimited\": false}}}")
invites Invite[]
invites Invite[]
}
enum MembershipRole {
@@ -419,6 +441,8 @@ model Membership {
role MembershipRole
@@id([userId, teamId])
@@index([userId])
@@index([teamId])
}
model Invite {
@@ -436,7 +460,8 @@ model Invite {
expiresAt DateTime
role MembershipRole @default(admin)
@@index([email, teamId], name: "email_teamId_unique")
@@index([email, teamId])
@@index([teamId])
}
model ApiKey {
@@ -447,6 +472,8 @@ model ApiKey {
hashedKey String @unique()
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
environmentId String
@@index([environmentId])
}
enum IdentityProvider {
@@ -475,6 +502,7 @@ model Account {
session_state String?
@@unique([provider, providerAccountId])
@@index([userId])
}
enum Role {
@@ -529,6 +557,8 @@ model User {
/// @zod.custom(imports.ZUserNotificationSettings)
/// [UserNotificationSettings]
notificationSettings Json @default("{}")
@@index([email])
}
model ShortUrl {

View File

@@ -84,7 +84,7 @@ export const getAttributeClasses = async (
return attributeClasses.map(formatAttributeClassDateFields);
};
export const updatetAttributeClass = async (
export const updateAttributeClass = async (
attributeClassId: string,
data: Partial<TAttributeClassUpdateInput>
): Promise<TAttributeClass | null> => {

View File

@@ -319,39 +319,6 @@ export const getOrCreatePersonByUserId = async (userId: string, environmentId: s
return transformPrismaPerson(personPrisma);
};
export const getMonthlyActivePeopleCount = async (environmentId: string): Promise<number> =>
await unstable_cache(
async () => {
validateInputs([environmentId, ZId]);
const now = new Date();
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const personAggregations = await prisma.person.aggregate({
_count: {
id: true,
},
where: {
environmentId,
sessions: {
some: {
createdAt: {
gte: firstDayOfMonth,
},
},
},
},
});
return personAggregations._count.id;
},
[`getMonthlyActivePeopleCount-${environmentId}`],
{
tags: [personCache.tag.byEnvironmentId(environmentId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
export const updatePersonAttribute = async (
personId: string,
attributeClassId: string,

View File

@@ -533,35 +533,3 @@ export const getResponseCountBySurveyId = async (surveyId: string): Promise<numb
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
export const getMonthlyResponseCount = async (environmentId: string): Promise<number> =>
await unstable_cache(
async () => {
validateInputs([environmentId, ZId]);
const now = new Date();
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const responseAggregations = await prisma.response.aggregate({
_count: {
id: true,
},
where: {
survey: {
environmentId,
type: "web",
},
createdAt: {
gte: firstDayOfMonth,
},
},
});
return responseAggregations._count.id;
},
[`getMonthlyResponseCount-${environmentId}`],
{
tags: [responseCache.tag.byEnvironmentId(environmentId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();

View File

@@ -279,7 +279,7 @@ export const getSurveys = async (environmentId: string, page?: number): Promise<
}));
};
export async function updateSurvey(updatedSurvey: TSurvey): Promise<TSurvey> {
export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> => {
validateInputs([updatedSurvey, ZSurvey]);
const surveyId = updatedSurvey.id;
@@ -449,7 +449,7 @@ export async function updateSurvey(updatedSurvey: TSurvey): Promise<TSurvey> {
throw error;
}
}
};
export async function deleteSurvey(surveyId: string) {
validateInputs([surveyId, ZId]);
@@ -486,7 +486,7 @@ export async function deleteSurvey(surveyId: string) {
return deletedSurvey;
}
export async function createSurvey(environmentId: string, surveyBody: TSurveyInput): Promise<TSurvey> {
export const createSurvey = async (environmentId: string, surveyBody: TSurveyInput): Promise<TSurvey> => {
validateInputs([environmentId, ZId]);
if (surveyBody.attributeFilters) {
@@ -530,9 +530,9 @@ export async function createSurvey(environmentId: string, surveyBody: TSurveyInp
});
return transformedSurvey;
}
};
export async function duplicateSurvey(environmentId: string, surveyId: string) {
export const duplicateSurvey = async (environmentId: string, surveyId: string) => {
const existingSurvey = await getSurvey(surveyId);
if (!existingSurvey) {
@@ -596,4 +596,4 @@ export async function duplicateSurvey(environmentId: string, surveyId: string) {
revalidateSurveyByAttributeClassId(newAttributeFilters);
return newSurvey;
}
};

View File

@@ -7,11 +7,9 @@ import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TTeam, TTeamUpdateInput, ZTeamUpdateInput } from "@formbricks/types/teams";
import { Prisma } from "@prisma/client";
import { unstable_cache } from "next/cache";
import { getMonthlyActivePeopleCount } from "../person/service";
import { getProducts } from "../product/service";
import { getMonthlyResponseCount } from "../response/service";
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { environmentCache } from "../environment/cache";
import { getProducts } from "../product/service";
import { validateInputs } from "../utils/validate";
import { teamCache } from "./cache";
@@ -291,19 +289,34 @@ export const getMonthlyActiveTeamPeopleCount = async (teamId: string): Promise<n
async () => {
validateInputs([teamId, ZId]);
// Define the start of the month
const now = new Date();
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
// Get all environment IDs for the team
const products = await getProducts(teamId);
const environmentIds = products.flatMap((product) => product.environments.map((env) => env.id));
let peopleCount = 0;
// Aggregate the count of active people across all environments
const peopleAggregations = await prisma.person.aggregate({
_count: {
id: true,
},
where: {
AND: [
{ environmentId: { in: environmentIds } },
{
sessions: {
some: {
createdAt: { gte: firstDayOfMonth },
},
},
},
],
},
});
for (const product of products) {
for (const environment of product.environments) {
const peopleInThisEnvironment = await getMonthlyActivePeopleCount(environment.id);
peopleCount += peopleInThisEnvironment;
}
}
return peopleCount;
return peopleAggregations._count.id;
},
[`getMonthlyActiveTeamPeopleCount-${teamId}`],
{
@@ -317,19 +330,30 @@ export const getMonthlyTeamResponseCount = async (teamId: string): Promise<numbe
async () => {
validateInputs([teamId, ZId]);
// Define the start of the month
const now = new Date();
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
// Get all environment IDs for the team
const products = await getProducts(teamId);
const environmentIds = products.flatMap((product) => product.environments.map((env) => env.id));
let responseCount = 0;
// Use Prisma's aggregate to count responses for all environments
const responseAggregations = await prisma.response.aggregate({
_count: {
id: true,
},
where: {
AND: [
{ survey: { environmentId: { in: environmentIds } } },
{ survey: { type: "web" } },
{ createdAt: { gte: firstDayOfMonth } },
],
},
});
for (const product of products) {
for (const environment of product.environments) {
const responsesInEnvironment = await getMonthlyResponseCount(environment.id);
responseCount += responsesInEnvironment;
}
}
return responseCount;
// The result is an aggregation of the total count
return responseAggregations._count.id;
},
[`getMonthlyTeamResponseCount-${teamId}`],
{