# Create, update & delete

> Insert, change, and remove rows with create, createMany, update, updateMany, delete, and deleteMany.

URL: https://better-drizzle.com/docs/writing/crud

Use these methods for everyday writes. Single-row methods return the affected row (or `null`); batch methods return `{ count, data? }`. For per-row values in one statement see [`updateEach`](/docs/writing/update-each), and for create-or-update see [upsert](/docs/writing/upsert).

## `create`

Inserts one row and returns the created record:



**better-drizzle**

```ts  
const user = await client.users.create({
	data: {
		email: 'alice@example.com',
		name: 'Alice',
		active: true,
	},
});
```

**Drizzle**

```ts  
const [user] = await db
	.insert(users)
	.values({
		email: 'alice@example.com',
		name: 'Alice',
		active: true,
	})
	.returning();
```

## `createMany`

Inserts many rows in one statement and returns `{ count, data? }`:



**better-drizzle**

```ts  
const {
	count, // number inserted
	data, // inserted rows, when the driver supports RETURNING
} = await client.users.createMany({
	data: [
		{ email: 'a@example.com', name: 'A', active: true },
		{ email: 'b@example.com', name: 'B', active: false },
	],
});
```

**Drizzle**

```ts  
const rows = await db
	.insert(users)
	.values([
		{ email: 'a@example.com', name: 'A', active: true },
		{ email: 'b@example.com', name: 'B', active: false },
	])
	.returning();

// shape the batch summary by hand
const count = rows.length;
const data = rows;
```

### Skip duplicates

`create` and `createMany` accept `skipDuplicates`: `true`, or the column keys of the conflict target:



**better-drizzle**

```ts  
const maybeCreated = await client.users.create({
	data: {
		email: 'alice@example.com',
		name: 'Alice',
		active: true,
	},
	skipDuplicates: ['email'],
});
```

**Drizzle**

```ts  
const [maybeCreated = null] = await db
	.insert(users)
	.values({
		email: 'alice@example.com',
		name: 'Alice',
		active: true,
	})
	.onConflictDoNothing({ target: users.email })
	.returning();
```

With single-row `create`, a skipped insert returns `null`. With `createMany`, `count` only includes rows that were actually inserted.

### Insert in chunks

(Added in 0.3.0)

For large inputs, `batchSize` splits `data` into one `INSERT` per chunk so a single statement stays under driver parameter limits. `count` sums the inserted rows and `data` keeps input order. Hooks and plugins still run once per call. Chunks run one after another without an implicit transaction, the same as `upsertMany`; wrap the call in `db.transaction(...)` when all chunks must commit together. `batchSize` must be a positive integer.

```ts
const { count } = await client.users.createMany({
	data: rows,
	batchSize: 500,
	skipDuplicates: true,
});
```

## `update`

Updates at most one matching row. The result is nullable (the target may not exist) and also exposes [`.throw()`](/docs/writing/throwing-results).

