Optimizely Commerce database querying 101
Howdy y'all!
When working in projects based on Optimizely (formerly Episerver) Commerce there is often a need to look deep within the database structure to gather information or troubleshoot issues. Having worked with this structure for the past ten years I feel that I could dump some of the "usual suspects" on the interwebs for other developers to copy paste.
When querying production data, use WITH(NOLOCK)
or SET TRANSACTION LEVEL READ UNCOMMITTED
to avoid locks as we most likely do not really care about dirty reads etc when doing these things.
Below I may refer to the concepts of content types and meta class interchangably. Meta class is the old school concept that still very much lives in the database schema, while in code it is referred to as ContentType and is handled very similar to "real" content types in the CMS.
As always, avoid querying default database entities from production code directly as they may change between versions.
The structure
There are a lot of tables, views and procedures in the commerce database, most of which I will not dive into in this post. The most important (or basic) ones from my viewpoint are listed below. Note that some views are generated based on defined content types and your milage may vary.
The key to remember is that catalog and order entities are based on the meta data plus system, which will map an entity table (e.g. CatalogEntry) to meta properties (CatalogEntryEx/CatalogEntryEx_Localization) mapped on primary key in the former table to ObjectId column in the others.
Catalog entitites
CatalogEntry
The most basal data for all entries in the catalogCatalogEntryRelation
The relation between entries, e.g. product and variationCatalogNode
The equivalent basal data for nodes in the catalogCatalogNodeRelation
Secondary relations for nodes, the primary node is set on each node explicitlyNodeEntryRelation
The relations between nodes and entriesCatalogEntryEx_ProductContent
A view containing property values for the ProductContent content typeCatalogEntryEx_ProductContent_Localization
A view for the CultureSpecific property values for the ProductContent content typeCatalogEntryEx_VariationContent
A view containing property values for the VariationContent content typeCatalogEntryEx_VariationContent_Localization
A view for the CultureSpecific property values for the VariationContent content type
Orders
OrderGroup
The base table for orders, for legacy carts, this is also usedOrderGroup_PurchaseOrder
Property values for purchase ordersOrderGroup_PurchaseOrder_Localization
CultureSpecific property values for purchase ordersOrderForm
Base table for order forms, typically there is one form per order but can be multiple if required/neededOrderFormEx
Property values for formsOrderFormEx_Localization
CultureSpecific property values for formsLineItem
Base table for order linesLineItemEx
Property values for linesLineItemEx_Localization
CultureSpecific property values for linesSerializableCart
Table for ongoing carts, soft modelled on JSON
Sample queries
Getting variation/sku information
Get entry and parent entry data for each language based on SKU code. Note that this example is trivial and expects only one meta class for product and variation.
DECLARE @SkuCode NVARCHAR(255) = '12345';
SELECT *
FROM [CatalogEntry] ce
JOIN [CatalogEntryEx_VariationContent] v ON v.ObjectId = ce.CatalogEntryId
JOIN [CatalogEntryEx_VariationContent_Localization] vl ON vl.ObjectId = ce.CatalogEntryId
JOIN [CatalogEntryRelation] cer ON cer.ChildEntryId = ce.CatalogEntryId
JOIN [CatalogEntry] ce2 ON ce2.CatalogEntryId = cer.ParentEntryId
JOIN [CatalogEntryEx_ProductContent] p ON p.ObjectId = ce2.CatalogEntryId
JOIN [CatalogEntryEx_ProductContent] pl ON pl.ObjectId = ce2.CatalogEntryId AND pl.Language = vl.Language
WHERE ce.Code = @SkuCode;
Getting order data
Get order data for a specific TrackingNumber/OrderNumber. Given a basic setup with default purchase order, form and line item meta classes.
DECLARE @TrackingNumber NVARCHAR(255) = '12345';
SELECT *
FROM [OrderGroup] og
JOIN [OrderGroup_PurchaseOrder] po ON po.ObjectId = og.OrderGroupId
JOIN [OrderGroup_PurchaseOrder_Localization] pl ON pl.ObjectId = og.OrderGroupId
JOIN [OrderForm] f ON f.OrderGroupId = og.OrderGroupId
JOIN [OrderFormEx] fe ON fe.ObjectId = og.OrderGroupId
JOIN [OrderFormEx] fl ON fl.ObjectId = og.OrderGroupId
JOIN [LineItem] li ON li.OrderGroupId = og.OrderGroupId
JOIN [LineItemEx] lie ON lie.ObjectId = li.LineItemId
JOIN [LineItemEx_Localization] liel ON liel.ObjectId = li.LineItemId
WHERE po.TrackingNumber = @TrackingNumber;
Getting even more order data
In the above example excluded shipping/shipment and payment data. Of course this could be included as well. Payment method is typically extended with meta classes and could be queried on that level too, omitted here for brevity.
DECLARE @TrackingNumber NVARCHAR(255) = '12345';
SELECT *
FROM [OrderGroup] og
JOIN [OrderGroup_PurchaseOrder] po ON po.ObjectId = og.OrderGroupId
JOIN [OrderGroup_PurchaseOrder_Localization] pl ON pl.ObjectId = og.OrderGroupId
JOIN [OrderForm] f ON f.OrderGroupId = og.OrderGroupId
JOIN [OrderFormEx] fe ON fe.ObjectId = og.OrderGroupId
JOIN [OrderFormEx] fl ON fl.ObjectId = og.OrderGroupId
JOIN [Shipment] s ON s.OrderFormId = f.OrderFormId
JOIN [ShipmentEx] se ON se.ObjectId = s.ShipmentId
JOIN [ShipmentEx_Localization] sel ON sel.ObjectId = s.ShipmentId
JOIN [ShippingMethod] sm ON sm.ShippingMethodId = s.ShippingMethodId
JOIN [OrderFormPayment] ofp ON ofp.OrderFormId = f.OrderFormId
JOIN [PaymentMethod] pm ON pm.PaymentMethodId = ofp.PaymentMethodId
JOIN [LineItem] li ON li.OrderGroupId = og.OrderGroupId
JOIN [LineItemEx] lie ON lie.ObjectId = li.LineItemId
JOIN [LineItemEx_Localization] liel ON liel.ObjectId = li.LineItemId
WHERE po.TrackingNumber = @TrackingNumber;
Getting (serializable) cart based on property value
Using the new-ish SerializableCart table is a bit more cumbersome (or easier depending 🤷♂️). The example queries are bit more advanced than previous examples but should be a nice starting point for querying on the data that matters to you.
Say that you have a property (Cart_IsAwesome
) on carts that you would like to query on. The table has just a couple of columns: CartId
, CustomerId
, Name
, MarketId
, Created
, Modified
and Data
. The last one is the target for our query as it contains the JSON representation of our cart state.
In my example we have something similar (but not pretty printed) in the Data column for a SerializableCart row.
{
"Id": 1,
"Created": "2021-09-24T11:37:54.003Z",
"Currency": "SEK",
"CustomerId": "00000000-0000-0000-0000-000000000000",
"Forms": [ // Stripped down for readability
{
"Shipments": [
{
"LineItems": [
{
"Code": "12345",
"DisplayName": "Acme Floor Polish",
"Quantity": 1.0
}
]
}
]
}
],
"MarketId": "DEFAULT",
"MarketName": "Default market",
"PricesIncludeTax": true,
"Modified": "2021-09-24T11:38:12.7381036Z",
"Name": "Default",
"Notes": [],
"OrderLink": {
"OrderGroupId": 1,
"Name": "Default",
"CustomerId": "00000000-0000-0000-0000-000000000000",
"OrderType": "EPiServer.Commerce.Order.Internal.SerializableCart, EPiServer.Business.Commerce, Version=13.21.0.0, Culture=neutral, PublicKeyToken=8fe83dea738b45b7"
},
"OrderStatus": 4,
"Properties": {
"Cart_IsAwesome": {
"$type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
"$value": true
}
},
"IsTaxTotalUpToDate": true,
"TaxTotal": 1.00
}
If you haven't queried JSON in SQL Server before it's doable but I find it a bit cumbersome and restricted. Look up JSON_QUERY
, JSON_VALUE
and OPENJSON
on your favorite SQL Server documentation website for more details on how and why.
SELECT *
FROM SerializableCart c
WHERE JSON_VALUE([Data], '$.Properties.Cart_IsAwesome."$value"') = 'true'
Even though the JSON property is a boolean we query based on the string representation in this case. Also note that the $value
property needs to be wrapped in quotation marks.
Say that you instead would like to query for line items in JSON format for the first shipment of the first form in carts matching the above criteria, then something like the below would do the trick.
SELECT JSON_QUERY([Data], '$.Forms[0].Shipments[0].LineItems')
FROM SerializableCart c
WHERE JSON_VALUE([Data], '$.Properties.Cart_IsAwesome."$value"') = 'true'
And taking it even further, we can define the expected structure of a LineItem and make it into a table. Thus we can get the included line items in the returned carts.
SELECT c.CartId, LineItem.*
FROM SerializableCart c
CROSS APPLY
OPENJSON([Data], '$.Forms[0].Shipments[0].LineItems')
WITH (
Code NVARCHAR(255) '$.Code',
DisplayName NVARCHAR(255) '$.DisplayName',
Quantity DECIMAL(38, 9) '$.Quantity'
) AS LineItem
The collections for properties are available on the other objects in the JSON as well. Having just the one form and shipment makes it easier ($.Forms[0].Shipments[0]
) than opening the JSON and query each element in the collection for instance. A more simplistic approach could use LIKE
but now we have expanded our toolbox!
Wrapping up
So that's all for today. Variations on the above (using aggregates, CTEs etc) are good tools both in development and production. Be careful when accessing production databases though. All info and queries delivered as-is, I take no responsibility for your actions. Check, double and triple check!
Cheers!