soql-null-ordering-patterns
Use when SOQL ORDER BY behavior with NULL values surprises a query — null records sorting before non-null, paginated results inconsistent across pages, NULLS FIRST/LAST clauses needed. Triggers: 'soql nulls first', 'soql null sort order', 'pagination missing records with null fields', 'order by skipping null records', 'consistent ordering with optional fields'. NOT for general SOQL optimization (use data/soql-query-optimization) or for ordering of relationship-traversed fields.
Best use case
soql-null-ordering-patterns is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Use when SOQL ORDER BY behavior with NULL values surprises a query — null records sorting before non-null, paginated results inconsistent across pages, NULLS FIRST/LAST clauses needed. Triggers: 'soql nulls first', 'soql null sort order', 'pagination missing records with null fields', 'order by skipping null records', 'consistent ordering with optional fields'. NOT for general SOQL optimization (use data/soql-query-optimization) or for ordering of relationship-traversed fields.
Teams using soql-null-ordering-patterns should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/soql-null-ordering-patterns/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How soql-null-ordering-patterns Compares
| Feature / Agent | soql-null-ordering-patterns | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Use when SOQL ORDER BY behavior with NULL values surprises a query — null records sorting before non-null, paginated results inconsistent across pages, NULLS FIRST/LAST clauses needed. Triggers: 'soql nulls first', 'soql null sort order', 'pagination missing records with null fields', 'order by skipping null records', 'consistent ordering with optional fields'. NOT for general SOQL optimization (use data/soql-query-optimization) or for ordering of relationship-traversed fields.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# SOQL NULL Ordering Patterns
Activate when a SOQL query's ORDER BY behaves unexpectedly because of NULL values — null records appearing first when they should appear last, paginated results dropping records, or the same query returning records in a different order on subsequent runs. The skill produces a corrected ORDER BY clause, a pagination-stable cursor, and test data that reproduces the original surprise.
---
## Before Starting
Gather this context before working on anything in this domain:
- The exact SOQL query and the field(s) in the ORDER BY clause. Note which are required vs. optional in the schema.
- How results are consumed: rendering in a UI list, paginating with OFFSET/LIMIT, batching across an Apex Batch, exporting to a downstream system. Pagination is the context where null ordering most often produces *missing* records, not just misplaced ones.
- Approximately what percentage of records have null in the sort field. With <1% null, the issue may surface only on one specific report; with 30% null, it's a daily complaint.
---
## Core Concepts
### Default null ordering in SOQL
SOQL's default ordering treatment of nulls is:
- `ORDER BY field ASC` → NULLS **FIRST** (nulls come before non-null values)
- `ORDER BY field DESC` → NULLS **LAST** (nulls come after non-null values)
The default was chosen for consistency with SQL's "nulls are smallest" convention but is the *opposite* of what most users expect for ASC ("blank should mean 'unknown', sort to end"). Salesforce supports an explicit `NULLS FIRST` and `NULLS LAST` clause to override the default.
### Composite tiebreakers
ORDER BY on a single field with duplicates produces non-deterministic intra-tie ordering. The same query rerun may return ties in a different order. This becomes a bug under pagination: if records A and B tie on the sort field and the page boundary lands between them, page 2 may start with A (shifted from page 1) or with B, causing record duplication or omission.
The fix is a composite tiebreaker on a guaranteed-unique field, almost always `Id`:
```soql
ORDER BY Last_Activity_Date__c DESC NULLS LAST, Id ASC
```
`Id` ASC is deterministic and effectively free at the index level.
### Pagination-stable cursors
OFFSET-based pagination breaks when the underlying data shifts between pages. Cursor-based pagination (filter on the last seen sort key + tiebreaker) is the only stable approach above a few thousand records:
```soql
WHERE (Last_Activity_Date__c < :cursorDate
OR (Last_Activity_Date__c = :cursorDate AND Id > :cursorId))
ORDER BY Last_Activity_Date__c DESC NULLS LAST, Id ASC
LIMIT 200
```
The `cursorDate` may be null on the first page; the WHERE clause must handle that case explicitly (commonly, treat the first page as `LIMIT 200` with no WHERE).
---
## Common Patterns
### Pattern: ASC with nulls at the end
**When to use:** UI list "sort by Last Activity ascending" — users expect oldest activity first, blanks last.
**How it works:** `ORDER BY Last_Activity_Date__c ASC NULLS LAST, Id ASC`. The explicit `NULLS LAST` overrides SOQL's default.
**Why not the alternative:** `ORDER BY ... ASC` alone places null records at the top of the list, confusing users.
### Pattern: pagination over a nullable sort field
**When to use:** Batch processing 200k records sorted by `Last_Activity_Date__c`.
**How it works:** Cursor-based pagination keyed by `(sortField, Id)`. First page fetches `LIMIT 200`; subsequent pages filter with `WHERE` clause referencing the last seen `(sortField, Id)`.
**Why not the alternative:** OFFSET/LIMIT is bounded at 2,000 in SOQL and produces unstable results when records are inserted/updated mid-pagination.
### Pattern: composite sort with mixed null behavior
**When to use:** Sort by region (required, ASC) then by score (nullable, DESC nulls last).
**How it works:** `ORDER BY Region__c ASC, Score__c DESC NULLS LAST, Id ASC`. Each field gets its own NULLS clause as needed.
---
## Decision Guidance
| Situation | Recommended Approach | Reason |
|---|---|---|
| Single-field ASC with nullable field | `... ASC NULLS LAST, Id ASC` | Matches user mental model for blanks |
| Single-field DESC with nullable field | `... DESC NULLS LAST, Id ASC` | DESC default already places nulls last; tiebreaker still needed for pagination |
| Pagination over a nullable field | Cursor pagination, never OFFSET | OFFSET breaks when data shifts |
| Sort by relationship field (`Account.Name`) | Treat the same way; relationship fields can be null too | Same NULLS clause grammar |
| Streaming export sorted by date | Cursor on `(date, Id)` | Stable across the multi-hour export window |
---
## Recommended Workflow
1. Identify the sort field(s) and check schema for `required=false` or formula fields that may emit null.
2. Decide where nulls should appear given the user/consumer mental model. Default SOQL behavior is rarely what users want for ASC.
3. Add explicit `NULLS FIRST` / `NULLS LAST` to every nullable-field ORDER BY clause.
4. Add `Id` (or another guaranteed-unique field) as the final tiebreaker — always.
5. If pagination is in scope, switch from OFFSET to cursor-based pagination keyed on `(sortField, Id)`. The first-page cursor handles the null-cursor case.
6. Build a test dataset with mixed null and non-null values; verify pagination across a page boundary that splits ties.
7. Document the ordering contract (NULLS LAST, tiebreaker by Id) in the data layer's interface so future readers don't reinvent.
---
## Review Checklist
- [ ] Every nullable field in ORDER BY has explicit NULLS FIRST/LAST
- [ ] Every ORDER BY has `Id` (or unique field) as final tiebreaker
- [ ] No OFFSET above ~500 records — switch to cursor pagination
- [ ] First-page cursor case handled (typically a no-WHERE first call)
- [ ] Tests cover mixed null/non-null data and tie-on-boundary pagination
- [ ] Ordering contract documented in selector/repository class
---
## Salesforce-Specific Gotchas
1. **Default null position flips with direction** — ASC defaults to NULLS FIRST; DESC defaults to NULLS LAST. Many bugs come from assuming a single default.
2. **OFFSET caps at 2,000** — Beyond that, SOQL throws. Cursor pagination is the only path for large result sets.
3. **Formula fields can be null even with non-null inputs** — A formula's null-handling (`BLANKVALUE`, `IF(ISBLANK(...))`) determines whether the formula emits null. Sorting on a formula needs the same NULLS clause discipline.
4. **Indexes don't include nulls by default** — Sorting on a high-null-percentage field can full-scan even when the field is custom-indexed. Custom indexes can be configured with null-inclusion via Support.
5. **`NULLS LAST` is a keyword pair, not a function** — `NULLS_LAST`, `NULLSLAST`, or `NULLS-LAST` are syntax errors.
---
## Output Artifacts
| Artifact | Description |
|---|---|
| Corrected ORDER BY clause | With NULLS FIRST/LAST and Id tiebreaker |
| Pagination cursor pattern | If pagination in scope, the cursor-based replacement |
| Test data factory | Apex factory producing mixed null/non-null records |
| Data-layer ordering contract | Documented in the selector class header |
---
## Related Skills
- data/soql-query-optimization — for the broader query plan and indexing concerns
- apex/soql-fundamentals — for foundational SOQL grammar
- apex/test-data-factory-patterns — for building the mixed null/non-null test fixturesRelated Skills
mfa-enforcement-patterns
Design MFA enforcement: auto-enablement, Salesforce Authenticator rollout, exceptions, service accounts, API-only users, SSO interop, and audit. Trigger keywords: MFA, multi-factor, two-factor, Salesforce Authenticator, MFA exception, MFA SSO, api-only MFA. Does NOT cover: end-user password policies, device-trust posture, or non-Salesforce IdP configuration.
encrypted-field-query-patterns
Design SOQL, filters, reporting, and indexes against Shield Platform Encryption fields. Trigger keywords: Shield Platform Encryption, encrypted field query, probabilistic vs deterministic encryption, encrypted SOQL filter, encrypted field index. Does NOT cover: Classic Encryption (deprecated), field-level security policy, or tenant secret key rotation.
apex-managed-sharing-patterns
Grant row-level access programmatically via __Share records when declarative sharing rules cannot express the policy. NOT for OWD, role hierarchy, or criteria-based sharing rule design.
omnistudio-testing-patterns
Use when testing or validating OmniStudio components — OmniScript preview, Integration Procedure step debugging, DataRaptor field-mapping validation, and end-to-end UTAM-based automation. NOT for Apex unit testing or standard Flow debugging.
omnistudio-error-handling-patterns
Use when designing fault behavior across Integration Procedures, DataRaptors, OmniScripts, and FlexCards — error routing, user-facing messaging, retry semantics, and idempotency. Triggers: 'omnistudio error', 'integration procedure fault', 'dataraptor error handling', 'omniscript retry', 'flexcard action failure'. NOT for general Apex exception design or Flow fault paths.
omnistudio-ci-cd-patterns
Use when designing or implementing CI/CD pipelines for OmniStudio components — DataPack export/import, versioning, environment promotion, and automated deployment. NOT for standard Salesforce metadata CI/CD or Apex-only pipelines.
omniscript-design-patterns
Use when designing or reviewing OmniScripts for guided experiences, step structure, branching, save/resume, and the boundary between OmniScript, Integration Procedures, DataRaptors, and custom LWCs. Triggers: 'omniscript design', 'too many steps in omniscript', 'save and resume omniscript', 'branching in omniscript', 'when should this be an integration procedure'. NOT for deep Integration Procedure or DataRaptor design when the guided interaction layer is not the main concern.
integration-procedure-cacheable-patterns
Use when designing Integration Procedures (IPs) with platform cache to cut latency and callout load. Covers cache key design, TTL selection, per-user vs org-wide partitions, invalidation on data changes, and safe fallback on cache miss/stale. Does NOT cover general IP authoring (see omnistudio-error-handling-patterns) or LWC client-side caching.
flexcard-design-patterns
Use when designing, building, or reviewing OmniStudio FlexCards — including data source selection, card states, actions, conditional visibility, flyout configuration, and child card iteration. Triggers: 'FlexCard', 'card template', 'flyout', 'card action', 'card state', 'data source', 'child card', 'conditional visibility'. NOT for OmniScript design, standalone LWC development, or Apex controller architecture outside the FlexCard context.
dataraptor-patterns
Use when designing or reviewing OmniStudio DataRaptors, especially Extract versus Turbo Extract versus Transform versus Load, field mapping strategy, performance tradeoffs, and when to move work into Integration Procedures or Apex. Triggers: 'DataRaptor Extract', 'Turbo Extract', 'DataRaptor Load', 'DataRaptor Transform', 'OmniStudio data mapping'. NOT for overall OmniScript journey design or Integration Procedure sequencing when the main question is not the DataRaptor shape itself.
wire-service-patterns
Use when designing or reviewing Lightning Web Components that use `@wire`, Lightning Data Service, UI API, or the GraphQL wire adapter, especially for reactive parameters, cache behavior, and refresh strategy. Triggers: 'wire service', 'refreshApex', 'reactive parameter', 'getRecord', 'wire vs imperative Apex'. NOT for component communication or generic lifecycle issues when data provisioning is not the main concern.
message-channel-patterns
Use when implementing Lightning Message Service (LMS) to enable cross-DOM communication between LWC, Aura, and Visualforce components on the same Lightning page, using message channels. Triggers: 'communicate between unrelated LWC components', 'send data between Visualforce and LWC', 'lightning message service not working', 'APPLICATION_SCOPE vs default scope', 'message channel metadata deployment'. NOT for parent-child component communication (use component-communication) or server-side events.