Certain Appian applications are a mix of transaction heavy OLTP (Online Transaction Processing) - with a high volume of insert, update and delete transactions and read-heavy or CPU intensive OLAP (Online Analytical Processing) with reporting and analytical capabilities. The database schema needs to be designed to cater reasonably well to both design paradigms. This document is intended to assist the lead Appian application designer, by demonstrating a sample Appian application’s database design and explaining the thought behind each design decision taken. This document is also meant to complement the information provided in the Database Performance Best Practices playbook play which highlights the best practices for a performant database.
Consider an application called Quote to Order for this case study, whose Entity Relationship diagram is shown above. This app is used by the Acme sales team to track sales opportunities. New accounts for each prospective customer are created in the app by an Acme Account Executive. When a new sales opportunity is identified for an account, a quote is prepared and presented to the customer. Any available offer prices are included in the quote. If the customer accepts the quote, an order is generated. OLTP and OLAP design considerations for this application are explained below.
It may be beneficial to create new database schema in order to separate data within a single instance. We recommend creating schema only if users require segregated database access. Too many schemas could contribute to performance degradation due to contention over pooled database connections. Users should access any shared data through a shared schema. Refer to Data Migration to New Schema for information on creating additional schemas.
In this example, the listPrice is stored in both the Quote Line Item table and the Order Line Item table. This might seem like a redundancy but in this case it is required to make a distinction between the listPrice when the quote is generated and when the order is created.
It is also good practice to have a master reference table to store data that is used to populate dropdowns from which the user needs to pick from a set of predefined values. If a new value needs to be added to the reference list in the future, it's just a quick database change.
In the Quote to Order application, the stages of Opportunity can only be one of the following: Prospecting, Needs Analysis, Value Proposition, Price Quote, Negotiation. These values can be stored in the Reference table QTOT_REFERENCE and the primary key REF_ID is stored as the foreign key in the QTOT_OPPORTUNITY table where it is referenced.
SELECT a.accountName, a.accountExec, o.oppName, o.oppStage FROM QTO_account a INNER JOIN QTO_opportunity o WHERE a.accountId = o.accountId AND a.accountName = "Fictional Corp";
CREATE INDEX IDX_QTO_account_accountName ON QTO_account(accountName);
Also refer to An Introduction on Query Optimization for a deeper dive into query optimization.
CREATE PROCEDURE QTO_updateAccountStatus (IN inactiveDaysThreshold INT) -- Begin UPDATE QTO_account AS account INNER JOIN QTO_opportunity AS opportunity ON account.accountId = opportunity.accountId SET accountStatus = "Inactive" WHERE opportunity.oppStage = "Prospecting" AND DATEDIFF(NOW(), lastUpdated) > inactiveDaysThreshold; -- End
Triggers should be used when there is a need to track the changes in values of attributes. This is especially useful for a historical audit.
For example: In the Quote to Order application, a trigger can be specified to run on update of the Account table and store the values of accId, accStatus, and time of update to an Audit table.
CREATE TRIGGER TRG_AFTER_QTO_accountStatusUpdate AFTER UPDATE ON QTO_account FOR EACH ROW BEGIN IF (NEW.accountStatus <> OLD.accountStatus) THEN INSERT INTO QTO_accountAudit (accountId, lastUpdated, oldValue, newValue) VALUES (NEW.accountId, NOW(), OLD.accountStatus, NEW.accountStatus); END IF; END;
Stored procedure for QTOA_HISTORICAL_ORDERS:
CREATE PROCEDURE QTO_updateHistoricalOrders() -- Begin INSERT INTO QTOA_HISTORICAL_ORDERS ( orderId, totalQuoteListPrice, totalOrderListPrice, totalOrderActualPrice, orderDate, accountId, accountName, accountExecutiveId, accountExecutiveName ) SELECT QTO_order.orderId, SUM(QTO_quoteLineItem.listPrice), SUM(QTO_orderLineItem.listPrice), SUM(QTO_orderLineItem.actualPrice), QTO_order.orderEffectiveDate, QTO_account.accountId, QTO_account.accountName, QTO_account.accountName, QTO_accountExecutive.accountExecutiveId, CONCAT(QTO_accountExecutive.firstName, " ", QTO_accountExecutive.lastName) FROM QTO_order JOIN QTO_orderLineItem ON QTO_order.orderId = QTO_orderLineItem.orderId JOIN QTO_priceQuote ON QTO_order.priceQuoteId = QTO_priceQuote.priceQuoteId JOIN QTO_quoteLineItem ON QTO_priceQuote.priceQuoteId = QTO_quoteLineItem.priceQuoteId JOIN QTO_opportunity ON QTO_priceQuote.opportunityId = QTO_opportunity.opportunityId JOIN QTO_account ON QTO_opportunity.accountId = QTO_account.accountId JOIN QTO_accountExecutive ON QTO_account.accountExecutiveId = QTO_accountExecutive.accountExecutiveId WHERE DATEDIFF(NOW(), QTO_order.lastUpdated) = 1 -- Update only the previous day's changes GROUP BY QTO_order.orderId; --Delete orders older than 3 years DELETE FROM QTOA_HISTORICAL_ORDERS WHERE DATEDIFF(NOW(), QTOA_HISTORICAL_ORDERS.orderDate) > 1094; -- End