When Salesforce Uses an Index
Cũng có bằng 19 ngôn ngữ khác
English · Español · 日本語 · Português (Brasil) · Français · Deutsch · Italiano · 한국어 · 简体中文 · Português (Portugal) · 繁體中文 · Bahasa Indonesia · Türkçe · Polski · ไทย · Українська · Español (México) · Français (Canada) · Deutsch (Schweiz)

Module 3 of 5 in the chapter Query and Retrieval Architecture, chapter 8 of the Data Architect course. Each module stands on its own. ⟲ Trước: Salesforce tìm dữ liệu của bạn thế nào · Tiếp theo: Chỉ mục, skinny table và thiết kế lại
The question this module answers: When can Salesforce use an index? ⟲
In the last module we built the card catalog: what an index is, the cost of every copy on every save, and which fields Salesforce indexes for you, which ones you can ask for, and which ones can’t have an index at all. So now we have an index. Can it be used in all circumstances? No. Let me tell you a story first. ⟲
The Nitro Boost
Phần tiêu đề “The Nitro Boost”Picture a street race. Two cars side by side, engines roaring. Near the end, one driver presses a button and the car jumps forward and wins. That button is the nitro: a boost of extra fuel that gives the engine a burst of speed for a few seconds. ⟲
But the nitro doesn’t fire just because you press the button. A nitro system is usually built so it only fires when three things are true at once: ⟲
- The system is installed and switched on. No nitro in the car, no boost. ⟲
- The driver has the pedal all the way down. Press it halfway, and the system stays off. ⟲
- The engine is in the right speed range. Too slow or too fast, and the system won’t fire. ⟲
All three, and the car jumps ahead. Miss any one, and you’re just driving. ⟲
An index is your query’s nitro. And it works the same way: three things have to be true before Salesforce uses it. ⟲
| The car | Your query |
|---|---|
| The nitro is installed and switched on | There’s an index on the field you filter on |
| The pedal is all the way down | You ask in a selective way: equals, starts with |
| The engine is in the right range | Not too many records match: you’re under the threshold |
This module takes them one at a time. Let’s start with the first. ⟲
Is There an Index?
Phần tiêu đề “Is There an Index?”Let’s start with the question the catalog was built for. Walk up to the desk and ask for Plumbing Basics. The librarian goes to the drawer of cards by title, finds the card, reads the shelf (B4), walks over and takes the book down. One card, one book. In a library of 10 million books, that’s one card and one book instead of 10 million books. That’s what an index is for, and it’s what Salesforce does when you filter on an indexed field: find the entry, read which record it points to, go straight there. ⟲
Now ask a different question: “Can I have every book from Harbor Press?” We have drawers by title, by subject and by author. Is there a drawer by publisher? No. So what does the librarian do? Walks every aisle and opens every book. That’s a table scan, and the three drawers we paid for didn’t help at all. ⟲
In Salesforce it’s the same. Filter on a field that isn’t indexed, and the indexes on the other fields can’t help that filter. That’s why the last module’s tables matter: before you worry about anything else, check whether the field you filter on has an index, standard or custom. That’s the first of the three: is the nitro installed? ⟲
By title, there’s a drawer: find the card, get the book. Fast. ⟲
By publisher, there’s no drawer: no match in the index. Go around it and open every book: a table scan. Slow. ⟲
Asking the Index the Right Way
Phần tiêu đề “Asking the Index the Right Way”Now say there is a drawer. The title drawer, sorted A to Z. Which questions can it answer quickly? ⟲
- Find the book titled Plumbing Basics. Go to P, find the card. Fast. ⟲
- Find every book whose title starts with “Plumbing”. Go to P, read the cards that start with “Plumbing”, stop when they don’t. Fast. ⟲
- Find these three titles. Three trips to the drawer. Fast. ⟲
- And with a drawer of cards by date, find every book added this year: go to the first card for this year and read until the year ends. Fast. ⟲
Each of those questions tells the librarian where to start in the drawer. Now some questions the same drawer can’t answer that way: ⟲
- Find every book whose title contains “Plumbing”. “Advanced Plumbing” is filed under A. “Home Plumbing Guide” is filed under H. The drawer is sorted by how a title starts, so there’s no place in it to start looking. The drawer can’t answer, and the librarian is back to walking every shelf. ⟲
- Find every book whose title ends with “Basics”. Same problem: the end of a title tells you nothing about where it’s filed. ⟲
- Find every book that isn’t Plumbing Basics. Nearly every card matches. The drawer narrows nothing. ⟲
- Find every book with no title. In Salesforce’s drawers, there’s no card for an empty value unless someone asked for one (we saw that in the last module). ⟲
None of these tells the librarian where to start. In class I say it this way: indexes work from left to right. Ask for the start of the value and the index can help. Ask for the middle, the end, or everything except one value, and it can’t: no match in the index, and Salesforce falls back to a table scan. ⟲
Starts with: straight to P. One card, one book. Fast. ⟲
Contains: no way into the drawer, no match in the index. Go around it and open every book: a table scan. Slow. ⟲
Here’s the same list in SOQL: ⟲
| Ask it this way | In SOQL | Can the index help? |
|---|---|---|
| Equals | Name = 'Plumbing Basics' | Yes |
| Starts with | Name LIKE 'Plumbing%' | Yes |
| One of a short list | Id IN ('a01…', 'a02…', 'a03…') | Yes |
| A range of dates or numbers | CreatedDate = THIS_YEAR, Amount > 10000 | Yes |
| Contains | Name LIKE '%Plumbing%' | No |
| Ends with | Name LIKE '%Basics' | No |
| Not equal, not like, not in | Name != 'Plumbing Basics' | No |
| Empty | Region__c = null, Name != '' | No, by default |
That’s not just my rule of thumb. Salesforce lists the cases where a custom index typically isn’t used, including “The filter operator is a negative operator such as NOT EQUAL TO (or !=), NOT CONTAINS, and NOT STARTS WITH” and “You’re comparing with an empty value (Name != ”)” (Working with Very Large SOQL Queries (mở trong thẻ mới), Apex Developer Guide, Winter ‘27). And: “Custom indexes are never used for the following operations: NOT EQUAL TO comparisons, null value comparisons (e.g., Name = ”), leading wildcard searches (e.g., Name LIKE ‘%Smith’)” (Developer Console Query Plan Tool FAQ (mở trong thẻ mới), Help, version 5). ⟲
And if what you really need is to find a word anywhere in the text, remember the two ways to ask from the last module: that’s what FIND and the search engine are built for. Module 5 covers it. ⟲
The Word for It: Selective
Phần tiêu đề “The Word for It: Selective”So there are two things to check before an index can speed up your query. One: is there an index on the field? Two: are you asking for the data in a way the index can use? Equals, starts with, a short list, a range of dates or numbers: yes. Contains, ends with, not equal, empty: no. ⟲
When you ask the second way, I say you’re asking in a selective manner. That’s the word to learn: selective. A selective filter is one Salesforce can answer through an index instead of walking every record. Reports, list views and SOQL all care about it. ⟲
Salesforce’s own definition has one more part, and it’s the reason this module isn’t over yet: “A query is selective when one of the query filters is on an indexed field and the query filter reduces the resulting number of rows below a system-defined threshold” (Working with Very Large SOQL Queries (mở trong thẻ mới), Winter ‘27). An index, asked the right way, and not too many records. That’s the second of the three, the pedal all the way down. The third check, the engine in the right range, is where people get into trouble, and it comes next. ⟲
Test knowledge. A filter can use an index only if the field is indexed and you ask in a selective way. Selective: equals, starts with (
LIKE 'abc%'), a shortINlist, a range on a date or number. Not selective: contains and ends with (a leading%), negative operators (!=,NOT LIKE,NOT IN), and comparisons with an empty value. ⟲
A note on encrypted fields. If a field is encrypted with Shield Platform Encryption, check before you build an important filter on it. Salesforce’s own Well-Architected pages don’t agree today. Trust (mở trong thẻ mới) says probabilistic encryption can’t be used to filter, sort or aggregate, and that deterministic encryption allows exact-match filters. Data Protection Patterns (mở trong thẻ mới) says probabilistic encryption “supports all query operations”. Read both (checked 2026-10-21) and test the filter in your own org. ⟲
Everybody Lives in Arizona: When an Index Won’t Help
Phần tiêu đề “Everybody Lives in Arizona: When an Index Won’t Help”So you have an index, and you’re asking it the right way: equals, nothing fancy. Is that enough? Not always. Here’s the example I use in class. ⟲
Say we have the addresses of 10 million people from all over the United States, and the state field is indexed. Ask for everyone in Arizona: State = 'AZ'. Arizona is a small slice of the whole country, so the index narrows 10 million records down to a small group. Go to the drawer, pull the Arizona cards, get those records. Fast. That’s the index doing its job. ⟲
Now change the data, not the query. Same field, same index, same State = 'AZ'. But this time everybody lives in Arizona. Every one of the 10 million records matches. The index doesn’t narrow anything. Going to the index first, then fetching 10 million records one card at a time, is actually slower than just reading the whole table. The index isn’t broken. It just isn’t useful for this question. ⟲
And here’s the key: it depends on what you’re looking for. Keep the all-Arizona data and add one person in Texas. Ask for State = 'TX': one match. The index takes you straight to that one record. Fast. Ask for State = 'AZ' on the same data: almost everything matches. Slow. Same field, same index, same data. The only difference is how many records match what you asked for. When a large number of records match what you’re looking for, the index stops helping. ⟲
Watch for this with fields like case status. Status = 'Closed' can be a good filter when you launch, and stop being one as closed cases pile up year after year. We’ll come back to that in The Closed-Cases Trap. ⟲
People from all over the US: Arizona is a small slice. Fast. ⟲
Everybody lives in Arizona: every card matches. Reading the table is faster. Slow. ⟲
The same data, one person in Texas: one card. Fast. What you’re looking for decides. ⟲
Why Too Many Matches Is Slower
Phần tiêu đề “Why Too Many Matches Is Slower”Think about the library again. You want one book: find the card, walk to the shelf, done. Now you want 9 million of the 10 million books, and you insist on using the card catalog. Find a card, walk to the shelf, come back to the drawer, next card, walk to another shelf, come back… nine million times. It would be faster to start at the first aisle and walk the shelves once, in order. ⟲
Salesforce builds that limit into the index itself: “The index table places upper limits on the number of records that an indexed search can effectively return” (Indexes (mở trong thẻ mới), Winter ‘27). So before using an index, Salesforce counts: how many records would this filter match? If the answer is a small part of the object, the index is worth it. If the matches rise above a certain percentage of the records, Salesforce skips the index and reads the table. ⟲
That percentage has a name: the selectivity threshold. Remember Salesforce’s definition of a selective query: an indexed field, asked the right way, and a filter that “reduces the resulting number of rows below a system-defined threshold”. This is that threshold, and it’s the third check. Let’s look at the numbers. ⟲
Các ngưỡng
Phần tiêu đề “Các ngưỡng ”Why does the threshold matter so much? Salesforce’s Well-Architected guidance doesn’t soften it: “Query selectivity is the single largest lever on whether a solution scales to millions of records or times out at hundreds of thousands.” And: “Selectivity is capacity you don’t have to buy” (Resource and Cost Optimization (mở trong thẻ mới)). An efficient query leaves room for every other tenant in the building. ⟲
Salesforce sets the line as a percentage of the records, and the percentage depends on the kind of index: one rule for a standard index, a lower one for a custom index (Make Salesforce Platform SOQL Query Selective (mở trong thẻ mới), Help, version 4). Let’s start with the simple case and build up. ⟲
Start Simple: a Standard Index
Phần tiêu đề “Start Simple: a Standard Index”Take the Name field. It has a standard index. Say we have 100,000 accounts. For a standard index, the line is 30% of the records: 30,000. ⟲
Now ask for Name LIKE 'Steve%'. Steve’s Plumbing, Steve’s Garage, a few others: say 2,000 accounts. 2,000 is under 30,000, so the filter is selective and Salesforce uses the index. Fast. ⟲
Same field, same index, same 100,000 accounts. Now ask for Name LIKE 'S%', every account that starts with S. Say that’s 40,000. 40,000 is over the line. Not selective: Salesforce skips the index and reads the table. ⟲
Same index, same records. Under the line, the index is used. Over it, it isn’t. ⟲
Both filters ask in a selective way, a “starts with”. Both use an indexed field. The only difference is how many records match. ⟲
As the Object Grows
Phần tiêu đề “As the Object Grows”So the line is 30%? For the first million records, yes. After that, Salesforce counts only 15% of the records beyond the first million, and the line never goes above 1 million. In Salesforce’s words, the standard threshold “maxes out at 1 million total targeted records, which you could reach only if you had more than 5.6 million total records” (same source). ⟲
- 500,000 records: 30% of them, 150,000. ⟲
- 1 million records: 30% of them, 300,000. ⟲
- 2 million records: 300,000 for the first million, plus 15% of the second million (150,000): 450,000. ⟲
- 6 million records: the math would give 1,050,000, but the cap is 1 million. That’s the top end. ⟲
The line grows with the object, more slowly after the first million, and stops at 1 million. ⟲
Look at what happens to the share. At 1 million records, a filter can match 30% and still be selective. At 2 million, 22.5%. At 6 million, under 17%. The bigger the object, the more a filter has to narrow it down. Salesforce’s Well-Architected guidance puts it the same way: the thresholds “tighten as an object grows past its first million records” (Resource and Cost Optimization (mở trong thẻ mới)). ⟲
Custom Indexes: a Lower Line
Phần tiêu đề “Custom Indexes: a Lower Line”Now a custom index. Back to State = 'AZ': State isn’t a standard index, so it needs a custom one. A custom index gets a lower line: 10% of the first million records, plus 5% of the records beyond it, never more than 333,333 (same source). ⟲
- 100,000 records: 10% of them, 10,000. ⟲
- 1 million records: 10% of them, 100,000. ⟲
- 6 million records: the math would give 350,000, but the cap is 333,333. The top end. ⟲
A custom index: a third of the standard line, and a cap of 333,333. ⟲
So on a million contacts, State = 'AZ' is selective only if fewer than 100,000 of them live in Arizona. And on 10 million contacts, the line is still 333,333, about 3.3%. That’s the part people forget: eventually the thresholds stop growing. That’s why large-volume design isn’t “put an index on it.” The question is what will the distribution look like at scale? ⟲
Test knowledge. Know the two thresholds. Standard index: 30% of the first million records, plus 15% of the rest, up to 1 million. Custom index: 10% of the first million, plus 5% of the rest, up to 333,333. Be ready to work one out: a standard index on 2 million records allows 450,000; a custom index on 1 million allows 100,000. ⟲
Cái bẫy trường hợp đã đóng
Phần tiêu đề “Cái bẫy trường hợp đã đóng ”Một trong những ví dụ tôi thích nhất, vì truy vấn chạy hoàn hảo lúc ra mắt. Một bộ lọc IsClosed = true. Ban đầu: 100.000 trường hợp, 20.000 đã đóng. Tuyệt. Thời gian trôi qua, và các trường hợp có một thói quen thú vị: chúng được đóng lại. Giờ có 10 triệu trường hợp, 9 triệu đã đóng. Không có gì trong truy vấn, trường hay code thay đổi. Phân bố đã thay đổi. Bộ lọc từng thu hẹp tập dữ liệu giờ mô tả gần như toàn bộ nó. Nó đã chuyển từ chọn lọc sang không chọn lọc vì dữ liệu đã già đi. Một truy vấn có thể trở nên tệ mà bản thân truy vấn không hề thay đổi.
Thêm tăng trưởng 30%
Phần tiêu đề “Thêm tăng trưởng 30% ”Vì vậy tôi kết hợp tính chọn lọc với Số chữ số 0 và tăng trưởng. Nếu khách hàng không có dự báo, tôi dùng 30% mỗi năm làm mốc lập kế hoạch, không phải vì doanh nghiệp nào cũng tăng 30%, mà vì nó cho chúng ta một thứ để thiết kế dựa vào thay vì giả vờ khối lượng hôm nay là vĩnh viễn. Bắt đầu ở 1,5 triệu bản ghi: 1,95 triệu ở năm thứ hai, 2,54 ở năm thứ ba, 3,30 ở năm thứ tư, 4,28 ở năm thứ năm, 5,57 ở năm thứ sáu. Ở khoảng 5,6 triệu bản ghi, ngưỡng tùy chỉnh chưa tính trần là 100.000 cộng 5% của 4,6 triệu, 330.000: về cơ bản đã chạm mức trần 333.333. Từ đó dữ liệu tiếp tục tăng còn ngưỡng tùy chỉnh thực tế thì không. Hãy làm phép tính đó trước khi production dạy nó cho bạn.
Today’s selective filter can become tomorrow’s production problem. ⟲
Two different 30%s. Don’t mix up this 30% with another 30% you’ll meet in Salesforce’s Well-Architected guidance. Reliability (mở trong thẻ mới) says: “Build margin by targeting 70% of governor limits as the operational ceiling under normal conditions, reserving 30% for unexpected spikes.” That 30% is room to spare under the governor limits of a transaction. The 30% here is how fast the data grows each year. Different numbers for different things. And notice how Salesforce describes the 70% figure on another page: “a widely cited community best practice, not an official Salesforce specification” (The Agentic Enterprise (mở trong thẻ mới)). Treat it as good advice, not a rule (pages checked 2026-10-22). And remember: when your main records grow by 30%, the records under them can grow faster. Each new account brings orders, each order brings line items, and every one of them brings activities and history. Child objects and transaction data can multiply the growth, and you can’t always tell which one will grow toward a Salesforce limit first, or past it. So do the math for the children, not only the parents. ⟲
Steve Trucking: Truy vấn hôm nay, năm năm sau
Phần tiêu đề “Steve Trucking: Truy vấn hôm nay, năm năm sau ”Ra mắt với một triệu chuyến giao hàng; điều phối viên thường lọc theo Status = 'Scheduled', khoảng 100.000 bản ghi, 10%. Hợp lý. Năm năm sau có khoảng 9 triệu lệnh công việc tích lũy, nhưng số đang được lên lịch tại bất kỳ thời điểm nào phụ thuộc vào khung vận hành, nên nó vẫn nhỏ. Bộ lọc đó thậm chí có thể trở nên chọn lọc hơn khi lịch sử tăng lên. Giờ đến Status = 'Completed': các bản ghi đã hoàn tất chồng chất năm này qua năm khác, và bộ lọc đó ngày càng kém trong việc thu hẹp bất cứ thứ gì. Cùng trường, cùng chỉ mục, giá trị khác, phân bố khác, hành vi khác. Đó là tính chọn lọc.
Ngoài kỳ thi: AI thay đổi điều gì. Một trợ lý AI viết
WHERE Status = 'Completed'dễ dàng như một lập trình viên, và nó chạy được trong một org thử nghiệm. Production vẫn có cùng trình tối ưu hóa, chỉ mục, ngưỡng và phân bố. AI không bãi bỏ các định luật vật lý của cơ sở dữ liệu. Hãy kiểm thử các truy vấn do AI tạo với khối lượng và phân bố thực tế, không phải 500 trường hợp mẫu.
All Three: the Boost
Phần tiêu đề “All Three: the Boost”Back to the race. Three things have to be true for the nitro to fire, and now you know all three for a query: ⟲
- There’s an index on the field you filter on. (The nitro is installed.) ⟲
- You ask in a selective way: equals, starts with, a short list, a range of dates or numbers. (The pedal is all the way down.) ⟲
- Not too many records match: you’re under the threshold for that index, today and as the data grows. (The engine is in the right range.) ⟲
All three, and Salesforce goes to the index and your query jumps ahead. Miss any one, and Salesforce reads the table: you’re just driving. And notice that the third one can change on its own. The closed cases pile up, the object grows, and a query that had the boost last year doesn’t have it this year. ⟲
So who checks all three, every time a query runs? Not you, and not the developer. That’s the job of the librarian, and that’s the next module. ⟲
Test knowledge. For an index to be used, three things must be true: the field is indexed, the filter is selective in the way it asks (no leading wildcard, no negative operator, no comparison with an empty value), and the number of matching records is under the selectivity threshold. ⟲
Kiến trúc sư quyết định gì ở đây
Phần tiêu đề “Kiến trúc sư quyết định gì ở đây ”- Những bộ lọc khối lượng lớn nào phải giữ được tính chọn lọc hôm nay, và ở năm thứ ba, năm thứ năm và xa hơn.
- Whether the fields those filters use have an index, standard or custom, and whether one is worth its cost. ⟲
- Whether the filters ask in a selective way, or rely on contains, ends with, not equal or empty values. ⟲
- Whether the real distribution of values, not just the presence of an index, keeps each filter under its threshold as the data grows. ⟲
- Which queries, including AI-generated ones, need testing at realistic volume and distribution before production. ⟲
Đúng, hãy thuộc các ngưỡng: chuẩn, 30% của một triệu đầu tiên cộng 15% phần vượt quá, tối đa 1 triệu; tùy chỉnh, 10% cộng 5%, tối đa 333.333. Nhưng nếu đó là tất cả những gì bạn học được, bạn đã bỏ lỡ bài học. Tủ phiếu mục lục chỉ hữu ích khi nó tiết kiệm đủ công sức; một bộ lọc có chỉ mục trả về gần như mọi thứ thì không phải là lối tắt; và cùng một bộ lọc có thể trôi từ chọn lọc sang không chọn lọc khi dữ liệu tăng lên và già đi. Vì vậy khi ai đó nói “yên tâm, trường đó có chỉ mục rồi”, hãy đặt ba câu hỏi. Rồi làm phép tính.
Tìm hiểu thêm
Phần tiêu đề “Tìm hiểu thêm ”- Salesforce Developers, Apex Developer Guide (Winter ‘27), Working with Very Large SOQL Queries (mở trong thẻ mới), checked 2026-11-01: what makes a query selective, and when a custom index typically isn’t used. ⟲
- Salesforce Help, Make Salesforce Platform SOQL Query Selective (mở trong thẻ mới) (version 4, April 2026: the thresholds and their caps) and Developer Console Query Plan Tool FAQ (mở trong thẻ mới) (version 5, June 2026), checked 2026-11-01. ⟲
- Salesforce Architects, Well-Architected, Resource and Cost Optimization (mở trong thẻ mới), checked 2026-11-01: Query Selectivity. ⟲
- Salesforce Developers, Best Practices for Deployments with Large Data Volumes (Winter ‘27), Indexes (mở trong thẻ mới), checked 2026-11-01: the index table places upper limits on how many records an indexed search can return. ⟲
- Salesforce Architects, Well-Architected, Trust (mở trong thẻ mới) and Data Protection Patterns (mở trong thẻ mới), checked 2026-11-01: filtering on encrypted fields (the two pages differ). ⟲
- Salesforce Architects, Well-Architected, Reliability (mở trong thẻ mới) and The Agentic Enterprise (mở trong thẻ mới), checked 2026-11-01: the 70% governor-limit ceiling and its 30% reserve, and how Salesforce describes it. ⟲
