[{"data":1,"prerenderedAt":1801},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fquery-excel-files-with-sql-using-duckdb":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fquery-excel-files-with-sql-using-duckdb":1767,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fquery-excel-files-with-sql-using-duckdb":1776,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fquery-excel-files-with-sql-using-duckdb":1781,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fquery-excel-files-with-sql-using-duckdb":1782},{"id":4,"title":5,"body":6,"dateModified":1733,"datePublished":1733,"description":1734,"extension":1735,"faq":1736,"keyword":1745,"kind":1746,"meta":1747,"navigation":280,"path":1756,"seo":1757,"slug":1760,"stem":1761,"topics":1762,"type":1765,"__hash__":1766},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fquery-excel-files-with-sql-using-duckdb\u002Findex.md","Query Excel Files with SQL Using DuckDB",{"type":7,"value":8,"toc":1721},"minimark",[9,28,214,219,249,252,660,664,670,743,774,778,784,845,864,932,949,1047,1051,1060,1201,1209,1213,1220,1338,1358,1362,1373,1424,1435,1564,1568,1624,1628,1656,1660,1666,1672,1678,1684,1688,1717],[10,11,12,13,17,18,23,24,27],"p",{},"Some questions about Excel data are easiest to ask in SQL: totals by region joined to a targets table, the top ten products per month, rows in this month's export that were not in last month's. With DuckDB you can ask them directly against the ",[14,15,16],"code",{},".xlsx"," files, without loading each one into pandas first and without a database server. This guide, part of ",[19,20,22],"a",{"href":21},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002F","Moving Data Between Excel and Databases",", uses DuckDB's ",[14,25,26],{},"read_xlsx"," on report-style workbooks — a title above the table, a stray \"n\u002Fa\" in a number column — and covers the options that make those files behave.",[29,30,38,39,38,43,38,47,38,55,38,65,38,76,38,83,38,88,38,92,38,98,38,103,38,108,38,115,38,119,38,122,38,125,38,129,38,132,38,135,38,141,38,146,38,150,38,154,38,158,38,162,38,165,38,168,38,174,38,178,38,181,38,184,38,188,38,191,38,196,38,203,38,208,38,211],"svg",{"viewBox":31,"role":32,"ariaLabelledBy":33,"xmlns":36,"style":37},"4 0 752 160","img",[34,35],"dk-flow-t","dk-flow-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[40,41,42],"title",{"id":34},"From Excel files to SQL results and back",[44,45,46],"desc",{"id":35},"Load the excel extension, read each workbook's table range with read_xlsx, combine the files with UNION ALL BY NAME, join them with pandas DataFrames or other files in SQL, and write the result back to Excel with COPY or to pandas with df().",[48,49],"rect",{"x":50,"y":51,"width":52,"height":53,"fill":54},"4","0","752","160","#ffffff",[48,56],{"x":57,"y":58,"width":59,"height":60,"rx":61,"fill":62,"stroke":63,"style":64},"20.0","16.0","115.2","128.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[66,67,69],"g",{"transform":68},"translate(65.6,37.8) scale(1.000)",[70,71],"path",{"d":72,"fill":73,"stroke":74,"style":75},"M12 2.5l8.5 4.7v9.6L12 21.5l-8.5-4.7V7.2z M3.5 7.2l8.5 4.7 8.5-4.7 M12 11.9v9.6","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[77,78,82],"text",{"x":79,"y":80,"style":81},"77.6","84.2","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","LOAD excel",[77,84,87],{"x":79,"y":85,"style":86},"103.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","once per",[77,89,91],{"x":79,"y":90,"style":86},"121.2","connection",[93,94],"line",{"x1":95,"y1":96,"x2":97,"y2":96,"stroke":63,"style":64},"140.2","80.0","162.2",[99,100],"polygon",{"points":101,"fill":102},"166.2,80.0 157.2,85.0 157.2,75.0","#5b5cf0",[48,104],{"x":105,"y":58,"width":59,"height":60,"rx":61,"fill":106,"stroke":107,"style":64},"171.2","#d9f4f1","var(--teal,#0f9488)",[66,109,111],{"transform":110},"translate(216.8,37.8) scale(1.000)",[70,112],{"d":113,"fill":73,"stroke":114,"style":75},"M4 4h16v16H4z M4 9h16 M4 14h16 M10 4v16","var(--teal-ink,#0b6157)",[77,116,26],{"x":117,"y":80,"style":118},"228.8","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[77,120,121],{"x":117,"y":85,"style":86},"range,",[77,123,124],{"x":117,"y":90,"style":86},"stop_at_empty",[93,126],{"x1":127,"y1":96,"x2":128,"y2":96,"stroke":63,"style":64},"291.4","313.4",[99,130],{"points":131,"fill":102},"317.4,80.0 308.4,85.0 308.4,75.0",[48,133],{"x":134,"y":58,"width":59,"height":60,"rx":61,"fill":62,"stroke":63,"style":64},"322.4",[66,136,138],{"transform":137},"translate(368.0,29.0) scale(1.000)",[70,139],{"d":140,"fill":73,"stroke":74,"style":75},"M12 3l9 5-9 5-9-5z M3 13l9 5 9-5 M3 17l9 5 9-5",[77,142,145],{"x":143,"y":144,"style":81},"380.0","75.5","Combine",[77,147,149],{"x":143,"y":148,"style":81},"93.0","files",[77,151,153],{"x":143,"y":152,"style":86},"112.5","UNION ALL BY",[77,155,157],{"x":143,"y":156,"style":86},"130.0","NAME",[93,159],{"x1":160,"y1":96,"x2":161,"y2":96,"stroke":63,"style":64},"442.6","464.6",[99,163],{"points":164,"fill":102},"468.6,80.0 459.6,85.0 459.6,75.0",[48,166],{"x":167,"y":58,"width":59,"height":60,"rx":61,"fill":62,"stroke":63,"style":64},"473.6",[66,169,171],{"transform":170},"translate(519.2,37.8) scale(1.000)",[70,172],{"d":173,"fill":73,"stroke":74,"style":75},"M5 6c0-1.7 3.1-3 7-3s7 1.3 7 3-3.1 3-7 3-7-1.3-7-3z M5 6v12c0 1.7 3.1 3 7 3s7-1.3 7-3V6 M5 12c0 1.7 3.1 3 7 3s7-1.3 7-3",[77,175,177],{"x":176,"y":80,"style":81},"531.2","SQL + joins",[77,179,180],{"x":176,"y":85,"style":86},"pandas, CSV,",[77,182,183],{"x":176,"y":90,"style":86},"Parquet",[93,185],{"x1":186,"y1":96,"x2":187,"y2":96,"stroke":63,"style":64},"593.8","615.8",[99,189],{"points":190,"fill":102},"619.8,80.0 610.8,85.0 610.8,75.0",[48,192],{"x":193,"y":58,"width":59,"height":60,"rx":61,"fill":194,"stroke":195,"style":64},"624.8","#fdefd8","var(--gold,#b4740a)",[66,197,199],{"transform":198},"translate(670.4,37.8) scale(1.000)",[70,200],{"d":201,"fill":73,"stroke":202,"style":75},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6","var(--gold-ink,#7a4e06)",[77,204,207],{"x":205,"y":80,"style":206},"682.4","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Results out",[77,209,210],{"x":205,"y":85,"style":86},"COPY TO xlsx,",[77,212,213],{"x":205,"y":90,"style":86},"df()",[215,216,218],"h2",{"id":217},"prerequisites","Prerequisites",[220,221,226],"pre",{"className":222,"code":223,"language":224,"meta":225,"style":225},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install duckdb pandas openpyxl\n","bash","",[14,227,228],{"__ignoreMap":225},[229,230,232,236,240,243,246],"span",{"class":93,"line":231},1,[229,233,235],{"class":234},"sMTad","pip",[229,237,239],{"class":238},"srMev"," install",[229,241,242],{"class":238}," duckdb",[229,244,245],{"class":238}," pandas",[229,247,248],{"class":238}," openpyxl\n",[10,250,251],{},"The excel extension is downloaded the first time you install it and cached afterwards; on a machine without internet access, install it once on a connected machine and copy the extension directory. Two monthly exports with a title line above the table:",[220,253,257],{"className":254,"code":255,"language":256,"meta":225,"style":225},"language-python shiki shiki-themes github-light github-dark-high-contrast","import datetime as dt\n\nimport duckdb\nimport pandas as pd\nfrom openpyxl import Workbook\n\ndata = {\n    \"2026-01\": [[\"North\", \"Tea\", 120, dt.date(2026, 1, 5)], [\"South\", \"Tea\", 80, dt.date(2026, 1, 9)]],\n    \"2026-02\": [[\"North\", \"Coffee\", 95, dt.date(2026, 2, 2)], [\"East\", \"Tea\", \"n\u002Fa\", dt.date(2026, 2, 3)]],\n}\nfor month, rows in data.items():\n    wb = Workbook()\n    ws = wb.active\n    ws.title = \"Sales\"\n    ws.append([f\"Sales report {month}\"])\n    ws.append([])\n    ws.append([\"region\", \"product\", \"revenue\", \"date\"])\n    for row in rows:\n        ws.append(row)\n    wb.save(f\"sales_{month}.xlsx\")\n\ncon = duckdb.connect()\ncon.sql(\"INSTALL excel\")\ncon.sql(\"LOAD excel\")\n","python",[14,258,259,275,282,290,303,317,322,334,405,467,473,488,499,510,521,549,555,580,594,600,623,628,639,650],{"__ignoreMap":225},[229,260,261,265,269,272],{"class":93,"line":231},[229,262,264],{"class":263},"s-kum","import",[229,266,268],{"class":267},"skGVy"," datetime ",[229,270,271],{"class":263},"as",[229,273,274],{"class":267}," dt\n",[229,276,278],{"class":93,"line":277},2,[229,279,281],{"emptyLinePlaceholder":280},true,"\n",[229,283,285,287],{"class":93,"line":284},3,[229,286,264],{"class":263},[229,288,289],{"class":267}," duckdb\n",[229,291,293,295,298,300],{"class":93,"line":292},4,[229,294,264],{"class":263},[229,296,297],{"class":267}," pandas ",[229,299,271],{"class":263},[229,301,302],{"class":267}," pd\n",[229,304,306,309,312,314],{"class":93,"line":305},5,[229,307,308],{"class":263},"from",[229,310,311],{"class":267}," openpyxl ",[229,313,264],{"class":263},[229,315,316],{"class":267}," Workbook\n",[229,318,320],{"class":93,"line":319},6,[229,321,281],{"emptyLinePlaceholder":280},[229,323,325,328,331],{"class":93,"line":324},7,[229,326,327],{"class":267},"data ",[229,329,330],{"class":263},"=",[229,332,333],{"class":267}," {\n",[229,335,337,340,343,346,349,352,354,358,361,364,366,369,371,374,377,380,382,384,386,389,391,393,395,397,399,402],{"class":93,"line":336},8,[229,338,339],{"class":238},"    \"2026-01\"",[229,341,342],{"class":267},": [[",[229,344,345],{"class":238},"\"North\"",[229,347,348],{"class":267},", ",[229,350,351],{"class":238},"\"Tea\"",[229,353,348],{"class":267},[229,355,357],{"class":356},"sP0c6","120",[229,359,360],{"class":267},", dt.date(",[229,362,363],{"class":356},"2026",[229,365,348],{"class":267},[229,367,368],{"class":356},"1",[229,370,348],{"class":267},[229,372,373],{"class":356},"5",[229,375,376],{"class":267},")], [",[229,378,379],{"class":238},"\"South\"",[229,381,348],{"class":267},[229,383,351],{"class":238},[229,385,348],{"class":267},[229,387,388],{"class":356},"80",[229,390,360],{"class":267},[229,392,363],{"class":356},[229,394,348],{"class":267},[229,396,368],{"class":356},[229,398,348],{"class":267},[229,400,401],{"class":356},"9",[229,403,404],{"class":267},")]],\n",[229,406,408,411,413,415,417,420,422,425,427,429,431,434,436,438,440,443,445,447,449,452,454,456,458,460,462,465],{"class":93,"line":407},9,[229,409,410],{"class":238},"    \"2026-02\"",[229,412,342],{"class":267},[229,414,345],{"class":238},[229,416,348],{"class":267},[229,418,419],{"class":238},"\"Coffee\"",[229,421,348],{"class":267},[229,423,424],{"class":356},"95",[229,426,360],{"class":267},[229,428,363],{"class":356},[229,430,348],{"class":267},[229,432,433],{"class":356},"2",[229,435,348],{"class":267},[229,437,433],{"class":356},[229,439,376],{"class":267},[229,441,442],{"class":238},"\"East\"",[229,444,348],{"class":267},[229,446,351],{"class":238},[229,448,348],{"class":267},[229,450,451],{"class":238},"\"n\u002Fa\"",[229,453,360],{"class":267},[229,455,363],{"class":356},[229,457,348],{"class":267},[229,459,433],{"class":356},[229,461,348],{"class":267},[229,463,464],{"class":356},"3",[229,466,404],{"class":267},[229,468,470],{"class":93,"line":469},10,[229,471,472],{"class":267},"}\n",[229,474,476,479,482,485],{"class":93,"line":475},11,[229,477,478],{"class":263},"for",[229,480,481],{"class":267}," month, rows ",[229,483,484],{"class":263},"in",[229,486,487],{"class":267}," data.items():\n",[229,489,491,494,496],{"class":93,"line":490},12,[229,492,493],{"class":267},"    wb ",[229,495,330],{"class":263},[229,497,498],{"class":267}," Workbook()\n",[229,500,502,505,507],{"class":93,"line":501},13,[229,503,504],{"class":267},"    ws ",[229,506,330],{"class":263},[229,508,509],{"class":267}," wb.active\n",[229,511,513,516,518],{"class":93,"line":512},14,[229,514,515],{"class":267},"    ws.title ",[229,517,330],{"class":263},[229,519,520],{"class":238}," \"Sales\"\n",[229,522,524,527,530,533,537,540,543,546],{"class":93,"line":523},15,[229,525,526],{"class":267},"    ws.append([",[229,528,529],{"class":263},"f",[229,531,532],{"class":238},"\"Sales report ",[229,534,536],{"class":535},"sSjpA","{",[229,538,539],{"class":267},"month",[229,541,542],{"class":535},"}",[229,544,545],{"class":238},"\"",[229,547,548],{"class":267},"])\n",[229,550,552],{"class":93,"line":551},16,[229,553,554],{"class":267},"    ws.append([])\n",[229,556,558,560,563,565,568,570,573,575,578],{"class":93,"line":557},17,[229,559,526],{"class":267},[229,561,562],{"class":238},"\"region\"",[229,564,348],{"class":267},[229,566,567],{"class":238},"\"product\"",[229,569,348],{"class":267},[229,571,572],{"class":238},"\"revenue\"",[229,574,348],{"class":267},[229,576,577],{"class":238},"\"date\"",[229,579,548],{"class":267},[229,581,583,586,589,591],{"class":93,"line":582},18,[229,584,585],{"class":263},"    for",[229,587,588],{"class":267}," row ",[229,590,484],{"class":263},[229,592,593],{"class":267}," rows:\n",[229,595,597],{"class":93,"line":596},19,[229,598,599],{"class":267},"        ws.append(row)\n",[229,601,603,606,608,611,613,615,617,620],{"class":93,"line":602},20,[229,604,605],{"class":267},"    wb.save(",[229,607,529],{"class":263},[229,609,610],{"class":238},"\"sales_",[229,612,536],{"class":535},[229,614,539],{"class":267},[229,616,542],{"class":535},[229,618,619],{"class":238},".xlsx\"",[229,621,622],{"class":267},")\n",[229,624,626],{"class":93,"line":625},21,[229,627,281],{"emptyLinePlaceholder":280},[229,629,631,634,636],{"class":93,"line":630},22,[229,632,633],{"class":267},"con ",[229,635,330],{"class":263},[229,637,638],{"class":267}," duckdb.connect()\n",[229,640,642,645,648],{"class":93,"line":641},23,[229,643,644],{"class":267},"con.sql(",[229,646,647],{"class":238},"\"INSTALL excel\"",[229,649,622],{"class":267},[229,651,653,655,658],{"class":93,"line":652},24,[229,654,644],{"class":267},[229,656,657],{"class":238},"\"LOAD excel\"",[229,659,622],{"class":267},[215,661,663],{"id":662},"step-1-read-the-table-not-the-title","Step 1 — Read the table, not the title",[10,665,666,667,669],{},"By default ",[14,668,26],{}," takes the first row of the sheet as the header and stops at the first empty row. For a report with a title in A1 and a blank row under it, that returns an empty table with a column called \"Sales report 2026-01\". Point it at the table instead:",[220,671,673],{"className":254,"code":672,"language":256,"meta":225,"style":225},"jan = con.sql(\"\"\"\n    SELECT * FROM read_xlsx('sales_2026-01.xlsx', sheet = 'Sales',\n                            range = 'A3:D100000', stop_at_empty = true)\n\"\"\")\nprint(jan.df())\nprint(con.sql(\"DESCRIBE SELECT * FROM jan\").df()[[\"column_name\", \"column_type\"]].values.tolist())\n# [['region', 'VARCHAR'], ['product', 'VARCHAR'], ['revenue', 'DOUBLE'], ['date', 'DATE']]\n",[14,674,675,688,693,698,705,713,737],{"__ignoreMap":225},[229,676,677,680,682,685],{"class":93,"line":231},[229,678,679],{"class":267},"jan ",[229,681,330],{"class":263},[229,683,684],{"class":267}," con.sql(",[229,686,687],{"class":238},"\"\"\"\n",[229,689,690],{"class":93,"line":277},[229,691,692],{"class":238},"    SELECT * FROM read_xlsx('sales_2026-01.xlsx', sheet = 'Sales',\n",[229,694,695],{"class":93,"line":284},[229,696,697],{"class":238},"                            range = 'A3:D100000', stop_at_empty = true)\n",[229,699,700,703],{"class":93,"line":292},[229,701,702],{"class":238},"\"\"\"",[229,704,622],{"class":267},[229,706,707,710],{"class":93,"line":305},[229,708,709],{"class":356},"print",[229,711,712],{"class":267},"(jan.df())\n",[229,714,715,717,720,723,726,729,731,734],{"class":93,"line":319},[229,716,709],{"class":356},[229,718,719],{"class":267},"(con.sql(",[229,721,722],{"class":238},"\"DESCRIBE SELECT * FROM jan\"",[229,724,725],{"class":267},").df()[[",[229,727,728],{"class":238},"\"column_name\"",[229,730,348],{"class":267},[229,732,733],{"class":238},"\"column_type\"",[229,735,736],{"class":267},"]].values.tolist())\n",[229,738,739],{"class":93,"line":324},[229,740,742],{"class":741},"s-wDw","# [['region', 'VARCHAR'], ['product', 'VARCHAR'], ['revenue', 'DOUBLE'], ['date', 'DATE']]\n",[10,744,745,746,749,750,753,754,757,758,761,762,765,766,769,770,773],{},"The ",[14,747,748],{},"range"," starts at the header row. Give it a generous end row and add ",[14,751,752],{},"stop_at_empty = true",": without that flag, DuckDB returns every row of the range, blank ones included — ask for ",[14,755,756],{},"A3:D"," and you get over a million empty rows. Column types are inferred from the cells, so the dates arrive as real ",[14,759,760],{},"DATE"," values and the revenue as ",[14,763,764],{},"DOUBLE",". Saving a relation in a Python variable, as ",[14,767,768],{},"jan"," here, lets later SQL refer to it by name. Relations are lazy — the file is read again each time a query uses one — so for a workbook you query many times, materialise it once with ",[14,771,772],{},"CREATE TABLE",".",[215,775,777],{"id":776},"step-2-handle-text-in-numeric-columns","Step 2 — Handle text in numeric columns",[10,779,780,781,783],{},"The February file has \"n\u002Fa\" typed into the revenue column. Type inference picks ",[14,782,764],{}," for the column and then fails on that cell:",[220,785,787],{"className":254,"code":786,"language":256,"meta":225,"style":225},"try:\n    con.sql(\"SELECT * FROM read_xlsx('sales_2026-02.xlsx', range = 'A3:D100000', stop_at_empty = true)\").df()\nexcept duckdb.Error as exc:\n    print(str(exc)[:99])\n# Invalid Input Error: read_xlsx: Failed to parse cell 'C5': Could not convert string 'n\u002Fa' to DOUBLE\n",[14,788,789,797,808,821,840],{"__ignoreMap":225},[229,790,791,794],{"class":93,"line":231},[229,792,793],{"class":263},"try",[229,795,796],{"class":267},":\n",[229,798,799,802,805],{"class":93,"line":277},[229,800,801],{"class":267},"    con.sql(",[229,803,804],{"class":238},"\"SELECT * FROM read_xlsx('sales_2026-02.xlsx', range = 'A3:D100000', stop_at_empty = true)\"",[229,806,807],{"class":267},").df()\n",[229,809,810,813,816,818],{"class":93,"line":284},[229,811,812],{"class":263},"except",[229,814,815],{"class":267}," duckdb.Error ",[229,817,271],{"class":263},[229,819,820],{"class":267}," exc:\n",[229,822,823,826,829,832,835,838],{"class":93,"line":292},[229,824,825],{"class":356},"    print",[229,827,828],{"class":267},"(",[229,830,831],{"class":356},"str",[229,833,834],{"class":267},"(exc)[:",[229,836,837],{"class":356},"99",[229,839,548],{"class":267},[229,841,842],{"class":93,"line":305},[229,843,844],{"class":741},"# Invalid Input Error: read_xlsx: Failed to parse cell 'C5': Could not convert string 'n\u002Fa' to DOUBLE\n",[10,846,847,848,851,852,855,856,859,860,863],{},"Two options change that. ",[14,849,850],{},"ignore_errors = true"," keeps the inferred types and turns cells that do not fit into ",[14,853,854],{},"NULL",". ",[14,857,858],{},"all_varchar = true"," reads every cell as text, which never fails — but dates then arrive as Excel serial numbers such as ",[14,861,862],{},"46055",", not as dates:",[220,865,867],{"className":254,"code":866,"language":256,"meta":225,"style":225},"feb = con.sql(\"\"\"SELECT * FROM read_xlsx('sales_2026-02.xlsx', range = 'A3:D100000',\n                 stop_at_empty = true, ignore_errors = true)\"\"\")\nprint(feb.df()[\"revenue\"].tolist())                    # [95.0, nan]\nraw = con.sql(\"\"\"SELECT revenue, date FROM read_xlsx('sales_2026-02.xlsx', range = 'A3:D100000',\n                 stop_at_empty = true, all_varchar = true)\"\"\").df()\nprint(raw.values.tolist())                             # [['95', '46055'], ['n\u002Fa', '46056']]\n",[14,868,869,881,888,903,915,922],{"__ignoreMap":225},[229,870,871,874,876,878],{"class":93,"line":231},[229,872,873],{"class":267},"feb ",[229,875,330],{"class":263},[229,877,684],{"class":267},[229,879,880],{"class":238},"\"\"\"SELECT * FROM read_xlsx('sales_2026-02.xlsx', range = 'A3:D100000',\n",[229,882,883,886],{"class":93,"line":277},[229,884,885],{"class":238},"                 stop_at_empty = true, ignore_errors = true)\"\"\"",[229,887,622],{"class":267},[229,889,890,892,895,897,900],{"class":93,"line":284},[229,891,709],{"class":356},[229,893,894],{"class":267},"(feb.df()[",[229,896,572],{"class":238},[229,898,899],{"class":267},"].tolist())                    ",[229,901,902],{"class":741},"# [95.0, nan]\n",[229,904,905,908,910,912],{"class":93,"line":292},[229,906,907],{"class":267},"raw ",[229,909,330],{"class":263},[229,911,684],{"class":267},[229,913,914],{"class":238},"\"\"\"SELECT revenue, date FROM read_xlsx('sales_2026-02.xlsx', range = 'A3:D100000',\n",[229,916,917,920],{"class":93,"line":305},[229,918,919],{"class":238},"                 stop_at_empty = true, all_varchar = true)\"\"\"",[229,921,807],{"class":267},[229,923,924,926,929],{"class":93,"line":319},[229,925,709],{"class":356},[229,927,928],{"class":267},"(raw.values.tolist())                             ",[229,930,931],{"class":741},"# [['95', '46055'], ['n\u002Fa', '46056']]\n",[10,933,934,937,938,940,941,944,945,773],{},[14,935,936],{},"ignore_errors"," is the practical default for reporting; count the ",[14,939,854],{},"s it produces so the \"n\u002Fa\" does not disappear unnoticed. Use ",[14,942,943],{},"all_varchar"," when you want to inspect exactly what was typed, and convert columns yourself. Placeholder text in general is covered in ",[19,946,948],{"href":947},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Freplace-placeholder-text-with-nan-in-pandas\u002F","Replace Placeholder Text with NaN in pandas",[29,950,38,955,38,958,38,961,38,964,38,970,38,975,38,981,38,986,38,992,38,994,38,998,38,1001,38,1004,38,1006,38,1010,38,1013,38,1016,38,1018,38,1022,38,1025,38,1028,38,1030,38,1034,38,1037,38,1041,38,1043],{"viewBox":951,"role":32,"ariaLabelledBy":952,"xmlns":36,"style":37},"4 0 752 242",[953,954],"dk-options-t","dk-options-d",[40,956,957],{"id":953},"read_xlsx options for report-style workbooks",[44,959,960],{"id":954},"range skips title rows and selects the table. stop_at_empty ends the read at the first blank row. ignore_errors turns cells that do not match the inferred type into NULL. all_varchar reads every cell as text, including dates as serial numbers. sheet chooses the worksheet by name.",[48,962],{"x":50,"y":51,"width":52,"height":963,"fill":54},"242",[48,965],{"x":57,"y":58,"width":966,"height":967,"rx":51,"fill":54,"stroke":968,"style":969},"171.3","40.0","var(--line,#cdd5e6)","stroke-width:1px",[48,971],{"x":972,"y":58,"width":973,"height":967,"rx":51,"fill":62,"stroke":63,"style":974},"191.3","548.7","stroke-width:1.2px",[77,976,980],{"x":977,"y":978,"style":979},"465.7","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","What it solves",[48,982],{"x":57,"y":983,"width":966,"height":984,"rx":51,"fill":985,"stroke":968,"style":969},"56.0","34.0","#e7ebef",[77,987,991],{"x":988,"y":989,"style":990},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","range = 'A3:D100000'",[48,993],{"x":972,"y":983,"width":973,"height":984,"rx":51,"fill":54,"stroke":968,"style":969},[77,995,997],{"x":977,"y":996,"style":86},"77.0","title rows above the table",[48,999],{"x":57,"y":1000,"width":966,"height":984,"rx":51,"fill":985,"stroke":968,"style":969},"90.0",[77,1002,752],{"x":988,"y":1003,"style":990},"112.0",[48,1005],{"x":972,"y":1000,"width":973,"height":984,"rx":51,"fill":54,"stroke":968,"style":969},[77,1007,1009],{"x":977,"y":1008,"style":86},"111.0","empty rows to the end of the range",[48,1011],{"x":57,"y":1012,"width":966,"height":984,"rx":51,"fill":985,"stroke":968,"style":969},"124.0",[77,1014,850],{"x":988,"y":1015,"style":990},"146.0",[48,1017],{"x":972,"y":1012,"width":973,"height":984,"rx":51,"fill":54,"stroke":968,"style":969},[77,1019,1021],{"x":977,"y":1020,"style":86},"145.0","'n\u002Fa' in a number column → NULL",[48,1023],{"x":57,"y":1024,"width":966,"height":984,"rx":51,"fill":985,"stroke":968,"style":969},"158.0",[77,1026,858],{"x":988,"y":1027,"style":990},"180.0",[48,1029],{"x":972,"y":1024,"width":973,"height":984,"rx":51,"fill":54,"stroke":968,"style":969},[77,1031,1033],{"x":977,"y":1032,"style":86},"179.0","everything as text; dates as serials",[48,1035],{"x":57,"y":1036,"width":966,"height":984,"rx":51,"fill":985,"stroke":968,"style":969},"192.0",[77,1038,1040],{"x":988,"y":1039,"style":990},"214.0","sheet = 'Sales'",[48,1042],{"x":972,"y":1036,"width":973,"height":984,"rx":51,"fill":54,"stroke":968,"style":969},[77,1044,1046],{"x":977,"y":1045,"style":86},"213.0","not the first worksheet",[215,1048,1050],{"id":1049},"step-3-combine-several-workbooks","Step 3 — Combine several workbooks",[10,1052,1053,1055,1056,1059],{},[14,1054,26],{}," reads one file per call; it rejects a list of paths, and in our test a wildcard pattern such as ",[14,1057,1058],{},"'sales_*.xlsx'"," returned the rows of a single file rather than all of them. Build one query that unions a read per file, adding the file name as a column:",[220,1061,1063],{"className":254,"code":1062,"language":256,"meta":225,"style":225},"from pathlib import Path\n\nfiles = sorted(Path(\".\").glob(\"sales_*.xlsx\"))\nparts = [f\"\"\"SELECT *, '{p.name}' AS source FROM read_xlsx('{p.as_posix()}', sheet = 'Sales',\n             range = 'A3:D100000', stop_at_empty = true, ignore_errors = true)\"\"\" for p in files]\nsales = con.sql(\"\\nUNION ALL BY NAME\\n\".join(parts))\nprint(sales.df()[[\"region\", \"revenue\", \"source\"]].values.tolist())\n",[14,1064,1065,1077,1081,1106,1141,1157,1181],{"__ignoreMap":225},[229,1066,1067,1069,1072,1074],{"class":93,"line":231},[229,1068,308],{"class":263},[229,1070,1071],{"class":267}," pathlib ",[229,1073,264],{"class":263},[229,1075,1076],{"class":267}," Path\n",[229,1078,1079],{"class":93,"line":277},[229,1080,281],{"emptyLinePlaceholder":280},[229,1082,1083,1086,1088,1091,1094,1097,1100,1103],{"class":93,"line":284},[229,1084,1085],{"class":267},"files ",[229,1087,330],{"class":263},[229,1089,1090],{"class":356}," sorted",[229,1092,1093],{"class":267},"(Path(",[229,1095,1096],{"class":238},"\".\"",[229,1098,1099],{"class":267},").glob(",[229,1101,1102],{"class":238},"\"sales_*.xlsx\"",[229,1104,1105],{"class":267},"))\n",[229,1107,1108,1111,1113,1116,1118,1121,1123,1126,1128,1131,1133,1136,1138],{"class":93,"line":292},[229,1109,1110],{"class":267},"parts ",[229,1112,330],{"class":263},[229,1114,1115],{"class":267}," [",[229,1117,529],{"class":263},[229,1119,1120],{"class":238},"\"\"\"SELECT *, '",[229,1122,536],{"class":535},[229,1124,1125],{"class":267},"p.name",[229,1127,542],{"class":535},[229,1129,1130],{"class":238},"' AS source FROM read_xlsx('",[229,1132,536],{"class":535},[229,1134,1135],{"class":267},"p.as_posix()",[229,1137,542],{"class":535},[229,1139,1140],{"class":238},"', sheet = 'Sales',\n",[229,1142,1143,1146,1149,1152,1154],{"class":93,"line":305},[229,1144,1145],{"class":238},"             range = 'A3:D100000', stop_at_empty = true, ignore_errors = true)\"\"\"",[229,1147,1148],{"class":263}," for",[229,1150,1151],{"class":267}," p ",[229,1153,484],{"class":263},[229,1155,1156],{"class":267}," files]\n",[229,1158,1159,1162,1164,1166,1168,1171,1174,1176,1178],{"class":93,"line":319},[229,1160,1161],{"class":267},"sales ",[229,1163,330],{"class":263},[229,1165,684],{"class":267},[229,1167,545],{"class":238},[229,1169,1170],{"class":535},"\\n",[229,1172,1173],{"class":238},"UNION ALL BY NAME",[229,1175,1170],{"class":535},[229,1177,545],{"class":238},[229,1179,1180],{"class":267},".join(parts))\n",[229,1182,1183,1185,1188,1190,1192,1194,1196,1199],{"class":93,"line":324},[229,1184,709],{"class":356},[229,1186,1187],{"class":267},"(sales.df()[[",[229,1189,562],{"class":238},[229,1191,348],{"class":267},[229,1193,572],{"class":238},[229,1195,348],{"class":267},[229,1197,1198],{"class":238},"\"source\"",[229,1200,736],{"class":267},[10,1202,1203,1205,1206,1208],{},[14,1204,1173],{}," matches columns by name rather than position, so files whose columns are in a different order — or where one month has an extra column — still line up, with ",[14,1207,854],{}," for columns a file lacks. File names come from your own glob, so building the SQL string is safe here; never paste untrusted text into SQL.",[215,1210,1212],{"id":1211},"step-4-query-and-join-with-plain-sql","Step 4 — Query and join with plain SQL",[10,1214,1215,1216,1219],{},"With the combined relation named ",[14,1217,1218],{},"sales",", the questions are ordinary SQL — the same SQL you would run against a database table, so analysts who know SQL but not pandas can read and review the logic. DuckDB can also read pandas DataFrames in the same Python scope by their variable name, which is handy for a small targets table kept in code or another sheet:",[220,1221,1223],{"className":254,"code":1222,"language":256,"meta":225,"style":225},"targets = pd.DataFrame({\"region\": [\"North\", \"South\", \"East\"], \"goal\": [200, 100, 50]})\nsummary = con.sql(\"\"\"\n    SELECT s.region,\n           sum(s.revenue)          AS revenue,\n           any_value(t.goal)       AS goal,\n           count(*) FILTER (WHERE s.revenue IS NULL) AS missing_values\n    FROM sales AS s\n    LEFT JOIN targets AS t USING (region)\n    GROUP BY s.region\n    ORDER BY s.region\n\"\"\").df()\nprint(summary.values.tolist())\n",[14,1224,1225,1274,1285,1290,1295,1300,1305,1310,1315,1320,1325,1331],{"__ignoreMap":225},[229,1226,1227,1230,1232,1235,1237,1240,1242,1244,1246,1248,1250,1253,1256,1258,1261,1263,1266,1268,1271],{"class":93,"line":231},[229,1228,1229],{"class":267},"targets ",[229,1231,330],{"class":263},[229,1233,1234],{"class":267}," pd.DataFrame({",[229,1236,562],{"class":238},[229,1238,1239],{"class":267},": [",[229,1241,345],{"class":238},[229,1243,348],{"class":267},[229,1245,379],{"class":238},[229,1247,348],{"class":267},[229,1249,442],{"class":238},[229,1251,1252],{"class":267},"], ",[229,1254,1255],{"class":238},"\"goal\"",[229,1257,1239],{"class":267},[229,1259,1260],{"class":356},"200",[229,1262,348],{"class":267},[229,1264,1265],{"class":356},"100",[229,1267,348],{"class":267},[229,1269,1270],{"class":356},"50",[229,1272,1273],{"class":267},"]})\n",[229,1275,1276,1279,1281,1283],{"class":93,"line":277},[229,1277,1278],{"class":267},"summary ",[229,1280,330],{"class":263},[229,1282,684],{"class":267},[229,1284,687],{"class":238},[229,1286,1287],{"class":93,"line":284},[229,1288,1289],{"class":238},"    SELECT s.region,\n",[229,1291,1292],{"class":93,"line":292},[229,1293,1294],{"class":238},"           sum(s.revenue)          AS revenue,\n",[229,1296,1297],{"class":93,"line":305},[229,1298,1299],{"class":238},"           any_value(t.goal)       AS goal,\n",[229,1301,1302],{"class":93,"line":319},[229,1303,1304],{"class":238},"           count(*) FILTER (WHERE s.revenue IS NULL) AS missing_values\n",[229,1306,1307],{"class":93,"line":324},[229,1308,1309],{"class":238},"    FROM sales AS s\n",[229,1311,1312],{"class":93,"line":336},[229,1313,1314],{"class":238},"    LEFT JOIN targets AS t USING (region)\n",[229,1316,1317],{"class":93,"line":407},[229,1318,1319],{"class":238},"    GROUP BY s.region\n",[229,1321,1322],{"class":93,"line":469},[229,1323,1324],{"class":238},"    ORDER BY s.region\n",[229,1326,1327,1329],{"class":93,"line":475},[229,1328,702],{"class":238},[229,1330,807],{"class":267},[229,1332,1333,1335],{"class":93,"line":490},[229,1334,709],{"class":356},[229,1336,1337],{"class":267},"(summary.values.tolist())\n",[10,1339,745,1340,1343,1344,1346,1347,1349,1350,1353,1354,1357],{},[14,1341,1342],{},"FILTER"," clause counts the cells ",[14,1345,936],{}," turned into ",[14,1348,854],{},", so East's missing revenue is visible in the result instead of silently lowering its total. The same query could join a CSV with ",[14,1351,1352],{},"read_csv"," or a Parquet file with ",[14,1355,1356],{},"read_parquet",", which is where DuckDB shines compared with loading everything into pandas: each source is read directly, and only the result becomes a DataFrame.",[215,1359,1361],{"id":1360},"step-5-write-the-result-back-to-excel","Step 5 — Write the result back to Excel",[10,1363,1364,1365,1368,1369,1372],{},"The excel extension can also write. ",[14,1366,1367],{},"COPY … TO"," with ",[14,1370,1371],{},"FORMAT xlsx"," creates a workbook from any query:",[220,1374,1376],{"className":254,"code":1375,"language":256,"meta":225,"style":225},"con.sql(\"\"\"\n    COPY (SELECT region, revenue, goal FROM summary ORDER BY region)\n    TO 'summary.xlsx' (FORMAT xlsx, HEADER true, SHEET 'Summary')\n\"\"\")\nprint(pd.read_excel(\"summary.xlsx\", sheet_name=\"Summary\").values.tolist())\n",[14,1377,1378,1384,1389,1394,1400],{"__ignoreMap":225},[229,1379,1380,1382],{"class":93,"line":231},[229,1381,644],{"class":267},[229,1383,687],{"class":238},[229,1385,1386],{"class":93,"line":277},[229,1387,1388],{"class":238},"    COPY (SELECT region, revenue, goal FROM summary ORDER BY region)\n",[229,1390,1391],{"class":93,"line":284},[229,1392,1393],{"class":238},"    TO 'summary.xlsx' (FORMAT xlsx, HEADER true, SHEET 'Summary')\n",[229,1395,1396,1398],{"class":93,"line":292},[229,1397,702],{"class":238},[229,1399,622],{"class":267},[229,1401,1402,1404,1407,1410,1412,1416,1418,1421],{"class":93,"line":305},[229,1403,709],{"class":356},[229,1405,1406],{"class":267},"(pd.read_excel(",[229,1408,1409],{"class":238},"\"summary.xlsx\"",[229,1411,348],{"class":267},[229,1413,1415],{"class":1414},"sa561","sheet_name",[229,1417,330],{"class":263},[229,1419,1420],{"class":238},"\"Summary\"",[229,1422,1423],{"class":267},").values.tolist())\n",[10,1425,1426,1427,1430,1431,773],{},"The output is a plain data sheet — no formatting, widths or number formats. For a finished report, fetch the result with ",[14,1428,1429],{},".df()"," and write it with pandas and openpyxl or XlsxWriter, as in ",[19,1432,1434],{"href":1433},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fexport-sql-query-results-to-excel-with-python\u002F","Export SQL Query Results to Excel with Python",[29,1436,38,1441,38,1444,38,1447,38,1450,38,1455,38,1461,38,1466,38,1471,38,1476,38,1483,38,1487,38,1492,38,1496,38,1502,38,1506,38,1513,38,1516,38,1522,38,1527,38,1531,38,1535,38,1540,38,1543,38,1548,38,1551,38,1556,38,1559],{"viewBox":1437,"role":32,"ariaLabelledBy":1438,"xmlns":36,"style":37},"4 0 752 233",[1439,1440],"dk-vs-pandas-t","dk-vs-pandas-d",[40,1442,1443],{"id":1439},"DuckDB SQL or pandas for Excel data",[44,1445,1446],{"id":1440},"DuckDB suits set-based questions across several files and formats, keeps intermediate data out of Python memory and expresses joins and filters in SQL. pandas suits cell-level cleaning, reshaping and row-by-row logic, and gives finer control over how Excel files are read and written.",[48,1448],{"x":50,"y":51,"width":52,"height":1449,"fill":54},"233",[48,1451],{"x":57,"y":58,"width":1452,"height":1453,"rx":1454,"fill":106,"stroke":107,"style":64},"351.0","201.0","14",[66,1456,1458],{"transform":1457},"translate(182.5,29.0) scale(1.083)",[70,1459],{"d":173,"fill":73,"stroke":114,"style":1460},"stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[77,1462,1465],{"x":1463,"y":989,"style":1464},"195.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","DuckDB read_xlsx",[93,1467],{"x1":1468,"y1":1469,"x2":1470,"y2":1469,"stroke":107,"style":969},"36.0","91.0","355.0",[77,1472,1475],{"x":1473,"y":1008,"style":1474},"60.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","SQL joins across files and formats",[66,1477,1479],{"transform":1478},"translate(38.5,99.5) scale(0.625)",[70,1480],{"d":1481,"fill":73,"stroke":107,"style":1482},"M5 12.5l4.5 4.5L19 7","stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[77,1484,1486],{"x":1473,"y":1485,"style":1474},"137.0","only the result enters Python",[66,1488,1490],{"transform":1489},"translate(38.5,125.5) scale(0.625)",[70,1491],{"d":1481,"fill":73,"stroke":107,"style":1482},[77,1493,1495],{"x":1473,"y":1494,"style":1474},"163.0","fewer options for messy sheets",[66,1497,1499],{"transform":1498},"translate(38.5,151.5) scale(0.625)",[70,1500],{"d":1501,"fill":73,"stroke":195,"style":1482},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4",[77,1503,1505],{"x":1473,"y":1504,"style":1474},"189.0","one file per read_xlsx call",[66,1507,1509],{"transform":1508},"translate(38.5,177.5) scale(0.625)",[70,1510],{"d":1511,"fill":73,"stroke":1512,"style":1482},"M6 6l12 12 M18 6L6 18","var(--accent,#f43f8f)",[48,1514],{"x":1515,"y":58,"width":1452,"height":1453,"rx":1454,"fill":62,"stroke":63,"style":64},"389.0",[66,1517,1519],{"transform":1518},"translate(551.5,29.0) scale(1.083)",[70,1520],{"d":1521,"fill":73,"stroke":74,"style":1460},"M12 3c-4 0-4 1.6-4 3v2h4.2v1H6c-2 0-3 1.4-3 4s1 4 3 4h2v-2.6c0-1.6 1.3-2.9 2.9-2.9h4.2c1.3 0 2.4-1.1 2.4-2.4V6c0-1.6-1.8-3-5.5-3z M10 5.5h.01 M12 21c4 0 4-1.6 4-3v-2h-4.2v-1H18c2 0 3-1.4 3-4s-1-4-3-4h-2v2.6c0 1.6-1.3 2.9-2.9 2.9H8.9c-1.3 0-2.4 1.1-2.4 2.4V18c0 1.6 1.8 3 5.5 3z M14 18.5h.01",[77,1523,1526],{"x":1524,"y":989,"style":1525},"564.5","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","pandas read_excel",[93,1528],{"x1":1529,"y1":1469,"x2":1530,"y2":1469,"stroke":63,"style":969},"405.0","724.0",[77,1532,1534],{"x":1533,"y":1008,"style":1474},"429.0","fine control over parsing",[66,1536,1538],{"transform":1537},"translate(407.5,99.5) scale(0.625)",[70,1539],{"d":1481,"fill":73,"stroke":107,"style":1482},[77,1541,1542],{"x":1533,"y":1485,"style":1474},"reshaping and cleaning",[66,1544,1546],{"transform":1545},"translate(407.5,125.5) scale(0.625)",[70,1547],{"d":1481,"fill":73,"stroke":107,"style":1482},[77,1549,1550],{"x":1533,"y":1494,"style":1474},"everything loaded into memory",[66,1552,1554],{"transform":1553},"translate(407.5,151.5) scale(0.625)",[70,1555],{"d":1501,"fill":73,"stroke":195,"style":1482},[77,1557,1558],{"x":1533,"y":1504,"style":1474},"direct link to openpyxl styling",[66,1560,1562],{"transform":1561},"translate(407.5,177.5) scale(0.625)",[70,1563],{"d":1481,"fill":73,"stroke":107,"style":1482},[215,1565,1567],{"id":1566},"common-pitfalls","Common pitfalls",[1569,1570,1571,1585,1594,1604],"ul",{},[1572,1573,1574,1578,1579,1581,1582,773],"li",{},[1575,1576,1577],"strong",{},"Forgetting stop_at_empty with a range."," Without it, the empty rows of the range come back as ",[14,1580,854],{}," rows and inflate ",[14,1583,1584],{},"count(*)",[1572,1586,1587,1590,1591,773],{},[1575,1588,1589],{},"Reading dates with all_varchar."," They become serial-number strings. Keep inferred types for date columns, or convert with ",[14,1592,1593],{},"DATE '1899-12-30' + CAST(col AS INTEGER)",[1572,1595,1596,1599,1600,1603],{},[1575,1597,1598],{},"Guessing the sheet."," Without ",[14,1601,1602],{},"sheet",", DuckDB reads the first worksheet, which in many exports is a cover page or a pivot. Name the sheet explicitly so a reordered workbook fails loudly instead of returning the wrong table.",[1572,1605,1606,1609,1610,1612,1613,1615,1616,1619,1620,773],{},[1575,1607,1608],{},"Legacy .xls files."," ",[14,1611,26],{}," reads only the newer ",[14,1614,16],{}," format; convert ",[14,1617,1618],{},".xls"," first, as in ",[19,1621,1623],{"href":1622},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-xls-to-xlsx-with-python\u002F","Convert XLS to XLSX with Python",[215,1625,1627],{"id":1626},"conclusion","Conclusion",[10,1629,1630,1631,1633,1634,1636,1637,1639,1640,1642,1643,1645,1646,1648,1649,1651,1652,1655],{},"DuckDB turns Excel files into SQL tables. Load the excel extension, read each workbook with ",[14,1632,26],{}," using ",[14,1635,748],{}," and ",[14,1638,124],{}," to skip report headers, choose ",[14,1641,936],{}," or ",[14,1644,943],{}," for text in numeric columns, and combine files with ",[14,1647,1173],{},". Join with pandas DataFrames or other file formats in plain SQL, count the values the reader turned into ",[14,1650,854],{},", and write results back with ",[14,1653,1654],{},"COPY … (FORMAT xlsx)"," or through pandas for formatted reports.",[215,1657,1659],{"id":1658},"frequently-asked-questions","Frequently asked questions",[10,1661,1662,1665],{},[1575,1663,1664],{},"Can DuckDB read Excel files?","\nYes. The excel extension provides read_xlsx, which reads a sheet or a range of an .xlsx file as a table you can query with SQL. Install it once with INSTALL excel and load it with LOAD excel.",[10,1667,1668,1671],{},[1575,1669,1670],{},"How do I skip the title rows above the table?","\nPass range with the table's top-left corner, such as range='A3:D100000', together with stop_at_empty=true so reading stops at the first blank row instead of returning empty rows up to the end of the range.",[10,1673,1674,1677],{},[1575,1675,1676],{},"Why does read_xlsx fail with \"Could not convert string to DOUBLE\"?","\nIt infers each column's type from the data and a later cell does not fit — typically text such as \"n\u002Fa\" in a numeric column. Pass ignore_errors=true to turn such cells into NULL, or all_varchar=true to read everything as text.",[10,1679,1680,1683],{},[1575,1681,1682],{},"Can read_xlsx read several files at once?","\nIt takes one file per call. Build a query that combines one read_xlsx per file with UNION ALL BY NAME, which also lines up columns by name when the files differ in order.",[215,1685,1687],{"id":1686},"related","Related",[1569,1689,1690,1696,1703,1710],{},[1572,1691,1692,1693,1695],{},"Up one level: ",[19,1694,22],{"href":21}," — Excel and SQL in both directions.",[1572,1697,1698,1702],{},[19,1699,1701],{"href":1700},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fload-an-excel-file-into-a-sql-database-with-pandas\u002F","Load an Excel File into a SQL Database with pandas"," — when the data should live in a database.",[1572,1704,1705,1709],{},[19,1706,1708],{"href":1707},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fupsert-excel-rows-into-a-database-table\u002F","Upsert Excel Rows into a Database Table with Python"," — repeated loads without duplicates.",[1572,1711,1712,1716],{},[19,1713,1715],{"href":1714},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fread-large-excel-file-in-chunks-with-pandas\u002F","Read Large Excel Files in Chunks with pandas"," — the pandas route for big files.",[1718,1719,1720],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}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 .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}",{"title":225,"searchDepth":277,"depth":277,"links":1722},[1723,1724,1725,1726,1727,1728,1729,1730,1731,1732],{"id":217,"depth":277,"text":218},{"id":662,"depth":277,"text":663},{"id":776,"depth":277,"text":777},{"id":1049,"depth":277,"text":1050},{"id":1211,"depth":277,"text":1212},{"id":1360,"depth":277,"text":1361},{"id":1566,"depth":277,"text":1567},{"id":1626,"depth":277,"text":1627},{"id":1658,"depth":277,"text":1659},{"id":1686,"depth":277,"text":1687},"2026-09-23","Run SQL directly against .xlsx files with DuckDB's excel extension — read a range below a report header, stop at the first empty row, handle text in numeric columns, combine several workbooks, join Excel with pandas DataFrames and write query results back to a workbook.","md",[1737,1739,1741,1743],{"q":1664,"a":1738},"Yes. The excel extension provides read_xlsx, which reads a sheet or a range of an .xlsx file as a table you can query with SQL. Install it once with INSTALL excel and load it with LOAD excel.",{"q":1670,"a":1740},"Pass range with the table's top-left corner, such as range='A3:D100000', together with stop_at_empty=true so reading stops at the first blank row instead of returning empty rows up to the end of the range.",{"q":1676,"a":1742},"It infers each column's type from the data and a later cell does not fit — typically text such as \"n\u002Fa\" in a numeric column. Pass ignore_errors=true to turn such cells into NULL, or all_varchar=true to read everything as text.",{"q":1682,"a":1744},"It takes one file per call. Build a query that combines one read_xlsx per file with UNION ALL BY NAME, which also lines up columns by name when the files differ in order.","query excel files with sql using duckdb",null,{"breadcrumb":1748},[1749,1752,1755],{"name":1750,"item":1751},"Home","\u002F",{"name":1753,"item":1754},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":22,"item":21},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fquery-excel-files-with-sql-using-duckdb",{"title":1758,"description":1759},"Query Excel Files with SQL Using DuckDB (read_xlsx)","Use DuckDB's read_xlsx to query Excel with SQL from Python — range and stop_at_empty, type inference and ignore_errors, all_varchar and date serials, UNION ALL BY NAME across files, joins with pandas and COPY TO xlsx.","query-excel-files-with-sql-using-duckdb","advanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fquery-excel-files-with-sql-using-duckdb\u002Findex",[1763,1764],"data-sources","large-files","how-to","AhC8_XZ6PHYWY0Ye691jLiZl_vV1LYXAAPQ-SMdspl0",[1768,1772],{"title":1769,"path":1770,"stem":1771,"children":-1},"Query a Database for IDs Listed in an Excel Sheet","\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fquery-a-database-for-ids-listed-in-an-excel-sheet","advanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fquery-a-database-for-ids-listed-in-an-excel-sheet\u002Findex",{"title":1773,"path":1774,"stem":1775,"children":-1},"Read Excel Attachments into a Database Nightly","\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fread-excel-attachments-into-a-database-nightly","advanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fread-excel-attachments-into-a-database-nightly\u002Findex",[1777,1779],{"path":1778,"title":1753},"\u002Fadvanced-data-transformation-and-cleaning",{"path":1780,"title":22},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases",[],[1783,1786,1789,1792,1795,1798],{"path":1784,"title":1785,"kind":1765},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fread-an-excel-file-directly-from-amazon-s3-with-pandas","Read an Excel File Directly from Amazon S3 with pandas",{"path":1787,"title":1788,"kind":1765},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fcache-api-responses-when-building-excel-reports","Cache API Responses When Building Excel Reports",{"path":1790,"title":1791,"kind":1765},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fcompare-an-excel-file-with-a-database-table","Compare an Excel File with a Database Table",{"path":1793,"title":1794,"kind":1765},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fdownload-new-excel-files-from-an-sftp-server","Download New Excel Files from an SFTP Server",{"path":1796,"title":1797,"kind":1765},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fexport-a-large-database-table-to-excel-in-chunks","Export a Large Database Table to Excel in Chunks",{"path":1799,"title":1800,"kind":1765},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fexport-database-lookup-tables-as-excel-dropdown-lists","Export Database Lookup Tables as Excel Dropdown Lists",1790972096856]