Note
As databases are only accessible at runtime, all the queries described in this page can only be used at runtime.
- Acknowledgements
- Guidance and Best Practices
- SELECT queries
- UPDATE Queries
- DELETE Queries
- INSERT Queries
- Temporary Tables
- Dynamic queries using variables
- Known Limitations and Workarounds
- Queries on UI objects
FactoryTalk Optix does not support the full set of SQL queries, but has an own subset based on the SQL ANSI standard. The reason for this is that FactoryTalk Optix will automatically act as translation layer between the UI (where the custom queries are written) and the underlying database, which may be different based on the user selection (SQLite, SQL Server, MySQL, InfluxDB, etc).
This means that if the user later switches to a different database, the queries will still work without any change, as FactoryTalk Optix will take care of translating them to the right dialect. This is a great advantage for portability, but it also means that some queries that are valid in a specific SQL dialect may not be supported by FactoryTalk Optix if they are not compatible with all the supported databases.
When writing queries for use in FactoryTalk Optix or similar embedded database contexts, keep these recommendations in mind:
- Prefer explicit column lists instead of
SELECT *in production code to avoid unexpected schema changes and reduce bandwidth. - When using
DISTINCT, be explicit about the columns you need -DISTINCT *removes duplicate entire rows which can be expensive. - Avoid updating temporary tables when portability is a concern some backends restrict updates on temporary objects.
- Use table aliases (for example
t1,t2) when joining multiple tables to prevent ambiguity and improve readability. - Be cautious with deep subqueries or multiple nested levels - they can impact performance and readability.
Some features documented here are available starting from specific versions of FactoryTalk Optix (noted inline). Always test queries on the target database before deploying to production.
FactoryTalk Optix always provides parameterized queries when inserting data to the database using the dedicated Insert method of the store. This helps prevent SQL injection attacks and ensures data integrity.
Retrieves all columns from a table.
SELECT * FROM TestTable1Note
This feature is available starting from FactoryTalk Optix version 1.7.x.
The DISTINCT keyword eliminates duplicate rows from the result set. The ALL qualifier includes all rows, including duplicates (which is the default behavior).
SELECT DISTINCT Column1 FROM Table1
SELECT DISTINCT Column1, Column2 FROM Table1
SELECT DISTINCT * FROM Table1Retrieves specific columns from a table.
SELECT ID, Name FROM TestTable1Uses aliases to rename columns in the result set.
SELECT Name AS EmployeeName, Salary AS EmployeeSalary FROM TestTable1Filters rows based on conditions using WHERE clause.
SELECT * FROM TestTable1 WHERE Salary > 60000
SELECT * FROM TestTable1 WHERE Salary > 60000 AND DepartmentID = 101
SELECT * FROM TestTable1 WHERE DepartmentID = 101 OR DepartmentID = 102
SELECT * FROM TestTable1 WHERE NOT DepartmentID = 103
SELECT * FROM TestTable2 WHERE Location = 'Chicago'
SELECT Username FROM Users WHERE PrivateKey = 'test1234' AND Username <> 'test-user'
SELECT Username FROM Users WHERE PrivateKey = 'test1234' AND NOT (A = 6)Filters rows where a column value matches any value in a specified list.
SELECT * FROM Table1 WHERE Column1 IN (10, 20, 30)Filters rows where a column value falls within a specified range, inclusive of the boundaries.
SELECT * FROM Table1 WHERE Column1 BETWEEN 100 AND 200Filters rows using pattern matching with wildcards. Supports escape characters for literal matching.
SELECT * FROM Table1 WHERE column1 LIKE '%a'
SELECT * FROM Table1 WHERE column1 LIKE '%a%'
SELECT * FROM Table1 WHERE column1 LIKE '%bbpi!%ppo%' ESCAPE '!'Orders the result set by specified columns.
SELECT * FROM TestTable1 ORDER BY Salary ASC
SELECT * FROM TestTable1 ORDER BY Salary DESCNote
This feature is available starting from FactoryTalk Optix version 1.7.x.
Supports fully qualified table.column references in the ORDER BY clause.
SELECT * FROM MyTable ORDER BY MyTable.MyColumn
SELECT Recipes.Name, RecipeMetadata_MyRecipeSchema.MyMetadata1
FROM Recipes JOIN RecipeMetadata_MyRecipeSchema ON Recipes.Id = RecipeMetadata_MyRecipeSchema.RecipeId
WHERE RecipeMetadata_MyRecipeSchema.MyMetadata1 = 'BB'
ORDER BY Recipes.NameLimits the number of rows returned and supports offset.
SELECT * FROM TestTable1 LIMIT 5
SELECT * FROM TestTable1 LIMIT 5 OFFSET 5Combines multiple conditions with AND and OR operators.
SELECT * FROM TestTable1
WHERE (Salary > 60000 AND DepartmentID = 101) OR
(Salary < 50000 AND DepartmentID = 102)Adds additional columns to the result set alongside all columns.
SELECT *, Timestamp FROM TestTable4Note
This feature is available starting from FactoryTalk Optix version 1.7.x.
Supports table.* syntax to select all columns from a specific table in JOIN queries.
SELECT A.*, B.* FROM Table1 AS A JOIN Table2 AS B on A.Id = B.Table1Id WHERE ...Adds literal values as new columns alongside all existing columns.
SELECT *, 'StaticValue' AS LiteralColumn FROM TestTable4Handles column names with special characters using double quotes.
SELECT "Water level", ID FROM TestTable1Note
This feature is available starting from FactoryTalk Optix version 1.7.x.
The CASE WHEN expression allows conditional logic in SQL statements, supporting both SELECT and UPDATE operations.
SELECT
CASE
WHEN Id = 1 THEN 'A'
WHEN Id = 2 THEN 'B'
END
FROM Table1
WHERE Id IN (1, 2, 3)SELECT
CASE
WHEN Id = 1 THEN 'A'
WHEN Id = 2 THEN 'B'
ELSE 'X'
END
FROM Table1UPDATE Table1 SET Value =
CASE
WHEN Id = 1 THEN 'A'
WHEN Id = 2 THEN 'B'
ELSE 'X'
ENDUPDATE Table1 SET Value =
CASE
WHEN Id = 1 THEN 'A'
WHEN Id = 2 THEN 'B'
END
WHERE Id IN (1, 2, 3)Filters rows based on NULL values.
SELECT * FROM TestTable1 WHERE DepartmentID IS NULL
SELECT * FROM TestTable1 WHERE DepartmentID IS NOT NULLThe NOT operator negates conditions and applies to all other operators, such as IN, BETWEEN, EXISTS, IS NULL, etc.
SELECT * FROM Table1 WHERE column1 IS NOT NULL
SELECT * FROM Table1 WHERE column1 NOT IN (10, 20)
SELECT * FROM Table1 WHERE column1 NOT BETWEEN 100 AND 200Performs basic aggregation operations on columns.
SELECT COUNT(*) AS TotalCount FROM TestTable1
SELECT AVG(Salary) AS AverageSalary FROM TestTable1
SELECT SUM(Salary) AS TotalSalary FROM TestTable1
SELECT MAX(Salary) AS HighestSalary FROM TestTable1
SELECT MIN(Salary) AS LowestSalary FROM TestTable1Groups rows by DepartmentID, counts employees and sums salaries for those with salary > 50000.
Example Output:
| DepartmentID | EmployeeCount | TotalSalary |
|---|---|---|
| 101 | 3 | 210000 |
| 102 | 2 | 105000 |
SELECT DepartmentID, COUNT(*) AS EmployeeCount, SUM(Salary) AS TotalSalary
FROM TestTable1
WHERE Salary > 50000
GROUP BY DepartmentID
SELECT DepartmentID, AVG(Salary) AS AvgSalary
FROM TestTable1
GROUP BY DepartmentIDGroups by department and calculates average salary, then filters groups where average salary > 65000.
Example Output:
| DepartmentID | AvgSalary |
|---|---|
| 101 | 67000 |
| 103 | 79000 |
SELECT DepartmentID, AVG(Salary) AS AvgSalary
FROM TestTable1
GROUP BY DepartmentID
HAVING AVG(Salary) > 65000Adds a total row count column to all columns from TestTable4 using window function.
Example Output:
| ID | Timestamp | EventName | Duration | TotalCount |
|---|---|---|---|---|
| 1 | 1/1/2025 12:00 PM | Maintenance | 2.5 | 13 |
| 2 | 1/1/2025 12:15 PM | Adjustment | 1.5 | 13 |
SELECT *, COUNT(*) OVER () AS TotalCount FROM TestTable4Joins TestTable1 and TestTable2 on DepartmentID, returning employees with their department names.
Input Tables:
TestTable1 (Employees):
| ID | Name | DepartmentID |
|---|---|---|
| 1 | Alice | 101 |
| 2 | Bob | 102 |
TestTable2 (Departments):
| DepartmentID | DepartmentName |
|---|---|
| 101 | Engineering |
| 102 | Sales |
Output:
| Name | DepartmentName |
|---|---|
| Alice | Engineering |
| Bob | Sales |
SELECT t1.Name, t2.DepartmentName
FROM TestTable1 AS t1
INNER JOIN TestTable2 AS t2
ON t1.DepartmentID = t2.DepartmentIDJoins tables without using aliases.
SELECT TestTable1.Name, TestTable2.DepartmentName
FROM TestTable1
INNER JOIN TestTable2
ON TestTable1.DepartmentID = TestTable2.DepartmentIDJoins employees and departments, then groups by department name to count employees and average salary.
Example Output:
| DepartmentName | EmployeeCount | AvgSalary |
|---|---|---|
| Engineering | 3 | 67000 |
| HR | 2 | 79000 |
| IT | 2 | 64000 |
SELECT t2.DepartmentName, COUNT(t1.ID) AS EmployeeCount, AVG(t1.Salary) AS AvgSalary
FROM TestTable1 AS t1
INNER JOIN TestTable2 AS t2
ON t1.DepartmentID = t2.DepartmentID
GROUP BY t2.DepartmentNameIncludes all rows from left table and matching from right.
SELECT t1.Name, t2.Location FROM TestTable1 AS t1 LEFT JOIN TestTable2 AS t2 ON t1.DepartmentID = t2.DepartmentIDPerforms LEFT JOIN between employees and departments, then filters out rows where department name is NULL.
Example Output:
| Name | DepartmentName |
|---|---|
| Alice | Engineering |
| Bob | Sales |
SELECT t1.Name, t2.DepartmentName
FROM TestTable1 AS t1
LEFT JOIN TestTable2 AS t2
ON t1.DepartmentID = t2.DepartmentID
WHERE t2.DepartmentName IS NOT NULLLEFT JOIN employees and departments, group by department name, count employees and average salary. Includes departments with no employees (NULL counts).
Explanation: A LEFT JOIN returns all rows from the left table (TestTable1), along with matching rows from the right table (TestTable2). When no match is found, NULL values are included for columns from the right table.
SELECT t2.DepartmentName,
COUNT(t1.ID) AS EmployeeCount,
AVG(t1.Salary) AS AvgSalary
FROM TestTable1 AS t1
LEFT JOIN TestTable2 AS t2
ON t1.DepartmentID = t2.DepartmentID
GROUP BY t2.DepartmentNameCreates Cartesian product of employees and departments, pairing each employee with every department.
Example Output (Partial):
| Name | DepartmentName |
|---|---|
| Alice | Engineering |
| Alice | Sales |
| Bob | Engineering |
| Bob | Sales |
Explanation: A CROSS JOIN produces a Cartesian product, where each row from the first table is paired with every row from the second table. This can result in a large dataset.
SELECT t1.Name, t2.DepartmentName
FROM TestTable1 AS t1
CROSS JOIN TestTable2 AS t2Uses a subquery to calculate average salaries per department, then filters departments with avg salary > 65000.
Example Output:
| DepartmentID | AvgSalary |
|---|---|
| 101 | 67000 |
| 103 | 79000 |
Explanation: The subquery calculates the average salary grouped by department, and the outer query filters the results to include only departments where the average salary exceeds 65,000.
SELECT *
FROM (
SELECT DepartmentID, AVG(Salary) AS AvgSalary
FROM TestTable1
GROUP BY DepartmentID
) AS SubQuery
WHERE AvgSalary > 65000Joins employees with a subquery that calculates average salary per department.
Example Output:
| Name | AvgSalary |
|---|---|
| Alice | 67000 |
| Bob | 55500 |
Explanation: The subquery calculates the average salary for each department. The main query then joins this result with TestTable1 to associate employee names with their respective department's average salary.
SELECT t1.Name, SubQuery.AvgSalary
FROM TestTable1 AS t1
INNER JOIN (
SELECT DepartmentID, AVG(Salary) AS AvgSalary
FROM TestTable1
GROUP BY DepartmentID
) AS SubQuery
ON t1.DepartmentID = SubQuery.DepartmentIDFilters employees whose salary is in the list of salaries from department 101.
Example Output:
| Name |
|---|
| Alice |
| Charlie |
| Grace |
Description: Subqueries in the WHERE clause with the IN operator are supported. This query correctly filtered names where Salary matches the subquery result.
SELECT Name FROM TestTable1 WHERE Salary IN (SELECT Salary FROM TestTable1 WHERE DepartmentID = 101)Note
This feature is available starting from FactoryTalk Optix version 1.7.x.
Supports subqueries in WHERE clause using comparison operators.
SELECT * FROM Table1 WHERE Column > (SELECT Value FROM Table2)Finds employees in departments located in 'New York' using EXISTS subquery.
Example Output:
| Name |
|---|
| Alice |
| Charlie |
| Grace |
Description: Queries with EXISTS are supported, correctly identifying employees in departments located in "New York."
SELECT Name FROM TestTable1 WHERE EXISTS (SELECT 1 FROM TestTable2 WHERE TestTable2.DepartmentID = TestTable1.DepartmentID AND TestTable2.Location = 'New York')When using the new RecipeX module, multiple recipes with same name and different versions can be created. This query will extract only the recipes with the highest version for each recipe name, by using a subquery in the WHERE clause to filter out recipes that have a higher version available.
SELECT
R.*,
M.*
FROM Recipes AS R
JOIN RecipeMetadata_RecipeSchema1 AS M
ON R.Id = M.RecipeId
WHERE R.RecipeSchemaName = {#RecipeSchemaName:sql_literal}
AND NOT EXISTS (
SELECT 1
FROM Recipes AS R2
WHERE R2.RecipeSchemaName = R.RecipeSchemaName
AND R2.Name = R.Name
AND R2.Version > R.Version
)
ORDER BY R.NameNOTE: This query should be placed in a string formatter and the parameter RecipeSchemaName should be passed with a DynamicLink to retrieve the list of recipes for a given recipe schema name.
Uses subquery for aggregation in FROM.
SELECT AVG(Salary) AS AvgSalary FROM (SELECT * FROM TestTable1 WHERE DepartmentID = 101) AS SubQueryCombines multiple IN subqueries.
SELECT Name FROM TestTable1 WHERE DepartmentID IN (SELECT DepartmentID FROM TestTable2 WHERE Location = 'New York') AND Salary IN (SELECT Salary FROM TestTable1 WHERE DepartmentID = 101)Combines multiple EXISTS subqueries.
SELECT Name FROM TestTable1 WHERE EXISTS (SELECT 1 FROM TestTable2 WHERE TestTable1.DepartmentID = TestTable2.DepartmentID AND TestTable2.Location = 'Chicago') AND EXISTS (SELECT 1 FROM TestTable1 AS Sub WHERE Sub.Salary > 60000 AND Sub.DepartmentID = 101)Mixes IN and EXISTS with correlation.
SELECT Name FROM TestTable1 WHERE DepartmentID IN (SELECT DepartmentID FROM TestTable2 WHERE Location = 'Chicago') AND EXISTS (SELECT 1 FROM TestTable2 WHERE TestTable2.DepartmentID = TestTable1.DepartmentID AND TestTable2.Location = 'New York')Nested IN conditions in subquery.
SELECT Name FROM TestTable1 WHERE DepartmentID IN (SELECT DepartmentID FROM TestTable2 WHERE Location IN (SELECT Location FROM TestTable2 WHERE DepartmentID = 101))Nested EXISTS conditions.
SELECT Name FROM TestTable1 WHERE EXISTS (SELECT 1 FROM TestTable2 WHERE TestTable1.DepartmentID = TestTable2.DepartmentID AND EXISTS (SELECT 1 FROM TestTable1 AS Sub WHERE Sub.Salary > 70000))Deeper nesting with IN.
SELECT Name FROM TestTable1 WHERE DepartmentID IN (SELECT DepartmentID FROM TestTable2 WHERE Location IN (SELECT Location FROM TestTable2 WHERE Location IN (SELECT Location FROM TestTable2 WHERE DepartmentID = 101)))Deeper nesting with EXISTS.
SELECT Name FROM TestTable1 WHERE EXISTS (SELECT 1 FROM TestTable2 WHERE TestTable1.DepartmentID = TestTable2.DepartmentID AND EXISTS (SELECT 1 FROM TestTable1 WHERE EXISTS (SELECT 1 FROM TestTable2 WHERE TestTable2.DepartmentID = TestTable1.DepartmentID)))Complex nested conditions.
SELECT Name FROM TestTable1 WHERE DepartmentID IN (SELECT DepartmentID FROM TestTable2 WHERE Location IN (SELECT Location FROM TestTable2 WHERE EXISTS (SELECT 1 FROM TestTable1 WHERE Salary > 70000)))Subquery in JOIN ON clause.
SELECT t1.Name, t2.DepartmentName FROM TestTable1 AS t1 INNER JOIN (SELECT * FROM TestTable2 WHERE Location = 'Chicago') AS t2 ON t1.DepartmentID = t2.DepartmentIDNested subqueries in JOIN.
SELECT t1.Name FROM TestTable1 AS t1 INNER JOIN (SELECT DepartmentID FROM TestTable2 WHERE EXISTS (SELECT 1 FROM TestTable1 WHERE TestTable1.DepartmentID = TestTable2.DepartmentID)) AS t2 ON t1.DepartmentID = t2.DepartmentIDCombines subqueries and joins.
SELECT t1.Name, t2.DepartmentName FROM TestTable1 AS t1 LEFT JOIN (SELECT DepartmentID, DepartmentName FROM TestTable2 WHERE EXISTS (SELECT 1 FROM TestTable1 WHERE Salary > 60000)) AS t2 ON t1.DepartmentID = t2.DepartmentIDFiltered subquery in FROM.
SELECT * FROM (SELECT * FROM TestTable4 WHERE Duration > 2) AS SubQueryAssigns unique sequential numbers to rows within each department partition, ordered by salary descending.
Example Output:
| Name | DepartmentID | Rank |
|---|---|---|
| Grace | 101 | 1 |
| Charlie | 101 | 2 |
| Alice | 101 | 3 |
Explanation: The ROW_NUMBER function assigns a unique rank to each row within a partition (grouped by DepartmentID), ordered by Salary in descending order.
SELECT Name, DepartmentID,
ROW_NUMBER() OVER (PARTITION BY DepartmentID ORDER BY Salary DESC) AS Rank
FROM TestTable1Assigns ranks within department partitions, handling ties by giving same rank to equal salaries.
Explanation: The RANK function assigns a rank to each row within a partition, ordered by Salary in descending order. Tied rows receive the same rank, with subsequent ranks skipping as needed.
SELECT Name, DepartmentID,
RANK() OVER (PARTITION BY DepartmentID ORDER BY Salary DESC) AS Rank
FROM TestTable1Calculates total salary for each department and adds it to every row in that department.
Example Output:
| Name | DepartmentID | TotalDeptSalary |
|---|---|---|
| Alice | 101 | 201000 |
| Charlie | 101 | 201000 |
Explanation: The SUM function calculates the total Salary for all employees within each DepartmentID. This value is applied to every row in the respective partition.
SELECT Name, DepartmentID,
SUM(Salary) OVER (PARTITION BY DepartmentID) AS TotalDeptSalary
FROM TestTable1Calculates averages within partitions.
SELECT Name, DepartmentID,
AVG(Salary) OVER (PARTITION BY DepartmentID) AS AvgDeptSalary
FROM TestTable1Ranks all employees globally by salary in descending order.
Example Output:
| Name | Salary | DepartmentID | GlobalRank |
|---|---|---|---|
| Diana | 80000 | 103 | 1 |
| Ivy | 78000 | 103 | 2 |
| Grace | 71000 | 101 | 3 |
Explanation: The ROW_NUMBER function assigns a unique global rank to each employee, ordered by salary in descending order. No partitioning is applied, so the ranking is across all rows.
SELECT Name, Salary, DepartmentID,
ROW_NUMBER() OVER (ORDER BY Salary DESC) AS GlobalRank
FROM TestTable1Ranks within department by ascending salary.
SELECT Name, DepartmentID,
ROW_NUMBER() OVER (PARTITION BY DepartmentID ORDER BY Salary ASC) AS RankLowToHigh
FROM TestTable1Ranks with tie-breaking by multiple columns.
SELECT Name, DepartmentID,
ROW_NUMBER() OVER (PARTITION BY DepartmentID ORDER BY Salary DESC, Name ASC) AS Rank
FROM TestTable1Adds window function column to all columns.
SELECT *, ROW_NUMBER() OVER (ORDER BY Timestamp) AS RowNum FROM TestTable4Extracts all datetime components from Timestamp column.
Example Output (Partial):
| ID | Year | Month | Day | Hour | Minute | Second |
|---|---|---|---|---|---|---|
| 1 | 2025 | 1 | 1 | 12 | 0 | 0 |
| 12 | 2025 | 2 | 28 | 23 | 59 | 59 |
| 13 | 2024 | 2 | 29 | 12 | 0 | 0 |
Explanation: The query uses the EXTRACT operator multiple times within the same SELECT statement to retrieve and display year, month, day, hour, minute, and second for each record in TestTable4.
SELECT ID,
EXTRACT(YEAR FROM Timestamp) AS Year,
EXTRACT(MONTH FROM Timestamp) AS Month,
EXTRACT(DAY FROM Timestamp) AS Day,
EXTRACT(HOUR FROM Timestamp) AS Hour,
EXTRACT(MINUTE FROM Timestamp) AS Minute,
EXTRACT(SECOND FROM Timestamp) AS Second
FROM TestTable4Groups events by hour and calculates average duration for each hour.
Example Output:
| Hour | AvgDuration |
|---|---|
| 00 | 2.8 |
| 08 | 3.75 |
| 12 | 2.5625 |
| 23 | 2.93 |
Explanation: The AVG function calculates the average Duration for each hour extracted from the Timestamp. Grouping by Hour ensures the calculation is performed for each unique hour.
SELECT EXTRACT(HOUR FROM Timestamp) AS Hour,
AVG(Duration) AS AvgDuration
FROM TestTable4
GROUP BY Hour
ORDER BY HourGroups events by day and finds maximum duration for each day.
Example Output:
| Day | MaxDuration |
|---|---|
| 01 | 2.5 |
| 02 | 4.0 |
| 31 | 3.0 |
Explanation: The MAX function identifies the maximum Duration for each day extracted from the Timestamp. Grouping by Day ensures the calculation is performed for each unique day.
SELECT EXTRACT(DAY FROM Timestamp) AS Day,
MAX(Duration) AS MaxDuration
FROM TestTable4
GROUP BY Day
ORDER BY DayGroups events by month and counts total events per month.
Example Output:
| Month | EventCount |
|---|---|
| 01 | 11 |
| 02 | 2 |
Explanation: The COUNT(*) function calculates the total number of events for each month extracted from the Timestamp. Grouping by Month ensures events are counted for each unique month.
SELECT EXTRACT(MONTH FROM Timestamp) AS Month,
COUNT(*) AS EventCount
FROM TestTable4
GROUP BY Month
ORDER BY MonthGroups by year, month, day, hour and sums duration.
SELECT
EXTRACT(YEAR FROM Timestamp) AS Year,
EXTRACT(MONTH FROM Timestamp) AS Month,
EXTRACT(DAY FROM Timestamp) AS Day,
EXTRACT(HOUR FROM Timestamp) AS Hour,
SUM(Duration) AS TotalDuration
FROM TestTable4
GROUP BY Year, Month, Day, Hour
ORDER BY Year, Month, Day, HourGroups by hour and sums duration.
SELECT EXTRACT(HOUR FROM Timestamp) AS Hour,
SUM(Duration) AS TotalDuration
FROM TestTable4
GROUP BY HourGroups by full hour periods using subquery.
SELECT Year, Month, Day, Hour,
SUM(Duration) AS TotalDuration
FROM (
SELECT
EXTRACT(YEAR FROM Timestamp) AS Year,
EXTRACT(MONTH FROM Timestamp) AS Month,
EXTRACT(DAY FROM Timestamp) AS Day,
EXTRACT(HOUR FROM Timestamp) AS Hour,
Duration
FROM TestTable4
) AS SubQuery
GROUP BY Year, Month, Day, Hour
ORDER BY Year, Month, Day, HourGroups by extracted data with additional columns.
SELECT EventName,
SUM(Duration) AS TotalDuration
FROM (
SELECT
EXTRACT(YEAR FROM Timestamp) AS Year,
EXTRACT(MONTH FROM Timestamp) AS Month,
EXTRACT(DAY FROM Timestamp) AS Day,
EXTRACT(HOUR FROM Timestamp) AS Hour,
Duration,
EventName
FROM TestTable4
) AS SubQuery
GROUP BY Year, Month, Day, Hour
ORDER BY Year, Month, Day, HourGroups using subquery with extracted values.
SELECT Hour,
SUM(Duration) AS TotalDuration
FROM (
SELECT EXTRACT(HOUR FROM Timestamp) AS Hour,
Duration
FROM TestTable4
) AS SubQuery
GROUP BY Hour
ORDER BY HourFilters by year using string comparison.
SELECT EXTRACT(HOUR FROM Timestamp) AS Hour, SUM(Duration) AS TotalDuration
FROM TestTable4
WHERE EXTRACT(YEAR FROM Timestamp) = '2025'
GROUP BY Hour
ORDER BY HourFilters by month using string comparison.
SELECT EXTRACT(DAY FROM Timestamp) AS Day, AVG(Duration) AS AvgDuration
FROM TestTable4
WHERE EXTRACT(MONTH FROM Timestamp) = '01'
GROUP BY Day
ORDER BY DaySimple single-row update (set Salary for a single Id):
UPDATE UpdateTest SET Salary = 56000 WHERE Id = 2Update rows by matching text column (rename a person):
UPDATE UpdateTest SET Name = 'Eve-Updated' WHERE Name = 'Eve'Update multiple rows using IN:
UPDATE UpdateTest SET Salary = Salary + 1000 WHERE Id IN (1, 3)These are used as a CASE/branching workaround when CASE expressions inside UPDATE may not be accepted by the store (FactoryTalk Optix versions prior to 1.7.x):
-- set High for very large salaries
UPDATE UpdateTest SET Status = 'High' WHERE Salary >= 80000
-- set Medium for mid-range salaries
UPDATE UpdateTest SET Status = 'Medium' WHERE Salary >= 65000 AND Salary < 80000
-- set Low for lower salaries
UPDATE UpdateTest SET Status = 'Low' WHERE Salary < 65000Note
This feature is available starting from FactoryTalk Optix version 1.7.x.
UPDATE UpdateTest
SET Status = CASE
WHEN Salary >= 80000 THEN 'High'
WHEN Salary >= 65000 THEN 'Medium'
ELSE 'Low'
ENDIf that raises a syntax error on your target (FactoryTalk Optix versions prior to 1.7.x), use the grouped-update workaround demonstrated earlier.
Note
This feature is available starting from FactoryTalk Optix version 1.7.x.
Updates values in a table, supporting schema-qualified table names.
UPDATE schema1.table1 SET column1 = 3, ...The following UPDATE forms were tested against the embedded database. Results are summarized and example workarounds are provided.
Attempting to set a column using a scalar subquery (for example, mapping values from another table in a single UPDATE statement) is currently not supported.
Example of unsupported query (will fail):
UPDATE UpdateTest
SET Status = (SELECT NewStatus FROM StatusMap WHERE StatusMap.OldStatus = UpdateTest.Status)
WHERE Status IN (SELECT OldStatus FROM StatusMap)Workaround: perform one simple UPDATE per mapping row (this is compatible and fast for small mapping tables):
-- mapping table has rows (OldStatus, NewStatus) = ('Active','Enabled'), ('Inactive','Disabled')
UPDATE UpdateTest SET Status = 'Enabled' WHERE Status = 'Active'
UPDATE UpdateTest SET Status = 'Disabled' WHERE Status = 'Inactive'Some SQL dialects support UPDATE ... FROM <join> to update rows using a join with another table. These queries are not supported and will produce a syntax error.
Example of unsupported query (will fail):
UPDATE UpdateTest SET Status = StatusMap.NewStatus FROM StatusMap WHERE UpdateTest.Status = StatusMap.OldStatusWorkaround: use per-mapping UPDATE statements as shown above, or run a small client-side loop that queries mapping rows and issues single UPDATE statements for each mapping.
Running UPDATE ... WHERE EXISTS (SELECT 1 FROM Other WHERE Other.key = Target.key ...) is currently not supported. If you rely on correlated EXISTS for updates, translate to an explicit set of WHERE conditions or use per-row updates retrieved by a separate SELECT.
FactoryTalk Optix reliably accepts straightforward UPDATE ... SET ... WHERE <condition> forms. More advanced forms that embed subqueries inside the SET expression, use correlated EXISTS in WHERE, or use UPDATE ... FROM may fail depending on FactoryTalk Optix version. When unsupported, the recommended approach is to break the update into multiple simple UPDATE statements or perform the logic in the client and apply targeted UPDATEs.
Deletes rows from a table based on a condition.
DELETE FROM DeleteTestDemo WHERE id = 1DELETE FROM DeleteTestDemo WHERE id IN (2,3,4)DELETE FROM DeleteTestDemo WHERE id IN (SELECT id FROM OtherTable WHERE Flag = 1)DELETE FROM DeleteTestDemoDELETE FROM DeleteTestDemo WHERE GroupID IN (SELECT GroupID FROM OtherTable WHERE Flag = 1)DELETE FROM DeleteTestExtra2 WHERE Val LIKE 'a%'
DELETE FROM DeleteTestExtra2 WHERE Salary BETWEEN 50000 AND 53000During testing the server did not accept several DELETE variants:
- Correlated EXISTS deletes in the form
DELETE ... WHERE EXISTS (SELECT ... WHERE o.col = target.col ...)are not supported. - Multi-table DELETE / DELETE with JOIN (MySQL/SQL Server style) are not supported.
- CTE-based DELETE (
WITH ... DELETE ...) are not supported. DELETE ... RETURNINGand explicit transaction control (BEGIN/ROLLBACK) are not supported.
If you depend on any of the unsupported forms, ask for help translating to an equivalent supported pattern (for example, using IN with a subquery instead of a correlated EXISTS).
INSERT queries are only allowed using the Insert method of the store. See the Database interactions for details.
Temporary tables provide a way to store intermediate results during query process. Key characteristics include:
- Temporary tables, identified by the
##prefix, can be created and accessed using double-quoted identifiers. - Supported operations include querying, joining, and aggregations however, update operations are not permitted.
- Temporary tables can be dropped successfully, facilitating proper resource management.
Creates temporary table with quoted name.
CREATE TEMPORARY TABLE "##TempTable" AS SELECT DepartmentID, AVG(Salary) AS AvgSalary FROM TestTable1 GROUP BY DepartmentIDQueries data from the temporary table created with department average salaries.
Example Output:
| DepartmentID | AvgSalary |
|---|---|
| 101 | 67000 |
| 102 | 55500 |
| 103 | 79000 |
| 104 | 64000 |
| 105 | 62000 |
SELECT * FROM "##TempTable"Joins employees table with temporary table containing department average salaries.
Example Output:
| Name | AvgSalary |
|---|---|
| Alice | 67000 |
| Bob | 55500 |
| Charlie | 67000 |
| Diana | 79000 |
| Eve | 64000 |
SELECT t1.Name, t2.AvgSalary FROM TestTable1 AS t1 INNER JOIN "##TempTable" AS t2 ON t1.DepartmentID = t2.DepartmentIDRemoves temporary table.
DROP TABLE "##TempTable"Creates temporary table with filtered data.
CREATE TEMPORARY TABLE "##FilteredTemp" AS SELECT ID, Name, Salary FROM TestTable1 WHERE Salary > 60000Queries the temporary table filtered to employees with salary > 60000.
Example Output:
| ID | Name | Salary |
|---|---|---|
| 3 | Charlie | 70000 |
| 4 | Diana | 80000 |
| 5 | Eve | 65000 |
| 7 | Grace | 71000 |
SELECT * FROM "##FilteredTemp"Counts high-salary employees from the filtered temporary table.
Example Output:
| HighSalaryCount |
|---|
| 7 |
SELECT COUNT(*) AS HighSalaryCount FROM "##FilteredTemp"FactoryTalk Optix supports user-defined variables in SQL queries by using string formatting in your application code to construct the SQL query with the desired values before executing it against the database.
Variables has to be escaped or formatted using two dedicated formatters:
sql_literalfor string values, which adds single quotes, escapes internal quotes and formats dates appropriately.sql_identifierfor identifiers (like table or column names), which adds double quotes and escapes internal quotes.
For example:
SELECT * FROM {#TableName:sql_identifier} WHERE Name = {#NameValue:sql_literal}As FactoryTalk Optix does not support all SQL features, some queries may fail due to syntax errors or unsupported constructs. If you encounter issues, consider the following workarounds:
- Use a StringFormatter to build dynamic queries that adjust based on supported features.
CONCATand the||operator are not supported, but you can concatenate a string with something likeSELECT {#Param1:sql_literal} AS NewColumnName FROM Tableand compose the query dynamically.
This query returns the count of unique values in the column Code of the table SQLiteStoreTable1 to populate a pie chard. Each slice of the pie chart will represent a unique value in the column Code and the size of the slice will be the count of the occurrences of that value.
The PieChard object should be populated with:
- Model:
EmbeddedDatabase1 - Query:
SELECT Code, COUNT(*) AS Count FROM SQLiteStoreTable1 GROUP BY Code ORDER BY Count DESC - Label:
{Item}/Code - Value:
{Item}/Count
This query returns the count of unique values in the column StatusMachine of the table SQLiteStoreTable1 to populate a histogram chart. Each bar of the histogram will represent a unique value in the column StatusMachine and the height of the bar will be the count of the occurrences of that value.
- Model:
EmbeddedDatabase1 - Query:
SELECT StatusMachine, COUNT(*) AS Occurrences FROM Machine_state WHERE StatusMachine >= 0 GROUP BY StatusMachine ORDER BY Occurrences DESC - Label:
{Item}/StatusMachine - Value:
{Item}/Occurrences