From 7709c61d79d7403829277173456fbf3ff7f71cf3 Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Thu, 10 Oct 2024 07:14:58 +0200 Subject: [PATCH 1/3] feat: add repeated query --- optimize/optimize-repeated-query/.env.example | 2 + optimize/optimize-repeated-query/.gitignore | 5 + optimize/optimize-repeated-query/README.md | 183 ++++++++++++++++++ .../optimize-repeated-query/environment.d.ts | 10 + .../images/edit-recording-name-chip.png | Bin 0 -> 17291 bytes optimize/optimize-repeated-query/package.json | 21 ++ .../prisma/schema.prisma | 26 +++ .../optimize-repeated-query/prisma/seed.ts | 45 +++++ optimize/optimize-repeated-query/script.ts | 57 ++++++ .../optimize-repeated-query/tsconfig.json | 9 + optimize/optimize-repeated-query/utils/db.ts | 8 + .../optimize-repeated-query/utils/index.ts | 1 + .../.env.example | 2 + .../optimize-select-returning-all/.gitignore | 5 + .../optimize-select-returning-all/README.md | 117 +++++++++++ .../environment.d.ts | 10 + .../images/edit-recording-name-chip.png | Bin 0 -> 17291 bytes .../package.json | 21 ++ .../prisma/schema.prisma | 26 +++ .../prisma/seed.ts | 45 +++++ .../optimize-select-returning-all/script.ts | 43 ++++ .../tsconfig.json | 9 + .../optimize-select-returning-all/utils/db.ts | 8 + .../utils/index.ts | 1 + 24 files changed, 654 insertions(+) create mode 100644 optimize/optimize-repeated-query/.env.example create mode 100644 optimize/optimize-repeated-query/.gitignore create mode 100644 optimize/optimize-repeated-query/README.md create mode 100644 optimize/optimize-repeated-query/environment.d.ts create mode 100644 optimize/optimize-repeated-query/images/edit-recording-name-chip.png create mode 100644 optimize/optimize-repeated-query/package.json create mode 100644 optimize/optimize-repeated-query/prisma/schema.prisma create mode 100644 optimize/optimize-repeated-query/prisma/seed.ts create mode 100644 optimize/optimize-repeated-query/script.ts create mode 100644 optimize/optimize-repeated-query/tsconfig.json create mode 100644 optimize/optimize-repeated-query/utils/db.ts create mode 100644 optimize/optimize-repeated-query/utils/index.ts create mode 100644 optimize/optimize-select-returning-all/.env.example create mode 100644 optimize/optimize-select-returning-all/.gitignore create mode 100644 optimize/optimize-select-returning-all/README.md create mode 100644 optimize/optimize-select-returning-all/environment.d.ts create mode 100644 optimize/optimize-select-returning-all/images/edit-recording-name-chip.png create mode 100644 optimize/optimize-select-returning-all/package.json create mode 100644 optimize/optimize-select-returning-all/prisma/schema.prisma create mode 100644 optimize/optimize-select-returning-all/prisma/seed.ts create mode 100644 optimize/optimize-select-returning-all/script.ts create mode 100644 optimize/optimize-select-returning-all/tsconfig.json create mode 100644 optimize/optimize-select-returning-all/utils/db.ts create mode 100644 optimize/optimize-select-returning-all/utils/index.ts diff --git a/optimize/optimize-repeated-query/.env.example b/optimize/optimize-repeated-query/.env.example new file mode 100644 index 000000000000..90cfec3bf6c7 --- /dev/null +++ b/optimize/optimize-repeated-query/.env.example @@ -0,0 +1,2 @@ +DATABASE_URL="" +OPTIMIZE_API_KEY="" diff --git a/optimize/optimize-repeated-query/.gitignore b/optimize/optimize-repeated-query/.gitignore new file mode 100644 index 000000000000..2484fd1582e2 --- /dev/null +++ b/optimize/optimize-repeated-query/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +*.env +package-lock.json +prisma/migrations diff --git a/optimize/optimize-repeated-query/README.md b/optimize/optimize-repeated-query/README.md new file mode 100644 index 000000000000..0ca78efa9739 --- /dev/null +++ b/optimize/optimize-repeated-query/README.md @@ -0,0 +1,183 @@ +# Prisma Optimize Example: Applying the "Repeated query" Recommendation + +This repository demonstrates how to use [Prisma Optimize](https://pris.ly/optimize) to improve query performance using the "Repeated query" recommendation. + +## Prerequisites + +To successfully run the project, you will need the following: + +1. A **database connection string** supported by Prisma Optimize and Prisma Accelerate. +2. An Optimize API key, which you can obtain from your [Prisma Data Platform](https://pris.ly/pdp) account. +3. An Accelerate API key, which you can obtain from your [Prisma Data Platform](https://pris.ly/pdp) account. + +## Getting started + +### 1. Clone the repository + +Clone the repository, navigate into it, and install the dependencies: + +```bash +git clone git@github.com:prisma/prisma-examples.git --depth=1 +cd prisma-examples/optimize/optimize-repeated-query +npm install +``` + +### 2. Configure environment variables + +Create a `.env` file in the root of the project directory: + +```bash +cp .env.example .env +``` + +Next, open the `.env` file and update the `DATABASE_URL` with your database connection string and the `OPTIMIZE_API_KEY` with your Optimize API key: + +```env +# .env +DATABASE_URL="__YOUR_DATABASE_CONNECTION_STRING__" +# Replace __YOUR_DATABASE_CONNECTION_STRING__ with your actual connection string. +OPTIMIZE_API_KEY="your_secure_optimize_api_key" +``` + +- `DATABASE_URL`: The connection string to your database. +- `OPTIMIZE_API_KEY`: Reference the [Environment API Keys](https://www.prisma.io/docs/platform/about#environment) section in our documentation to learn how to obtain an API key for your project using Optimize. + +### 3. Set up the project + +Perform a database migration to prepare the project: + +```bash +npx prisma migrate dev --name init +``` + +### 4. Open the Optimize dashboard + +You can create [recordings](https://pris.ly/optimize-recordings) and view detailed insights into your queries, along with optimization [recommendations](https://pris.ly/optimize-recommendations), in the Optimize dashboard. To access the dashboard: + +1. Log in to your [Prisma Data Platform](https://console.prisma.io/optimize) account. If you haven't already, complete the onboarding process for Optimize by clicking the **Get Started** button. +2. If Optimize hasn't been launched yet, click the **Launch Optimize** button. +3. If you want to use a different workspace, navigate to your desired [Workspace](https://www.prisma.io/docs/platform/about#workspace), click the **Optimize** tab on the left sidebar to open the Optimize dashboard. Then, if Optimize is not yet launched, click the **Launch Optimize** button. + +### 5. Run the script + +Let's run the [script with unoptimized Prisma queries](./script.ts): + +1. In the Optimize dashboard, click the **Start new recording** button. +2. In the project terminal, run the project with: + + ```bash + npm run dev + ``` + +3. After the script completes, you'll see a log saying "Done." Then, in the Optimize dashboard, click the **Stop recording** button. +4. Observe the queries with high latencies highlighted in red, and review the recommendations in the **Recommendations** tab. You should see the recommendation: + - **Repeated query** + > For more insights on this recommendation, click the **Ask AI** button and interact with the [AI Explainer](https://pris.ly/optimize-ai-chatbot) chatbot. +5. To create a reference for comparison with other recordings, rename the recording to _Unoptimized queries_ by clicking the green recording label chip in the top left corner and typing "Unoptimized queries". + + ![Rename recording](./images/edit-recording-name-chip.png) + +### Add Prisma Accelerate to the project + +To apply the recommendation, you need to add Prisma Accelerate to the project. To do that: + +1. Use your database connection string and enable Prisma Accelerate in your [Prisma Data Platform account](https://pris.ly/pdp). +2. Install the required dependencies: + ```bash + npm install @prisma/client@latest @prisma/extension-accelerate + ``` +3. Update [the database connection string to use the Accelerate connection string](https://www.prisma.io/docs/accelerate/getting-started#21-update-your-database-connection-string). +4. Regenerate `PrismaClient`: + ```bash + npx prisma generate --no-engine + ``` +5. Extend `PrismaClient` by using the Accelerate client extension in [utils/db.ts](./utils/db.ts) folder: + ```diff + import { PrismaClient } from '@prisma/client' + + import { withAccelerate } from '@prisma/extension-accelerate' + import { withOptimize } from '@prisma/extension-optimize' + + export const prisma = new PrismaClient().$extends( + withOptimize({ + apiKey: process.env.OPTIMIZE_API_KEY!, + }), + ) + + .$extends(withAccelerate()); + ``` + +Afterward, continue with the next steps to complete the recommendation. + +### Optimize example: Applying the "Repeated query" recommendation + +Next, let’s follow the recommendation provided by Optimize to improve the performance of the queries: + +1. To enhance the performance of [**Query 1**](./script.ts) through [**Query 5**](./script.ts) by addressing the "Repeated query" recommendation, add a [cache strategy](https://prisma.io/docs/accelerate/caching) to the queries: + + ```diff + // Query 1 + await prisma.user.findFirst({ + select: { + name: true, + }, + + cacheStrategy: { + + ttl: 120 + + } + }) + + // Query 2 + await prisma.user.findFirst({ + select: { + name: true, + }, + + cacheStrategy: { + + ttl: 120 + + } + }) + + // Query 3 + await prisma.user.findFirst({ + select: { + name: true, + }, + + cacheStrategy: { + + ttl: 120 + + } + }) + + // Query 4 + await prisma.user.findFirst({ + select: { + name: true, + }, + + cacheStrategy: { + + ttl: 120 + + } + }) + + // Query 5 + await prisma.user.findFirst({ + select: { + name: true, + }, + + cacheStrategy: { + + ttl: 120 + + } + }) + ``` +2. Click the **Start new recording** button to begin a new recording and check for any performance improvements. +3. In the project terminal, run the project with: + ```bash + npm run dev + ``` +4. After the script completes, click the **Stop recording** button. +5. Rename the recording to _Optimized queries_ by clicking the recording chip in the top left corner and typing "Optimized queries." + +You can now compare performance improvements by navigating to the "Optimized queries" and "Unoptimized queries" recording tabs and observing the query latency differences. + +--- + +## Next steps + +- Check out the [Optimize docs](https://pris.ly/d/optimize). +- Share your feedback on the [Prisma Discord](https://pris.ly/discord/). +- Create issues and ask questions on [GitHub](https://github.com/prisma/prisma/). diff --git a/optimize/optimize-repeated-query/environment.d.ts b/optimize/optimize-repeated-query/environment.d.ts new file mode 100644 index 000000000000..4dbd16ff6472 --- /dev/null +++ b/optimize/optimize-repeated-query/environment.d.ts @@ -0,0 +1,10 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + DATABASE_URL: string + OPTIMIZE_API_KEY: string | undefined + } + } +} + +export {} diff --git a/optimize/optimize-repeated-query/images/edit-recording-name-chip.png b/optimize/optimize-repeated-query/images/edit-recording-name-chip.png new file mode 100644 index 0000000000000000000000000000000000000000..ed4d482cfb4c21248e9429241d648139bbe47ca6 GIT binary patch literal 17291 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 { + await prisma.post.deleteMany({}) + await prisma.user.deleteMany({}) + + for (let index = 0; index < TOTAL - 1; index++) { + await prisma.user.create({ + data: { + email: `${Math.round(Math.random() * 1000)}${faker.internet.email()}`, + name: faker.internet.displayName(), + posts: { + create: { + title: faker.lorem.sentences(1), + content: faker.lorem.text(), + published: faker.datatype.boolean(), + }, + }, + }, + }) + + console.log(`Inserted ${index + 1}/${TOTAL} item.`) + } + + await prisma.user.create({ + data: { + name: 'Nikolas Burk', + email: 'niko@gmail.com', + posts: { + create: { + title: 'The great gatsby', + content: 'The story had a nice ending.', + }, + }, + }, + }) + + console.log(`Inserted ${5000}/${TOTAL} item.`) +} + +main().then(() => console.log('🌿 Seeding completed.')) diff --git a/optimize/optimize-repeated-query/script.ts b/optimize/optimize-repeated-query/script.ts new file mode 100644 index 000000000000..e3e9b521e92f --- /dev/null +++ b/optimize/optimize-repeated-query/script.ts @@ -0,0 +1,57 @@ +import { prisma } from './utils' + +// A `main` function so that we can use async/await +async function main() { + // A simple query to create the database connection as the database connection usually takes a lot of time + await prisma.post.findFirst({ + select: { + id: true, + }, + }) + + // Query 1 + await prisma.user.findFirst({ + select: { + name: true, + }, + }) + + // Query 2 + await prisma.user.findFirst({ + select: { + name: true, + }, + }) + + // Query 3 + await prisma.user.findFirst({ + select: { + name: true, + }, + }) + + // Query 4 + await prisma.user.findFirst({ + select: { + name: true, + }, + }) + + // Query 5 + await prisma.user.findFirst({ + select: { + name: true, + }, + }) +} + +main() + .then(async () => { + await prisma.$disconnect() + console.log('Done') + }) + .catch(async (e) => { + console.error(e) + await prisma.$disconnect() + process.exit(1) + }) diff --git a/optimize/optimize-repeated-query/tsconfig.json b/optimize/optimize-repeated-query/tsconfig.json new file mode 100644 index 000000000000..86758c0f7a5d --- /dev/null +++ b/optimize/optimize-repeated-query/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "sourceMap": true, + "outDir": "dist", + "strict": true, + "lib": ["esnext"], + "esModuleInterop": true + } +} diff --git a/optimize/optimize-repeated-query/utils/db.ts b/optimize/optimize-repeated-query/utils/db.ts new file mode 100644 index 000000000000..e36241dd0511 --- /dev/null +++ b/optimize/optimize-repeated-query/utils/db.ts @@ -0,0 +1,8 @@ +import { PrismaClient } from '@prisma/client' +import { withOptimize } from '@prisma/extension-optimize' + +export const prisma = new PrismaClient().$extends( + withOptimize({ + apiKey: process.env.OPTIMIZE_API_KEY!, + }), +) diff --git a/optimize/optimize-repeated-query/utils/index.ts b/optimize/optimize-repeated-query/utils/index.ts new file mode 100644 index 000000000000..1beb455f5e39 --- /dev/null +++ b/optimize/optimize-repeated-query/utils/index.ts @@ -0,0 +1 @@ +export * from './db' diff --git a/optimize/optimize-select-returning-all/.env.example b/optimize/optimize-select-returning-all/.env.example new file mode 100644 index 000000000000..90cfec3bf6c7 --- /dev/null +++ b/optimize/optimize-select-returning-all/.env.example @@ -0,0 +1,2 @@ +DATABASE_URL="" +OPTIMIZE_API_KEY="" diff --git a/optimize/optimize-select-returning-all/.gitignore b/optimize/optimize-select-returning-all/.gitignore new file mode 100644 index 000000000000..2484fd1582e2 --- /dev/null +++ b/optimize/optimize-select-returning-all/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +*.env +package-lock.json +prisma/migrations diff --git a/optimize/optimize-select-returning-all/README.md b/optimize/optimize-select-returning-all/README.md new file mode 100644 index 000000000000..91c005957656 --- /dev/null +++ b/optimize/optimize-select-returning-all/README.md @@ -0,0 +1,117 @@ +# Prisma Optimize Example: Applying the "Query filtering on an unindexed column" Recommendation + +This repository demonstrates how to use [Prisma Optimize](https://pris.ly/optimize) to improve query performance using the "Query filtering on an unindexed column" recommendation. + +## Prerequisites + +To successfully run the project, you will need the following: + +1. A **database connection string** supported by Prisma Optimize. +2. An Optimize API key, which you can obtain from your [Prisma Data Platform](https://pris.ly/pdp) account. + +## Getting started + +### 1. Clone the repository + +Clone the repository, navigate into it, and install the dependencies: + +```bash +git clone git@github.com:prisma/prisma-examples.git --depth=1 +cd prisma-examples/optimize/optimize-unindexed-column +npm install +``` + +### 2. Configure environment variables + +Create a `.env` file in the root of the project directory: + +```bash +cp .env.example .env +``` + +Next, open the `.env` file and update the `DATABASE_URL` with your database connection string and the `OPTIMIZE_API_KEY` with your Optimize API key: + +```env +# .env +DATABASE_URL="__YOUR_DATABASE_CONNECTION_STRING__" +# Replace __YOUR_DATABASE_CONNECTION_STRING__ with your actual connection string. +OPTIMIZE_API_KEY="your_secure_optimize_api_key" +``` + +- `DATABASE_URL`: The connection string to your database. +- `OPTIMIZE_API_KEY`: Reference the [Environment API Keys](https://www.prisma.io/docs/platform/about#environment) section in our documentation to learn how to obtain an API key for your project using Optimize. + +### 3. Set up the project + +Perform a database migration to prepare the project: + +```bash +npx prisma migrate dev --name init +``` + +### 4. Open the Optimize dashboard + +You can create [recordings](https://pris.ly/optimize-recordings) and view detailed insights into your queries, along with optimization [recommendations](https://pris.ly/optimize-recommendations), in the Optimize dashboard. To access the dashboard: + +1. Log in to your [Prisma Data Platform](https://console.prisma.io/optimize) account. If you haven't already, complete the onboarding process for Optimize by clicking the **Get Started** button. +2. If Optimize hasn't been launched yet, click the **Launch Optimize** button. +3. If you want to use a different workspace, navigate to your desired [Workspace](https://www.prisma.io/docs/platform/about#workspace), click the **Optimize** tab on the left sidebar to open the Optimize dashboard. Then, if Optimize is not yet launched, click the **Launch Optimize** button. + +### 5. Run the script + +Let's run the [script with unoptimized Prisma queries](./script.ts): + +1. In the Optimize dashboard, click the **Start new recording** button. +2. In the project terminal, run the project with: + + ```bash + npm run dev + ``` + +3. After the script completes, you'll see a log saying "Done." Then, in the Optimize dashboard, click the **Stop recording** button. +4. Observe the queries with high latencies highlighted in red, and review the recommendations in the **Recommendations** tab. You should see the recommendation: + - **Query filtering on an unindexed column** + > For more insights on this recommendation, click the **Ask AI** button and interact with the [AI Explainer](https://pris.ly/optimize-ai-chatbot) chatbot. +5. To create a reference for comparison with other recordings, rename the recording to _Unoptimized queries_ by clicking the green recording label chip in the top left corner and typing "Unoptimized queries". + + ![Rename recording](./images/edit-recording-name-chip.png) + +### Optimize example: Applying the "Query filtering on an unindexed column" recommendation + +Next, let’s follow the recommendation provided by Optimize to improve the performance of the queries: + +1. To enhance the performance of [**Query 1**](./script.ts) through [**Query 3**](./script.ts) by addressing the [**Query filtering on an unindexed column**](https://pris.ly/optimize/r/unindexed-column) recommendation, add an `index` to the `name` column (commonly used in the queries) in the `User` model within the [`schema.prisma`](./prisma/schema.prisma) file: + + ```diff + model User { + id Int @id @default(autoincrement()) + email String @unique + name String? + posts Post[] + + @@index(name) + } + ``` + +2. After making these changes, migrate them to your database using: + + ```bash + npx prisma migrate dev --name create-name-index-on-user-model + ``` + +3. Click the **Start new recording** button to begin a new recording and check for any performance improvements. +4. In the project terminal, run the project with: + ```bash + npm run dev + ``` +5. After the script completes, click the **Stop recording** button. +6. Rename the recording to _Optimized queries_ by clicking the recording chip in the top left corner and typing "Optimized queries." + +You can now compare performance improvements by navigating to the "Optimized queries" and "Unoptimized queries" recording tabs and observing the query latency differences. + +--- + +## Next steps + +- Check out the [Optimize docs](https://pris.ly/d/optimize). +- Share your feedback on the [Prisma Discord](https://pris.ly/discord/). +- Create issues and ask questions on [GitHub](https://github.com/prisma/prisma/). diff --git a/optimize/optimize-select-returning-all/environment.d.ts b/optimize/optimize-select-returning-all/environment.d.ts new file mode 100644 index 000000000000..4dbd16ff6472 --- /dev/null +++ b/optimize/optimize-select-returning-all/environment.d.ts @@ -0,0 +1,10 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + DATABASE_URL: string + OPTIMIZE_API_KEY: string | undefined + } + } +} + +export {} diff --git a/optimize/optimize-select-returning-all/images/edit-recording-name-chip.png b/optimize/optimize-select-returning-all/images/edit-recording-name-chip.png new file mode 100644 index 0000000000000000000000000000000000000000..ed4d482cfb4c21248e9429241d648139bbe47ca6 GIT binary patch literal 17291 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 { + await prisma.post.deleteMany({}) + await prisma.user.deleteMany({}) + + for (let index = 0; index < TOTAL - 1; index++) { + await prisma.user.create({ + data: { + email: `${Math.round(Math.random() * 1000)}${faker.internet.email()}`, + name: faker.internet.displayName(), + posts: { + create: { + title: faker.lorem.sentences(1), + content: faker.lorem.text(), + published: faker.datatype.boolean(), + }, + }, + }, + }) + + console.log(`Inserted ${index + 1}/${TOTAL} item.`) + } + + await prisma.user.create({ + data: { + name: 'Nikolas Burk', + email: 'niko@gmail.com', + posts: { + create: { + title: 'The great gatsby', + content: 'The story had a nice ending.', + }, + }, + }, + }) + + console.log(`Inserted ${5000}/${TOTAL} item.`) +} + +main().then(() => console.log('🌿 Seeding completed.')) diff --git a/optimize/optimize-select-returning-all/script.ts b/optimize/optimize-select-returning-all/script.ts new file mode 100644 index 000000000000..2fb0f0ec5dd2 --- /dev/null +++ b/optimize/optimize-select-returning-all/script.ts @@ -0,0 +1,43 @@ +import { prisma } from './utils' + +// A `main` function so that we can use async/await +async function main() { + // A simple query to create the database connection as the database connection usually takes a lot of time + await prisma.user.findFirst() + + // Query 1 + await prisma.user.findFirst({ + where: { + name: 'Nikolas Burk', + }, + }) + + // Query 2 + await prisma.user.findMany({ + where: { + name: 'Nikolas Burk', + }, + take: 10, + }) + + // Query 3 + await prisma.post.findMany({ + where: { + author: { + name: 'Nikolas Burk', + }, + }, + take: 10, + }) +} + +main() + .then(async () => { + await prisma.$disconnect() + console.log('Done') + }) + .catch(async (e) => { + console.error(e) + await prisma.$disconnect() + process.exit(1) + }) diff --git a/optimize/optimize-select-returning-all/tsconfig.json b/optimize/optimize-select-returning-all/tsconfig.json new file mode 100644 index 000000000000..86758c0f7a5d --- /dev/null +++ b/optimize/optimize-select-returning-all/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "sourceMap": true, + "outDir": "dist", + "strict": true, + "lib": ["esnext"], + "esModuleInterop": true + } +} diff --git a/optimize/optimize-select-returning-all/utils/db.ts b/optimize/optimize-select-returning-all/utils/db.ts new file mode 100644 index 000000000000..e36241dd0511 --- /dev/null +++ b/optimize/optimize-select-returning-all/utils/db.ts @@ -0,0 +1,8 @@ +import { PrismaClient } from '@prisma/client' +import { withOptimize } from '@prisma/extension-optimize' + +export const prisma = new PrismaClient().$extends( + withOptimize({ + apiKey: process.env.OPTIMIZE_API_KEY!, + }), +) diff --git a/optimize/optimize-select-returning-all/utils/index.ts b/optimize/optimize-select-returning-all/utils/index.ts new file mode 100644 index 000000000000..1beb455f5e39 --- /dev/null +++ b/optimize/optimize-select-returning-all/utils/index.ts @@ -0,0 +1 @@ +export * from './db' From 2fd3d5e9f7a91bbb63caacd5da8a0b3d8af3ea60 Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Thu, 10 Oct 2024 09:10:22 +0200 Subject: [PATCH 2/3] feat: add example 4 --- .../optimize-select-returning-all/README.md | 58 ++++++++++--------- .../optimize-select-returning-all/script.ts | 50 +++++++++++----- 2 files changed, 67 insertions(+), 41 deletions(-) diff --git a/optimize/optimize-select-returning-all/README.md b/optimize/optimize-select-returning-all/README.md index 91c005957656..16e537a77054 100644 --- a/optimize/optimize-select-returning-all/README.md +++ b/optimize/optimize-select-returning-all/README.md @@ -1,6 +1,6 @@ -# Prisma Optimize Example: Applying the "Query filtering on an unindexed column" Recommendation +# Prisma Optimize Example: Applying the `SELECT/RETURNING *` Recommendation -This repository demonstrates how to use [Prisma Optimize](https://pris.ly/optimize) to improve query performance using the "Query filtering on an unindexed column" recommendation. +This repository demonstrates how to use [Prisma Optimize](https://pris.ly/optimize) to improve query performance using the `SELECT/RETURNING *` recommendation and avoiding over-fetching of data. ## Prerequisites @@ -17,7 +17,7 @@ Clone the repository, navigate into it, and install the dependencies: ```bash git clone git@github.com:prisma/prisma-examples.git --depth=1 -cd prisma-examples/optimize/optimize-unindexed-column +cd prisma-examples/optimize/optimize-select-returning-all npm install ``` @@ -70,7 +70,7 @@ Let's run the [script with unoptimized Prisma queries](./script.ts): 3. After the script completes, you'll see a log saying "Done." Then, in the Optimize dashboard, click the **Stop recording** button. 4. Observe the queries with high latencies highlighted in red, and review the recommendations in the **Recommendations** tab. You should see the recommendation: - - **Query filtering on an unindexed column** + - **`SELECT/RETURNING *`** > For more insights on this recommendation, click the **Ask AI** button and interact with the [AI Explainer](https://pris.ly/optimize-ai-chatbot) chatbot. 5. To create a reference for comparison with other recordings, rename the recording to _Unoptimized queries_ by clicking the green recording label chip in the top left corner and typing "Unoptimized queries". @@ -80,31 +80,37 @@ Let's run the [script with unoptimized Prisma queries](./script.ts): Next, let’s follow the recommendation provided by Optimize to improve the performance of the queries: -1. To enhance the performance of [**Query 1**](./script.ts) through [**Query 3**](./script.ts) by addressing the [**Query filtering on an unindexed column**](https://pris.ly/optimize/r/unindexed-column) recommendation, add an `index` to the `name` column (commonly used in the queries) in the `User` model within the [`schema.prisma`](./prisma/schema.prisma) file: - - ```diff - model User { - id Int @id @default(autoincrement()) - email String @unique - name String? - posts Post[] - + @@index(name) - } - ``` - -2. After making these changes, migrate them to your database using: - - ```bash - npx prisma migrate dev --name create-name-index-on-user-model - ``` - -3. Click the **Start new recording** button to begin a new recording and check for any performance improvements. -4. In the project terminal, run the project with: +1. To enhance the performance of [**Query 1**](./script.ts) by addressing the `SELECT/RETURNING *` recommendation: + ```diff + // Query 1 + const result = await prisma.user.findFirst({ + where: { + name: 'Nikolas Burk', + }, + - include: { + - posts: { + - take: 10, + - }, + - }, + + select: { + + name: true, + + email: true, + + posts: { + + select: { + + id: true, + + }, + + take: 10, + + }, + + }, + }) + ``` +2. Click the **Start new recording** button to begin a new recording and check for any performance improvements. +3. In the project terminal, run the project with: ```bash npm run dev ``` -5. After the script completes, click the **Stop recording** button. -6. Rename the recording to _Optimized queries_ by clicking the recording chip in the top left corner and typing "Optimized queries." +4. After the script completes, click the **Stop recording** button. +5. Rename the recording to _Optimized queries_ by clicking the recording chip in the top left corner and typing "Optimized queries." You can now compare performance improvements by navigating to the "Optimized queries" and "Unoptimized queries" recording tabs and observing the query latency differences. diff --git a/optimize/optimize-select-returning-all/script.ts b/optimize/optimize-select-returning-all/script.ts index 2fb0f0ec5dd2..38ac952a2c41 100644 --- a/optimize/optimize-select-returning-all/script.ts +++ b/optimize/optimize-select-returning-all/script.ts @@ -3,32 +3,52 @@ import { prisma } from './utils' // A `main` function so that we can use async/await async function main() { // A simple query to create the database connection as the database connection usually takes a lot of time - await prisma.user.findFirst() - - // Query 1 await prisma.user.findFirst({ - where: { - name: 'Nikolas Burk', + select: { + name: true, }, }) - // Query 2 - await prisma.user.findMany({ + // Query 1 + const result = await prisma.user.findFirst({ where: { name: 'Nikolas Burk', }, - take: 10, + include: { + posts: { + take: 10, + }, + }, }) - // Query 3 - await prisma.post.findMany({ - where: { - author: { + console.log({ + name: result?.name, + email: result?.email, + postIds: [...(result?.posts?.map((post) => post.id) ?? [])], + }) + + // Query 1 + const result2 = await prisma.user.findFirst({ + where: { name: 'Nikolas Burk', }, - }, - take: 10, - }) + select: { + name: true, + email: true, + posts: { + select: { + id: true, + }, + take: 10, + }, + }, + }) + + console.log({ + name: result?.name, + email: result?.email, + postIds: [...(result?.posts?.map((post) => post.id) ?? [])], + }) } main() From 1c7bf036ecba1f3503be919db06e46dc67df349a Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Thu, 10 Oct 2024 09:16:07 +0200 Subject: [PATCH 3/3] fix: change the title --- optimize/optimize-select-returning-all/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optimize/optimize-select-returning-all/README.md b/optimize/optimize-select-returning-all/README.md index 16e537a77054..b205fbeafbe7 100644 --- a/optimize/optimize-select-returning-all/README.md +++ b/optimize/optimize-select-returning-all/README.md @@ -76,7 +76,7 @@ Let's run the [script with unoptimized Prisma queries](./script.ts): ![Rename recording](./images/edit-recording-name-chip.png) -### Optimize example: Applying the "Query filtering on an unindexed column" recommendation +### Optimize example: Applying the `SELECT/RETURNING *` recommendation Next, let’s follow the recommendation provided by Optimize to improve the performance of the queries: