{"id":45800,"date":"2026-11-08T05:33:08","date_gmt":"2026-11-08T05:33:08","guid":{"rendered":"https:\/\/www.withdata.com\/blog\/?p=45800"},"modified":"2026-11-08T05:33:08","modified_gmt":"2026-11-08T05:33:08","slug":"redshift-to-excel-row-limits-precision-and-time-zones","status":"publish","type":"post","link":"https:\/\/www.withdata.com\/blog\/dbcopier\/redshift-to-excel-row-limits-precision-and-time-zones.html","title":{"rendered":"Redshift to Excel: Row Limits, Precision and Time Zones"},"content":{"rendered":"<p>Exporting a Redshift query to Excel looks like a solved problem \u2014 until someone opens the file and asks why the invoice number lost its last five digits, why the timestamp moved eight hours, and why there are seventeen sheets instead of one.<\/p>\n<p>None of those are bugs in the query. They are what happens when data that is wider than Excel meets Excel. This article shows the boundaries with numbers measured on a real export run.<\/p>\n<hr\/>\n<h2>1. What was measured, and with what<\/h2>\n<p>Excel is the same destination whatever the source is, so the limits below are properties of the Excel writer, not of Redshift. To keep the test reproducible we ran the export against a local <strong>PostgreSQL 15<\/strong> database using Redshift&#8217;s data types (<code>numeric(38,10)<\/code>, <code>timestamptz<\/code>, <code>boolean<\/code>, <code>varchar(65535)<\/code>), which is the same type family Redshift exposes).<\/p>\n<p>Two notes on the test itself, because they matter if you try to repeat it:<\/p>\n<ul>\n<li>The Redshift provider in this product <strong>refuses a PostgreSQL server on purpose<\/strong> (it validates the<\/li>\n<\/ul>\n<p>server banner: &#8220;PostgreSQL servers are not supported&#8221;). It was measured against a PG source with the PostgreSQL provider \u2014 the file writer, which produces the limits below, is the same code path.<\/p>\n<ul>\n<li>While testing we found that the <strong>command line could not connect to Redshift at all<\/strong>: the loader<\/li>\n<\/ul>\n<p>builds its session file by hand and never wrote the <code>Endpoint<\/code> key the Redshift logon option reads, so it failed with <code>\"\" is an invalid integer<\/code>. Fixed in the build dated 2026-11-08.<\/p>\n<p>Environment: Windows, PostgreSQL 15.1 on the same machine, Excel writer of DBToFile.<\/p>\n<h2>2. The sheet split: 65,536 rows per sheet<\/h2>\n<p>Excel 2007 and later support 1,048,576 rows per sheet \u2014 but the writer splits at the <strong>older 65,536-row boundary<\/strong>, which is what Excel 97\u20132003 supported.<\/p>\n<table>\n<thead>\n<tr>\n<th>Rows exported<\/th>\n<th>Sheets produced<\/th>\n<th>Rows per sheet<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>200,000<\/td>\n<td><strong>4<\/strong> (<code>events<\/code>, <code>events_2<\/code>, <code>events_3<\/code>, <code>events_4<\/code>)<\/td>\n<td>65,535 + header<\/td>\n<\/tr>\n<tr>\n<td>1,100,000<\/td>\n<td><strong>17<\/strong><\/td>\n<td>16 \u00d7 65,535 + 51,440<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>So a 1.1-million-row pull arrives as seventeen worksheets, each headed with the same column names. That is often exactly what you want (a pivot source that fits in memory), but it is a surprise if you expected one sheet and wrote a formula that references it by name.<\/p>\n<p>Two practical consequences:<\/p>\n<ul>\n<li><strong>Any downstream formula must union the sheets.<\/strong> <code>events!A:A<\/code> only sees the first 65,535 rows.<\/li>\n<li><strong>The split is by row count, not by key.<\/strong> If the sort order changes between exports, the same<\/li>\n<\/ul>\n<p>record can land on a different sheet.<\/p>\n<h2>3. <code>numeric(38,10)<\/code> is not a number Excel has<\/h2>\n<p>Excel stores every number as a double: 15 significant digits. A Redshift <code>numeric(38,10)<\/code> keeps 38 digits. The conversion is silent \u2014 no warning, no error, just digits that are no longer there.<\/p>\n<p>Measured on the same export:<\/p>\n<table>\n<thead>\n<tr>\n<th>Value in Redshift (<code>numeric(38,10)<\/code>)<\/th>\n<th>Value in the cell<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>1.0000000001<\/code><\/td>\n<td><code>1.0000000001<\/code> (fits)<\/td>\n<\/tr>\n<tr>\n<td><code>12345678901234567890.1234567890<\/code><\/td>\n<td><code>1.23456789012346E+19<\/code> (only 15 significant digits survive)<\/td>\n<\/tr>\n<tr>\n<td><code>9999999999999999999999999999.9999999999<\/code><\/td>\n<td><code>1E+28<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>0.0000000001<\/code><\/td>\n<td><code>1E-10<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>What this means in practice:<\/p>\n<ul>\n<li><strong>Amounts with cents are fine<\/strong> (<code>numeric(12,2)<\/code> is well inside 15 digits).<\/li>\n<li><strong>Ids, hashes, bank accounts and long decimals are not.<\/strong> If a column identifies something rather<\/li>\n<\/ul>\n<p>than measuring it, export it as text: cast it in the query (<code>id::text<\/code>) before it becomes a cell.<\/p>\n<ul>\n<li>Excel also drops trailing zeros, so <code>1.10<\/code> and <code>1.1<\/code> are the same cell. Reports that need two<\/li>\n<\/ul>\n<p>decimals must format the column afterwards.<\/p>\n<h2>4. <code>timestamptz<\/code> becomes local wall-clock time<\/h2>\n<p>A Redshift <code>timestamptz<\/code> (or PG <code>timestamptz<\/code>) carries an offset. Excel cells carry a date and a time and nothing else. The writer converts to the <strong>machine&#8217;s local time zone<\/strong> and drops the offset:<\/p>\n<table>\n<thead>\n<tr>\n<th>Stored value<\/th>\n<th>Cell content<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>2024-03-01 00:00:00+00<\/code><\/td>\n<td><code>2024-03-01 08:00:00<\/code> (machine is at +08:00)<\/td>\n<\/tr>\n<tr>\n<td><code>2024-03-01 12:00:00+08<\/code><\/td>\n<td><code>2024-03-01 12:00:00<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>2024-07-01 00:00:00+00<\/code><\/td>\n<td><code>2024-07-01 08:00:00<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>If the workbook is opened on a machine in another time zone, the numbers no longer mean the same instant. For anything with an audit trail, either export <code>to_char(ts at time zone 'UTC', 'YYYY-MM-DD HH24:MI:SS')<\/code> as text, or keep a separate UTC column.<\/p>\n<h2>5. <code>boolean<\/code> and <code>NULL<\/code><\/h2>\n<ul>\n<li><strong><code>boolean<\/code> arrives as text<\/strong> \u2014 <code>True<\/code> \/ <code>False<\/code> \u2014 not as an Excel boolean. <code>SUM()<\/code> over such a<\/li>\n<\/ul>\n<p>column returns 0; <code>COUNTIF(range,\"True\")<\/code> works.<\/p>\n<ul>\n<li><strong><code>NULL<\/code> arrives as an empty cell<\/strong>, which is the same as an empty string in Excel. <code>COUNTBLANK()<\/code><\/li>\n<\/ul>\n<p>will count both, and <code>=A1=\"\"<\/code> is true for both. If the difference matters, export a <code>IS NULL<\/code> flag column.<\/p>\n<ul>\n<li><strong>Long text is preserved<\/strong>: a 2,000-character <code>varchar<\/code> column came through complete in the same<\/li>\n<\/ul>\n<p>export (the 32,767-character cell limit is the one to watch; anything above that is truncated).<\/p>\n<h2>6. What it costs<\/h2>\n<table>\n<thead>\n<tr>\n<th>Table<\/th>\n<th>Rows<\/th>\n<th>Columns<\/th>\n<th>Time<\/th>\n<th>File size<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>events<\/code><\/td>\n<td>200,000<\/td>\n<td>6 (incl. <code>numeric(38,10)<\/code>, <code>timestamptz<\/code>, <code>boolean<\/code>, 2,000-char text)<\/td>\n<td><strong>9.6 s<\/strong><\/td>\n<td>7.3 MB<\/td>\n<\/tr>\n<tr>\n<td><code>events_big<\/code><\/td>\n<td>1,100,000<\/td>\n<td>2<\/td>\n<td><strong>21.0 s<\/strong><\/td>\n<td>16.9 MB<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Both are local-to-local on a laptop-class machine; a networked Redshift will add transfer time, and the xlsx writer is CPU-bound, so expect the file side to dominate on wide tables.<\/p>\n<h2>7. When Excel is the wrong destination<\/h2>\n<ul>\n<li><strong>Above ~1 million rows<\/strong> you get a sheet split, which is fine for analysis but painful as a<\/li>\n<\/ul>\n<p>deliverable. CSV has no such limit (one file, 1.1M rows, no splitting).<\/p>\n<ul>\n<li><strong>When the values must stay exact<\/strong> (ids, money with more than 15 digits, timestamps with an<\/li>\n<\/ul>\n<p>offset), Excel cannot hold them. CSV or a database-to-database copy can.<\/p>\n<ul>\n<li><strong>When it is a recurring job<\/strong>: schedule the export and let it overwrite the same file, rather than<\/li>\n<\/ul>\n<p>keeping a manual &#8220;export 2026-11-08 final v3.xlsx&#8221; collection.<\/p>\n<h2>8. Verifying an export before you send it<\/h2>\n<ol>\n<li><strong>Compare row counts per sheet<\/strong> with the source count; 17 sheets of 65,535 means the split ran.<\/li>\n<li><strong>Check one id column<\/strong> for a value with more than 15 digits \u2014 if it shows as <code>1.23E+19<\/code>, the column<\/li>\n<\/ol>\n<p>should have been text.<\/p>\n<ol>\n<li><strong>Open a timestamp cell<\/strong> and compare it with the stored value; if they differ by whole hours, the<\/li>\n<\/ol>\n<p>offset was dropped locally.<\/p>\n<ol>\n<li><strong>Search for <code>TRUE<\/code>\/<code>FALSE<\/code> as text<\/strong> in a boolean column, and decide whether the report needs real<\/li>\n<\/ol>\n<p>booleans (it usually does not, but filters do).<\/p>\n<ol>\n<li><strong>Check the last sheet<\/strong> \u2014 the final partial sheet is where a truncated export shows up first.<\/li>\n<\/ol>\n<h2>9. Doing it without a script<\/h2>\n<p>The direct pair page <a href=\"https:\/\/www.withdata.com\/tools\/db-to-file\/redshift-to-excel\" target=\"_blank\" rel=\"noopener noreferrer\">Redshift to Excel<\/a> runs the export locally (no data leaves the machine), and <a href=\"https:\/\/www.withdata.com\/tools\/file-to-db\/excel-to-redshift\" target=\"_blank\" rel=\"noopener noreferrer\">Excel to Redshift<\/a> does the reverse. For a scheduled pull, the same GUI session can be saved and run from the command line.<\/p>\n<hr\/>\n<h2>FAQ<\/h2>\n<h3>Why did my export produce multiple sheets?<\/h3>\n<p>The writer splits at 65,536 rows per worksheet (the Excel 97\u20132003 limit). 1,100,000 rows becomes 17 sheets: sixteen full ones plus a partial last sheet.<\/p>\n<h3>Why did a long number lose digits?<\/h3>\n<p>Excel stores numbers as doubles: 15 significant digits. A <code>numeric(38,10)<\/code> value with more digits is rounded silently. Cast identifier-like columns to text in the query.<\/p>\n<h3>Why did the timestamps shift by a few hours?<\/h3>\n<p><code>timestamptz<\/code> was converted to the machine&#8217;s local time zone and the offset was dropped. Export a UTC string if the workbook has to be time-zone independent.<\/p>\n<h3>Are booleans real booleans in the file?<\/h3>\n<p>No \u2014 they arrive as the text <code>True<\/code> \/ <code>False<\/code>. Numeric aggregation over them returns 0.<\/p>\n<h3>How do I keep leading zeros (zip codes, account numbers)?<\/h3>\n<p>They are numbers to Excel and lose the zeros. Export them as text (<code>lpad(zip::text, 5, '0')<\/code>) or prefix with <code>'<\/code>.<\/p>\n<h3>Does the cell limit matter?<\/h3>\n<p>Excel caps a cell at 32,767 characters. A 2,000-character text column is safe; a 50,000-character document field will be cut.<\/p>\n<h3>Can the export be scheduled?<\/h3>\n<p>Yes \u2014 save the session in the GUI and run it from the command line on a schedule. The CLI reads the same session file.<\/p>\n<h3>Is CSV better for big pulls?<\/h3>\n<p>For anything above ~1M rows or with values that must stay exact, yes. CSV keeps every digit and does not split; Excel is for the reader, not for the data.<\/p>\n<hr\/>\n<p><em>Measured 2026-11-08 against PostgreSQL 15.1 (Redshift type family) on a local machine, exporting with DBToFile. Row limits, numeric rounding and time-zone behaviour are properties of the Excel writer and were read back from the produced workbook, not taken from documentation. The Redshift provider validates the server banner and refuses PostgreSQL, so the Redshift handshake itself was not exercised here; the command-line Endpoint defect found during this test was fixed the same day.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Exporting a Redshift query to Excel looks like a solved problem \u2014 until someone opens the file and asks why the invoice number lost its last five digits, why the timestamp moved eight hours, and why there are seventeen sheets instead of one. None of those are bugs in the query. They are what happens [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2774],"tags":[13772,13852,13851,13853,5573,13850],"class_list":["post-45800","post","type-post","status-publish","format-standard","hentry","category-dbcopier","tag-dbtofile","tag-excel-row-limit","tag-export-redshift-to-excel","tag-numeric-precision","tag-redshift-to-csv","tag-redshift-to-excel"],"_links":{"self":[{"href":"https:\/\/www.withdata.com\/blog\/wp-json\/wp\/v2\/posts\/45800","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.withdata.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.withdata.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.withdata.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.withdata.com\/blog\/wp-json\/wp\/v2\/comments?post=45800"}],"version-history":[{"count":1,"href":"https:\/\/www.withdata.com\/blog\/wp-json\/wp\/v2\/posts\/45800\/revisions"}],"predecessor-version":[{"id":45801,"href":"https:\/\/www.withdata.com\/blog\/wp-json\/wp\/v2\/posts\/45800\/revisions\/45801"}],"wp:attachment":[{"href":"https:\/\/www.withdata.com\/blog\/wp-json\/wp\/v2\/media?parent=45800"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.withdata.com\/blog\/wp-json\/wp\/v2\/categories?post=45800"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.withdata.com\/blog\/wp-json\/wp\/v2\/tags?post=45800"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}