When `where` matches several rows, only one of them is updated, on every dialect. Use [`updateMany`](#updatemany) to update every match.



**better-drizzle**

```ts  
const updated = await client.users.update({
	where: { id: 1 },
	data: { name: 'Alice Updated' },
});

if (!updated) {
	throw new Error('Update target not found');
}
```

**Drizzle**

```ts  
import { eq } from 'drizzle-orm';

const [updated] = await db
	.update(users)
	.set({ name: 'Alice Updated' })
	.where(eq(users.id, 1))
	.returning();

if (!updated) {
	throw new Error('Update target not found');
}
```

An update whose `data` resolves to no values (`{}`, or every field `undefined` after plugins run) skips the `UPDATE` and returns the current row, or `null` when nothing matches.

## `updateMany`

(Added in 0.3.0)

Updates every matching row and returns `{ count, data? }`. `data` holds the updated rows:



**better-drizzle**

```ts  
const { count, data } = await client.users.updateMany({
	where: { active: true },
	data: { active: false },
}); // count: rows updated
```

**Drizzle**

```ts  
import { eq } from 'drizzle-orm';

const rows = await db
	.update(users)
	.set({ active: false })
	.where(eq(users.active, true))
	.returning();

const count = rows.length;
```

PostgreSQL and SQLite return complete updated rows in `data` by default using native `RETURNING`. Pass a scalar `select` to narrow those rows, for example `select: { id: true, active: true }`. Relation projections are unsupported. When no row matches, the result is `{ count: 0 }` with no `data`; MySQL always returns only `{ count }`.

## `delete`

Deletes at most one matching row and returns it (nullable, with `.throw()`). When `where` matches several rows, only one is deleted; use [`deleteMany`](#deletemany) to delete every match:



**better-drizzle**

```ts  
const deleted = await client.users.delete({
	where: { id: 1 },
});
```

**Drizzle**

```ts  
import { eq } from 'drizzle-orm';

const [deleted = null] = await db
	.delete(users)
	.where(eq(users.id, 1))
	.returning();
```

## `deleteMany`

Deletes every matching row and returns `{ count, data? }`:



**better-drizzle**

```ts  
const { count, data } = await client.users.deleteMany({
	where: { active: false },
}); // count: rows deleted
```

**Drizzle**

```ts  
import { eq } from 'drizzle-orm';

const rows = await db
	.delete(users)
	.where(eq(users.active, false))
	.returning();

const count = rows.length;
```

On PostgreSQL and SQLite, `data` contains the deleted rows, or the scalar fields requested by `select`, from native `RETURNING`. Empty results omit `data`. MySQL returns only `{ count }`. Relation projections are unsupported.

## Write relations

`create`, `update`, and `upsert` accept `connect`, `disconnect`, and `set` inside `data`, so you attach related rows without resolving foreign keys yourself:

```ts
const post = await client.posts.create({
	data: {
		title: 'Connected post',
		author: { connect: { email: 'alice@example.com' } },
	},
});
```

See [relation writes](/docs/writing/relation-writes) for every command, its rules, and the raw Drizzle equivalent.

## Project write results

`create`, `update`, `delete`, `upsert`, `createMany`, `updateEach`, and `upsertMany` can project results. `updateEach` and `upsertMany` support `select`; the others support `select` and `include`:



**better-drizzle**

```ts  
const user = await client.users.create({
	data: { email: 'writer@example.com', name: 'Writer', active: true },
	select: { id: true, email: true },
});
//    ^? { id: number; email: string }
```

**Drizzle**

```ts  
const [user] = await db
	.insert(users)
	.values({ email: 'writer@example.com', name: 'Writer', active: true })
	.returning({ id: users.id, email: users.email });
//    ^? { id: number; email: string }
```

## Edge cases

**How update and delete pick one row**

A `where` that pins the whole primary key or every column of a unique key (a unique column, a `unique()` constraint, or a non-partial `uniqueIndex()` on plain columns) runs as a plain `UPDATE` / `DELETE`. Any other `where` is limited to one row in the same statement: a `LIMIT 1` subquery on PostgreSQL and SQLite.

MySQL has no `RETURNING`, so the read and the write run in one transaction: the active one, or an implicit one when none is open. The first matching row by primary key is locked with `SELECT ... FOR UPDATE` and written by that key, so the returned row is the one that changed. On a MySQL table without a primary key, the row is read without a lock and written with `LIMIT 1`, so a concurrent write can make the returned row differ from the changed one.

**Returned rows depend on RETURNING**

PostgreSQL and SQLite return rows through native `RETURNING`. MySQL returns only `{ count }` from `createMany`, `updateMany`, and `deleteMany`. Empty results omit `data`.

**Lifecycle behavior**

Timestamps, soft delete, and audit trails belong in [plugins](/docs/plugins/overview) or [hooks](/docs/advanced/hooks), not in each call.

## Reference

| Method       | Writes                      | Returns                                           | `select` / `include` |
| ------------ | --------------------------- | ------------------------------------------------- | -------------------- |
| `create`     | one row                     | the row, or `null` when `skipDuplicates` skips it | both                 |
| `createMany` | many rows                   | `{ count, data? }`                                | both                 |
| `update`     | at most one row             | the row or `null`, with `.throw()`                | both                 |
| `updateMany` | every match                 | `{ count, data? }`                                | scalar `select`      |
| `updateEach` | listed rows, per-row values | `{ count, data? }`                                | scalar `select`      |
| `delete`     | at most one row             | the row or `null`, with `.throw()`                | both                 |
| `deleteMany` | every match                 | `{ count, data? }`                                | scalar `select`      |
| `upsert`     | one row                     | the row                                           | both                 |
| `upsertMany` | many rows                   | `{ count, data? }`                                | scalar `select`      |
