Query Ownership in an Easy Chart DB Pen
An Easy Chart DB pen does not run a query you write. You give it a table name, a value column, a timestamp column and an optional WHERE fragment. The component then builds its own SELECT and adds a timestamp range filter from the chart's current start and end dates. Paste a full statement such as SELECT * FROM (SELECT ... ROW_NUMBER() OVER ...) x WHERE x.count % 2 = 0 into the pen's SQL field and the component wraps it inside its own generated query. The result is invalid SQL, or a pen that never returns rows.
That wrapping is why a custom decimation query fails on a DB pen with no useful error. The pen expects a table-shaped source and cannot accept an arbitrary result set. There are two ways around it: give the pen a table-shaped object that already contains the decimation, or move to a component whose data you bind directly to a query.
Row Stride Against Rows per Timestamp
Before choosing a component, check the query itself. The number that matters is rows per timestamp. In the sample output, even-numbered rows 2 through 16 all carry 2013-11-07 20:00:00.000, and row 18 starts 20:10:00.000. That means 16 rows share each timestamp. The values also repeat: row 2 (-6.4999) matches row 18 (-6.4999), and row 4 (1558.476) matches row 20 (1556.476). This is a tall table in which each timestamp holds one row per channel.
Two consequences follow:
-
Ties in the ORDER BY.
ROW_NUMBER() OVER (ORDER BY myDate)gives 16 tied rows per timestamp. The database can number tied rows in any order, and that order can change between executions or after an index rebuild.% 2 = 0then keeps a different set of 8 channels on different runs. - Stride and channel count. With 16 rows per timestamp, stride 2 keeps 8 channels per timestamp. Stride 3 does not divide 16, so the selected channels rotate at every timestamp (derived from 16 mod 3 = 1). Each plotted series then becomes a mix of different channels.
If the goal is fewer points per channel over time, partition the numbering by channel. If the goal is fewer channels, filter by channel and do not use row numbers at all. The column that identifies the channel is not in the sample output. The code below uses seriesId as a placeholder for it.
-- T-SQL style; % is the modulo operator
SELECT myDate, myValue
FROM (
SELECT myDate, myValue,
ROW_NUMBER() OVER (PARTITION BY seriesId ORDER BY myDate) AS rn
FROM myTable
WHERE seriesId = 1 -- one channel per pen/series
) x
WHERE x.rn % 2 = 0
ORDER BY myDate;
Approaches Compared
You can get the decimated result onto a chart in three ways. Tags are not a data source for the history itself. They are useful as parameters inside the query, for example a tag that holds the stride or the channel number.
| Approach | Where the query lives | Date range handling | Result | Main pitfall |
|---|---|---|---|---|
| Full SQL in Easy Chart DB pen | Pen SQL field | Automatic | Fails: the pen wraps the text in its own SELECT | No clear error; blank pen |
| Database view + Easy Chart DB pen | View on the database server | Automatic; the Easy Chart adds a range filter on the view's timestamp column | Works; keeps the Easy Chart pen tools and zoom | Row numbering runs over the whole table, not the visible window |
Classic Chart with SQL query binding on Data
|
Binding on the component | Manual; bind properties or tags into the WHERE clause | Works; confirmed in the field | Column order: timestamp must come first |
Recommended Path: Classic Chart Data Binding
Use a Classic Chart with a SQL query binding on its Data property. This setup is confirmed to plot the decimated result. It also gives you full control over where the date filter sits relative to ROW_NUMBER(), and that choice changes which points appear.
The view approach is the right choice when the operator workflow depends on Easy Chart features such as pen selection and interactive zoom. Its limit is that the Easy Chart's range filter is applied outside the view. The numbering therefore runs over every row in the table on every refresh, and the chart only trims the output afterwards.
| Filter placement | Effect on points | Effect on database load |
|---|---|---|
Date filter inside the subquery (before ROW_NUMBER()) |
Numbering restarts at the window start; the kept points shift when the range moves | Low: an index on myDate limits the scan to the window |
| Date filter outside the subquery (after numbering) | Kept points are fixed to absolute row position and stay stable across windows | High: every refresh numbers the entire table |
Put the filter inside for trending screens that refresh often. Put it outside only when points must line up exactly across different windows, and keep the table small or archived.
Blank Chart Diagnostics
A Classic Chart that shows nothing and reports no error almost always received a dataset in a shape it cannot plot. It does not mean the query failed. The chart reads the first column as the X axis and every column after it as a series.
| Symptom | Cause | Where to read it | Fix |
|---|---|---|---|
| Chart blank, no error, query returns rows |
SELECT * returned myValue first and myDate second, so the X axis is not a date |
Binding preview / dataset viewer on Data: check column 0's type |
Select myDate, myValue explicitly, timestamp first |
| Extra straight rising line | Row-number column (count/rn) left in the output and plotted as a series |
Column list in the dataset viewer | Leave the row-number column out of the outer SELECT |
| Easy Chart DB pen empty | Full SELECT typed into the pen SQL field | Pen configuration | Create a view, or use the Classic Chart binding |
| Series jumps between very different magnitudes (e.g. ~-6 to ~1558) | Several channels per timestamp mixed into one series by row decimation | Count the rows per timestamp in the raw table |
PARTITION BY the channel column and filter per series |
| Different points on each refresh | Tied ORDER BY myDate gives non-deterministic numbering |
Run the query twice and compare | Add a unique tie-breaker column to the ORDER BY
|
| Binding error after adding the date range | Property reference not enclosed in single quotes | Binding error overlay / diagnostics | Wrap each inserted property or tag reference in '...'
|
Binding Procedure With a Date Range Component
The Classic Chart has no start date or end date field. The time window exists only in the WHERE clause of the binding, so the range goes into the query text.
- Place a Classic Chart and a Date Range component on the same window.
- Open the binding editor on the Classic Chart's
Dataproperty and choose a SQL query binding against the correct database connection. - Enter the query with the timestamp column first and no row-number column in the outer SELECT.
- Place the cursor where the start date goes. Use the property-browse button on the right side of the query dialog to insert the Date Range component's start-date property. Repeat for the end date. Use the tag-browse button in the same place if the range or stride comes from tags.
- Enclose each inserted reference in single quotes. The binding substitutes the value as text, and an unquoted date string is a SQL syntax error.
- Set the binding's polling to match how fast rows arrive. With 10-minute sample spacing, fast polling adds load and no new points.
- Close the editor and check the dataset in the designer before changing chart appearance.
SELECT myDate, myValue
FROM (
SELECT myDate, myValue,
ROW_NUMBER() OVER (PARTITION BY seriesId ORDER BY myDate) AS rn
FROM myTable
WHERE seriesId = 1
AND myDate BETWEEN '{<Date Range start property>}'
AND '{<Date Range end property>}'
) x
WHERE x.rn % 2 = 0
ORDER BY myDate
The <...> tokens are placeholders. Replace them with the exact paths the property browser inserts. Do not type the paths by hand. To make the stride adjustable, replace the literal 2 with a reference to a numeric property or tag. A number needs no quotes.
For the view alternative, create the view once on the database. Then point the Easy Chart DB pen at the view name as its table, with myDate as the timestamp column and myValue as the value column:
CREATE VIEW myTable_decimated AS
SELECT myDate, myValue, seriesId
FROM (
SELECT myDate, myValue, seriesId,
ROW_NUMBER() OVER (PARTITION BY seriesId ORDER BY myDate) AS rn
FROM myTable
) x
WHERE x.rn % 2 = 0;
Keep seriesId in the view so each pen's WHERE fragment can select one channel.
Verification Quantities
Check the result against counts. A chart that looks right can still be showing the wrong subset of rows.
| Quantity | Expected value | Where to read it |
|---|---|---|
| Rows returned per channel | About raw rows in window ÷ stride | Row count in the binding preview vs. COUNT(*) on the raw table for the same window |
| Column 0 type | Date/time | Dataset viewer column header |
| Series count | Equal to value columns selected, with no row-number series | Chart legend |
| Repeatability | Identical rows on two consecutive executions | Run the query twice in a database query tool |
| Query duration | Well below the polling interval | Database query tool execution time; server activity monitor |
Move the Date Range handles and confirm three things: the row count scales with the window width, the first timestamp follows the range start, and the query duration stays flat. If duration grows with table size rather than window size, the date filter is sitting outside the numbering, or myDate has no index. This is database load, not chart logic.
FAQ
What happens if I paste a full SELECT with ROW_NUMBER into an Easy Chart DB pen?
The pen wraps your text in its own generated SELECT and timestamp range filter. The combined statement is invalid or returns nothing, so the pen stays empty. Put the query in a database view and point the pen at the view, or use a Classic Chart with a SQL binding on Data.
What happens if a Classic Chart query uses SELECT * with the value column first?
The chart reads column 0 as the X axis. A numeric value there instead of a timestamp gives a blank chart with no error. Select myDate, myValue explicitly, timestamp first, and leave out the row-number column.
What happens if several channels share each timestamp and I decimate with ROW_NUMBER() % 3?
With 16 rows per timestamp, a stride of 3 selects a different set of channels at each timestamp, so every series becomes a mix of unrelated values. Add PARTITION BY on the channel column and filter one channel per series.
How do I bind a Date Range component to a Classic Chart SQL query?
Insert the Date Range start and end properties into the WHERE clause with the property button in the binding dialog. Enclose each reference in single quotes, for example myDate BETWEEN '{start}' AND '{end}'. Tags can go into the query the same way through the tag button.
What happens if the view and the binding both look correct but the chart is still blank?
Run the exact substituted query in a database query tool against the same connection, and read the gateway and client logs for query errors. If the query returns correctly typed rows there but the component still shows nothing, stop changing SQL. Open a case with Inductive Automation support and include the dataset preview, the binding text and the log entries.