7.3 DML Operations & Bulkification

Key Takeaways

  • Standalone DML (insert/update/upsert/delete/undelete) is all-or-none by default; one failure rolls back that DML operation’s work for the statement
  • Database.insert/update/upsert/delete/undelete methods accept allOrNone; false enables partial success with SaveResult arrays
  • Bulkify by collecting sObjects in lists outside loops and performing one DML per object type (or few batched statements)—never DML or SOQL inside per-record loops
  • upsert can match on Id or an External ID field to insert-or-update integration rows cleanly
  • DmlException and SaveResult/Error objects expose status codes and fields for handling and user messaging
Last updated: August 2026

7.3 DML Operations & Bulkification

Quick Answer: Apex DML persists sObjects: insert, update, upsert, delete, and undelete. Standalone DML is all-or-none. Database.* methods can set allOrNone = false for partial success and return SaveResult (or DeleteResult/UpsertResult) arrays. Bulkification means gather records in collections, query once, DML once—never SOQL/DML inside tight per-record loops. Upsert with an External ID is the integration workhorse.

This section sits at the center of trigger and service design. Platform Developer I constantly tests whether you create 200 DML statements or one.

The Five DML Operations

OperationEffect
insertCreate new rows; platform assigns Id on success
updateModify existing rows; Id required
upsertInsert or update based on Id or External ID match
deleteSoft-delete to Recycle Bin (most objects)
undeleteRestore from Recycle Bin
Account a = new Account(Name = 'Acme West');
insert a; // a.Id now populated

a.Name = 'Acme West HQ';
update a;

delete a;
undelete a;

Lists work the same way and are the default bulk form:

List<Contact> contacts = new List<Contact>();
for (Account acc : accounts) {
    contacts.add(new Contact(LastName = 'Primary', AccountId = acc.Id));
}
insert contacts; // one DML statement for many rows

Row limits vs statement limits: One insert contacts with 200 rows is one DML statement and 200 DML rows. Two hundred insert c; calls inside a loop are 200 DML statements—and will fail governors long before business logic finishes (see 7.4).

Standalone DML vs Database Methods

Standalone (all-or-none)

try {
    insert accounts;
} catch (DmlException e) {
    // Entire insert list fails if any row fails
    System.debug(e.getMessage());
    System.debug(e.getDmlFieldNames(0));
    System.debug(e.getDmlStatusCode(0));
}

If any record in the list fails validation, required fields, or duplicates (depending on rules), the whole standalone DML operation fails and throws DmlException (transaction partial rollback behavior depends on savepoints and surrounding context—but the statement does not partially apply successes the way allOrNone=false does).

Database methods (optional partial success)

List<Database.SaveResult> results = Database.insert(accounts, false);
for (Integer i = 0; i < results.size(); i++) {
    if (!results[i].isSuccess()) {
        for (Database.Error err : results[i].getErrors()) {
            System.debug(err.getStatusCode() + ': ' + err.getMessage());
            System.debug(err.getFields());
        }
    } else {
        System.debug('Inserted Id: ' + results[i].getId());
    }
}
APIReturnsallOrNone
Database.insert(list, allOrNone)SaveResult[]true (default overload) / false
Database.updateSaveResult[]same
Database.upsertUpsertResult[]same
Database.deleteDeleteResult[]same
Database.undeleteUndeleteResult[]same

When partial success helps: data loads, integration batches, “save what you can and report errors.” When all-or-none is better: multi-object business transactions that must not leave orphan children (often combined with Savepoints for multi-step rollback).

System.Savepoint sp = Database.setSavepoint();
try {
    insert parents;
    insert children;
} catch (DmlException e) {
    Database.rollback(sp);
    throw e;
}

Bulk Patterns: Collect Outside Loops

Anti-pattern (fails exams and production)

// BAD: SOQL + DML inside loop
for (Account a : Trigger.new) {
    List<Contact> cons = [SELECT Id FROM Contact WHERE AccountId = :a.Id];
    for (Contact c : cons) {
        c.Description = 'Synced';
        update c; // DML per contact!
    }
}

