If you've spent any time with relational databases before stepping into Salesforce development, SOQL will feel familiar enough to be comfortable — and just different enough to trip you up in ways that aren't always obvious at first.
The surface-level resemblance is real. SOQL uses SELECT, FROM, WHERE, ORDER BY, and LIMIT. If you've written SQL queries before, your first SOQL query will probably look reasonable and even run correctly. Then you'll hit something unexpected — a relationship you can't join the way you're used to, a subquery that doesn't work the way SQL subqueries do, a governor limit error that SQL developers have never encountered — and you'll realize the two languages, despite appearances, operate on fundamentally different assumptions.
This post covers those differences systematically. Not just what SOQL can't do that SQL can, but why — and what the SOQL equivalents or workarounds look like in practice. Whether you're an experienced database developer moving into Salesforce or a Salesforce professional trying to sharpen your query skills, this is the comparison guide worth bookmarking.
What Is SOQL? The Foundation Before the Comparison
SOQL stands for Salesforce Object Query Language. It's a query language designed specifically for retrieving records from Salesforce's cloud database — the Force.com platform — and it's the primary way Apex code interacts with Salesforce data.
Like SQL, SOQL lets you retrieve specific fields from specific objects based on filter conditions. Unlike SQL, SOQL operates within the constraints of Salesforce's multi-tenant cloud architecture, which introduces both limitations and features that have no direct equivalent in traditional relational databases.
SOQL is used in:
- Apex classes and triggers to retrieve records programmatically
- Developer Console to query Salesforce data directly
- Salesforce CLI and workbench tools for data inspection
- Flow and other declarative tools (in the background, even when you don't see the query)
SOQL is read-only. It retrieves records — it does not insert, update, or delete them. That's handled separately via DML (Data Manipulation Language) statements in Apex. This is a fundamental structural difference from SQL, where SELECT, INSERT, UPDATE, and DELETE are all part of the same language.
Side-by-Side: SOQL vs SQL at a Glance
Before diving into specifics, here's a high-level comparison table that maps the conceptual territory:
| Feature | SQL | SOQL |
| Language purpose | Query, insert, update, delete | Query only (read-only) |
| Data source | Relational database tables | Salesforce objects |
| JOIN support | Full JOIN support (INNER, LEFT, RIGHT, FULL) | No traditional JOINs — uses relationship traversal |
| Subqueries | Flexible, nested subqueries | Limited, relationship-based subqueries only |
| Wildcard field selection | SELECT * supported | SELECT * NOT supported — must list fields explicitly |
| Aggregate functions | COUNT, SUM, AVG, MIN, MAX | COUNT, COUNT_DISTINCT, SUM, AVG, MIN, MAX |
| Governor limits | None (database-level limits only) | Strict: 100 queries/transaction, 50,000 records max |
| Full-text search | LIKE with wildcards | Handled by SOSL (separate language) |
| Date functions | Extensive built-in date functions | Date literals (THIS_WEEK, LAST_MONTH, etc.) |
| NULL handling | IS NULL / IS NOT NULL | #ERROR! |
| Schema | Tables with foreign keys | Objects with relationship fields |
Difference 1: SELECT * Does Not Exist in SOQL
In SQL, the wildcard select is one of the first things you learn:
sql
-- SQL: Works fine
SELECT * FROM Account;
In SOQL, this does not work. Full stop.
apex
// SOQL: This will throw an error
SELECT * FROM Account // INVALID
// SOQL: You must explicitly list every field you want
SELECT Id, Name, Industry, AnnualRevenue, OwnerId FROM Account
This isn't an oversight — it's a deliberate design choice tied to Salesforce's multi-tenant architecture and governor limits. Every field you retrieve consumes heap memory and counts toward query performance limits. Forcing explicit field declaration keeps queries efficient and prevents accidental over-retrieval of sensitive or unnecessary data.
Practical implication: Before writing SOQL in Apex, you need to know which fields you actually need. Get into the habit of querying only the fields your code will actually use — both for governor limit efficiency and for security reasons (you shouldn't retrieve sensitive field data your code doesn't need).
Difference 2: SOQL Uses Relationship Traversal, Not JOINs
This is the difference that catches SQL developers most off guard. Traditional SQL uses JOIN syntax to combine data from multiple tables:
sql
-- SQL JOIN: Standard approach
SELECT Account.Name, Contact.FirstName, Contact.LastName
FROM Contact
INNER JOIN Account ON Contact.AccountId = Account.Id
WHERE Account.Industry = 'Technology';
SOQL has no JOIN keyword. Instead, it uses relationship traversal — dot-notation to walk between related objects in a single query.
Parent-to-Child Relationship Queries (Subquery approach)
When you want data from a parent object and related child records:
apex
// SOQL: Get Accounts with their related Contacts
SELECT Id, Name,
(SELECT FirstName, LastName, Email FROM Contacts)
FROM Account
WHERE Industry = 'Technology'
Note: The inner query (SELECT ... FROM Contacts) uses the relationship name — not the object API name. For standard objects, this is often the plural form (Contacts, Opportunities). For custom objects, it's the relationship name defined on the field plus __r.
Child-to-Parent Relationship Queries (Dot-notation)
When you're querying child records and want fields from the parent:
apex
// SOQL: Get Contacts with their parent Account name
SELECT FirstName, LastName, Email, Account.Name, Account.Industry
FROM Contact
WHERE Account.Industry = 'Technology'
Here Account.Name traverses the relationship from Contact to its parent Account using dot-notation. You can traverse up to 5 levels of parent relationships in a single query.
What's Missing Compared to SQL JOINs?
- No LEFT JOIN equivalent for unrelated objects. If two Salesforce objects have no relationship field connecting them, you cannot query them together in SOQL. SQL can JOIN any two tables that share a common value; SOQL can only traverse defined Salesforce relationships.
- No FULL OUTER JOIN. SOQL subqueries in parent-to-child direction return null for parents with no children (similar to LEFT JOIN behavior), but there's no equivalent of FULL OUTER JOIN.
- No cross-object aggregation without relationships. If you need to aggregate data across objects that aren't related in Salesforce, you'd need to make separate SOQL queries and handle the join logic in Apex.
Difference 3: Governor Limits Are a Hard Reality in SOQL
This is perhaps the most operationally significant difference between SOQL and SQL — and it's one that SQL developers almost always underestimate when they first start working in Salesforce.
In a traditional database, query limits are soft — the database might slow down if you query too much, but it rarely throws an exception that crashes your application. In Salesforce, governor limits are hard runtime exceptions that terminate your transaction immediately when breached.
Key SOQL governor limits per synchronous Apex transaction:
| Limit | Cap |
| Total SOQL queries | 100 |
| Total records retrieved by SOQL | 50,000 |
| Total records retrieved by a single Database.getQueryLocator | 10,000 (50 million in Batch Apex) |
| Total heap size | 6 MB |
These limits exist because Salesforce is a multi-tenant platform — every org shares the same infrastructure. If one tenant's runaway query consumed unlimited resources, every other org on the same server would suffer. Governor limits are the architectural solution to that problem.
What this means for SOQL in practice:
You can never write SOQL inside a loop. This is the cardinal rule of Salesforce development, and it's the most common mistake SQL developers make when they transition to Apex:
apex
// WRONG — SOQL inside a loop: will hit 101 SOQL limit error
for (Account acc : accountList) {
List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :acc.Id];
// Do something
}
// RIGHT — Single SOQL query with IN clause, process in Apex
Set<Id> accountIds = new Map<Id, Account>(accountList).keySet();
List<Contact> contacts = [SELECT Id, AccountId FROM Contact
WHERE AccountId IN :accountIds];
Map<Id, List<Contact>> contactsByAccount = new Map<Id, List<Contact>>();
for (Contact c : contacts) {
if (!contactsByAccount.containsKey(c.AccountId)) {
contactsByAccount.put(c.AccountId, new List<Contact>());
}
contactsByAccount.get(c.AccountId).add(c);
}
The IN clause with a set of Ids replaces what a SQL developer would do with a JOIN — one query retrieves all related records at once, and the filtering is done in Apex memory.
Difference 4: SOQL Date Handling Is Completely Different
SQL databases typically have rich date function libraries:
sql
-- SQL: Date functions
SELECT * FROM Opportunity
WHERE YEAR(CloseDate) = 2026
AND MONTH(CloseDate) = 6;
SELECT DATEDIFF(day, CreatedDate, CloseDate) AS DaysToClose
FROM Opportunity;
SOQL takes a different approach — it uses date literals rather than date functions:
apex
// SOQL date literals
SELECT Id, Name, CloseDate FROM Opportunity
WHERE CloseDate = THIS_YEAR;
SELECT Id, Name, CloseDate FROM Opportunity
WHERE CloseDate > LAST_N_DAYS:30;
SELECT Id, Name, CloseDate FROM Opportunity
WHERE CloseDate = THIS_MONTH;
Common SOQL date literals:
| Date Literal | What It Means |
| TODAY | Current day |
| YESTERDAY | Previous day |
| THIS_WEEK | Current week (Sunday to Saturday) |
| LAST_WEEK | Previous week |
| THIS_MONTH | Current calendar month |
| LAST_MONTH | Previous calendar month |
| THIS_QUARTER | Current fiscal quarter |
| THIS_YEAR | Current calendar year |
| LAST_N_DAYS:n | Last n days (e.g., LAST_N_DAYS:90) |
| NEXT_N_DAYS:n | Next n days |
| LAST_N_MONTHS:n | Last n calendar months |
What's not available:
- You cannot compute date differences within SOQL (no DATEDIFF equivalent)
- You cannot extract date parts (no YEAR(), MONTH(), DAY() functions)
- Complex date arithmetic must be handled in Apex after the query returns results
Difference 5: Aggregate Functions Work, But With Restrictions
SQL's aggregate functions are flexible and can be used in complex nested scenarios:
sql
-- SQL aggregation
SELECT AccountId, COUNT(*) AS ContactCount, AVG(AnnualRevenue) AS AvgRevenue
FROM Contact
GROUP BY AccountId
HAVING COUNT(*) > 5
ORDER BY ContactCount DESC;
SOQL supports aggregate functions but with important constraints:
apex
// SOQL aggregation
SELECT AccountId, COUNT(Id) ContactCount
FROM Contact
GROUP BY AccountId
HAVING COUNT(Id) > 5
ORDER BY COUNT(Id) DESC
LIMIT 200
SOQL aggregate functions available:
- COUNT() — counts non-null values
- COUNT_DISTINCT() — counts distinct non-null values (no direct SQL equivalent needed — SQL uses COUNT(DISTINCT column))
- SUM(fieldName) — sum of numeric field
- AVG(fieldName) — average of numeric field
- MIN(fieldName) — minimum value
- MAX(fieldName) — maximum value
Key restrictions SQL developers need to know:
Aggregate queries don't return sObjects. In standard SOQL, queries return List<SObject> or typed lists like List<Account>. Aggregate queries return List<AggregateResult> — a different type with no strongly-typed field access. You access values using the get() method:
apex
List<AggregateResult> results = [
SELECT AccountId, COUNT(Id) cnt
FROM Contact
GROUP BY AccountId
];
for (AggregateResult ar : results) {
Id accountId = (Id) ar.get('AccountId');
Integer contactCount = (Integer) ar.get('cnt');
}
You cannot mix aggregate and non-aggregate fields freely. In SQL you can sometimes get away with selecting non-aggregated fields without grouping them (depending on your DB). In SOQL, every non-aggregate field in a SELECT with aggregate functions must appear in the GROUP BY clause — no exceptions.
Difference 6: Subqueries in SOQL Are Relationship-Bound
SQL subqueries can be used in many flexible ways — correlated subqueries, subqueries in SELECT, WHERE, or FROM clauses, multi-level nesting:
sql
-- SQL: Flexible subquery options
SELECT Name FROM Account
WHERE Id IN (
SELECT AccountId FROM Opportunity
WHERE Amount > 100000
AND StageName = 'Closed Won'
);
-- SQL: Subquery in SELECT
SELECT Name,
(SELECT COUNT(*) FROM Contact WHERE AccountId = Account.Id) AS ContactCount
FROM Account;
SOQL subqueries work but with significant restrictions:
In WHERE clause — similar to SQL but object-bound:
apex
// SOQL: Subquery in WHERE — works for related objects
SELECT Name FROM Account
WHERE Id IN (
SELECT AccountId FROM Opportunity
WHERE Amount > 100000
AND StageName = 'Closed Won'
);
This works — but the subquery must be on a child object of the outer query object, and must return Ids of the outer object.
In SELECT clause — only for child relationships:
apex
// SOQL: Subquery in SELECT — only parent-to-child
SELECT Name,
(SELECT FirstName, LastName FROM Contacts),
(SELECT StageName, Amount FROM Opportunities)
FROM Account
WHERE Industry = 'Technology'
What's NOT possible in SOQL subqueries:
- Subqueries in the FROM clause (no derived tables)
- More than one level of nesting in SELECT subqueries
- Subqueries involving unrelated objects
- Aggregate functions within WHERE subqueries
Difference 7: NULL Handling Syntax Differs
A small but practically important difference:
sql
-- SQL NULL syntax
SELECT * FROM Contact WHERE Email IS NULL;
SELECT * FROM Contact WHERE Email IS NOT NULL;
apex
// SOQL NULL syntax
SELECT Id, FirstName, LastName FROM Contact WHERE Email = null;
SELECT Id, FirstName, LastName FROM Contact WHERE Email != null;
SOQL uses = null and != null rather than IS NULL and IS NOT NULL. This trips up SQL developers frequently, especially since IS NULL is so deeply ingrained. Both syntaxes look plausible, but only = null works in SOQL.
Difference 8: SOQL Has No UPDATE, INSERT, or DELETE
This cannot be overstated because it genuinely surprises SQL developers who think of SQL as a complete database language:
In SQL:
sql
-- SQL handles all CRUD operations
SELECT * FROM Account WHERE Name = 'Test Corp';
INSERT INTO Account (Name, Industry) VALUES ('New Corp', 'Technology');
UPDATE Account SET AnnualRevenue = 5000000 WHERE Name = 'Test Corp';
DELETE FROM Account WHERE Name = 'Old Corp';
In Salesforce, these are completely separate concerns:
apex
// SOQL handles only SELECT
List<Account> accounts = [SELECT Id, Name FROM Account WHERE Name = 'Test Corp'];
// DML handles insert, update, delete — completely separate from SOQL
Account newAcc = new Account(Name = 'New Corp', Industry = 'Technology');
insert newAcc;
Account updateAcc = accounts[0];
updateAcc.AnnualRevenue = 5000000;
update updateAcc;
delete accounts;
DML operations have their own governor limits (150 DML statements per transaction, 10,000 records per DML statement) that are tracked separately from SOQL limits. SOQL and DML together consume from a shared transaction budget — understanding both sets of limits is essential for writing production-quality Apex.
Difference 9: SOQL Has No Full-Text Search — That's SOSL's Job
In SQL, you can do basic text searching within queries:
sql
-- SQL: Text search with LIKE
SELECT * FROM Contact WHERE LastName LIKE '%Smith%';
SELECT * FROM Product WHERE Description LIKE '%wireless%';
SOQL supports LIKE with wildcards for simple pattern matching:
apex
// SOQL: LIKE works for simple patterns
SELECT Id, LastName FROM Contact WHERE LastName LIKE '%Smith%';
But for cross-object full-text search — finding records matching a keyword across multiple objects simultaneously — SQL databases typically use full-text index features or separate search infrastructure. In Salesforce, this is handled by a completely separate language: SOSL (Salesforce Object Search Language).
apex
// SOSL: Cross-object full-text search
List<List<SObject>> results = [
FIND 'Cloud Intellect' IN ALL FIELDS
RETURNING Account(Name, Industry),
Contact(FirstName, LastName, Email),
Lead(FirstName, LastName, Company)
];
SOSL searches Salesforce's search index (similar to a search engine index) rather than querying the database directly, which makes it efficient for keyword-based search across multiple objects but less precise than SOQL for structured data retrieval.
When to use which:
- SOQL: You know exactly which object(s) you're querying and what conditions to filter on — structured, precise retrieval
- SOSL: You're searching for a keyword or phrase across multiple objects — keyword-based, cross-object search
SOQL Features That Have No Direct SQL Equivalent
It's not all restrictions — SOQL has some features that SQL databases don't natively offer:
Binding Apex Variables Directly in Queries
apex
// SOQL: Bind Apex variables with : notation
String industryFilter = 'Technology';
Decimal revenueThreshold = 1000000;
List<Account> accounts = [
SELECT Id, Name, AnnualRevenue
FROM Account
WHERE Industry = :industryFilter
AND AnnualRevenue > :revenueThreshold
];
The colon (:) before a variable name binds it directly into the query. This is cleaner and safer than SQL string concatenation (which creates SQL injection vulnerabilities) — SOQL binding is always treated as a value, never as executable query syntax.
Semi-Join and Anti-Join Shorthand
apex
// SOQL Semi-join: Accounts that HAVE related Won Opportunities
SELECT Id, Name FROM Account
WHERE Id IN (SELECT AccountId FROM Opportunity WHERE IsWon = true);
// SOQL Anti-join: Accounts that DO NOT have any related Opportunities
SELECT Id, Name FROM Account
WHERE Id NOT IN (SELECT AccountId FROM Opportunity WHERE AccountId != null);
These are structurally similar to SQL subqueries but are called semi-joins and anti-joins in SOQL documentation — and they're particularly useful for data cleanup and targeted batch operations.
FOR UPDATE — Record Locking
apex
// SOQL: Lock records during retrieval to prevent concurrent updates
List<Account> accounts = [SELECT Id, Name FROM Account
WHERE Industry = 'Technology'
FOR UPDATE];
FOR UPDATE locks the retrieved records for the duration of the transaction, preventing other transactions from modifying them simultaneously. This is useful in scenarios where you need to read and then update records without the risk of a race condition with another concurrent process.
SOQL Best Practices for Developers Coming From SQL
Based on everything covered above, here's a practical guide for SQL-experienced developers writing production SOQL:
1. Always query only the fields you need. No SELECT *, explicit field lists always. Not just a best practice — it affects heap memory usage against governor limits.
2. Never write SOQL inside loops. Move all queries outside loops, use IN clauses with collections of Ids, and process results in memory. This is the single most important performance rule in Salesforce development.
3. Use relationship traversal instead of trying to JOIN. Learn the relationship names for objects you work with — both the standard Salesforce ones and any custom relationships in your org.
4. Use variable binding (colon notation) consistently. It's safer than string manipulation and cleaner to read.
5. Filter early and filter specifically. SOQL queries benefit from selective WHERE clauses that hit indexed fields. AccountId, OwnerId, CreatedDate, and custom fields marked as External Id are indexed by default. Queries on non-indexed fields with large object record counts can time out.
6. Understand your aggregate result type. Aggregate queries return AggregateResult, not typed sObjects. Know the syntax for accessing aggregate result values.
7. Keep an eye on the 50,000 record limit. If your query could plausibly return more than 50,000 records in a production org, you need Batch Apex with Database.getQueryLocator — not a standard inline SOQL query.
A Developer's Perspective: What Clicked When I Learned SOQL Properly
"I came into Salesforce development with about three years of SQL experience from a previous role. I genuinely thought SOQL would be a quick afternoon of reading. It was not.
The syntax looked familiar, which was the problem — it lulled me into overconfidence. My first real SOQL issue was exactly what you'd expect: I wrote a trigger that ran a query inside a for loop, tested it on three records in my sandbox, and it worked perfectly. Then in training, the instructor pointed out why that would fail the moment it processed more than 100 records in production. That was my first real 'oh' moment with SOQL.
I learned Salesforce development at Cloud Intellect, and what genuinely helped me bridge the SQL-to-SOQL gap was the way the trainer structured the technical sessions. Rather than just listing SOQL syntax, they'd show a SQL query first, then ask us to figure out how to rewrite it in SOQL — and explain what architectural choices in Salesforce made those differences necessary. Understanding the 'why' behind SOQL's constraints made them logical rather than arbitrary.
The governor limits section specifically was something I didn't fully appreciate from Trailhead alone. The trainer ran live exercises where we deliberately triggered governor limit errors in a developer sandbox — not to be dramatic, but to see exactly what breaks, what the error messages look like, and how to restructure the code to avoid it. That kind of deliberate breaking-and-fixing is something you can only do properly with an instructor who's done it before and knows how to debrief you on what just happened.
If you're a developer based in Pune looking to transition into Salesforce, the salesforce training institute in pune program at Cloud Intellect handles technical content at the right level of depth — they don't oversimplify SOQL for developers who already have programming backgrounds. The sessions expect you to think like a developer, not like a complete beginner, and that distinction matters a lot when you're covering something like query optimisation or governor limit architecture.
There's also a strong batch in salesforce classes in pune where they regularly bring in working Salesforce developers to do code review sessions on student-written SOQL and Apex. Having actual production developers look at your queries and point out efficiency issues — before you're in a job where those issues have real consequences — is genuinely valuable.
And for people in Nagpur considering their options, a colleague who completed the salesforce course nagpur with placement at Cloud Intellect said the technical depth on Apex and SOQL was comparable to what I experienced in Pune. He specifically mentioned that the instructor made them write and debug SOQL queries from scratch in every session rather than copy-pasting from examples, which forced a level of genuine understanding that you don't get from watching demos."
Quick FAQ: Salesforce SOQL vs SQL
Can I use SQL syntax directly in Salesforce?
No. Salesforce uses SOQL (and SOSL for search), not standard SQL. While the syntax has similarities, SQL queries will not work in Apex or Salesforce tooling. You need to learn SOQL-specific syntax, relationship traversal, and governor limit constraints.
Does SOQL support JOINs?
Not in the SQL sense. SOQL uses relationship traversal — dot-notation for parent fields and subqueries in SELECT for child records — instead of JOIN keywords. You can only traverse defined Salesforce object relationships; you cannot join unrelated objects.
What happens if my SOQL query returns more than 50,000 records?
Salesforce throws a System.LimitException: Too many query rows: 50001 error. For large-volume data processing, use Batch Apex with Database.getQueryLocator, which can handle up to 50 million records by processing them in chunks.
Can I use SELECT * in SOQL?
No. SOQL requires you to explicitly list every field you want to retrieve. There is no wildcard field selector.
What is the difference between SOQL and SOSL?
SOQL is a structured query language for retrieving records from specific Salesforce objects based on filter conditions — precise and structured. SOSL is a full-text search language that searches across multiple objects simultaneously using a keyword index — broader and cross-object. SOSL cannot be used in Apex Triggers; SOQL can.
Are SOQL queries safe from injection attacks?
When using variable binding (the :variableName syntax), yes — bound variables are always treated as values, never as executable query syntax, making SOQL injection impossible with properly written queries. Avoid dynamic SOQL (String-concatenated queries) unless absolutely necessary, and use String.escapeSingleQuotes() when you must.
What is a SOQL semi-join?
A semi-join is a SOQL WHERE clause that uses IN (SELECT ...) to filter records based on the existence of related records. For example: WHERE Id IN (SELECT AccountId FROM Opportunity WHERE IsWon = true) retrieves only Accounts that have at least one won Opportunity. The anti-join uses NOT IN for the inverse.
Final Thoughts: SOQL Is SQL's Cloud-Native Cousin
SOQL and SQL solve the same fundamental problem — retrieving structured data based on conditions — but they solve it in architecturally different environments. SQL assumes a privately owned, flexible relational database where you have full control. SOQL assumes a shared, multi-tenant cloud platform where resource fairness is enforced at the language level.
Once you internalize that context, SOQL's differences from SQL stop feeling like limitations and start feeling like logical consequences of the platform it lives on. Governor limits make sense when you understand multi-tenancy. Relationship traversal makes sense when you understand Salesforce's object model. The absence of SELECT * makes sense when you understand heap memory constraints.
The practical journey from SQL to SOQL is genuinely achievable — especially with structured guidance that explains not just the syntax but the reasoning. Query optimization, governor limit management, and proper relationship traversal are learnable skills that become second nature with enough hands-on practice.
If you're making this transition and want to build genuinely solid Salesforce development foundations — not just surface familiarity with SOQL syntax — structured training that covers both the technical depth and the practical application is the most reliable path. The difference between a developer who understands SOQL properly and one who knows just enough to get by shows up quickly in code reviews, in production bugs, and in technical interviews.
Sign in to leave a comment.