Tables, Fields, and Indexes
Master the foundation of D365 F&O data architecture β create and extend tables, define Extended Data Types, configure field groups, set up table relations and delete actions, and build clustered and non-clustered indexes for performance.
Why tables are the starting point
Every screen, every report, every integration in D365 Finance & Operations reads or writes a table. Before you build forms, entities, or classes, you need tables.
Think of a warehouse with labelled shelves.
Each shelf (table) holds a specific kind of item β one shelf for customer records, another for sales orders, another for product details. Every shelf has compartments (fields) with labels so you always know what goes where. Some compartments are linked β the customer shelf has a tag that points to the address shelf so you never store the same address twice.
Indexes are like the signs on the end of each aisle β they help you find the right shelf without walking every row.
In D365 F&O, tables live in the Application Object Tree (AOT). You build them in Visual Studio, define their fields, link them together with relations, and add indexes so the database can find data fast.
Extended Data Types (EDTs)
Before you add a field to a table, you should define an Extended Data Type (EDT). EDTs are reusable type definitions that ensure consistency across the entire application.
What an EDT gives you
| Benefit | Without EDT | With EDT |
|---|---|---|
| Label | Set on every field individually | Set once, inherited everywhere |
| Size / precision | Defined per field | Defined once, consistent |
| Display format | Manual per form control | Automatic from EDT |
| Relations | Must create manually | EDT can carry a table reference |
| Find usages | Search field by field | Find every field using this EDT |
Creating an EDT
In Visual Studio: right-click your project β Add New Item β Data Types β Extended Data Type.
// EDT: AxionCustomerId
// Extends: str (String)
// Label: Customer ID
// Size: 20
// String size: 20
// Help text: Unique customer identifier assigned by Axion Dynamics
Scenario: Vik's EDT strategy at Axion Dynamics
Vik Kapoor is building a custom module for Axion Dynamicsβ quality inspection system. PM Fatima asks: βWe need an Inspection ID on five different tables β inspections, results, photos, defects, and sign-offs.β
Instead of creating a string(15) field on each table with manually matching labels, Vik creates one EDT called AxionInspectionId β string, size 15, label βInspection IDβ. Every table that needs this field uses the same EDT. If Fatima later says βmake it 20 characters,β Vik changes the EDT once and all five tables update.
Creating a new table
Table properties that matter for the exam
| Property | What It Controls | Common Values |
|---|---|---|
| TableGroup | Categorises the table for form data sources | Transaction, WorksheetHeader, WorksheetLine, Group, Main, Parameter, Miscellaneous |
| CreatedBy / ModifiedBy | Automatically tracks who created/modified a row | Yes / No |
| CreatedDateTime / ModifiedDateTime | Automatically timestamps row changes | Yes / No |
| PrimaryIndex | The clustered index used for primary lookups | Usually the surrogate key or natural key |
| ClusterIndex | Which index is physically clustered | Often same as PrimaryIndex |
| CacheLookup | How aggressively the runtime caches reads | None, NotInTTS, Found, FoundAndEmpty, EntireTable |
| SaveDataPerCompany | Whether data is filtered by legal entity (DataAreaId) | Yes (most tables) / No (shared tables) |
Exam tip: SaveDataPerCompany
When SaveDataPerCompany = Yes, D365 automatically filters every query by the userβs current legal entity. You never see another companyβs data unless you explicitly cross company boundaries with crossCompany in X++.
Exam trap: If a question asks why a custom table shows all companiesβ data, check whether SaveDataPerCompany was accidentally set to No.
Table creation in X++ (code-first pattern)
While most table creation happens visually in Visual Studio, understanding the underlying structure helps for exam scenarios:
// Table: AxionInspectionTable
// Fields:
// InspectionId (EDT: AxionInspectionId, Mandatory: Yes)
// Description (EDT: Description, Mandatory: No)
// InspectorWorker (EDT: HcmWorkerRecId, Mandatory: Yes)
// InspectionDate (EDT: TransDate, Mandatory: Yes)
// Status (Enum: AxionInspectionStatus)
// The find method β standard pattern on every table
public static AxionInspectionTable find(
AxionInspectionId _inspectionId,
boolean _forUpdate = false)
{
AxionInspectionTable table;
if (_inspectionId)
{
table.selectForUpdate(_forUpdate);
select firstonly table
where table.InspectionId == _inspectionId;
}
return table;
}
Field groups
Field groups organise fields that appear together on forms. Instead of dragging individual fields onto a form, you add a field group β and if the group changes, every form using it updates automatically.
Standard field groups
| Field Group | Purpose | When It Appears |
|---|---|---|
| AutoSummary | Fields shown in FactBoxes and preview panes | Hovering over a record, FactBox area |
| AutoLookup | Fields shown in lookup dropdowns | When a user clicks a lookup control |
| AutoReport | Default fields for auto-generated reports | Quick reports, basic exports |
| AutoIdentification | Fields that identify a record in logs | Change tracking, alerts |
Scenario: Nikhil's lookup problem
Junior dev Nikhil at Axion adds a new Inspection Type table. Users complain: βWhen I look up an inspection type, I only see the ID β I need the description too.β
Vik shows him: βAdd Description to the AutoLookup field group on the table. The form controlβs lookup dropdown automatically shows all fields in that group.β Nikhil adds it, rebuilds, and the dropdown now shows both ID and description β no form changes needed.
Table relations (foreign keys)
Relations define how tables connect. In the AOT, a relation specifies:
- Related table β which table this one points to
- Field mapping β which field(s) match between the two tables
- Cardinality β how many records on each side (not enforced at DB level, but documents intent)
Relation example
// On AxionInspectionLine table:
// Relation: AxionInspectionTable
// AxionInspectionLine.InspectionId == AxionInspectionTable.InspectionId
// Cardinality: ZeroMore (lines) to ExactlyOne (header)
// RelatedTableCardinality: ExactlyOne
Delete actions
When a parent record is deleted, what happens to the children?
| Delete Action | Behaviour | When to Use |
|---|---|---|
| Cascade | Deletes all child records automatically | Lines that have no meaning without the header (e.g., order lines when deleting an order) |
| Restricted | Blocks deletion if child records exist | When children must be reviewed before parent deletion (e.g., customer with open invoices) |
| Cascade + Restricted | Restricted if children exist, cascade if forced | Rare β used for conditional cleanup |
| None | No action β child records become orphaned | When orphans are acceptable or handled by business logic |
Exam tip: delete actions are NOT SQL foreign keys
D365 F&O delete actions look like SQL cascade/restrict rules, but they are enforced by the X++ runtime, not by SQL Server foreign key constraints. The database does not have FK constraints β the application layer handles integrity.
This means: if you bypass the application (direct SQL) you bypass delete actions. The exam may test this distinction.
Indexes
Indexes make queries fast. Without them, SQL Server scans every row (table scan). With the right index, it jumps directly to matching rows.
Clustered vs non-clustered
| Feature | Clustered Index | Non-Clustered Index |
|---|---|---|
| Physical order | Physically sorts the table data on disk | Separate structure that points to table rows |
| How many per table | Exactly one (or none) | Up to 999 |
| Speed for range queries | Fastest β data is already in order | Good β but requires a bookmark lookup to get remaining columns |
| Speed for inserts | Can cause page splits if data isn't sequential | Minimal impact on insert order |
| Set via | Table property: ClusterIndex | Index property: AllowDuplicates = No/Yes |
| Typical use | Primary key, RecId, or most common sort order | Fields used in WHERE clauses, lookups, joins |
Creating an index in the AOT
// Index: InspectionIdx on AxionInspectionTable
// Fields: InspectionId
// AllowDuplicates: No (unique index)
// This index is set as both PrimaryIndex and ClusterIndex on the table
Full Text Indexes
Full Text Indexes enable LIKE and contains searches on large text fields. They are rarely tested on MB-500 but good to know:
- Created on
stringormemofields - Use SQL Server Full-Text Search under the hood
- Set
FullTextIndex = Yeson the index property
Extending existing tables
You never modify Microsoftβs tables directly. Instead, create a table extension:
- In Application Explorer, find the table (e.g.,
CustTable) - Right-click β Create extension
- A new object appears in your model:
CustTable.MyModel - Add fields, field groups, relations, indexes, or methods
What you CAN do in a table extension
- β Add new fields
- β Add new field groups
- β Add new relations
- β Add new indexes
- β Add new methods
- β Modify certain properties on existing fields (e.g., label on extended fields)
What you CANNOT do
- β Remove existing fields
- β Change existing field types
- β Remove existing indexes
- β Change the tableβs SaveDataPerCompany property
- β Modify existing methods (use event handlers instead β covered in Module 10)
Scenario: Vik extends CustTable
Axion needs a βPreferred Inspectorβ field on customer records. Vik creates an extension of CustTable in his model, adds a new field AxionPreferredInspectorWorker (EDT: HcmWorkerRecId), and adds it to the AutoSummary field group.
He prefixes the field with Axion to avoid naming collisions with other ISV solutions. This is a best practice β always prefix extension elements with your solutionβs namespace.
CacheLookup strategies
Table caching improves read performance by reducing SQL round-trips.
| CacheLookup | Behaviour | Best For |
|---|---|---|
| None | No caching β every read hits the database | Transaction tables with frequent changes |
| NotInTTS | Cache reads outside transactions; bypass cache inside ttsbegin/ttscommit | Tables read often but modified in transactions |
| Found | Cache successful lookups only | Reference tables where most lookups find a record |
| FoundAndEmpty | Cache both hits and misses | Reference tables where βnot foundβ is common |
| EntireTable | Load the entire table into memory | Small parameter/setup tables (< ~128 KB) |
Exam tip: EntireTable caching
EntireTable caching loads all rows into the AOS memory. Itβs fast but dangerous on large tables β it wastes memory and the cache invalidates whenever any row changes.
Exam rule of thumb: Only use EntireTable on tables with fewer than ~100 rows that rarely change (e.g., parameter tables, number sequences setup). Never on transaction tables.
Putting it all together: table design checklist
When Vik builds a new table at Axion, he follows this checklist:
- Define EDTs first β reusable types for every field
- Set table properties β TableGroup, SaveDataPerCompany, CacheLookup, CreatedDateTime/ModifiedDateTime
- Add fields β using EDTs, set Mandatory where needed
- Create field groups β AutoSummary, AutoLookup, AutoReport at minimum
- Define relations β link to parent/reference tables
- Set delete actions β Cascade for dependent children, Restricted for protected references
- Create indexes β unique index on natural key, set as PrimaryIndex and ClusterIndex
- Write the find() method β static lookup by natural key
- Add to extension β if modifying an existing table, use extension model
Vik creates a new AxionInspectionLine table that stores individual inspection measurements. Each line belongs to exactly one AxionInspectionTable header. If an inspection is deleted, lines are meaningless and should be removed. Which delete action should Vik configure on the relation?
Nikhil is extending the standard PurchTable to add a custom field. He creates PurchTable.AxionModel extension and adds a new field called 'InspectionRequired' (NoYesId EDT). What else should he do to ensure the field appears in lookup dropdowns and FactBoxes?
Axion's AxionInspectionSetup table has 12 rows and is read on every inspection form load but never modified during normal operations. Users report slow form loads. Which CacheLookup setting would best improve performance?
A developer creates a new field on CustTable called 'PreferredInspector' without any prefix. Another ISV on the same environment also adds a field called 'PreferredInspector' to CustTable. What happens?
Next up: Views, Queries, and Maps β build reusable data retrieval layers using AOT views, queries, and maps.