Build a large-scale order management system
Use Tablestore search indexes and multi-condition queries to build an order management system at billion-row scale.
Solution overview
In e-commerce, finance, and logistics, order volumes grow from millions to billions of rows as businesses scale. Traditional relational databases face storage bottlenecks and degraded query performance at this scale. The core challenge: querying massive order data by user, time range, and amount range while aggregating sales by product category.
A single Tablestore table supports petabyte-level storage and tens of millions of transactions per second (TPS). Search indexes support KEYWORD exact match, DOUBLE and LONG range queries, and TEXT tokenized fuzzy search. BoolQuery enables multi-condition filtering, and GroupBy handles aggregation.
Traditional order systems built on MySQL sharding with Elasticsearch face the following challenges at billion-row scale.
|
Dimension |
Traditional (MySQL sharding + ES) |
Tablestore |
|
Storage |
Requires sharding at billion-row scale with complex operations |
Single table with petabyte-level storage and automatic sharding |
|
Multi-dimensional queries |
Requires a separate ES cluster; dual-write consistency is hard to maintain |
Built-in search indexes with automatic data sync |
|
Scaling |
Shard migrations cause connection bottlenecks |
Serverless with automatic scaling |
|
Maintenance |
Three systems to manage: MySQL, ES, and a sync pipeline |
Single pay-as-you-go service, O&M-free |
For new order systems, use Tablestore directly. For existing MySQL-based systems, use tools such as DTS, Canal, or DataX to synchronize data to Tablestore in real time. Tablestore handles multi-dimensional search and aggregation while MySQL focuses on online transaction processing.
Solution design
All order data goes into a single wide table. Attribute columns are schema-free. A search index on the table enables multi-dimensional queries and aggregation. The following table lists the fields and their index types.
|
Field type |
Field |
Data type |
Index field type |
Description |
|
Primary key |
_id |
String |
— |
MD5 hash of the order ID. Distributes data across partitions to avoid write hotspots. |
|
Attribute columns |
orderId |
String |
KEYWORD |
Order ID for exact-match queries. |
|
userId |
String |
KEYWORD |
User ID for filtering orders by user. |
|
|
productName |
String |
TEXT |
Product name. Uses max-semantic tokenization for fuzzy search. |
|
|
brand |
String |
KEYWORD |
Product brand. |
|
|
category |
String |
KEYWORD |
Product category (such as phones, computers, and headphones) for group-by aggregation. |
|
|
price |
Double |
DOUBLE |
Unit price in CNY. |
|
|
quantity |
Integer |
LONG |
Purchase quantity. |
|
|
totalPrice |
Double |
DOUBLE |
Total order amount in CNY for range queries and aggregation. |
|
|
orderTime |
Integer |
LONG |
Order timestamp in milliseconds for time-range queries and sorting. |
|
|
status |
String |
KEYWORD |
Order status: paid, shipped, completed, or canceled. |
Implementation
Before you begin, activate Tablestore and create an instance. Skip this if you already have one.
The following examples use Java and Python. Before you begin, install and configure Java SDK or Python SDK.
Step 1: Create a data table and a search index
When you create a data table, specify only the primary key columns. Attribute columns are schema-free. After you create the table, create a search index. The index uses the following field types: KEYWORD for exact match, TEXT for tokenized fuzzy search, DOUBLE for range queries, and LONG for range queries and sorting.
Java
// Create a data table
private static void createTable(SyncClient client) {
TableMeta tableMeta = new TableMeta("order_contract");
tableMeta.addPrimaryKeyColumn(new PrimaryKeySchema("_id", PrimaryKeyType.STRING));
TableOptions tableOptions = new TableOptions();
tableOptions.setMaxVersions(1);
tableOptions.setTimeToLive(-1);
client.createTable(new CreateTableRequest(tableMeta, tableOptions));
}
// Create a search index
private static void createSearchIndex(SyncClient client) {
CreateSearchIndexRequest request = new CreateSearchIndexRequest();
request.setTableName("order_contract");
request.setIndexName("order_index");
IndexSchema indexSchema = new IndexSchema();
indexSchema.setFieldSchemas(Arrays.asList(
new FieldSchema("orderId", FieldType.KEYWORD).setIndex(true).setEnableSortAndAgg(true),
new FieldSchema("userId", FieldType.KEYWORD).setIndex(true).setEnableSortAndAgg(true),
new FieldSchema("productName", FieldType.TEXT).setIndex(true)
.setAnalyzer(FieldSchema.Analyzer.MaxWord),
new FieldSchema("brand", FieldType.KEYWORD).setIndex(true).setEnableSortAndAgg(true),
new FieldSchema("category", FieldType.KEYWORD).setIndex(true).setEnableSortAndAgg(true),
new FieldSchema("price", FieldType.DOUBLE).setIndex(true).setEnableSortAndAgg(true),
new FieldSchema("quantity", FieldType.LONG).setIndex(true).setEnableSortAndAgg(true),
new FieldSchema("totalPrice", FieldType.DOUBLE).setIndex(true).setEnableSortAndAgg(true),
new FieldSchema("orderTime", FieldType.LONG).setIndex(true).setEnableSortAndAgg(true),
new FieldSchema("status", FieldType.KEYWORD).setIndex(true).setEnableSortAndAgg(true)
));
request.setIndexSchema(indexSchema);
client.createSearchIndex(request);
}
Python
def create_table_and_index(client):
# Create a data table
table_meta = tablestore.TableMeta("order_contract", [("_id", "STRING")])
table_options = tablestore.TableOptions(time_to_live=-1, max_version=1)
reserved = tablestore.ReservedThroughput(tablestore.CapacityUnit(0, 0))
client.create_table(table_meta, table_options, reserved)
# Create a search index
fields = [
tablestore.FieldSchema("orderId", tablestore.FieldType.KEYWORD,
index=True, enable_sort_and_agg=True),
tablestore.FieldSchema("userId", tablestore.FieldType.KEYWORD,
index=True, enable_sort_and_agg=True),
tablestore.FieldSchema("productName", tablestore.FieldType.TEXT,
index=True, analyzer="max_word"),
tablestore.FieldSchema("brand", tablestore.FieldType.KEYWORD,
index=True, enable_sort_and_agg=True),
tablestore.FieldSchema("category", tablestore.FieldType.KEYWORD,
index=True, enable_sort_and_agg=True),
tablestore.FieldSchema("price", tablestore.FieldType.DOUBLE,
index=True, enable_sort_and_agg=True),
tablestore.FieldSchema("quantity", tablestore.FieldType.LONG,
index=True, enable_sort_and_agg=True),
tablestore.FieldSchema("totalPrice", tablestore.FieldType.DOUBLE,
index=True, enable_sort_and_agg=True),
tablestore.FieldSchema("orderTime", tablestore.FieldType.LONG,
index=True, enable_sort_and_agg=True),
tablestore.FieldSchema("status", tablestore.FieldType.KEYWORD,
index=True, enable_sort_and_agg=True),
]
client.create_search_index("order_contract", "order_index",
tablestore.SearchIndexMeta(fields))
Step 2: Write test data
Use BatchWriteRow to write 10,000 test orders in batches. Each order contains a randomly generated user ID, product details, quantity, total amount, timestamp, and status. The primary key _id is the MD5 hash of the order ID. BatchWriteRow writes up to 200 rows per request.
Java
private static void writeTestData(SyncClient client) throws Exception {
Random rand = new Random(42);
long start = System.currentTimeMillis();
int written = 0;
long baseTime = 1704067200000L; // 2024-01-01 UTC
long timeSpan = 2L * 365 * 24 * 3600 * 1000; // 2 years
String[] PRODUCTS = {"iPhone 15", "MacBook Pro", "AirPods", "iPad Air",
"Galaxy S24", "Pixel 8", "ThinkPad X1", "Surface Pro",
"Xiaomi 14", "Huawei Mate 60"};
String[] BRANDS = {"Apple", "Apple", "Apple", "Apple",
"Samsung", "Google", "Lenovo", "Microsoft",
"Xiaomi", "Huawei"};
String[] CATEGORIES = {"Phone", "Computer", "Earphone", "Tablet",
"Phone", "Phone", "Computer", "Computer", "Phone", "Phone"};
double[] PRICES = {7999, 14999, 1799, 4599, 6999, 4999, 9999, 8999, 3999, 6999};
String[] USERS = {"u00001", "u00002", "u00003", "u00004", "u00005",
"u00006", "u00007", "u00008", "u00009", "u00010"};
String[] STATUSES = {"Paid", "Shipped", "Completed", "Cancelled"};
while (written < 10000) {
BatchWriteRowRequest batch = new BatchWriteRowRequest();
int batchSize = Math.min(200, 10000 - written);
for (int i = 0; i < batchSize; i++) {
int idx = written + i;
String orderId = String.format("ORD%08d", idx);
String id = md5(orderId);
String userId = USERS[rand.nextInt(USERS.length)];
int prodIdx = rand.nextInt(PRODUCTS.length);
int quantity = 1 + rand.nextInt(5);
double totalPrice = Math.round(PRICES[prodIdx] * quantity * 100.0) / 100.0;
long orderTime = baseTime + (long)(rand.nextDouble() * timeSpan);
PrimaryKeyBuilder pkb = PrimaryKeyBuilder.createPrimaryKeyBuilder();
pkb.addPrimaryKeyColumn("_id", PrimaryKeyValue.fromString(id));
RowPutChange row = new RowPutChange("order_contract", pkb.build());
row.addColumn("orderId", ColumnValue.fromString(orderId));
row.addColumn("userId", ColumnValue.fromString(userId));
row.addColumn("productName", ColumnValue.fromString(PRODUCTS[prodIdx]));
row.addColumn("brand", ColumnValue.fromString(BRANDS[prodIdx]));
row.addColumn("category", ColumnValue.fromString(CATEGORIES[prodIdx]));
row.addColumn("price", ColumnValue.fromDouble(PRICES[prodIdx]));
row.addColumn("quantity", ColumnValue.fromLong(quantity));
row.addColumn("totalPrice", ColumnValue.fromDouble(totalPrice));
row.addColumn("orderTime", ColumnValue.fromLong(orderTime));
row.addColumn("status", ColumnValue.fromString(
STATUSES[rand.nextInt(STATUSES.length)]));
batch.addRowChange(row);
}
client.batchWriteRow(batch);
written += batchSize;
}
System.out.println("Written 10000 rows in " + (System.currentTimeMillis() - start) + "ms");
}
Python
import hashlib, random
def write_test_data(client):
rand = random.Random(42)
base_time = 1704067200000 # 2024-01-01 UTC
time_span = 2 * 365 * 24 * 3600 * 1000 # 2 years
PRODUCTS = ["iPhone 15", "MacBook Pro", "AirPods", "iPad Air",
"Galaxy S24", "Pixel 8", "ThinkPad X1", "Surface Pro",
"Xiaomi 14", "Huawei Mate 60"]
BRANDS = ["Apple", "Apple", "Apple", "Apple",
"Samsung", "Google", "Lenovo", "Microsoft", "Xiaomi", "Huawei"]
CATEGORIES = ["Phone", "Computer", "Earphone", "Tablet",
"Phone", "Phone", "Computer", "Computer", "Phone", "Phone"]
PRICES = [7999, 14999, 1799, 4599, 6999, 4999, 9999, 8999, 3999, 6999]
USERS = [f"u{i:05d}" for i in range(1, 11)]
STATUSES = ["Paid", "Shipped", "Completed", "Cancelled"]
written = 0
while written < 10000:
batch_size = min(200, 10000 - written)
put_rows = []
for i in range(batch_size):
idx = written + i
order_id = f"ORD{idx:08d}"
_id = hashlib.md5(order_id.encode()).hexdigest()
user_id = rand.choice(USERS)
prod_idx = rand.randint(0, len(PRODUCTS) - 1)
quantity = 1 + rand.randint(0, 4)
total_price = round(PRICES[prod_idx] * quantity, 2)
order_time = base_time + int(rand.random() * time_span)
pk = [("_id", _id)]
cols = [
("orderId", order_id), ("userId", user_id),
("productName", PRODUCTS[prod_idx]),
("brand", BRANDS[prod_idx]),
("category", CATEGORIES[prod_idx]),
("price", float(PRICES[prod_idx])),
("quantity", quantity),
("totalPrice", float(total_price)),
("orderTime", order_time),
("status", rand.choice(STATUSES)),
]
row = tablestore.Row(pk, cols)
put_rows.append(tablestore.PutRowItem(
row, tablestore.Condition(tablestore.RowExistenceExpectation.IGNORE)))
request = tablestore.BatchWriteRowRequest()
request.add(tablestore.TableInBatchWriteRowItem("order_contract", put_rows))
client.batch_write_row(request)
written += batch_size
Step 3: Query data
After writing data, wait for the search index to synchronize — typically seconds to minutes depending on data volume. The following examples cover three common query scenarios.
Scenario 1: Query orders by user
Use TermQuery to match a user ID exactly and return all orders for that user, sorted by order time in descending order.
Java
// Query all orders for user u00001, sorted by order time in descending order
TermQuery userQuery = new TermQuery();
userQuery.setFieldName("userId");
userQuery.setTerm(ColumnValue.fromString("u00001"));
SearchQuery searchQuery = new SearchQuery();
searchQuery.setQuery(userQuery);
searchQuery.setGetTotalCount(true);
searchQuery.setLimit(5);
searchQuery.setSort(new Sort(Arrays.asList(
new FieldSort("orderTime", SortOrder.DESC))));
SearchRequest request = new SearchRequest("order_contract", "order_index", searchQuery);
SearchRequest.ColumnsToGet columnsToGet = new SearchRequest.ColumnsToGet();
columnsToGet.setReturnAll(true);
request.setColumnsToGet(columnsToGet);
SearchResponse response = client.search(request);
System.out.println("Total hits: " + response.getTotalCount());
for (Row row : response.getRows()) {
System.out.println(row);
}
Python
# Query all orders for user u00001, sorted by order time in descending order
result = client.search("order_contract", "order_index",
tablestore.SearchQuery(
tablestore.TermQuery("userId", "u00001"),
get_total_count=True, limit=5,
sort=tablestore.Sort(sorters=[
tablestore.FieldSort("orderTime", tablestore.SortOrder.DESC)])),
tablestore.ColumnsToGet(return_type=tablestore.ColumnReturnType.ALL))
print(f"Total hits: {result.total_count}")
for row in result.rows:
cols = {c[0]: c[1] for c in row[1]}
print(f"orderId={cols['orderId']} | product={cols['productName']}"
f" | totalPrice={cols['totalPrice']} | status={cols['status']}")
Scenario 2: Multi-condition compound query
Use BoolQuery to combine TermQuery (user filter), RangeQuery (amount range), and RangeQuery (time range). This retrieves orders for a specific user within a given time period where the total amount exceeds CNY 5,000.
Java
// Filter by user: userId = "u00001"
TermQuery userQuery = new TermQuery();
userQuery.setFieldName("userId");
userQuery.setTerm(ColumnValue.fromString("u00001"));
// Amount range: totalPrice >= 5000
RangeQuery priceQuery = new RangeQuery();
priceQuery.setFieldName("totalPrice");
priceQuery.greaterThanOrEqual(ColumnValue.fromDouble(5000));
// Time range: full year 2024
RangeQuery timeQuery = new RangeQuery();
timeQuery.setFieldName("orderTime");
timeQuery.greaterThanOrEqual(ColumnValue.fromLong(1704067200000L)); // 2024-01-01
timeQuery.lessThan(ColumnValue.fromLong(1735689600000L)); // 2025-01-01
// Combine three conditions with BoolQuery
BoolQuery boolQuery = new BoolQuery();
boolQuery.setMustQueries(Arrays.asList(userQuery, priceQuery, timeQuery));
SearchQuery searchQuery = new SearchQuery();
searchQuery.setQuery(boolQuery);
searchQuery.setGetTotalCount(true);
searchQuery.setLimit(5);
searchQuery.setSort(new Sort(Arrays.asList(
new FieldSort("totalPrice", SortOrder.DESC))));
SearchRequest request = new SearchRequest("order_contract", "order_index", searchQuery);
SearchRequest.ColumnsToGet columnsToGet = new SearchRequest.ColumnsToGet();
columnsToGet.setReturnAll(true);
request.setColumnsToGet(columnsToGet);
SearchResponse response = client.search(request);
System.out.println("Total hits: " + response.getTotalCount());
for (Row row : response.getRows()) {
System.out.println(row);
}
Python
# Filter by user
user_query = tablestore.TermQuery("userId", "u00001")
# Amount range: >= 5000
price_query = tablestore.RangeQuery("totalPrice",
range_from=5000, include_lower=True)
# Time range: full year 2024
time_query = tablestore.RangeQuery("orderTime",
range_from=1704067200000, range_to=1735689600000,
include_lower=True, include_upper=False)
# Combine three conditions with BoolQuery
bool_query = tablestore.BoolQuery(
must_queries=[user_query, price_query, time_query])
result = client.search("order_contract", "order_index",
tablestore.SearchQuery(bool_query, get_total_count=True, limit=5,
sort=tablestore.Sort(sorters=[
tablestore.FieldSort("totalPrice", tablestore.SortOrder.DESC)])),
tablestore.ColumnsToGet(return_type=tablestore.ColumnReturnType.ALL))
print(f"Total hits: {result.total_count}")
for row in result.rows:
cols = {c[0]: c[1] for c in row[1]}
print(f"orderId={cols['orderId']} | product={cols['productName']}"
f" | totalPrice={cols['totalPrice']}")
Scenario 3: Aggregate by product category
Use GroupByField to group orders by product category and calculate the order count and total revenue per category, sorted by total revenue in descending order.
Java
SearchRequest request = SearchRequest.newBuilder()
.tableName("order_contract").indexName("order_index")
.searchQuery(SearchQuery.newBuilder()
.query(QueryBuilders.matchAll())
.limit(0)
.addGroupBy(GroupByBuilders.groupByField("orderCategory", "category")
.size(20)
.addSubAggregation(AggregationBuilders.count("orderCount", "orderId"))
.addSubAggregation(AggregationBuilders.sum("totalRevenue", "totalPrice"))
.addGroupBySorter(GroupBySorter.subAggSortInDesc("totalRevenue"))
.build())
.build())
.build();
SearchResponse response = client.search(request);
GroupByFieldResult results = response.getGroupByResults()
.getAsGroupByFieldResult("orderCategory");
for (GroupByFieldResultItem item : results.getGroupByFieldResultItems()) {
long count = item.getSubAggregationResults()
.getAsCountAggregationResult("orderCount").getValue();
double revenue = item.getSubAggregationResults()
.getAsSumAggregationResult("totalRevenue").getValue();
System.out.println("Category: " + item.getKey()
+ " | Order count: " + count
+ " | Total revenue: " + String.format("%.2f", revenue));
}
Python
group_by = tablestore.GroupByField("category", size=20,
sub_aggs=[
tablestore.Count("orderId", name="orderCount"),
tablestore.Sum("totalPrice", name="totalRevenue"),
],
group_by_sort=[tablestore.SubAggSort(tablestore.SortOrder.DESC, "totalRevenue")],
name="orderCategory")
result = client.search("order_contract", "order_index",
tablestore.SearchQuery(tablestore.MatchAllQuery(), limit=0,
group_bys=[group_by]),
tablestore.ColumnsToGet(return_type=tablestore.ColumnReturnType.NONE))
for item in result.group_by_results[0].items:
aggs = {a.name: a.value for a in item.sub_aggs}
print(f"Category: {item.key} | Orders: {int(aggs['orderCount'])}"
f" | Total revenue: {aggs['totalRevenue']:.2f}")
Clean up resources
To avoid unnecessary charges, delete the resources created in this tutorial when you no longer need them.
Delete the search index first, then the data table. If you created a new instance for this tutorial, release it last.
Java
private static void cleanup(SyncClient client) {
// 1. Delete the search index
DeleteSearchIndexRequest deleteIndexReq = new DeleteSearchIndexRequest();
deleteIndexReq.setTableName("order_contract");
deleteIndexReq.setIndexName("order_index");
client.deleteSearchIndex(deleteIndexReq);
// 2. Delete the data table
client.deleteTable(new DeleteTableRequest("order_contract"));
}
Python
def cleanup(client):
# 1. Delete the search index
client.delete_search_index("order_contract", "order_index")
# 2. Delete the data table
client.delete_table("order_contract")