Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

upsertMany() functionality for bulk upsert #10362

Open
Ezard opened this issue Nov 20, 2021 · 12 comments
Open

upsertMany() functionality for bulk upsert #10362

Ezard opened this issue Nov 20, 2021 · 12 comments

Comments

@Ezard
Copy link

Ezard commented Nov 20, 2021

Problem

Upserting many records is inefficient. Currently, calling x upsert statements (e.g. as part of a single transaction) results in x SELECT statements followed by y INSERT statements and z UPDATE statements, where y + z = x.

Suggested solution

A more efficient flow would be to do a single bulk SELECT statement, followed by a bulk INSERT statement, followed by however many UPDATE statements are required.

e.g. to upsert 100 records, 50 of which already exist in the DB, Prisma will currently run a total of 100 SELECT statements, 50 INSERT statements and 50 UPDATE statements. With the described solution flow, this could be reduced to 1 SELECT statement, 1 INSERT statement and 50 UPDATE statements. This brings the total statement count down from 200 to 52.

Alternatives

An alternative would be for a developer to do this manually in each project that uses Prisma

@Ezard
Copy link
Author

Ezard commented Nov 22, 2021

Just discovered that Postgres has the ability to update multiple rows with different values (using the UPDATE...FROM functionality), so this would drop the total number of statements down from 200 to 3 (obviously other databases that don't support this functionality would need to use the less-efficient solution that I originally described)

@janpio janpio added kind/improvement An improvement to existing feature and code. team/client Issue for team Client. topic: performance/queries topic: upsert nested upsert labels Nov 22, 2021
@Akxe
Copy link
Contributor

Akxe commented Nov 25, 2021

@Ezard MySQL supports this too

@zipzapzanigan
Copy link

would love this feature. have to do it manually right now

@ADanilychevJr
Copy link

Would love this as well

@ChrisCates
Copy link

What do you guys need to actually make this happen?

@taylor-lindores-reeves
Copy link

What are the current solutions / workarounds?

@Akxe
Copy link
Contributor

Akxe commented Oct 27, 2022

Not much... you have to write the SQL manually

@janpio janpio changed the title Upsert Many Functionality upsertMany Functionality Nov 5, 2022
@ajhollowayvrm
Copy link

Another bump on this issue. Currently doing raw multiple inserts with on conflict do update set

@taylor-lindores-reeves
Copy link

taylor-lindores-reeves commented Nov 8, 2022

I got around this with a nested write. Both create and update within .upsert() call.

    return this.prisma.classified.upsert({
      where: { stockId: extendedStockId },
      update: {
        title,
        price,
        poa,
        status: classifiedStatusEnumMapper(status),
        liveDateTime: status === ClassifiedStatus.Live ? new Date() : null,
        Vehicle: {
          update: {
            ...vehicle,
            status: vehicleStatusEnumMapper(status),
            thumbnail: vehicle.mainImages[0],
          },
        },
      },
      create: {
        stockId: extendedStockId,
        title,
        price,
        poa,
        slug,
        uniqueId,
        classifiedType: ItemType.Vehicle,
        liveDateTime: status === ClassifiedStatus.Live ? new Date() : null,
        status: classifiedStatusEnumMapper(status),
        User: {
          connect: {
            uniqueId: dealerId,
          },
        },
        Vehicle: {
          create: {
            ...vehicle,
            uniqueId: generateUniqueId(year),
            status: vehicleStatusEnumMapper(status),
            thumbnail: mainImages[0],
            User: {
              connect: {
                uniqueId: dealerId,
              },
            },
          },
        },
      },
      include: {
        Vehicle: true,
        User: {
          select: {
            companyName: true,
            sellerType: true,
            slug: true,
          },
        },
      },
    });

@ChrisCates
Copy link

@leafyshark this implementation wouldn't work for me.

What's frustrating is the complete lack of responsiveness from the Prisma team.
We all know they aren't interested in our PRs and will make us jump hoops, to then end up discarding the external PR. And they also don't seem to care for a few thousand USD if any of us try to throw a bone.

For context of how much time this code would save. Look at this Postgres query I had to write (with JS Interpolation).

        INSERT INTO public."Orderbook"
            (${keys.map((key) => `"${key}"`).join(',')})
        VALUES
            ${orders.map((order) => `
                (${keys.map((key) => {
                    if (typeof order[key] === 'string') {
                        return `"${order[key]}"`
                    } else {
                        return `${order[key]}`
                    }
                }).join(',')})
            `).join(',')}
        ON CONFLICT ("id") DO UPDATE SET
            "status" = EXCLUDED."status"

Individual transactions even with phantom writes. Is insanely unscalable and only works for 10,000 records max at a time.
I am seriously considering moving back to Knex if the Prisma team doesn't come up with a proper response or at least is open to payment or PRs.

@jhnns
Copy link

jhnns commented Feb 19, 2023

Until this is implemented in Prisma, you can combine knex + $executeRawUnsafe to achieve it. Of course, this needs to be supported by your DB:

import * as knexModule from "knex";

const knex = knexModule.default.knex({
  // No connection settings necessary here because we only use the QueryBuilder
  client: "pg", // Using Postgres
});

const date = new Date().toISOString();

prismaClient.$executeRawUnsafe(
  knex("Client")
    .insert(
      clients.map((client): Prisma.ClientCreateManyInput => {
        return {
          ...client,
          createdAt: date,
          updatedAt: date,
        };
      })
    )
    .onConflict<keyof Prisma.ClientCreateManyInput>("number")
    .merge(["handle", "name", "archived", "updatedAt"] satisfies Array<
      keyof Prisma.ClientUpdateManyMutationInput
    >)
    .toString()
  );

It can be a little bit challenging to get the same type safety because Knex's types seem to be a little bit buggy. I decided to not add types to Knex itself, but to validate the arguments before passing them to Knex.

@janpio janpio added kind/feature A request for a new feature. topic: upsert() and removed kind/improvement An improvement to existing feature and code. labels Apr 29, 2024
@janpio janpio changed the title upsertMany Functionality upsertMany() functionality Apr 29, 2024
@janpio
Copy link
Member

janpio commented Apr 29, 2024

Related/duplicate: #4134

@janpio janpio removed kind/feature A request for a new feature. team/client Issue for team Client. topic: upsert nested upsert topic: performance/queries topic: upsert() labels Apr 29, 2024
@janpio janpio changed the title upsertMany() functionality upsertMany() functionality for bulk upsert May 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants