Study guide
7.3 DML Operations & Bulkification
Salesforce Certified Platform Developer I Exam
Track your progress as you read
Finished sections get a check mark.
1 Introduction & Exam Overview
1.1 Current Exam Facts 1.2 Blueprint Weights & Study Strategy
2 Platform Fundamentals
2.1 Multi-Tenant Architecture & Shared Resources 2.2 MVC Mapping on the Lightning Platform 2.3 Declarative vs Programmatic Decisions 2.4 Agentforce for Developers: Use Cases & Limitations
3 Data Modeling & Schema
3.1 Objects, Fields & Schema Design 3.2 Relationship Types & Implications 3.3 Schema Design Impact on Apex 3.4 Data Import & Export for Development
4 Platform Data Features
4.1 Formula Fields & Roll-Up Summaries 4.2 Core CRM Objects & Standard Schema 4.3 AppExchange Extensions & Platform Boundaries
5 Declarative Process Automation
5.1 Record-Triggered Flows & Declarative Automation 5.2 Approval Processes & Business Rules 5.3 When to Use Apex vs Declarative Tools
6 Apex Language Basics
6.1 Variables, Constants & Apex Data Types 6.2 Lists, Sets & Maps 6.3 Control Flow & Exception Handling 6.4 Classes, Interfaces & OOP Patterns
7 SOQL, SOSL & DML
7.1 SOQL Query Fundamentals 7.2 SOSL Search Fundamentals 7.3 DML Operations & Bulkification 7.4 Governor Limits & Transaction Boundaries
8 Apex Triggers & Order of Execution
8.1 Trigger Structure & Context Variables 8.2 Bulk Trigger Patterns & Best Practices 8.3 Save Order of Execution 8.4 Recursion, Cascading & Transaction Control
9 Asynchronous Apex
9.1 @future Methods 9.2 Queueable Apex 9.3 Batch Apex 9.4 Schedulable Apex & Job Orchestration
10 Security, Events & Schema Access
10.1 Sharing, FLS, CRUD & Injection Prevention 10.2 Platform Events Publish & Subscribe 10.3 Programmatic Schema Access (Describe)
11 Visualforce
11.1 Visualforce Pages & Markup 11.2 Standard, Custom Controllers & Extensions 11.3 Visualforce in Lightning Experience
12 Lightning Components
12.1 Lightning Component Framework Benefits 12.2 Aura Component Structure & Resources 12.3 Lightning Web Components Fundamentals 12.4 LWC Data Access: Wire, Imperative & Events
13 UI Composition & Apex Integration Patterns
13.1 Composing UI in Lightning Apps 13.2 UI Security, Performance & Accessibility Patterns 13.3 Apex Actions for Flow, LWC, and Agentforce
14 Apex Testing
14.1 Testing Framework & Coverage Requirements 14.2 Writing Unit Tests for Triggers & Classes 14.3 Test Data Strategies & Isolation 14.4 Executing Tests & Execute Anonymous Differences
15 Debugging & Developer Tools
15.1 Debug Logs & Monitoring 15.2 Developer Console, CLI, VS Code & Workbench 15.3 Sandboxes, Scratch Orgs & Environment Strategy
16 Deployment & Release
16.1 Change Sets & Metadata Deployment 16.2 Unlocked Packages, Metadata API & Salesforce DX 16.3 Release Readiness & Credential Maintenance
7.3 DML Operations & Bulkification
Key Takeaways Explain This Topic AI
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
- Operation Effect
- insert Create new rows; platform assigns Id on success
- update Modify existing rows; Id required
- upsert Insert or update based on Id or External ID match
- delete Soft-delete to Recycle Bin (most objects)
- undelete Restore 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());
- }
- }
- API Returns allOrNone
- Database.insert(list, allOrNone) SaveResult[] true (default overload) / false
- Database.update SaveResult[] same
- Database.upsert UpsertResult[] same
- Database.delete DeleteResult[] same
- Database.undelete UndeleteResult[] 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:
No SOQL inside loops over trigger batches or unpredictable sizes.
No DML inside loops that could run per record—accumulate List<SObject> / maps, then DML.
Use Maps keyed by Id for O(1) parent lookups after a single query.
Split lists by object type; mixed sObject DML has special rules—prefer homogeneous lists.
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
Count DML statements in the worst-case batch of 200.
Confirm lists are built outside inner loops.
Choose standalone vs Database.* based on all-or-none vs partial success.
For integrations, prefer upsert + External ID over blind insert.
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.