Bulk-safe pattern

// GOOD: one SOQL, in-memory work, one DML
Set<Id> accountIds = new Set<Id>();
for (Account a : Trigger.new) {
    accountIds.add(a.Id);
}
List<Contact> cons = [
    SELECT Id, AccountId, Description
    FROM Contact
    WHERE AccountId IN :accountIds
];
for (Contact c : cons) {
    c.Description = 'Synced';
}
update cons;

Rules of thumb:

  1. No SOQL inside loops over trigger batches or unpredictable sizes.
  2. No DML inside loops that could run per record—accumulate List<SObject> / maps, then DML.
  3. Use Maps keyed by Id for O(1) parent lookups after a single query.
  4. Split lists by object type; mixed sObject DML has special rules—prefer homogeneous lists.
  5. Guard empty lists (if (!toUpdate.isEmpty()) update toUpdate;) for clarity and to avoid useless statements.

Map-driven parent/child updates

Map<Id, Account> newMap = (Map<Id, Account>)Trigger.newMap;
List<Opportunity> opps = [
    SELECT Id, AccountId, Name
    FROM Opportunity
    WHERE AccountId IN :newMap.keySet()
];
for (Opportunity o : opps) {
    Account parent = newMap.get(o.AccountId);
    if (parent != null && parent.Name != null) {
        o.Name = parent.Name + ' – Renewal';
    }
}
update opps;

Upsert and External IDs

Upsert inserts when no match exists and updates when a match is found:

// Match on Salesforce Id when present
upsert accounts;

// Match on custom External ID field (API name ends with __c, marked External ID / Unique as designed)
List<Product__c> products = new List<Product__c>();
products.add(new Product__c(SKU__c = 'SKU-1', Name = 'Widget'));
products.add(new Product__c(SKU__c = 'SKU-2', Name = 'Gadget'));
upsert products Product__c.Fields.SKU__c;
// or: Database.upsert(products, Product__c.SKU__c, false);

Exam uses: middleware sends external keys; Salesforce must not create duplicates. External ID fields enable matching without a prior SOQL to resolve Salesforce Ids (still query when you need extra fields). Unique External IDs prevent ambiguous matches; non-unique external IDs can cause upsert errors when multiple rows match.

Reading Failures: DmlException and Results

DmlException helpers (standalone path):

  • getNumDml() — how many row-level errors
  • getDmlIndex(i) — index into the original list
  • getDmlId(i), getDmlMessage(i), getDmlStatusCode(i), getDmlFieldNames(i), getDmlType(i)

Database.Error (partial success path): getMessage(), getStatusCode(), getFields().

Status codes you will reason about include required-field failures, validation-rule failures, and duplicate rules—exam questions may ask which API surfaces field names for UI error display.

Related Operations (Awareness)

  • merge — combine duplicate leads/contacts/accounts (specialized DML).
  • convertLead — Database.LeadConvert patterns (often separate exam topics).
  • Recursive DML — updates that re-enter triggers require static recursion guards (covered deeply in the triggers chapter); bulkification alone does not stop recursion.

Checklist Before You Submit Code on the Exam

  1. Count DML statements in the worst-case batch of 200.
  2. Confirm lists are built outside inner loops.
  3. Choose standalone vs Database.* based on all-or-none vs partial success.
  4. For integrations, prefer upsert + External ID over blind insert.
  5. Handle DmlException or iterate SaveResult so errors are not silent.

Bulk-safe DML is how you stay under the 150 DML statements and 10,000 DML rows synchronous ceilings described next—and how real multi-tenant apps stay stable under load.

Test Your Knowledge

What is the main behavioral difference between insert accounts; and Database.insert(accounts, false);?

A
B
C
D
Test Your Knowledge

Which pattern correctly bulkifies DML when creating one Contact per Account in Trigger.new after insert?

A
B
C
D
Test Your Knowledge

Why do integrations often upsert using an External ID field?

A
B
C
D