[{"data":1,"prerenderedAt":1276},["ShallowReactive",2],{"blog-\u002Fblog\u002F2025\u002F08\u002Ftime-series-dashboard-flowfuse-postgresql":3},{"id":4,"title":5,"body":6,"description":12,"extension":1266,"meta":1267,"navigation":154,"path":1272,"seo":1273,"stem":1274,"__hash__":1275},"blog\u002Fblog\u002F2025\u002F08\u002Ftime-series-dashboard-flowfuse-postgresql.md","Building Historical Data Dashboard with FlowFuse Tables",{"type":7,"value":8,"toc":1253},"minimark",[9,13,16,21,24,27,31,34,58,67,71,74,79,82,96,162,167,189,235,261,265,268,271,294,363,396,882,897,901,904,908,911,1149,1157,1161,1164,1205,1214,1217,1222,1226,1229,1232,1235,1249],[10,11,12],"p",{},"In Industrial IoT, tracking data over time is crucial. Whether you’re monitoring temperature changes throughout the day, spotting machine downtime, or analyzing production trends across shifts, a historical data dashboard helps you see important patterns clearly.",[10,14,15],{},"This tutorial guides you through building such a dashboard using FlowFuse Tables. FlowFuse Tables currently provides a managed PostgreSQL database—a reliable and widely used system—which we will use throughout this tutorial to store time-series data.",[17,18,20],"h2",{"id":19},"why-postgresql-for-time-series-data?","Why PostgreSQL for Time-Series Data?",[10,22,23],{},"You might wonder if PostgreSQL can efficiently handle large volumes of time-series data. The answer is yes—when configured properly. Without optimization, query performance can slow as data grows. However, by using techniques like batch inserts and smart indexing, PostgreSQL delivers fast and reliable access even at an industrial scale.",[10,25,26],{},"PostgreSQL is selected as the first database offering in FlowFuse Tables because it is flexible, reliable, and open source. It serves as a solid foundation for FlowFuse Tables. Whether your data comes from IIoT sensors or other sources, PostgreSQL is well equipped to handle it.",[17,28,30],{"id":29},"prerequisites","Prerequisites",[10,32,33],{},"Before we begin, please ensure you have the following set up:",[35,36,37,41,49,55],"ul",{},[38,39,40],"li",{},"A FlowFuse Enterprise account, as FlowFuse Tables is an exclusive Enterprise feature.",[38,42,43,44,48],{},"The ",[45,46,47],"code",{},"@flowfuse\u002Fnode-red-dashboard"," package installed in your FlowFuse instance to create the user interface.",[38,50,43,51,54],{},[45,52,53],{},"node-red-contrib-moment"," node installed for handling date and time operations within your flows.",[38,56,57],{},"FlowFuse Tables configured for your FlowFuse Team with a managed PostgreSQL database.",[10,59,60,61,66],{},"If you are new to FlowFuse Tables, we highly recommend reading our ",[62,63,65],"a",{"href":64},"\u002Fblog\u002F2025\u002F08\u002Fgetting-started-with-flowfuse-tables\u002F","getting started guide"," to familiarize yourself with the basics.",[17,68,70],{"id":69},"creating-an-optimized-database-schema","Creating an Optimized Database Schema",[10,72,73],{},"Our first step is to design a database table structured for both high-speed writes and efficient queries.",[75,76,78],"h3",{"id":77},"step-1:-design-and-create-the-table","Step 1: Design and Create the Table",[10,80,81],{},"We will create a single table to hold all our sensor data. The key is to use appropriate data types and indexes to ensure performance.",[83,84,85,93],"ol",{},[38,86,87,88,92],{},"In your FlowFuse Node-RED instance, drag a ",[89,90,91],"strong",{},"Query node"," onto the canvas.",[38,94,95],{},"Configure the node with the following SQL statement to create the table and its indexes:",[97,98,103],"pre",{"className":99,"code":100,"language":101,"meta":102,"style":102},"language-sql shiki shiki-themes github-light github-dark","CREATE TABLE \"sensor_readings\" (\n    \"id\" SERIAL PRIMARY KEY,\n    \"timestamp\" TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n    \"sensor_id\" VARCHAR(50) NOT NULL,\n    \"location\" VARCHAR(100),\n    \"temperature\" DECIMAL(5,2)\n);\n\nCREATE INDEX \"idx_sensor_timestamp\" ON \"sensor_readings\"(\"sensor_id\", \"timestamp\" DESC);\n","sql","",[45,104,105,113,119,125,131,137,143,149,156],{"__ignoreMap":102},[106,107,110],"span",{"class":108,"line":109},"line",1,[106,111,112],{},"CREATE TABLE \"sensor_readings\" (\n",[106,114,116],{"class":108,"line":115},2,[106,117,118],{},"    \"id\" SERIAL PRIMARY KEY,\n",[106,120,122],{"class":108,"line":121},3,[106,123,124],{},"    \"timestamp\" TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n",[106,126,128],{"class":108,"line":127},4,[106,129,130],{},"    \"sensor_id\" VARCHAR(50) NOT NULL,\n",[106,132,134],{"class":108,"line":133},5,[106,135,136],{},"    \"location\" VARCHAR(100),\n",[106,138,140],{"class":108,"line":139},6,[106,141,142],{},"    \"temperature\" DECIMAL(5,2)\n",[106,144,146],{"class":108,"line":145},7,[106,147,148],{},");\n",[106,150,152],{"class":108,"line":151},8,[106,153,155],{"emptyLinePlaceholder":154},true,"\n",[106,157,159],{"class":108,"line":158},9,[106,160,161],{},"CREATE INDEX \"idx_sensor_timestamp\" ON \"sensor_readings\"(\"sensor_id\", \"timestamp\" DESC);\n",[10,163,164],{},[89,165,166],{},"Schema Breakdown:",[35,168,169,175],{},[38,170,171,174],{},[45,172,173],{},"TIMESTAMPTZ",": We use this data type to store timestamps with timezone information. This is critical for applications with sensors spread across different geographical locations, ensuring data is always consistent.",[38,176,177,180,181,184,185,188],{},[89,178,179],{},"Index",": The composite index on ",[45,182,183],{},"sensor_id"," and ",[45,186,187],{},"timestamp"," (in descending order) is vital for query speed. It allows PostgreSQL to quickly locate and return results for a specific sensor while already ordering them from newest to oldest. This avoids expensive sorting when fetching the most recent readings for a given sensor.",[190,191,192,202,232],"blockquote",{},[10,193,194,197,198,201],{},[89,195,196],{},"Important:"," The ",[45,199,200],{},"DESC"," in the composite index provides a significant performance boost for queries like:",[97,203,205],{"className":99,"code":204,"language":101,"meta":102,"style":102},"SELECT * \nFROM sensor_readings\nWHERE sensor_id = 'sensor_01'\nORDER BY timestamp DESC\nLIMIT 10;\n",[45,206,207,212,217,222,227],{"__ignoreMap":102},[106,208,209],{"class":108,"line":109},[106,210,211],{},"SELECT * \n",[106,213,214],{"class":108,"line":115},[106,215,216],{},"FROM sensor_readings\n",[106,218,219],{"class":108,"line":121},[106,220,221],{},"WHERE sensor_id = 'sensor_01'\n",[106,223,224],{"class":108,"line":127},[106,225,226],{},"ORDER BY timestamp DESC\n",[106,228,229],{"class":108,"line":133},[106,230,231],{},"LIMIT 10;\n",[10,233,234],{},"If you instead need results in chronological order, you could create an ascending index or let PostgreSQL reverse the order at query time.",[83,236,237,251],{"start":121},[38,238,239,240,243,244,246,247,250],{},"To execute this one-time setup, connect an ",[89,241,242],{},"Inject node"," to the input of the ",[89,245,91],{}," and a ",[89,248,249],{},"Debug node"," to its output.",[38,252,253,254,257,258,260],{},"Click ",[89,255,256],{},"Deploy",", then click the button on the ",[89,259,242],{}," to create your table.",[75,262,264],{"id":263},"step-2:-storing-sensor-data-efficiently-with-batch-inserts","Step 2: Storing Sensor Data Efficiently with Batch Inserts",[10,266,267],{},"Writing every single sensor reading to the database individually can create significant overhead and slow down performance. A much more efficient method is to \"batch\" readings together and write them in a single transaction.",[10,269,270],{},"Let's build a flow to simulate sensor data and batch-insert it.",[83,272,273,283],{},[38,274,275,276,278,279,282],{},"Add an ",[89,277,242],{}," configured to repeat every ",[89,280,281],{},"1 second"," to simulate a continuous stream of data.",[38,284,285,286,289,290,293],{},"Connect it to a ",[89,287,288],{},"Change node"," that generates a simulated sensor reading. Configure it to set ",[45,291,292],{},"msg.payload"," using the following JSONata expression:",[97,295,299],{"className":296,"code":297,"language":298,"meta":102,"style":102},"language-json shiki shiki-themes github-light github-dark","    {\n        \"sensor_id\": \"sensor_01\",\n        \"location\": \"Production Line A\",\n        \"temperature\": 20 + $random() * 5\n    }\n","json",[45,300,301,307,323,335,358],{"__ignoreMap":102},[106,302,303],{"class":108,"line":109},[106,304,306],{"class":305},"sVt8B","    {\n",[106,308,309,313,316,320],{"class":108,"line":115},[106,310,312],{"class":311},"sj4cs","        \"sensor_id\"",[106,314,315],{"class":305},": ",[106,317,319],{"class":318},"sZZnC","\"sensor_01\"",[106,321,322],{"class":305},",\n",[106,324,325,328,330,333],{"class":108,"line":121},[106,326,327],{"class":311},"        \"location\"",[106,329,315],{"class":305},[106,331,332],{"class":318},"\"Production Line A\"",[106,334,322],{"class":305},[106,336,337,340,342,345,349,352,355],{"class":108,"line":127},[106,338,339],{"class":311},"        \"temperature\"",[106,341,315],{"class":305},[106,343,344],{"class":311},"20",[106,346,348],{"class":347},"s7hpK"," +",[106,350,351],{"class":347}," $random()",[106,353,354],{"class":347}," *",[106,356,357],{"class":311}," 5\n",[106,359,360],{"class":108,"line":133},[106,361,362],{"class":305},"    }\n",[83,364,365,385],{"start":121},[38,366,367,368,370,371],{},"Add another ",[89,369,288],{}," to add a precise timestamp to each reading.",[35,372,373,379],{},[38,374,375,376],{},"Set ",[45,377,378],{},"msg.payload.timestamp",[38,380,381,382,384],{},"To the value type ",[89,383,187],{},".",[38,386,387,388,391,392,395],{},"Add a ",[89,389,390],{},"Function node"," and name it ",[89,393,394],{},"\"Batch Accumulator\"",". Paste the following JavaScript code into the node — it already includes inline comments explaining each step. This function will accumulate incoming readings in batches until the specified batch size is reached, and then creates the SQL query to perform batch inserts into the database.",[97,397,401],{"className":398,"code":399,"language":400,"meta":102,"style":102},"language-javascript shiki shiki-themes github-light github-dark","\u002F\u002F Set the number of records to collect before triggering a batch insert\nconst batchSize = 100;\n\n\u002F\u002F Retrieve previously stored readings from context (or start with an empty array)\nconst readings = context.get('readings') || [];\n\n\u002F\u002F Add the new reading (from msg.payload) to the readings array\nreadings.push(msg.payload);\n\n\u002F\u002F Check if we have enough readings to perform a batch insert\nif (readings.length >= batchSize) {\n\n    \u002F\u002F Generate parameter placeholders for each reading (4 fields per record)\n    \u002F\u002F Example: ($1, $2, $3, $4), ($5, $6, $7, $8), ...\n    const values = readings.map((_, i) => \n        `($${i * 4 + 1}, $${i * 4 + 2}, $${i * 4 + 3}, $${i * 4 + 4})`\n    ).join(',');\n\n    \u002F\u002F Build the SQL insert query with placeholders\n    msg.query = `\n        INSERT INTO sensor_readings\n        (timestamp, sensor_id, location, temperature)\n        VALUES ${values};\n    `;\n\n    \u002F\u002F Flatten the readings into a single array of values matching the placeholders\n    \u002F\u002F For each reading, we pass: current timestamp, sensor_id, location, temperature\n    msg.params = readings.flatMap(r => [\n        new Date(),          \u002F\u002F Or use r.timestamp if actual reading time is available\n        r.sensor_id,\n        r.location,\n        r.temperature\n    ]);\n\n    \u002F\u002F Clear stored readings in context now that they are being inserted\n    context.set('readings', []);\n\n    \u002F\u002F Return the msg with the SQL query and parameters for execution\n    return msg;\n}\n\n\u002F\u002F If not enough readings collected yet, store them back into context\ncontext.set('readings', readings);\n\n\u002F\u002F Do not send anything forward yet\nreturn null;\n","javascript",[45,402,403,409,427,431,436,467,471,476,487,491,497,515,520,526,532,570,630,646,651,657,669,675,681,693,701,706,712,718,742,757,763,769,775,781,786,792,808,813,819,828,834,839,845,860,865,871],{"__ignoreMap":102},[106,404,405],{"class":108,"line":109},[106,406,408],{"class":407},"sJ8bj","\u002F\u002F Set the number of records to collect before triggering a batch insert\n",[106,410,411,415,418,421,424],{"class":108,"line":115},[106,412,414],{"class":413},"szBVR","const",[106,416,417],{"class":311}," batchSize",[106,419,420],{"class":413}," =",[106,422,423],{"class":311}," 100",[106,425,426],{"class":305},";\n",[106,428,429],{"class":108,"line":121},[106,430,155],{"emptyLinePlaceholder":154},[106,432,433],{"class":108,"line":127},[106,434,435],{"class":407},"\u002F\u002F Retrieve previously stored readings from context (or start with an empty array)\n",[106,437,438,440,443,445,448,452,455,458,461,464],{"class":108,"line":133},[106,439,414],{"class":413},[106,441,442],{"class":311}," readings",[106,444,420],{"class":413},[106,446,447],{"class":305}," context.",[106,449,451],{"class":450},"sScJk","get",[106,453,454],{"class":305},"(",[106,456,457],{"class":318},"'readings'",[106,459,460],{"class":305},") ",[106,462,463],{"class":413},"||",[106,465,466],{"class":305}," [];\n",[106,468,469],{"class":108,"line":139},[106,470,155],{"emptyLinePlaceholder":154},[106,472,473],{"class":108,"line":145},[106,474,475],{"class":407},"\u002F\u002F Add the new reading (from msg.payload) to the readings array\n",[106,477,478,481,484],{"class":108,"line":151},[106,479,480],{"class":305},"readings.",[106,482,483],{"class":450},"push",[106,485,486],{"class":305},"(msg.payload);\n",[106,488,489],{"class":108,"line":158},[106,490,155],{"emptyLinePlaceholder":154},[106,492,494],{"class":108,"line":493},10,[106,495,496],{"class":407},"\u002F\u002F Check if we have enough readings to perform a batch insert\n",[106,498,500,503,506,509,512],{"class":108,"line":499},11,[106,501,502],{"class":413},"if",[106,504,505],{"class":305}," (readings.",[106,507,508],{"class":311},"length",[106,510,511],{"class":413}," >=",[106,513,514],{"class":305}," batchSize) {\n",[106,516,518],{"class":108,"line":517},12,[106,519,155],{"emptyLinePlaceholder":154},[106,521,523],{"class":108,"line":522},13,[106,524,525],{"class":407},"    \u002F\u002F Generate parameter placeholders for each reading (4 fields per record)\n",[106,527,529],{"class":108,"line":528},14,[106,530,531],{"class":407},"    \u002F\u002F Example: ($1, $2, $3, $4), ($5, $6, $7, $8), ...\n",[106,533,535,538,541,543,546,549,552,556,559,562,564,567],{"class":108,"line":534},15,[106,536,537],{"class":413},"    const",[106,539,540],{"class":311}," values",[106,542,420],{"class":413},[106,544,545],{"class":305}," readings.",[106,547,548],{"class":450},"map",[106,550,551],{"class":305},"((",[106,553,555],{"class":554},"s4XuR","_",[106,557,558],{"class":305},", ",[106,560,561],{"class":554},"i",[106,563,460],{"class":305},[106,565,566],{"class":413},"=>",[106,568,569],{"class":305}," \n",[106,571,573,576,578,580,583,585,588,591,593,595,597,599,602,604,606,608,610,612,615,617,619,621,623,625,627],{"class":108,"line":572},16,[106,574,575],{"class":318},"        `($${",[106,577,561],{"class":305},[106,579,354],{"class":413},[106,581,582],{"class":311}," 4",[106,584,348],{"class":413},[106,586,587],{"class":311}," 1",[106,589,590],{"class":318},"}, $${",[106,592,561],{"class":305},[106,594,354],{"class":413},[106,596,582],{"class":311},[106,598,348],{"class":413},[106,600,601],{"class":311}," 2",[106,603,590],{"class":318},[106,605,561],{"class":305},[106,607,354],{"class":413},[106,609,582],{"class":311},[106,611,348],{"class":413},[106,613,614],{"class":311}," 3",[106,616,590],{"class":318},[106,618,561],{"class":305},[106,620,354],{"class":413},[106,622,582],{"class":311},[106,624,348],{"class":413},[106,626,582],{"class":311},[106,628,629],{"class":318},"})`\n",[106,631,633,636,639,641,644],{"class":108,"line":632},17,[106,634,635],{"class":305},"    ).",[106,637,638],{"class":450},"join",[106,640,454],{"class":305},[106,642,643],{"class":318},"','",[106,645,148],{"class":305},[106,647,649],{"class":108,"line":648},18,[106,650,155],{"emptyLinePlaceholder":154},[106,652,654],{"class":108,"line":653},19,[106,655,656],{"class":407},"    \u002F\u002F Build the SQL insert query with placeholders\n",[106,658,660,663,666],{"class":108,"line":659},20,[106,661,662],{"class":305},"    msg.query ",[106,664,665],{"class":413},"=",[106,667,668],{"class":318}," `\n",[106,670,672],{"class":108,"line":671},21,[106,673,674],{"class":318},"        INSERT INTO sensor_readings\n",[106,676,678],{"class":108,"line":677},22,[106,679,680],{"class":318},"        (timestamp, sensor_id, location, temperature)\n",[106,682,684,687,690],{"class":108,"line":683},23,[106,685,686],{"class":318},"        VALUES ${",[106,688,689],{"class":305},"values",[106,691,692],{"class":318},"};\n",[106,694,696,699],{"class":108,"line":695},24,[106,697,698],{"class":318},"    `",[106,700,426],{"class":305},[106,702,704],{"class":108,"line":703},25,[106,705,155],{"emptyLinePlaceholder":154},[106,707,709],{"class":108,"line":708},26,[106,710,711],{"class":407},"    \u002F\u002F Flatten the readings into a single array of values matching the placeholders\n",[106,713,715],{"class":108,"line":714},27,[106,716,717],{"class":407},"    \u002F\u002F For each reading, we pass: current timestamp, sensor_id, location, temperature\n",[106,719,721,724,726,728,731,733,736,739],{"class":108,"line":720},28,[106,722,723],{"class":305},"    msg.params ",[106,725,665],{"class":413},[106,727,545],{"class":305},[106,729,730],{"class":450},"flatMap",[106,732,454],{"class":305},[106,734,735],{"class":554},"r",[106,737,738],{"class":413}," =>",[106,740,741],{"class":305}," [\n",[106,743,745,748,751,754],{"class":108,"line":744},29,[106,746,747],{"class":413},"        new",[106,749,750],{"class":450}," Date",[106,752,753],{"class":305},"(),          ",[106,755,756],{"class":407},"\u002F\u002F Or use r.timestamp if actual reading time is available\n",[106,758,760],{"class":108,"line":759},30,[106,761,762],{"class":305},"        r.sensor_id,\n",[106,764,766],{"class":108,"line":765},31,[106,767,768],{"class":305},"        r.location,\n",[106,770,772],{"class":108,"line":771},32,[106,773,774],{"class":305},"        r.temperature\n",[106,776,778],{"class":108,"line":777},33,[106,779,780],{"class":305},"    ]);\n",[106,782,784],{"class":108,"line":783},34,[106,785,155],{"emptyLinePlaceholder":154},[106,787,789],{"class":108,"line":788},35,[106,790,791],{"class":407},"    \u002F\u002F Clear stored readings in context now that they are being inserted\n",[106,793,795,798,801,803,805],{"class":108,"line":794},36,[106,796,797],{"class":305},"    context.",[106,799,800],{"class":450},"set",[106,802,454],{"class":305},[106,804,457],{"class":318},[106,806,807],{"class":305},", []);\n",[106,809,811],{"class":108,"line":810},37,[106,812,155],{"emptyLinePlaceholder":154},[106,814,816],{"class":108,"line":815},38,[106,817,818],{"class":407},"    \u002F\u002F Return the msg with the SQL query and parameters for execution\n",[106,820,822,825],{"class":108,"line":821},39,[106,823,824],{"class":413},"    return",[106,826,827],{"class":305}," msg;\n",[106,829,831],{"class":108,"line":830},40,[106,832,833],{"class":305},"}\n",[106,835,837],{"class":108,"line":836},41,[106,838,155],{"emptyLinePlaceholder":154},[106,840,842],{"class":108,"line":841},42,[106,843,844],{"class":407},"\u002F\u002F If not enough readings collected yet, store them back into context\n",[106,846,848,851,853,855,857],{"class":108,"line":847},43,[106,849,850],{"class":305},"context.",[106,852,800],{"class":450},[106,854,454],{"class":305},[106,856,457],{"class":318},[106,858,859],{"class":305},", readings);\n",[106,861,863],{"class":108,"line":862},44,[106,864,155],{"emptyLinePlaceholder":154},[106,866,868],{"class":108,"line":867},45,[106,869,870],{"class":407},"\u002F\u002F Do not send anything forward yet\n",[106,872,874,877,880],{"class":108,"line":873},46,[106,875,876],{"class":413},"return",[106,878,879],{"class":311}," null",[106,881,426],{"class":305},[83,883,884,894],{"start":133},[38,885,886,887,889,890,893],{},"Connect the output of the \"Batch Accumulator\" to a ",[89,888,91],{},". This node will receive the fully formed ",[45,891,892],{},"msg.query"," and execute the batch insert.",[38,895,896],{},"Deploy the flow. It will now collect 100 readings (over 100 seconds) and perform a single, highly efficient database write instead of 100 separate ones.",[17,898,900],{"id":899},"building-the-interactive-dashboard","Building the Interactive Dashboard",[10,902,903],{},"With data flowing into our database, let's create a user interface to query and visualize it.",[75,905,907],{"id":906},"step-3:-create-an-interactive-time-range-selector","Step 3: Create an Interactive Time Range Selector",[10,909,910],{},"We'll start with a form that allows users to select a date, time, and duration to view.",[83,912,913,932,981,1034,1051,1098,1105,1114,1129,1135],{},[38,914,915,916,919,920,923,928],{},"Drag a ",[89,917,918],{},"ui_form"," node onto the canvas. Create a new dashboard group for it and add the following form elements:",[921,922],"br",{},[924,925],"img",{"alt":926,"dataZoomable":102,"src":927},"Form widget configuration showing date, time, and window duration fields","\u002Fblog\u002F2025\u002F08\u002Fimages\u002Fform-widget.png",[929,930,931],"em",{},"Time Range Selector form configuration in Node-RED Dashboard",[38,933,934,935,938,939],{},"Connect the output of the form to a ",[89,936,937],{},"Change"," node to format the input. Add the following rules in the Change node:",[35,940,941,966],{},[38,942,375,943,946,947],{},[45,944,945],{},"msg.startDateTime"," to the JSONata expression:",[97,948,950],{"className":296,"code":949,"language":298,"meta":102,"style":102},"payload.start & \"T\" & payload.time & \":00\"\n",[45,951,952],{"__ignoreMap":102},[106,953,954,957,960,963],{"class":108,"line":109},[106,955,956],{"class":305},"payload.start & ",[106,958,959],{"class":318},"\"T\"",[106,961,962],{"class":305}," & payload.time & ",[106,964,965],{"class":318},"\":00\"\n",[38,967,375,968,971,972],{},[45,969,970],{},"msg.windowMinutes"," to the expression:",[97,973,975],{"className":296,"code":974,"language":298,"meta":102,"style":102},"payload.window\n",[45,976,977],{"__ignoreMap":102},[106,978,979],{"class":108,"line":109},[106,980,974],{"class":305},[38,982,387,983,986,987,1025,1027,1031],{},[89,984,985],{},"Date\u002FTime Formatter"," node. This is crucial for handling timezones correctly. Configure it as follows:",[35,988,989,997,1006,1018],{},[38,990,991,994,995],{},[89,992,993],{},"Input property:"," ",[45,996,945],{},[38,998,999,1002,1003],{},[89,1000,1001],{},"Input timezone:"," your local timezone, for example, ",[45,1004,1005],{},"Asia\u002FKolkata",[38,1007,1008,994,1011,1014,1015,1017],{},[89,1009,1010],{},"Output timezone:",[45,1012,1013],{},"Etc\u002FUTC"," (to match the database ",[45,1016,173],{}," standard)",[38,1019,1020,994,1023],{},[89,1021,1022],{},"Output property:",[45,1024,945],{},[921,1026],{},[924,1028],{"alt":1029,"dataZoomable":102,"src":1030},"Date\u002FTime Formatter node configuration showing timezone conversion settings","\u002Fblog\u002F2025\u002F08\u002Fimages\u002Fmoment.png",[929,1032,1033],{},"Configuring the Date\u002FTime Formatter node to convert from local timezone to UTC",[38,1035,367,1036,1038,1039,1042,1043],{},[89,1037,937],{}," node to set the query parameters for the SQL query. Set ",[45,1040,1041],{},"msg.params"," to the following JSONata expression:",[97,1044,1049],{"className":1045,"code":1047,"language":1048},[1046],"language-text","[\n    msg.startDateTime,\n    msg.windowMinutes & \" minutes\"\n]\n","text",[45,1050,1047],{"__ignoreMap":102},[38,1052,1053,1054,1057,1058],{},"Connect this to a ",[89,1055,1056],{},"Query"," node with the following parameterized SQL, which fetches data for the selected time window:",[97,1059,1061],{"className":99,"code":1060,"language":101,"meta":102,"style":102},"SELECT \n    \"timestamp\",\n    \"temperature\"\nFROM \"sensor_readings\"\nWHERE \"sensor_id\" = 'sensor_01'\n  AND \"timestamp\" >= $1::timestamptz\n  AND \"timestamp\" \u003C ($1::timestamptz + $2::interval) ORDER BY \"timestamp\" DESC;\n",[45,1062,1063,1068,1073,1078,1083,1088,1093],{"__ignoreMap":102},[106,1064,1065],{"class":108,"line":109},[106,1066,1067],{},"SELECT \n",[106,1069,1070],{"class":108,"line":115},[106,1071,1072],{},"    \"timestamp\",\n",[106,1074,1075],{"class":108,"line":121},[106,1076,1077],{},"    \"temperature\"\n",[106,1079,1080],{"class":108,"line":127},[106,1081,1082],{},"FROM \"sensor_readings\"\n",[106,1084,1085],{"class":108,"line":133},[106,1086,1087],{},"WHERE \"sensor_id\" = 'sensor_01'\n",[106,1089,1090],{"class":108,"line":139},[106,1091,1092],{},"  AND \"timestamp\" >= $1::timestamptz\n",[106,1094,1095],{"class":108,"line":145},[106,1096,1097],{},"  AND \"timestamp\" \u003C ($1::timestamptz + $2::interval) ORDER BY \"timestamp\" DESC;\n",[38,1099,1100,1101,1104],{},"Connect a ",[89,1102,1103],{},"Debug"," node after the Query node to test the flow.",[38,1106,915,1107,1110,1111,1113],{},[89,1108,1109],{},"Switch"," node onto the canvas and add a condition to check whether ",[45,1112,292],{}," is empty. Connect the output of the Query node to the Switch node.",[38,1115,1116,1117,1119,1120,1122,1123],{},"Connect the output for the \"empty\" condition from the Switch node to a ",[89,1118,937],{}," node that sets ",[45,1121,292],{}," to:",[97,1124,1127],{"className":1125,"code":1126,"language":1048},[1046],"No data found for the selected time range.\n",[45,1128,1126],{"__ignoreMap":102},[38,1130,915,1131,1134],{},[89,1132,1133],{},"ui_notification"," node, select the appropriate UI group, and connect it to the output of the Change node.",[38,1136,1137,1138,558,1141,1144,1145,1148],{},"Deploy the flow. On the dashboard, select a ",[89,1139,1140],{},"date",[89,1142,1143],{},"time",", and ",[89,1146,1147],{},"window"," using the form. Verify in the debug panel that the correct data is returned, or that a notification appears if no data is found.",[10,1150,1151,1155],{},[924,1152],{"alt":1153,"dataZoomable":102,"src":1154},"Complete time range selector form","\u002Fblog\u002F2025\u002F08\u002Fimages\u002Fform-time-range-selector.png",[929,1156,1153],{},[75,1158,1160],{"id":1159},"step-4:-display-the-data-in-a-chart","Step 4: Display the Data in a Chart",[10,1162,1163],{},"The final step is to visualize the query result.",[83,1165,1166,1176,1202],{},[38,1167,1168,1169,1171,1172,1175],{},"Connect the output of the final ",[89,1170,91],{}," to a ",[89,1173,1174],{},"ui_chart"," widget.",[38,1177,1178,1179],{},"Configure the chart node:",[35,1180,1181,1184,1187,1193,1199],{},[38,1182,1183],{},"Group: Create new group for chart.",[38,1185,1186],{},"Type: Line",[38,1188,1189,1190,1192],{},"X: Set to ",[45,1191,187],{}," as a key.",[38,1194,1195,1196,1192],{},"Y: Set to ",[45,1197,1198],{},"temperature",[38,1200,1201],{},"Series: Set to \"Temperature\" as string.",[38,1203,1204],{},"Deploy the flow. Your complete historical data dashboard is now live — you can explore it and experiment with different time ranges to see the results.",[10,1206,1207,1211],{},[924,1208],{"alt":1209,"dataZoomable":102,"src":1210},"historical data dashboard retrieving historical data nd displying it","\u002Fblog\u002F2025\u002F08\u002Fimages\u002Fhistorical-data-dashboard.gif",[929,1212,1213],{},"Historical data dashboard retrieving and displaying historical data",[10,1215,1216],{},"Below is the complete flow we built in this tutorial.",[1218,1219],"render-flow",{":height":1220,"flow":1221},"300","W3siaWQiOiI5MDEzMzcyMTZiNjBiMjVhIiwidHlwZSI6InRhYiIsImxhYmVsIjoiRmxvdyAxIiwiZGlzYWJsZWQiOmZhbHNlLCJpbmZvIjoiIiwiZW52IjpbXX0seyJpZCI6ImU5YTNiNzFkNDBhZGRkOGIiLCJ0eXBlIjoiZ3JvdXAiLCJ6IjoiOTAxMzM3MjE2YjYwYjI1YSIsIm5hbWUiOiJDcmVhdGUgVGFibGUiLCJzdHlsZSI6eyJsYWJlbCI6dHJ1ZX0sIm5vZGVzIjpbIjQ4YjEzODBmZjdiZmU3MTYiLCI0NGJiODc1MGQ5NjFlNDE1IiwiNGEzOTc2ZTA2MmIyZGRkMiJdLCJ4Ijo1NCwieSI6NTksInciOjU3MiwiaCI6ODJ9LHsiaWQiOiIwMTEzN2QwMmM0ODMyOTYyIiwidHlwZSI6Imdyb3VwIiwieiI6IjkwMTMzNzIxNmI2MGIyNWEiLCJuYW1lIjoiU2ltdWxhdGUgU2Vuc29yIGFuZCBwZXJmb3JtIGJhdGNoIGluc2VydCIsInN0eWxlIjp7ImxhYmVsIjp0cnVlfSwibm9kZXMiOlsiZTQ2NWQ0MzI4ZDlkNDJkOSIsIjUwZmI4NTBlM2UzZGI1OGUiLCJiMjFkNmRhYmY3MzFlODY2IiwiZjgyNjljZjQxMmI0MjAwZiIsIjk2ZmRkNDY3MzkxOWIwZDIiLCIwNDM0MGFkM2I5ODRhMmRmIl0sIngiOjU0LCJ5IjoxNTksInciOjEwOTIsImgiOjgyfSx7ImlkIjoiODljODYwYTI3ZmYzZGIxMCIsInR5cGUiOiJncm91cCIsInoiOiI5MDEzMzcyMTZiNjBiMjVhIiwibmFtZSI6Ikhpc3RvcmljYWwgZGF0YSBkYXNoYm9hcmQiLCJzdHlsZSI6eyJsYWJlbCI6dHJ1ZX0sIm5vZGVzIjpbImY0OTMyMjliMjhhYzhjMTEiLCJiOTAxNzY3NjU1M2RjMTAwIiwiMzIyZGVlMzVhMmQ3NzAxNSIsImYzY2E1NTVlZjgxMTQ1M2MiLCIwY2I4ZDMyMzI3YTEwNTMzIiwiYjVkYzJlZDBmYWMwN2YwMiIsImYzYmZkZWU4N2FhYjYxZjgiLCI5NDgxMjAyZWI3YTUwN2I2IiwiMTU5YmQyOThkZTRhOTVkMCJdLCJ4Ijo1NCwieSI6MjU5LCJ3IjoxNjEyLCJoIjoxMjJ9LHsiaWQiOiI2OGZiY2RiMDNlM2E1MzQ2IiwidHlwZSI6Imdyb3VwIiwieiI6IjkwMTMzNzIxNmI2MGIyNWEiLCJzdHlsZSI6eyJzdHJva2UiOiIjYjJiM2JkIiwic3Ryb2tlLW9wYWNpdHkiOiIxIiwiZmlsbCI6IiNmMmYzZmIiLCJmaWxsLW9wYWNpdHkiOiIwLjUiLCJsYWJlbCI6dHJ1ZSwibGFiZWwtcG9zaXRpb24iOiJudyIsImNvbG9yIjoiIzMyMzMzYiJ9LCJub2RlcyI6WyI0ZjI0YWI1YjU2MjhhMWJhIiwiZmJmMzZhN2IzYzU5NDYxZCJdLCJ4Ijo2MzQsInkiOjU5LCJ3IjozOTIsImgiOjgyfSx7ImlkIjoiNDhiMTM4MGZmN2JmZTcxNiIsInR5cGUiOiJpbmplY3QiLCJ6IjoiOTAxMzM3MjE2YjYwYjI1YSIsImciOiJlOWEzYjcxZDQwYWRkZDhiIiwibmFtZSI6IiIsInByb3BzIjpbXSwicmVwZWF0IjoiIiwiY3JvbnRhYiI6IiIsIm9uY2UiOmZhbHNlLCJvbmNlRGVsYXkiOjAuMSwidG9waWMiOiIiLCJ4IjoxNTAsInkiOjEwMCwid2lyZXMiOltbIjQ0YmI4NzUwZDk2MWU0MTUiXV19LHsiaWQiOiI0NGJiODc1MGQ5NjFlNDE1IiwidHlwZSI6InRhYmxlcy1xdWVyeSIsInoiOiI5MDEzMzcyMTZiNjBiMjVhIiwiZyI6ImU5YTNiNzFkNDBhZGRkOGIiLCJuYW1lIjoiQ3JlYXRlIFRhYmxlIiwicXVlcnkiOiIgICBDUkVBVEUgVEFCTEUgXCJzZW5zb3JfcmVhZGluZ3NcIiAoXG4gICAgICAgXCJpZFwiIFNFUklBTCBQUklNQVJZIEtFWSxcbiAgICAgICBcInRpbWVzdGFtcFwiIFRJTUVTVEFNUFRaIE5PVCBOVUxMIERFRkFVTFQgTk9XKCksXG4gICAgICAgXCJzZW5zb3JfaWRcIiBWQVJDSEFSKDUwKSBOT1QgTlVMTCxcbiAgICAgICBcImxvY2F0aW9uXCIgVkFSQ0hBUigxMDApLFxuICAgICAgIFwidGVtcGVyYXR1cmVcIiBERUNJTUFMKDUsMilcbiAgICk7XG5cbiAgIENSRUFURSBJTkRFWCBcImlkeF9zZW5zb3JfdGltZXN0YW1wXCIgT04gXCJzZW5zb3JfcmVhZGluZ3NcIihcInNlbnNvcl9pZFwiLCBcInRpbWVzdGFtcFwiIERFU0MpOyIsInNwbGl0IjpmYWxzZSwicm93c1Blck1zZyI6MSwieCI6MzMwLCJ5IjoxMDAsIndpcmVzIjpbWyI0YTM5NzZlMDYyYjJkZGQyIl1dfSx7ImlkIjoiNGEzOTc2ZTA2MmIyZGRkMiIsInR5cGUiOiJkZWJ1ZyIsInoiOiI5MDEzMzcyMTZiNjBiMjVhIiwiZyI6ImU5YTNiNzFkNDBhZGRkOGIiLCJuYW1lIjoiZGVidWcgMSIsImFjdGl2ZSI6dHJ1ZSwidG9zaWRlYmFyIjp0cnVlLCJjb25zb2xlIjpmYWxzZSwidG9zdGF0dXMiOmZhbHNlLCJjb21wbGV0ZSI6ImZhbHNlIiwic3RhdHVzVmFsIjoiIiwic3RhdHVzVHlwZSI6ImF1dG8iLCJ4Ijo1MjAsInkiOjEwMCwid2lyZXMiOltdfSx7ImlkIjoiZTQ2NWQ0MzI4ZDlkNDJkOSIsInR5cGUiOiJpbmplY3QiLCJ6IjoiOTAxMzM3MjE2YjYwYjI1YSIsImciOiIwMTEzN2QwMmM0ODMyOTYyIiwibmFtZSI6IiIsInByb3BzIjpbeyJwIjoicGF5bG9hZCJ9LHsicCI6InRvcGljIiwidnQiOiJzdHIifV0sInJlcGVhdCI6IjEiLCJjcm9udGFiIjoiIiwib25jZSI6ZmFsc2UsIm9uY2VEZWxheSI6MC4xLCJ0b3BpYyI6IiIsInBheWxvYWQiOiIiLCJwYXlsb2FkVHlwZSI6ImRhdGUiLCJ4IjoxNzAsInkiOjIwMCwid2lyZXMiOltbIjk2ZmRkNDY3MzkxOWIwZDIiXV19LHsiaWQiOiI1MGZiODUwZTNlM2RiNThlIiwidHlwZSI6InRhYmxlcy1xdWVyeSIsInoiOiI5MDEzMzcyMTZiNjBiMjVhIiwiZyI6IjAxMTM3ZDAyYzQ4MzI5NjIiLCJuYW1lIjoiUXVlcnkiLCJxdWVyeSI6ImNvbnN0IGJhdGNoU2l6ZSA9IDEwMDtcbmNvbnN0IHJlYWRpbmdzID0gY29udGV4dC5nZXQoJ3JlYWRpbmdzJykgfHwgW107XG5cbnJlYWRpbmdzLnB1c2gobXNnLnBheWxvYWQpO1xuXG5pZiAocmVhZGluZ3MubGVuZ3RoID49IGJhdGNoU2l6ZSkge1xuICAgIC8vIFByZXBhcmUgYmF0Y2ggaW5zZXJ0XG4gICAgY29uc3QgdmFsdWVzID0gcmVhZGluZ3MubWFwKChfLCBpKSA9PiBcbiAgICAgICAgYCgkJHtpKjcrMX0sICQke2kqNysyfSwgJCR7aSo3KzN9LCAkJHtpKjcrNH0sICQke2kqNys1fSwgJCR7aSo3KzZ9LCAkJHtpKjcrN30pYFxuICAgICkuam9pbignLCcpO1xuICAgIFxuICAgIG1zZy5xdWVyeSA9IGBcbiAgICAgICAgSU5TRVJUIElOVE8gXCJzZW5zb3JfcmVhZGluZ3NcIlxuICAgICAgICAoXCJ0aW1lc3RhbXBcIiwgXCJzZW5zb3JfaWRcIiwgXCJsb2NhdGlvblwiLCBcInRlbXBlcmF0dXJlXCIpXG4gICAgICAgIFZBTFVFUyAke3ZhbHVlc31cbiAgICBgO1xuICAgIFxuICAgIG1zZy5wYXJhbXMgPSByZWFkaW5ncy5mbGF0TWFwKHIgPT4gW1xuICAgICAgICBuZXcgRGF0ZSgpLFxuICAgICAgICByLnNlbnNvcl9pZCxcbiAgICAgICAgci5sb2NhdGlvbixcbiAgICAgICAgci50ZW1wZXJhdHVyZSxcbiAgICBdKTtcbiAgICBcbiAgICBjb250ZXh0LnNldCgncmVhZGluZ3MnLCBbXSk7XG4gICAgcmV0dXJuIG1zZztcbn1cblxuY29udGV4dC5zZXQoJ3JlYWRpbmdzJywgcmVhZGluZ3MpO1xucmV0dXJuIG51bGw7Iiwic3BsaXQiOmZhbHNlLCJyb3dzUGVyTXNnIjoxLCJ4Ijo4OTAsInkiOjIwMCwid2lyZXMiOltbImY4MjY5Y2Y0MTJiNDIwMGYiXV19LHsiaWQiOiJiMjFkNmRhYmY3MzFlODY2IiwidHlwZSI6ImZ1bmN0aW9uIiwieiI6IjkwMTMzNzIxNmI2MGIyNWEiLCJnIjoiMDExMzdkMDJjNDgzMjk2MiIsIm5hbWUiOiJCYXRjaCBBY2N1bXVsYXRvciIsImZ1bmMiOiIvLyBTZXQgdGhlIG51bWJlciBvZiByZWNvcmRzIHRvIGNvbGxlY3QgYmVmb3JlIHRyaWdnZXJpbmcgYSBiYXRjaCBpbnNlcnRcbmNvbnN0IGJhdGNoU2l6ZSA9IDEwMDtcblxuLy8gUmV0cmlldmUgcHJldmlvdXNseSBzdG9yZWQgcmVhZGluZ3MgZnJvbSBjb250ZXh0IChvciBzdGFydCB3aXRoIGFuIGVtcHR5IGFycmF5KVxuY29uc3QgcmVhZGluZ3MgPSBjb250ZXh0LmdldCgncmVhZGluZ3MnKSB8fCBbXTtcblxuLy8gQWRkIHRoZSBuZXcgcmVhZGluZyAoZnJvbSBtc2cucGF5bG9hZCkgdG8gdGhlIHJlYWRpbmdzIGFycmF5XG5yZWFkaW5ncy5wdXNoKG1zZy5wYXlsb2FkKTtcblxuLy8gQ2hlY2sgaWYgd2UgaGF2ZSBlbm91Z2ggcmVhZGluZ3MgdG8gcGVyZm9ybSBhIGJhdGNoIGluc2VydFxuaWYgKHJlYWRpbmdzLmxlbmd0aCA+PSBiYXRjaFNpemUpIHtcblxuICAgIC8vIEdlbmVyYXRlIHBhcmFtZXRlciBwbGFjZWhvbGRlcnMgZm9yIGVhY2ggcmVhZGluZyAoNCBmaWVsZHMgcGVyIHJlY29yZClcbiAgICAvLyBFeGFtcGxlOiAoJDEsICQyLCAkMywgJDQpLCAoJDUsICQ2LCAkNywgJDgpLCAuLi5cbiAgICBjb25zdCB2YWx1ZXMgPSByZWFkaW5ncy5tYXAoKF8sIGkpID0+IFxuICAgICAgICBgKCQke2kgKiA0ICsgMX0sICQke2kgKiA0ICsgMn0sICQke2kgKiA0ICsgM30sICQke2kgKiA0ICsgNH0pYFxuICAgICkuam9pbignLCcpO1xuXG4gICAgLy8gQnVpbGQgdGhlIFNRTCBpbnNlcnQgcXVlcnkgd2l0aCBwbGFjZWhvbGRlcnNcbiAgICBtc2cucXVlcnkgPSBgXG4gICAgICAgIElOU0VSVCBJTlRPIHNlbnNvcl9yZWFkaW5nc1xuICAgICAgICAodGltZXN0YW1wLCBzZW5zb3JfaWQsIGxvY2F0aW9uLCB0ZW1wZXJhdHVyZSlcbiAgICAgICAgVkFMVUVTICR7dmFsdWVzfTtcbiAgICBgO1xuXG4gICAgLy8gRmxhdHRlbiB0aGUgcmVhZGluZ3MgaW50byBhIHNpbmdsZSBhcnJheSBvZiB2YWx1ZXMgbWF0Y2hpbmcgdGhlIHBsYWNlaG9sZGVyc1xuICAgIC8vIEZvciBlYWNoIHJlYWRpbmcsIHdlIHBhc3M6IGN1cnJlbnQgdGltZXN0YW1wLCBzZW5zb3JfaWQsIGxvY2F0aW9uLCB0ZW1wZXJhdHVyZVxuICAgIG1zZy5wYXJhbXMgPSByZWFkaW5ncy5mbGF0TWFwKHIgPT4gW1xuICAgICAgICByLnRpbWVzdGFtcCxcbiAgICAgICAgci5zZW5zb3JfaWQsXG4gICAgICAgIHIubG9jYXRpb24sXG4gICAgICAgIHIudGVtcGVyYXR1cmVcbiAgICBdKTtcblxuICAgIC8vIENsZWFyIHN0b3JlZCByZWFkaW5ncyBpbiBjb250ZXh0IG5vdyB0aGF0IHRoZXkgYXJlIGJlaW5nIGluc2VydGVkXG4gICAgY29udGV4dC5zZXQoJ3JlYWRpbmdzJywgW10pO1xuXG4gICAgLy8gUmV0dXJuIHRoZSBtc2cgd2l0aCB0aGUgU1FMIHF1ZXJ5IGFuZCBwYXJhbWV0ZXJzIGZvciBleGVjdXRpb25cbiAgICByZXR1cm4gbXNnO1xufVxuXG4vLyBJZiBub3QgZW5vdWdoIHJlYWRpbmdzIGNvbGxlY3RlZCB5ZXQsIHN0b3JlIHRoZW0gYmFjayBpbnRvIGNvbnRleHRcbmNvbnRleHQuc2V0KCdyZWFkaW5ncycsIHJlYWRpbmdzKTtcblxucmV0dXJuIG51bGw7Iiwib3V0cHV0cyI6MSwidGltZW91dCI6MCwibm9lcnIiOjAsImluaXRpYWxpemUiOiIiLCJmaW5hbGl6ZSI6IiIsImxpYnMiOltdLCJ4Ijo3MzAsInkiOjIwMCwid2lyZXMiOltbIjUwZmI4NTBlM2UzZGI1OGUiXV19LHsiaWQiOiJmODI2OWNmNDEyYjQyMDBmIiwidHlwZSI6ImRlYnVnIiwieiI6IjkwMTMzNzIxNmI2MGIyNWEiLCJnIjoiMDExMzdkMDJjNDgzMjk2MiIsIm5hbWUiOiJkZWJ1ZyAyIiwiYWN0aXZlIjp0cnVlLCJ0b3NpZGViYXIiOnRydWUsImNvbnNvbGUiOmZhbHNlLCJ0b3N0YXR1cyI6ZmFsc2UsImNvbXBsZXRlIjoiZmFsc2UiLCJzdGF0dXNWYWwiOiIiLCJzdGF0dXNUeXBlIjoiYXV0byIsIngiOjEwNDAsInkiOjIwMCwid2lyZXMiOltdfSx7ImlkIjoiOTZmZGQ0NjczOTE5YjBkMiIsInR5cGUiOiJjaGFuZ2UiLCJ6IjoiOTAxMzM3MjE2YjYwYjI1YSIsImciOiIwMTEzN2QwMmM0ODMyOTYyIiwibmFtZSI6IlNpbXVsYXRlIFNlbnNvciIsInJ1bGVzIjpbeyJ0Ijoic2V0IiwicCI6InBheWxvYWQiLCJwdCI6Im1zZyIsInRvIjoie1x0ICAgXCJzZW5zb3JfaWRcIjogXCJzZW5zb3JfMDFcIixcdCAgIFwibG9jYXRpb25cIjogXCJQcm9kdWN0aW9uIExpbmUgQVwiLFx0ICAgXCJ0ZW1wZXJhdHVyZVwiOiAyMCArICRyYW5kb20oKSAqIDVcdH0iLCJ0b3QiOiJqc29uYXRhIn1dLCJhY3Rpb24iOiIiLCJwcm9wZXJ0eSI6IiIsImZyb20iOiIiLCJ0byI6IiIsInJlZyI6ZmFsc2UsIngiOjM0MCwieSI6MjAwLCJ3aXJlcyI6W1siMDQzNDBhZDNiOTg0YTJkZiJdXX0seyJpZCI6IjA0MzQwYWQzYjk4NGEyZGYiLCJ0eXBlIjoiY2hhbmdlIiwieiI6IjkwMTMzNzIxNmI2MGIyNWEiLCJnIjoiMDExMzdkMDJjNDgzMjk2MiIsIm5hbWUiOiJBZGQgdGltZXN0YW1wIiwicnVsZXMiOlt7InQiOiJzZXQiLCJwIjoicGF5bG9hZC50aW1lc3RhbXAiLCJwdCI6Im1zZyIsInRvIjoiaXNvIiwidG90IjoiZGF0ZSJ9XSwiYWN0aW9uIjoiIiwicHJvcGVydHkiOiIiLCJmcm9tIjoiIiwidG8iOiIiLCJyZWciOmZhbHNlLCJ4Ijo1NDAsInkiOjIwMCwid2lyZXMiOltbImIyMWQ2ZGFiZjczMWU4NjYiXV19LHsiaWQiOiJmNDkzMjI5YjI4YWM4YzExIiwidHlwZSI6InVpLWNoYXJ0IiwieiI6IjkwMTMzNzIxNmI2MGIyNWEiLCJnIjoiODljODYwYTI3ZmYzZGIxMCIsImdyb3VwIjoiOTI2MTM2OTA1NTRkNWJiOSIsIm5hbWUiOiJIaXN0b3JpY2FsIERhdGEgQ2hhcnQiLCJsYWJlbCI6IiIsIm9yZGVyIjoxLCJjaGFydFR5cGUiOiJsaW5lIiwiY2F0ZWdvcnkiOiJUZW1wZXJhdHVyZSIsImNhdGVnb3J5VHlwZSI6InN0ciIsInhBeGlzTGFiZWwiOiIiLCJ4QXhpc1Byb3BlcnR5IjoidGltZXN0YW1wIiwieEF4aXNQcm9wZXJ0eVR5cGUiOiJwcm9wZXJ0eSIsInhBeGlzVHlwZSI6InRpbWUiLCJ4QXhpc0Zvcm1hdCI6IiIsInhBeGlzRm9ybWF0VHlwZSI6ImNjYyBISDptbSIsInhtaW4iOiIiLCJ4bWF4IjoiIiwieUF4aXNMYWJlbCI6IiIsInlBeGlzUHJvcGVydHkiOiJ0ZW1wZXJhdHVyZSIsInlBeGlzUHJvcGVydHlUeXBlIjoicHJvcGVydHkiLCJ5bWluIjoiIiwieW1heCI6IiIsImJpbnMiOjEwLCJhY3Rpb24iOiJyZXBsYWNlIiwic3RhY2tTZXJpZXMiOmZhbHNlLCJwb2ludFNoYXBlIjoiY2lyY2xlIiwicG9pbnRSYWRpdXMiOjQsInNob3dMZWdlbmQiOnRydWUsInJlbW92ZU9sZGVyIjoxLCJyZW1vdmVPbGRlclVuaXQiOiIzNjAwIiwicmVtb3ZlT2xkZXJQb2ludHMiOiIiLCJjb2xvcnMiOlsiIzAwOTVmZiIsIiNmZjAwMDAiLCIjZmY3ZjBlIiwiIzJjYTAyYyIsIiNhMzQ3ZTEiLCIjZDYyNzI4IiwiI2ZmOTg5NiIsIiM5NDY3YmQiLCIjYzViMGQ1Il0sInRleHRDb2xvciI6WyIjNjY2NjY2Il0sInRleHRDb2xvckRlZmF1bHQiOnRydWUsImdyaWRDb2xvciI6WyIjZTVlNWU1Il0sImdyaWRDb2xvckRlZmF1bHQiOnRydWUsIndpZHRoIjoiMTIiLCJoZWlnaHQiOjgsImNsYXNzTmFtZSI6IiIsImludGVycG9sYXRpb24iOiJsaW5lYXIiLCJ4IjoxNTQwLCJ5IjozMDAsIndpcmVzIjpbW11dfSx7ImlkIjoiYjkwMTc2NzY1NTNkYzEwMCIsInR5cGUiOiJ1aS1mb3JtIiwieiI6IjkwMTMzNzIxNmI2MGIyNWEiLCJnIjoiODljODYwYTI3ZmYzZGIxMCIsIm5hbWUiOiIiLCJncm91cCI6ImI5ZTk3NjJjODFiNTVhMDUiLCJsYWJlbCI6IiIsIm9yZGVyIjoxLCJ3aWR0aCI6MCwiaGVpZ2h0IjowLCJvcHRpb25zIjpbeyJsYWJlbCI6IlN0YXJ0Iiwia2V5Ijoic3RhcnQiLCJ0eXBlIjoiZGF0ZSIsInJlcXVpcmVkIjp0cnVlLCJyb3dzIjpudWxsfSx7ImxhYmVsIjoiVGltZSIsImtleSI6InRpbWUiLCJ0eXBlIjoidGltZSIsInJlcXVpcmVkIjp0cnVlLCJyb3dzIjpudWxsfSx7ImxhYmVsIjoiV2luZG93IChtaW51dGVzKSIsImtleSI6IndpbmRvdyIsInR5cGUiOiJudW1iZXIiLCJyZXF1aXJlZCI6dHJ1ZSwicm93cyI6bnVsbH1dLCJmb3JtVmFsdWUiOnsic3RhcnQiOiIiLCJ0aW1lIjoiIiwid2luZG93IjoiIn0sInBheWxvYWQiOiIiLCJzdWJtaXQiOiJzdWJtaXQiLCJjYW5jZWwiOiJjbGVhciIsInJlc2V0T25TdWJtaXQiOnRydWUsInRvcGljIjoidG9waWMiLCJ0b3BpY1R5cGUiOiJtc2ciLCJzcGxpdExheW91dCI6IiIsImNsYXNzTmFtZSI6IiIsInBhc3N0aHJ1IjpmYWxzZSwiZHJvcGRvd25PcHRpb25zIjpbXSwieCI6MTMwLCJ5IjozMDAsIndpcmVzIjpbWyIzMjJkZWUzNWEyZDc3MDE1Il1dfSx7ImlkIjoiMzIyZGVlMzVhMmQ3NzAxNSIsInR5cGUiOiJjaGFuZ2UiLCJ6IjoiOTAxMzM3MjE2YjYwYjI1YSIsImciOiI4OWM4NjBhMjdmZjNkYjEwIiwibmFtZSI6IiIsInJ1bGVzIjpbeyJ0Ijoic2V0IiwicCI6InN0YXJ0RGF0ZVRpbWUiLCJwdCI6Im1zZyIsInRvIjoicGF5bG9hZC5zdGFydCAmIFwiVFwiICYgcGF5bG9hZC50aW1lICYgXCI6MDBcIiIsInRvdCI6Impzb25hdGEifSx7InQiOiJzZXQiLCJwIjoid2luZG93TWludXRlcyIsInB0IjoibXNnIiwidG8iOiJwYXlsb2FkLndpbmRvdyIsInRvdCI6Im1zZyJ9XSwiYWN0aW9uIjoiIiwicHJvcGVydHkiOiIiLCJmcm9tIjoiIiwidG8iOiIiLCJyZWciOmZhbHNlLCJ4IjozNDAsInkiOjMwMCwid2lyZXMiOltbIjBjYjhkMzIzMjdhMTA1MzMiXV19LHsiaWQiOiJmM2NhNTU1ZWY4MTE0NTNjIiwidHlwZSI6InRhYmxlcy1xdWVyeSIsInoiOiI5MDEzMzcyMTZiNjBiMjVhIiwiZyI6Ijg5Yzg2MGEyN2ZmM2RiMTAiLCJuYW1lIjoiIiwicXVlcnkiOiIgICBTRUxFQ1QgXG4gICAgICAgXCJ0aW1lc3RhbXBcIixcbiAgICAgICBcInRlbXBlcmF0dXJlXCJcbiAgIEZST00gXCJzZW5zb3JfcmVhZGluZ3NcIlxuICAgV0hFUkUgXCJzZW5zb3JfaWRcIiA9ICdzZW5zb3JfMDEnXG4gICAgIEFORCBcInRpbWVzdGFtcFwiID49ICQxOjp0aW1lc3RhbXB0elxuICAgICBBTkQgXCJ0aW1lc3RhbXBcIiA8ICgkMTo6dGltZXN0YW1wdHogKyAkMjo6aW50ZXJ2YWwpIE9SREVSIEJZIFwidGltZXN0YW1wXCIgREVTQzsiLCJzcGxpdCI6ZmFsc2UsInJvd3NQZXJNc2ciOjEsIngiOjkzMCwieSI6MzAwLCJ3aXJlcyI6W1siZjQ5MzIyOWIyOGFjOGMxMSIsImYzYmZkZWU4N2FhYjYxZjgiXV19LHsiaWQiOiIwY2I4ZDMyMzI3YTEwNTMzIiwidHlwZSI6Im1vbWVudCIsInoiOiI5MDEzMzcyMTZiNjBiMjVhIiwiZyI6Ijg5Yzg2MGEyN2ZmM2RiMTAiLCJuYW1lIjoiIiwidG9waWMiOiIiLCJpbnB1dCI6InN0YXJ0RGF0ZVRpbWUiLCJpbnB1dFR5cGUiOiJtc2ciLCJpblR6IjoiQXNpYS9Lb2xrYXRhIiwiYWRqQW1vdW50IjowLCJhZGpUeXBlIjoiZGF5cyIsImFkakRpciI6ImFkZCIsImZvcm1hdCI6IiIsImxvY2FsZSI6ImVuLVVTIiwib3V0cHV0Ijoic3RhcnREYXRlVGltZSIsIm91dHB1dFR5cGUiOiJtc2ciLCJvdXRUeiI6IkVUQy9VVEMiLCJ4Ijo1NDAsInkiOjMwMCwid2lyZXMiOltbImI1ZGMyZWQwZmFjMDdmMDIiXV19LHsiaWQiOiJiNWRjMmVkMGZhYzA3ZjAyIiwidHlwZSI6ImNoYW5nZSIsInoiOiI5MDEzMzcyMTZiNjBiMjVhIiwiZyI6Ijg5Yzg2MGEyN2ZmM2RiMTAiLCJuYW1lIjoiU2V0IFBhcmFtcyIsInJ1bGVzIjpbeyJ0Ijoic2V0IiwicCI6InBhcmFtcyIsInB0IjoibXNnIiwidG8iOiJbICAgICBtc2cuc3RhcnREYXRlVGltZSwgICAgIG1zZy53aW5kb3dNaW51dGVzICYgXCIgbWludXRlc1wiICAgXSIsInRvdCI6Impzb25hdGEifV0sImFjdGlvbiI6IiIsInByb3BlcnR5IjoiIiwiZnJvbSI6IiIsInRvIjoiIiwicmVnIjpmYWxzZSwieCI6NzcwLCJ5IjozMDAsIndpcmVzIjpbWyJmM2NhNTU1ZWY4MTE0NTNjIl1dfSx7ImlkIjoiZjNiZmRlZTg3YWFiNjFmOCIsInR5cGUiOiJzd2l0Y2giLCJ6IjoiOTAxMzM3MjE2YjYwYjI1YSIsImciOiI4OWM4NjBhMjdmZjNkYjEwIiwibmFtZSI6IklzIHBheWxvYWQgZW1wdHk\u002FIiwicHJvcGVydHkiOiJwYXlsb2FkIiwicHJvcGVydHlUeXBlIjoibXNnIiwicnVsZXMiOlt7InQiOiJlbXB0eSJ9XSwiY2hlY2thbGwiOiJ0cnVlIiwicmVwYWlyIjpmYWxzZSwib3V0cHV0cyI6MSwieCI6MTExMCwieSI6MzQwLCJ3aXJlcyI6W1siOTQ4MTIwMmViN2E1MDdiNiJdXX0seyJpZCI6Ijk0ODEyMDJlYjdhNTA3YjYiLCJ0eXBlIjoiY2hhbmdlIiwieiI6IjkwMTMzNzIxNmI2MGIyNWEiLCJnIjoiODljODYwYTI3ZmYzZGIxMCIsIm5hbWUiOiJOb3RpZmljYXRpb24gTWVzc2FnZSIsInJ1bGVzIjpbeyJ0Ijoic2V0IiwicCI6InBheWxvYWQiLCJwdCI6Im1zZyIsInRvIjoiTm8gZGF0YSBmb3VuZCBmb3IgdGhlIHNlbGVjdGVkIHRpbWUgcmFuZ2UuIiwidG90Ijoic3RyIn1dLCJhY3Rpb24iOiIiLCJwcm9wZXJ0eSI6IiIsImZyb20iOiIiLCJ0byI6IiIsInJlZyI6ZmFsc2UsIngiOjEzMjAsInkiOjM0MCwid2lyZXMiOltbIjE1OWJkMjk4ZGU0YTk1ZDAiXV19LHsiaWQiOiIxNTliZDI5OGRlNGE5NWQwIiwidHlwZSI6InVpLW5vdGlmaWNhdGlvbiIsInoiOiI5MDEzMzcyMTZiNjBiMjVhIiwiZyI6Ijg5Yzg2MGEyN2ZmM2RiMTAiLCJ1aSI6ImFmZWEwNGNlODczNWMwYTYiLCJwb3NpdGlvbiI6ImNlbnRlciBjZW50ZXIiLCJjb2xvckRlZmF1bHQiOnRydWUsImNvbG9yIjoiIzAwMDAwMCIsImRpc3BsYXlUaW1lIjoiMyIsInNob3dDb3VudGRvd24iOnRydWUsIm91dHB1dHMiOjEsImFsbG93RGlzbWlzcyI6dHJ1ZSwiZGlzbWlzc1RleHQiOiJDbG9zZSIsImFsbG93Q29uZmlybSI6ZmFsc2UsImNvbmZpcm1UZXh0IjoiQ29uZmlybSIsInJhdyI6ZmFsc2UsImNsYXNzTmFtZSI6IiIsIm5hbWUiOiIiLCJ4IjoxNTMwLCJ5IjozNDAsIndpcmVzIjpbW11dfSx7ImlkIjoiNGYyNGFiNWI1NjI4YTFiYSIsInR5cGUiOiJjYXRjaCIsInoiOiI5MDEzMzcyMTZiNjBiMjVhIiwiZyI6IjY4ZmJjZGIwM2UzYTUzNDYiLCJuYW1lIjoiIiwic2NvcGUiOm51bGwsInVuY2F1Z2h0IjpmYWxzZSwieCI6NzIwLCJ5IjoxMDAsIndpcmVzIjpbWyJmYmYzNmE3YjNjNTk0NjFkIl1dfSx7ImlkIjoiZmJmMzZhN2IzYzU5NDYxZCIsInR5cGUiOiJkZWJ1ZyIsInoiOiI5MDEzMzcyMTZiNjBiMjVhIiwiZyI6IjY4ZmJjZGIwM2UzYTUzNDYiLCJuYW1lIjoiZGVidWcgNiIsImFjdGl2ZSI6dHJ1ZSwidG9zaWRlYmFyIjp0cnVlLCJjb25zb2xlIjpmYWxzZSwidG9zdGF0dXMiOmZhbHNlLCJjb21wbGV0ZSI6ImZhbHNlIiwic3RhdHVzVmFsIjoiIiwic3RhdHVzVHlwZSI6ImF1dG8iLCJ4Ijo5MjAsInkiOjEwMCwid2lyZXMiOltdfSx7ImlkIjoiOTI2MTM2OTA1NTRkNWJiOSIsInR5cGUiOiJ1aS1ncm91cCIsIm5hbWUiOiJIaXN0b3JpY2FsIENoYXJ0IiwicGFnZSI6ImQwM2EzODY1MGJmMzA4MmYiLCJ3aWR0aCI6IjEyIiwiaGVpZ2h0IjoxLCJvcmRlciI6Miwic2hvd1RpdGxlIjp0cnVlLCJjbGFzc05hbWUiOiIiLCJ2aXNpYmxlIjoidHJ1ZSIsImRpc2FibGVkIjoiZmFsc2UiLCJncm91cFR5cGUiOiJkZWZhdWx0In0seyJpZCI6ImI5ZTk3NjJjODFiNTVhMDUiLCJ0eXBlIjoidWktZ3JvdXAiLCJuYW1lIjoiRm9ybSIsInBhZ2UiOiJkMDNhMzg2NTBiZjMwODJmIiwid2lkdGgiOiIxMiIsImhlaWdodCI6MSwib3JkZXIiOjEsInNob3dUaXRsZSI6ZmFsc2UsImNsYXNzTmFtZSI6IiIsInZpc2libGUiOiJ0cnVlIiwiZGlzYWJsZWQiOiJmYWxzZSIsImdyb3VwVHlwZSI6ImRlZmF1bHQifSx7ImlkIjoiYWZlYTA0Y2U4NzM1YzBhNiIsInR5cGUiOiJ1aS1iYXNlIiwibmFtZSI6IlVJIE5hbWUiLCJwYXRoIjoiL2Rhc2hib2FyZCIsImFwcEljb24iOiIiLCJpbmNsdWRlQ2xpZW50RGF0YSI6dHJ1ZSwiYWNjZXB0c0NsaWVudENvbmZpZyI6WyJ1aS1jb250cm9sIiwidWktbm90aWZpY2F0aW9uIl0sInNob3dQYXRoSW5TaWRlYmFyIjpmYWxzZSwiaGVhZGVyQ29udGVudCI6InBhZ2UiLCJuYXZpZ2F0aW9uU3R5bGUiOiJpY29uIiwidGl0bGVCYXJTdHlsZSI6ImRlZmF1bHQiLCJzaG93UmVjb25uZWN0Tm90aWZpY2F0aW9uIjp0cnVlLCJub3RpZmljYXRpb25EaXNwbGF5VGltZSI6NSwic2hvd0Rpc2Nvbm5lY3ROb3RpZmljYXRpb24iOnRydWUsImFsbG93SW5zdGFsbCI6dHJ1ZX0seyJpZCI6ImQwM2EzODY1MGJmMzA4MmYiLCJ0eXBlIjoidWktcGFnZSIsIm5hbWUiOiJIaXN0b3JpY2FsIERhdGEgRGFzaGJvYXJkIiwidWkiOiJhZmVhMDRjZTg3MzVjMGE2IiwicGF0aCI6Ii9oaXN0b3JpY2FsLWRhdGEiLCJpY29uIjoiaG9tZSIsImxheW91dCI6ImdyaWQiLCJ0aGVtZSI6IjZkOGJmZjVmM2ZkZWQ1YzIiLCJicmVha3BvaW50cyI6W3sibmFtZSI6IkRlZmF1bHQiLCJweCI6IjAiLCJjb2xzIjoiMyJ9LHsibmFtZSI6IlRhYmxldCIsInB4IjoiNTc2IiwiY29scyI6IjYifSx7Im5hbWUiOiJTbWFsbCBEZXNrdG9wIiwicHgiOiI3NjgiLCJjb2xzIjoiOSJ9LHsibmFtZSI6IkRlc2t0b3AiLCJweCI6IjEwMjQiLCJjb2xzIjoiMTIifV0sIm9yZGVyIjozLCJjbGFzc05hbWUiOiIiLCJ2aXNpYmxlIjoidHJ1ZSIsImRpc2FibGVkIjoiZmFsc2UifSx7ImlkIjoiNmQ4YmZmNWYzZmRlZDVjMiIsInR5cGUiOiJ1aS10aGVtZSIsIm5hbWUiOiJGRiBUaGVtZSIsImNvbG9ycyI6eyJzdXJmYWNlIjoiIzUwNDZlNSIsInByaW1hcnkiOiIjNTA0NmU1IiwiYmdQYWdlIjoiI2ZmZmZmZiIsImdyb3VwQmciOiIjZmZmZmZmIiwiZ3JvdXBPdXRsaW5lIjoiI2Q0ZDFmZiJ9LCJzaXplcyI6eyJkZW5zaXR5IjoiZGVmYXVsdCIsInBhZ2VQYWRkaW5nIjoiMTVweCIsImdyb3VwR2FwIjoiMTVweCIsImdyb3VwQm9yZGVyUmFkaXVzIjoiNHB4Iiwid2lkZ2V0R2FwIjoiMTJweCJ9fSx7ImlkIjoiYTBkYWIyYTVmOWY0ZTNkMiIsInR5cGUiOiJnbG9iYWwtY29uZmlnIiwiZW52IjpbXSwibW9kdWxlcyI6eyJAZmxvd2Z1c2UvbnItdGFibGVzLW5vZGVzIjoiMC4xLjAiLCJAZmxvd2Z1c2Uvbm9kZS1yZWQtZGFzaGJvYXJkIjoiMS4yNi4wIiwibm9kZS1yZWQtY29udHJpYi1tb21lbnQiOiI1LjAuMCJ9fV0=",[17,1223,1225],{"id":1224},"conclusion","Conclusion",[10,1227,1228],{},"You have successfully built a historical data dashboard using FlowFuse Tables and Node-RED. By implementing efficient batch inserts and optimized query patterns, you have created a solution that is both powerful and scalable for demanding Industrial IoT environments.",[10,1230,1231],{},"With FlowFuse Tables now part of the platform, you can build complete industrial applications without juggling external databases or leaving the FlowFuse environment. FlowFuse is now a comprehensive data platform with the ability to collect, connect, transform, store, and visualize data. Combined with FlowFuse's enterprise features—team collaboration, version control, device management, and secure deployments—you have everything needed to take your IIoT projects from prototype to production within one integrated platform.",[10,1233,1234],{},"This means less complexity and faster time to value for your industrial data initiatives. Your historical dashboards, real-time monitoring, and OEE dashboards can all live in the same ecosystem, managed by the same team, with consistent security and governance controls.",[10,1236,1237,1238,1244,1245],{},"Ready to build your own time-series dashboard? ",[62,1239,1243],{"href":1240,"rel":1241},"https:\u002F\u002Fapp.flowfuse.com\u002Faccount\u002Fcreate",[1242],"nofollow","Get started with FlowFuse Tables"," or ",[62,1246,1248],{"href":1247},"\u002Fblueprints\u002F","explore our industrial blueprints",[1250,1251,1252],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":102,"searchDepth":115,"depth":115,"links":1254},[1255,1256,1257,1261,1265],{"id":19,"depth":115,"text":20},{"id":29,"depth":115,"text":30},{"id":69,"depth":115,"text":70,"children":1258},[1259,1260],{"id":77,"depth":121,"text":78},{"id":263,"depth":121,"text":264},{"id":899,"depth":115,"text":900,"children":1262},[1263,1264],{"id":906,"depth":121,"text":907},{"id":1159,"depth":121,"text":1160},{"id":1224,"depth":115,"text":1225},"md",{"navTitle":5,"excerpt":1268},{"type":7,"value":1269},[1270],[10,1271,12],{},"\u002Fblog\u002F2025\u002F08\u002Ftime-series-dashboard-flowfuse-postgresql",{"title":5,"description":12},"blog\u002F2025\u002F08\u002Ftime-series-dashboard-flowfuse-postgresql","oygYQvyOUzLlOm6Ih8XUGPNvt1GmHZq9-fxb8_RAfTs",1780132428301]