binse

Documentation · Binse Enterprise · Explore · v2026.9.16.3

The Binse Explore user guide.

How to ask a question of a table in a warehouse without writing SQL: picking fields, the filter grammar, timeframes, saved reports and catalogues.

All the guides. What Binse Enterprise is. What reading a guide will not tell you.

Explore is part of Binse Enterprise. Binse Enterprise is in development, with no price and no date yet. Binse on its own is the Data Flow and Dashboards canvases and scheduled runs. Explore is the part of Enterprise that is already built, and this is its guide as it stands in version 2026.9.16.3. What Binse Enterprise is.

This is the Explore guide that the installer puts on your machine, converted from the file the product ships. No sentence has been added, removed or reworded. Three things change in the typesetting and nothing else: emphasis is set in weight, because the four typefaces on this site carry no italic face; an em dash is set as a spaced en dash; and a rightwards arrow is set as the two characters -> inside a command and as the word then in a sentence.

Copyright 2026 Binse Analytics. All rights reserved.

Binse Explore asks questions of tables that live in a data warehouse, without writing SQL and without copying the data anywhere. Somebody publishes a table in a catalogue, giving its columns names people recognise and defining the measures everybody should share. After that, asking a question is ticking a few fields, typing a filter such as last 12 months, and pressing Run.

Every question becomes one grouped query at the source, capped at a row limit. Nothing is extracted, nothing is copied to your machine, and the SQL tab shows you exactly what ran. On BigQuery the query runs as you: Google decides what you are allowed to see, not Binse.

Explore is the workspace Binse Enterprise adds to Binse. It is in the Binse Enterprise installer, needs a Binse Enterprise licence, and sits beside Data Flow, Dashboards and Schedules.

Contents

Getting started

  1. Open Binse and choose Explore, or go straight to http://127.0.0.1:8767/explore/.
  2. The home page lists Tables on the left and Reports on the right. A fresh install has a sample catalogue, Shipments (sample), over a file on your own machine, so everything below works before anybody has connected a warehouse.
  3. Click a table. The field picker fills the left of the screen: dimensions and measures, grouped as the catalogue arranged them.
  4. Tick something to group by (Destination country) and something to count or add up (Shipments). Nothing has run yet: ticking a field costs nothing.
  5. Press Run, or Ctrl + Enter. The result appears underneath as a table.
  6. Add a filter: hover Order date and press its funnel button, then type last 12 months or choose a preset. Press Run again.
  7. Switch to the Visualisation tab and pick a shape; switch to SQL to see the query that ran.
  8. Press Save… to keep it as a report, or Download for a CSV or an Excel file.

Two habits are worth learning on the first day. Nothing queries the warehouse until you ask it to, so build the whole question first and then run it once. And the address bar always holds the current question, so Copy link hands somebody the exact query rather than a description of it.

What Explore is for

Explore answers a question you have not asked before. It is for the moment when somebody says "how many parcels went to Ireland last quarter, by carrier", you do not have a report for it, and you would rather not wait for one.

Use Dashboards instead when the answer needs to be looked at repeatedly. A dashboard is a page of visuals that filter each other, laid out deliberately, exported as a single file that opens on a machine with no Binse installed. Explore has one result at a time and no canvas. The rule of thumb: if you would like to see it again next month, and other people will look at it too, build it in Dashboards. If you want to know something now, use Explore.

The two are joined. Add to Dashboards registers the query you built here as a dataset there, so the exploring that found the answer becomes the source of the thing that reports it.

Use Data Flow instead when the data has to be changed - joined, cleaned, reshaped, written out. Explore only reads, and it only reads what a catalogue publishes.

The three surfaces

Explore is the question-asking surface: the field picker, the filters, Run, and the result as a table, a chart or SQL. This is where a query is built.

Reports are saved queries. Opening one runs it and shows the result with the filters its author chose to leave open, and nothing else. There is no field picker and nothing to break, which is what makes a report safe to send to somebody who has never opened Explore. A report stores which fields it picked, never what they mean.

Catalogues decide what can be explored at all: which tables, which fields, and which measures. A catalogue is a JSON file. The one you author lives in your own Documents folder; a catalogue on a shared folder is read-only, which is the whole of publishing. Curation is the point: a catalogue with twenty well-named fields is far more useful than one with four hundred columns.

Picking fields

The picker lists every field the catalogue publishes for that table, in groups the author named (Order, Route, Money). Hidden fields are not listed at all.

  • Click a row to tick it. Ticked fields become the columns of the result, dimensions first, then measures, each in the order you picked them.
  • A time dimension is one row that unfolds. Click the chevron and it opens into its timeframes (Date, Week, Month, Quarter, Year and so on). You tick the timeframe, not the dimension: Order date › Month is a different question from Order date › Date.
  • The search box matches labels and field names, and it reaches inside the timeframes, so typing month finds every month a table has.
  • The funnel button on any row adds a filter on that field without ticking it. A field can be filtered without being shown.
  • The columns button on a dimension pivots it. Pivoting also ticks it, since a pivot has to be part of the query.
  • Drag the divider between the picker and the result to make the picker wider; a catalogue with long labels usually wants it.
  • The count beside a group, and beside a time dimension, is how many of its fields are ticked.

Hovering a field shows its name, its description if the author wrote one, and, for a measure, what kind of measure it is.

Dimensions and measures

This is the one idea worth reading twice, because everything else follows from it.

A dimension is something you group by: a country, a carrier, a month. It slices the rows into buckets. A measure is a number computed over each bucket: how many rows, the total revenue, the average transit time.

Pick two dimensions and one measure and you get one row per pair of dimension values, with the measure worked out over the rows in that bucket. Pick no dimensions at all and you get a single row for everything.

A rate is not a sum, and this is why measures live in the catalogue. Take average order value: revenue divided by the number of orders. If you took the average order value of each country and added them up you would get a meaningless number, and if you averaged them you would get the wrong answer, because a country with three orders would count as much as one with thirty thousand. The catalogue defines it as ${revenue} / NULLIF(${shipments}, 0) - one measure built out of two others - so at every level of grouping it is recomputed from the totals underneath. Group by country and you get each country's real average; remove the country and you get the real average over everything.

The same rule is why a rate has no total in the table's pivot totals: adding rates is nonsense, so those cells show a dash. The grand Totals row, which is a separate query, does give the correct figure, because it recomputes rather than adds.

Field kinds

A dimension has one of four types. The type decides how it is shown, how it sorts, and which filter grammar its box accepts.

Dimension typeWhat it isExample
stringText: names, codes, categories. Grouping by one gives a row per distinct value.carrier, destination_country
numberA quantity you might group by rather than add up: a bucket, a rating, a count of items.items, transit_days
yesnoTrue or false. Shown as Yes and No, filtered as yes, no or NULL.on_time, returned
timeA date, a datetime or a timestamp. Unfolds into the timeframes the catalogue offers.order_date, ship_date

A measure has one of eight kinds. Every one except count needs a SQL expression to work over.

Measure kindWhat it doesExample
countHow many rows are in the group. The only kind that needs no expression.Shipments: the number of parcels
count_distinctHow many different values an expression takes in the group.Countries served: count_distinct over destination_country
sumAdds an expression over the rows of the group. The workhorse.Revenue: sum over merchandise_value_eur
avgThe mean of an expression over the rows. Also how a rate is built, by averaging 1s and 0s.On-time rate: avg over CASE WHEN on_time THEN 1.0 ELSE 0.0 END
minThe smallest value in the group. Works on dates and text as well as numbers.First order date in each country
maxThe largest value in the group.Heaviest parcel per carrier
medianThe middle value. Not available on every dialect; see Warehouses and dialects.Median transit days
numberNot an aggregate at all: arithmetic over other measures, recomputed at every level of grouping. This is how a rate stays correct.Average order value: ${revenue} / NULLIF(${shipments}, 0)

Timeframes

A date column is not one field but several: the day, the week it fell in, the month, the quarter, the year, and parts of a date such as which day of the week it was. Explore calls those timeframes, and the catalogue decides which of them a date offers.

Picking Order date › Month groups the rows by month and gives you back the first day of each month, which is why the table shows Jan 2026 rather than a range: the value really is the first of January, labelled as a month.

TimeframeShown asWhat you get backWhat it means
rawRawthe column's own typeThe column exactly as it is, with no truncation at all. Rarely what you want to group by.
timeTimedatetimeThe moment, to the second. Only useful on a datetime or timestamp column.
dateDatedateThe day.
weekWeekdateThe Monday that starts the week the value fell in.
monthMonthdateThe first day of the month.
quarterQuarterdateThe first day of the quarter.
yearYeardateThe first day of the year.
day_of_weekDay of weeknumberWhich day of the week it was, as a number. 1 is Monday, 7 is Sunday. Shown by name.
day_of_monthDay of monthnumberWhich day of the month, 1 to 31.
month_of_yearMonth of yearnumberWhich month, 1 to 12, across every year at once. Shown by name.
hour_of_dayHour of daynumberWhich hour, 0 to 23. Needs a datetime or timestamp column.

Which timeframes a date offers is the catalogue's choice. Unless the author says otherwise, a field gets:

Column typeOffered by default
datedate, week, month, quarter, year
datetimetime, date, week, month, quarter, year, hour_of_day
timestamptime, date, week, month, quarter, year, hour_of_day

The *_of_* timeframes are different in kind. They return a number rather than a date, because "Tuesday" is not a point in time. That has two consequences: a filter on one of them is a number filter (day_of_week 2,3,4 is Tuesday to Thursday), and sorting by it sorts 1 to 7 rather than alphabetically.

Filters

A filter narrows the rows before anything is grouped. Add one with the funnel button beside any field, or from the picker; each filter appears as a chip above the result with its expression as editable text.

Filters are typed expressions, not a form. last 12 months, IE,GB,DE, >1000, -NULL are all valid, and what you type is what the report stores. That matters for two reasons: a relative date such as last month is resolved when the query runs, so a saved report still means last month next year; and an expression is easy to paste, read and correct.

The picker writes the grammar for you. The list button on a chip opens a helper: date presets and a from/to pair for a time field, a list of the real values for a text field (which is a query of its own against the warehouse, narrowed by your other filters), an operator and a value for a number field, and yes/no/null for a yes-no field. Whatever you choose is written into the box as an expression, so the helper is a way of learning the grammar rather than an alternative to it.

Three rules govern every type:

  • Commas mean or. IE,GB is Ireland or Great Britain.
  • A minus means not, and negatives are combined with and. IE,GB,-NULL is (Ireland or Great Britain) and not missing.
  • A "not" keeps the rows where the value is missing. Asking for "not DHL" gives you the parcels carried by somebody else and the parcels with no carrier recorded, which is what people mean by it. If you do not want them, add -NULL.

Leaving a chip empty (or typing any) removes the filter from the query without removing the chip. Changing a filter never runs anything; press Run, or Enter inside the box.

The filter grammar

Every form the engine understands, with what each one means. The wording in the It means the value… column is produced by the parser itself, so it is what the engine actually does with the expression, not a description of it.

Everything is case-insensitive: NULL, null and Null are one word.

Text fields

Type thisIt means the value…Note
IEis IE
IE,GB,DEis any of IE, GB, DE
-IEis not IE
not IEis not IE
IE,GB,-Dublinis any of IE, GB, and is not DublinPositives and negatives mix: any of the positives, and none of the negatives.
%express%matches %express%, where % is any run of characters and case is ignored% is the wildcard. A pattern anywhere in the text makes it a contains match.
DHL%matches DHL%, where % is any run of characters and case is ignoredA pattern at the end makes it a starts-with match.
%pressmatches %press, where % is any run of characters and case is ignoredAnd at the start, an ends-with match.
NULLhas no value
-NULLhas a value
EMPTYhas no value, or is empty textMissing or the empty string, which are different things in most databases.
-EMPTYhas a value and is not empty text
^-5is -5A caret escapes the next character, so this is the text -5 rather than a negation.
A^,Bis A,BThe same escape lets a value contain a comma.
anyno filter at all: every row is keptany, any value, * and an empty box all mean the filter says nothing.

A missing value is asked for with is null, null; a present one with !null, -null, is not null, not null. Writing *, any, any value, is any value, or leaving the box empty, means the filter says nothing at all. These words work on every type of field.

Number fields

Type thisIt means the value…Note
5is 5
5,7,9is any of 5, 7, 9
>5is more than 5
>=5is 5 or more
<5is less than 5
<=5is 5 or less
!=5is not 5
<>5is not 5
[1,10]is from 1 to 10, both ends includedSquare brackets include the end; round brackets exclude it. They may be mixed.
(1,10)is above 1 and below 10
[1,10)is from 1 up to but not including 10
1 to 10is from 1 to 10, both ends includedto is the same as [1,10].
[5,]is 5 or moreLeave an end out for an open range.
[,5]is 5 or less
[5,inf)is 5 or more
(-inf,5]is 5 or less
NOT 5is not 5NOT before any of these forms negates it.
-3is -3A leading minus is part of the number, not a negation. Unlike a text filter. Write NOT 3 to exclude a number.
1.5is 1.5
NULLhas no value
-NULLhas a value

A number filter is also what a filter on a measure takes, and what a filter on a day_of_week-style timeframe takes.

Yes / no fields

Type thisIt means the value…
yesis yes
nois no
-yesis not yes
not nois not no
NULLhas no value
-NULLhas a value

True may be written 1, t, true, y, yes; false 0, f, false, n, no. Anything else is refused rather than guessed at.

Dates and times

This is the table worth keeping open. The last column is the range each expression resolves to on 9 September 2026, computed by the engine itself, written as start to end.

Every range is half-open: the start is included, the end is not. That is what makes 2026-01-01 to 2026-02-01 exactly January, with no argument about midnight, and what makes consecutive ranges join up without double-counting a day.

A period may be any of day, week, month, quarter, year, singular or plural.

Type thisWhat it meansResolves to
todayToday only.2026-09-09 to 2026-09-10
yesterdayThe day before today.2026-09-08 to 2026-09-09
tomorrowThe day after today.2026-09-10 to 2026-09-11
nowThe same as today.2026-09-09 to 2026-09-10
this weekThe whole week today falls in. Weeks start on Monday.2026-09-07 to 2026-09-14
this monthThe whole calendar month today falls in.2026-09-01 to 2026-10-01
this quarterThe whole calendar quarter.2026-07-01 to 2026-10-01
this yearThe whole calendar year.2026-01-01 to 2027-01-01
last weekThe whole week before this one. previous week is the same.2026-08-31 to 2026-09-07
last monthThe whole month before this one, not the last thirty days.2026-08-01 to 2026-09-01
last quarterThe whole quarter before this one.2026-04-01 to 2026-07-01
last yearThe whole year before this one.2025-01-01 to 2026-01-01
next monthThe whole month after this one.2026-10-01 to 2026-11-01
last 7 daysThe seven complete days before today. Today is not included.2026-09-02 to 2026-09-09
7 daysExactly the same: last and past are optional.2026-09-02 to 2026-09-09
past 7 daysAnd so is past.2026-09-02 to 2026-09-09
last 3 monthsThe three complete months before this one. This month is not included.2026-06-01 to 2026-09-01
last 12 monthsThe twelve complete months before this one: the usual rolling year.2025-09-01 to 2026-09-01
3 days agoThat one day, three days back.2026-09-06 to 2026-09-07
2 months agoThat one whole month, two months back.2026-07-01 to 2026-08-01
3 months ago for 2 monthsTwo whole months, beginning three months back.2026-06-01 to 2026-08-01
next 7 daysThe seven complete days after today.2026-09-10 to 2026-09-17
year to date1 January up to and including today.2026-01-01 to 2026-09-10
quarter to dateThe start of this quarter up to and including today.2026-07-01 to 2026-09-10
month to dateThe 1st of this month up to and including today.2026-09-01 to 2026-09-10
week to dateMonday up to and including today.2026-09-07 to 2026-09-10
2026A whole calendar year, written out.2026-01-01 to 2027-01-01
2026-03A whole calendar month.2026-03-01 to 2026-04-01
2026-Q1A whole quarter. 2026q1 works too.2026-01-01 to 2026-04-01
2026-03-15One day. Year first, always.2026-03-15 to 2026-03-16
2026-03-15 14:30One minute, for a datetime column.2026-03-15T14:30:00 to 2026-03-15T14:31:00
2026-03-15 14:30:05One second.2026-03-15T14:30:05 to 2026-03-15T14:30:06
on 2026-03-15on may be written in front of any of these; it changes nothing.2026-03-15 to 2026-03-16
after 2026-01-01From the start of that day onwards, with no end. since and from are the same.2026-01-01 to …
before 2026-01-01Everything up to the start of that day.… to 2026-01-01
2026-01-01 to 2026-04-01The end is exclusive: January, February and March, not 1 April.2026-01-01 to 2026-04-01
2026-01 to 2026-03Both ends may be any expression above, and each contributes its start. So this is January and February.2026-01-01 to 2026-03-01
2026-01-01 - 2026-04-01A hyphen is another way of writing to; so is until.2026-01-01 to 2026-04-01
last month to todayMixing relative and absolute is allowed.2026-08-01 to 2026-09-09
2026-01, 2026-03Commas are or: January or March.2026-01-01 to 2026-02-01; 2026-03-01 to 2026-04-01
NULLThe date is missing.is null
-NULLThe date is present.is not null
not last monthEverything except last month. Rows with no date are kept.not 2026-08-01 to 2026-09-01
-2026-03The same idea written with a minus.not 2026-03-01 to 2026-04-01

Relative expressions are resolved when the query runs, against today, never against the newest date in the data. A report saved with last month means last month whenever somebody opens it.

Filters on measures

You can filter on a measure as well as on a dimension, and it is not the same operation.

A filter on a dimension throws rows away before the grouping happens: destination_country is IE,GB means only those parcels are counted at all. A filter on a measure throws away whole groups after they have been worked out: revenue >1000 means every country is counted in full, and then the countries whose total came to a thousand or less are dropped from the answer.

So the order is: filter the rows, group them, work out the measures, then filter the groups. That is why a measure filter cannot change what a measure adds up to, only which groups you see, and it is why measure filters always use the number grammar whatever the measure is.

One consequence worth knowing: the grand Totals row is a second query with no grouping, so a filter on a measure cannot apply to it. When you have both, the result says so with the warning "Totals ignore filters on measures". The total is the total of everything the row filters allowed, not of the groups that survived.

Sorting

Click a column header to sort by it: first click sorts largest (or newest) first, second click reverses it, third clears the sort.

Sorting re-runs the query, and that is deliberate. The result is capped at a row limit, so sorting the rows already on screen would tell you the largest of the five hundred you happen to have, not the largest there are. Re-running asks the warehouse for the top rows by that column.

If you pick fields without sorting, Explore chooses for you: the first measure largest first, or failing that the first date newest first, or failing that the first dimension A to Z.

The row limit

Every query carries a limit, shown in the Limit box beside Run. A new query starts at the default the catalogue set for that table, or 500 rows when it set none, and no query may exceed 5,000.

The limit is not a display setting: it is LIMIT in the SQL, so it is the number of rows the warehouse sends. That is what keeps an exploratory question from becoming an expensive one.

When the result comes back with exactly as many rows as the limit allowed, the row count reads "N rows of more" and the result is marked truncated. It means what it says: there were more rows and you are looking at the first ones in the sort order you asked for. A truncated result is perfectly good when you sorted by what you care about (the top twenty carriers) and misleading when you did not (an alphabetical list cut off at K). The cure is to group more coarsely, filter harder, or raise the limit.

Row limits apply to rows, not to the arithmetic: totals and measures are computed over every row in the warehouse that your filters allowed, not over the rows you were sent.

Totals

Tick Totals beside Run and the engine runs a second query: the same filters, the same table, no grouping. The answer appears as a bold row at the bottom of the table.

A rate total is the rate of the whole, not the average of the rows. If the on-time rate is 91.2% in Ireland and 88.0% in Great Britain, the total is not 89.6%. It is the on-time rate of all the parcels together, worked out from the totals underneath, which is the only figure that is true. That is the entire reason totals are a second query rather than a sum of the column: adding up a column of averages produces a number that is not the average of anything.

For the same reason, the pivot's own row and column totals, which are sums of what is on screen, only appear for measures that can honestly be added: sums and counts. Anything else shows a dash.

Pivots and row totals

A pivot turns the values of one dimension into columns. Group by carrier, pivot on Order date › Month, and instead of one long list of carrier-and-month pairs you get carriers down the side and months across the top.

Press the columns button that appears when you hover a dimension in the picker. Pivoting a field also ticks it. You can pivot more than one dimension, in which case the columns are every combination.

Some things to know:

  • The engine returns flat rows and Explore arranges them. The row limit therefore applies to the flat rows, before the pivot: a hundred carriers over twelve months is twelve hundred rows, not a hundred.
  • Pivot columns are sorted by value, so months read left to right whatever the sort is. The rows keep the order the warehouse returned them in, which is the order the limit was applied in.
  • Above about 40 distinct pivot values the result says the pivot is hard to read. It is a warning, not a refusal; a coarser timeframe or a filter usually fixes it.
  • Row totals is a checkbox above the pivoted table: it adds a total per row across the pivot columns. It only fills in sums and counts, for the reason in the section above.
  • You cannot pivot every dimension and pick no measures: there would be nothing left to put in the rows, and the engine says so.

Fields of your own

A catalogue cannot anticipate every question. When it has not got what you need, you can define a field for yourself, from the Your fields section at the foot of the field list. There are three kinds, and picking the right one is most of the work:

You wantChooseExample
A new way to group the rowsCustom dimensionHeavy, medium and light parcels
A new thing to count or totalCustom measureHow many distinct countries
Arithmetic on numbers already on screenCalculationRevenue divided by parcels

If you have used custom dimensions, custom measures and table calculations in another tool, these do the same jobs, so the menu will look familiar.

All three belong to your report, not to the catalogue. That is the difference that matters. A catalogue field is a shared definition everybody uses; one of yours is your own working. The field list keeps them apart, and any column you defined is marked in the table, so nobody can mistake your working for the governed number. If a definition turns out to be one everybody should have, it belongs in the catalogue, where it gets an owner and one spelling.

All three are saved with the report, survive a link, appear in a CSV or Excel download, and travel to Dashboards. The read-only viewer shows them and offers no way to edit them.

Custom dimensions

A custom dimension is an expression over the row's own columns, and it becomes a real dimension: you can group by it, pivot on it, sort by it and filter it, exactly like one from the catalogue.

IF [Weight (kg)] > 5 THEN "Heavy" ELSEIF [Weight (kg)] > 1 THEN "Medium" ELSE "Light" ENDIF

Put that on a report of parcels and the rows become three: Heavy, Medium and Light. Nothing is lost and nothing is double-counted, because every parcel falls into exactly one bucket.

Two more that come up constantly: [Carrier] + " / " + [Region] makes one label out of two fields, and [Weight (kg)] > 2 makes a yes/no flag you can group by.

It is compiled into the query itself, which is why it can group. Two consequences follow. You cannot use a measure in it, because a measure is a total and there are no totals yet when the rows are being made. And you cannot use the window functions such as RunningTotal or Rank, because they look along rows in order and a grouped query has no order to look along. Both refusals say so, and say which of the other two kinds to use instead.

Custom measures

A custom measure is a new aggregate. Pick what to do, pick a field, and optionally narrow it with a filter. This is the one to reach for when the catalogue has not counted something:

How many distinct destination countries does each carrier reach?

Choose Count of distinct values, choose Destination country, and that is the whole job.

Do thisNeeds a fieldNotes
CountnoCounts the rows in each group.
Count of distinct valuesyesCounts how many different values there are. Works on text as well as numbers.
SumyesNeeds a number.
AverageyesNeeds a number.
SmallestyesWorks on numbers, text and dates.
LargestyesWorks on numbers, text and dates.
MedianyesNeeds a number.

A report may have up to 20 custom measures and 20 custom dimensions.

Filters are the second half of it. A filter on a custom measure narrows what that measure sees and nothing else on the report. So "revenue, Europe only" can sit in a column next to plain revenue and both are right. That works because the filter is folded inside the aggregate rather than applied to the query, which would have changed every other column too.

The filter uses the same grammar as the report's own filters, so last 12 months, IE,GB and -NULL all mean what they mean everywhere else.

Two rules the editor enforces. You measure a dimension, never another measure, because a sum of a sum is not a thing. And Sum, Average and Median need a number: asked for one over text, the refusal points out that counting would work.

Calculations

A calculation is arithmetic across the rows the query has already returned. [Revenue (EUR)] / [Shipments] gives revenue per parcel. PercentOfTotal([Revenue (EUR)]) gives each row's share of the total. Rank([Revenue (EUR)]) numbers the rows from the biggest down.

Unlike the other two, a calculation is worked out after the query, over the result. That has three consequences worth knowing.

It cannot aggregate. By the time it runs the rows are already grouped and totalled, so there is nothing left to add up: a row saying DHL, 12,757 parcels, 1.9m revenue is already the sum. Asking for Sum(...) is refused with a note pointing at a custom measure, which is the thing that can.

Order matters for the window functions. RunningTotal, Rank, Difference and their kin look along the rows in the order the report is sorted, so re-sorting changes what a running total means. It is worth knowing before you wonder why a total ran differently after you clicked a column heading. It is also why a calculated column cannot be sorted by: the sort happens at the warehouse, before the calculation exists.

It is free to change. The result is remembered against the query, and a calculation never changes the query, so editing a formula re-uses the rows already fetched. On BigQuery that is the difference between a free edit and a billed one.

Calculations may build on each other, in the order you wrote them, so an awkward answer can be assembled in readable steps instead of one unreadable expression. The SQL tab lists them underneath the statement, because they are worked out after it runs and will not be found in it.

The expression language

Custom dimensions and calculations both use the Binse formula language, the same one the Data Flow canvas uses, so anything learned there works here. Fields are written in square brackets using the label you see in the picker: [Revenue (EUR)]. Both editors will insert one for you, so you rarely need to type a bracket.

The two differ only in what they may call, because they run in different places.

There are 73 of them. Aggregates are deliberately absent; see above.

Window

FunctionWhat it does
Difference(value)This row's value minus the row before it. The first row has no answer, so it is null.
PercentDifference(value)The change from the row before, as a fraction of it. Null on the first row and when the previous row is zero.
PercentOfTotal(value)This row's share of the total, as a fraction between 0 and 1. Format it as a percentage.
Rank(value)Position when the rows are put in order, largest first: 1 is the biggest. Ties share a place.
RankAscending(value)Position when the rows are put in order, smallest first: 1 is the smallest.
RunningTotal(value)Adds the value up as the rows go by, so the last row carries the whole total.
Total(value)The value added up over every row, repeated on each row. Useful as a denominator.

Conditional

FunctionWhat it does
Coalesce(a, b, ...)First non-null value.
IIF(condition, then, else)Returns then if condition is true, otherwise else.
Switch(value, default, case1, result1, case2, result2, ...)Compares value to each case and returns the matching result, or default.

Conversion

FunctionWhat it does
Int(value)Truncate to an integer.
ToDate(value)Convert a string or datetime to a date.
ToDateTime(value)Convert a string or date to a datetime.
ToNumber(value)Convert to a number (null if not numeric).
ToString(value, [decimals])Convert to a string.

DateTime

FunctionWhat it does
DateParse(string, format)Parse a string with a format such as "%d/%m/%Y". Returns a date.
DateTimeAdd(datetime, n, units)Add n units (days, months, years, hours, minutes, seconds).
DateTimeDay(datetime)Day of month.
DateTimeDiff(end, start, units)Difference end - start in units.
DateTimeFormat(datetime, format)Format a date/datetime as a string.
DateTimeHour(datetime)Hour 0-23.
DateTimeMinute(datetime)Minute 0-59.
DateTimeMonth(datetime)Month 1-12.
DateTimeNow()Current date and time.
DateTimeParse(string, format)Parse a string with a format such as "%d/%m/%Y". Returns a datetime.
DateTimeQuarter(datetime)Quarter 1-4.
DateTimeSecond(datetime)Second 0-59.
DateTimeToday()Today's date.
DateTimeTrim(datetime, part)Truncate to firstofmonth, lastofmonth, year, day, hour.
DateTimeWeekday(datetime)Day of week, Monday=1 to Sunday=7.
DateTimeYear(datetime)Year as a number.

Math

FunctionWhat it does
Abs(x)Absolute value.
Ceil(x)Round up.
Exp(x)e to the power x.
Floor(x)Round down.
Log(x)Natural logarithm.
Log10(x)Base-10 logarithm.
Mod(a, b)Remainder of a / b.
Pow(x, y)x to the power y.
Random()Random number between 0 and 1.
Round(x, [multiple])Round to the nearest multiple (default 1). Round(x, 0.01) gives 2 decimals.
RowNumber()1-based row number (within the group in Formula Across Rows).
Sqrt(x)Square root.

Regex

FunctionWhat it does
REGEX_CountMatches(string, pattern, [caseInsensitive])Number of matches.
REGEX_Extract(string, pattern, [group])Text of the first match's group (default 1).
REGEX_Match(string, pattern, [caseInsensitive])True if the whole string matches pattern.
REGEX_Replace(string, pattern, replacement, [caseInsensitive])Replace regex matches; use $1 for groups.

String

FunctionWhat it does
Contains(string, target, [caseInsensitive])True if string contains target.
CountWords(string)Number of whitespace-separated words.
EndsWith(string, target, [caseInsensitive])True if string ends with target.
FindString(string, target)0-based position of target, or -1.
GetPart(string, delimiter, n)The n-th part (0-based) after splitting on delimiter.
GetWord(string, n)The n-th word (0-based), split on spaces.
Left(string, n)First n characters.
Length(string)Number of characters.
Lowercase(string)Convert to lower case.
PadLeft(string, length, char)Pad on the left to length.
PadRight(string, length, char)Pad on the right to length.
Repeat(string, n)Repeat the string n times.
Replace(string, target, replacement)Replace every occurrence of target.
ReplaceChar(string, chars, replacement)Replace every character listed in chars with replacement.
ReverseString(string)Reverse the characters.
Right(string, n)Last n characters.
StartsWith(string, target, [caseInsensitive])True if string starts with target.
Substring(string, start, [length])Characters from start (0-based).
TitleCase(string)Capitalise each word.
Trim(string, [chars])Remove leading and trailing whitespace or chars.
TrimLeft(string, [chars])Remove leading whitespace or chars.
TrimRight(string, [chars])Remove trailing whitespace or chars.
Uppercase(string)Convert to upper case.

Test

FunctionWhat it does
IsEmpty(value)True if value is null or an empty string.
IsNull(value)True if value is null.
IsNumber(value)True if the value can be read as a number.

A reference to the row before is written [Row-1:Heading], and [Row+1:Heading] is the row after. The first and last rows have no answer, so they come back empty.

In a custom dimension

A custom dimension becomes part of the SQL sent to your warehouse, so what it may use depends on what that warehouse can express. The editor offers only the functions your connection actually supports, and refuses the rest with a reason rather than compiling something almost right.

WarehouseFunctions available
BigQuery67
Databricks67
PostgreSQL67
Snowflake67
Local files (DuckDB)66
MySQL62
SQL Server / Azure SQL60
SQLite55
Other (ODBC)54

These are never available in a custom dimension, whatever the warehouse, because they look along rows in an order a grouped query has not got. Use a calculation instead: Difference, PercentDifference, PercentOfTotal, Random, Rank, RankAscending, RowNumber, RunningTotal, Total.

Aggregates are refused too, with a note pointing at a custom measure. So is a reference to the previous row, [Row-1:Field], for the same reason as the window functions.

Drill-down

The catalogue can say what a field drills into: destination country drills into carrier and service level, region drills into country. Where it does, a cell in the table has a small button (and a right-click menu) offering Drill into ….

Drilling rewrites the query: it pins every dimension of the row you clicked as a filter, replaces the dimensions with the ones drilled to, keeps the measures you had, clears any pivot, and runs. Clicking Ireland on a country-by-revenue table therefore gives you carriers within Ireland, with the same revenue measure and a new chip reading destination_country: IE.

Because it is only a new query, the back button undoes it, and the filter chip it added can be edited or removed like any other.

The table

The table shows the result as it came back, formatted the way the catalogue asked: thousands separators, currency prefixes, percentages, dates by their timeframe. A missing value is drawn as ∅ rather than left blank, so an empty cell is never ambiguous.

  • Copy as TSV puts the whole grid on the clipboard as tab-separated text, with raw numbers and ISO dates rather than formatted ones, which is what Excel wants. Headers, pivot columns and the totals row come with it.
  • Long results scroll smoothly because only the visible rows are drawn once a result passes a few hundred rows.
  • The header is the sort control, as described above.
  • Day-of-week and month-of-year values are shown by name (Mon, Dec) although they are numbers underneath, which is why they still sort in calendar order.

The visualisation tab

The Visualisation tab draws the same rows the table holds. Choose a shape from the strip on the right; the options underneath change with it.

The mapping is fixed, so a saved report draws the same picture everywhere: the row dimensions become the categories along the axis, and each measure becomes a series. When a pivot is in play, each pivot value becomes a series instead.

There are nine shapes.

ShapeWhat it draws
TableThe grid itself, pivoted when you pivoted. The default, and the honest one.
ColumnVertical bars, one group per category. The everyday comparison.
BarHorizontal bars. Better when the category labels are long.
LineA line per series. For a date on the x axis, which is what a timeframe usually gives you.
AreaA line with the area under it filled. Stacked, it shows composition over time.
PieParts of one whole, from the first measure. Only readable with a handful of slices.
DonutThe same, with the middle open.
ScatterOne point per row. With two measures and no pivot, the first is the x axis and the second the y, which is the version worth using.
SingleOne big number: the first measure of the first row, with a second measure underneath as a comparison when you picked one.

The options:

  • Stacked (column, bar, line, area): stack the series on top of one another rather than beside.
  • Show values: print each number on the mark.
  • Legend: show the series names. It is on by default and only appears when there is more than one series.
  • Series from: when there is no pivot and you have more than one measure or more than one dimension, this spreads a dimension into series without changing the query. Only the first measure is drawn then, because a chart cannot honestly carry two legends.

Under the strip, a line says what is on the x axis and how many series there are. That is usually the fastest way to see why a chart looks wrong: forty series means a dimension is on the wrong axis.

The SQL tab

The SQL tab shows the query. When the result on screen is current, it is exactly the SQL that produced it. When you have changed the selection since the last run, it compiles the current selection without running it and says "Compiled for the current selection, not run yet".

It is there for three reasons. It is how you check that a measure means what you think it means. It is what you hand to a data engineer when a number looks wrong. And it is the honest answer to "where did this come from", which no summary of a query can be.

The bar above it names the dialect, shows the estimated bytes on BigQuery, and copies the SQL to the clipboard. Underneath, a line says who runs it: through a named connection as you, or locally with DuckDB over the catalogue's files.

Saving a report

Save… turns the current query into a report. You give it:

  • A name, which is what people will see.
  • A folder, written as a path with slashes (Commercial/Monthly). Folders exist only as long as a report is in them; there is nothing to create or delete.
  • A description, shown under the title in the viewer.
  • Which filters the viewer may change. Every filter on the query is offered, ticked by default. Untick one to freeze it: the report will always apply it and the reader will not see a box for it. That is how a report can be "European shipments, and the reader chooses the month".

The report keeps the query, the visualisation and its options, and the list of editable filters. It does not keep the result: opening a report runs it again, so the numbers are current.

Saving validates the query first. A report that could not run is not worth keeping, so a broken one is refused with the reason rather than saved.

Opening a report from Edit in Explore brings it back with everything as it was and marks the header "editing"; Save then updates that report rather than making a second one. Renaming it keeps its identity, so links do not break.

The report viewer

A report opens on its own page: the title, the description, the filters its author left open, a Run button, and the result. There is no field picker, no limit box and nothing to break, which is the point.

  • Changing an open filter and pressing Run (or Ctrl + Enter) re-runs it with that change. Nothing is saved: the report is unchanged for everybody else.
  • When the report was saved as a chart, a Table / Visualisation switch lets a reader see the numbers behind the picture.
  • Clicking a column header sorts, which re-runs the query, exactly as it does in Explore.
  • Download and Add to Dashboards are here too.
  • Edit in Explore opens the report's query in the full surface.

If the catalogue or table a report was built on has gone, the report says so plainly rather than failing: it is listed as missing on the home page and explains itself when opened.

Downloading

Download opens a dialog with four choices. The defaults are right most of the time, so if you are in a hurry press Download and read no further.

The formats

FormatFileDefault values
CSV.csvformatted
TXT (tab-separated values).txtformatted
Excel spreadsheet.xlsxraw
JSON.jsonraw
HTML.htmlformatted
Markdown.mdformatted

CSV is the one to reach for by default. Use tab-separated when the data itself contains commas and the other end is fussy, Excel when somebody will open it and work in it, JSON when a program will read it, and HTML or Markdown when the table is going into a document or a page.

As shown, everything, or a number you pick

  • Current result table gives exactly the rows on screen, so the file matches what you were looking at, down to the last row.
  • All results ignores the report's row limit. The limit exists to keep the screen responsive and has no business deciding what goes into a spreadsheet, which is why this exists.
  • Custom takes a number.

All results is still capped, by a setting rather than by the report, and if the export reaches that ceiling you are told so rather than quietly handed a short file. See Settings for the number.

Formatted or unformatted

This is the choice worth understanding, because both answers are right for different jobs.

  • Formatted writes what the report shows: €8,867, 84.2%, Yes. Right for a slide or a document, where the numbers are read rather than calculated with.
  • Unformatted writes the underlying value at full precision: 8867.42, 0.8423, true. Right for feeding another system, or for doing more arithmetic.

Excel defaults to unformatted on purpose. A formatted number arrives in a spreadsheet as text, and text cannot be summed. Finding that out after sending somebody a workbook is a bad afternoon, so the dialog picks the safe answer and lets you override it deliberately.

Remove all sorts

A tick box, and a performance option rather than a preference. Putting a large export in order costs the warehouse real work for an ordering the spreadsheet is about to replace anyway.

One caution: the window calculations follow the sort. A running total or a rank means something different in a different order, so leave the sorts alone if the file carries one.

Details worth knowing

The download runs the query again rather than saving what is on screen, so it is current. Anything you defined yourself is in the file: custom dimensions, custom measures and calculations all appear as columns, the same as on screen.

The totals row is not included. A spreadsheet's own total would then be counting twice, and a rate total does not survive being pasted anyway.

CSV and tab-separated files are UTF-8 with a byte-order mark, which is what makes Excel open accented characters correctly on a Windows machine.

There is no picture-of-the-chart option. A picture is taken by a browser and this is done by the engine, the same reason Schedules delivers a web page rather than a PDF. To put a chart in a slide, use Add to Dashboards and copy the visual from there.

Add to Dashboards

Add to Dashboards hands the query to the Dashboards workspace as a dataset. What actually happens depends on the catalogue:

  • A warehouse catalogue registers the compiled SQL as a connection dataset. Dashboards holds the query, not a copy of the rows, so refreshing the dashboard re-runs it against the warehouse. The aggregation, the filters and the row limit all travel with it, which is what keeps it small.
  • A local catalogue has no connection for Dashboards to reuse, so the result is written to a Parquet file the app keeps for it, and registered as a dataset over that file.

Either way what crosses is the aggregate, never the table. You give it a name, and Dashboards appears in the toast so you can go straight there.

This needs the whole product running. The Explore engine started on its own has no Dashboards to hand anything to and says so.

Authoring a catalogue

Press New catalogue on the home page, or the pencil on a catalogue that is already there.

1. Name it and choose the connection. The connection is either Local files (DuckDB), which reads Parquet, CSV and Excel files from your own machine, or one of the saved connection profiles this installation knows about. Connections are shared with the rest of the product, so a warehouse set up in Data Flow or Dashboards is already here. A profile whose driver is not installed is listed but cannot be chosen, and says what is missing.

The description is a good place for the owner's name. Somebody will need to know whom to ask.

2. Add a table. Add a table browses the connection: choose a dataset or schema (or, for local files, a folder), pick a table, and see its columns. From SQL instead takes a SELECT statement, which is how a join is published; see the next section.

Either way, Suggest fields does the first draft: it reads a sample of the table and turns every column into a dimension typed from its data type, adds a count measure, and adds a sum for each numeric column that looks like a quantity rather than an identifier. Labels are made readable (merchandise_value_eur becomes "Merchandise value (EUR)"). It is a draft: the work is deleting what nobody needs and naming what is left.

3. Describe the fields. The field grid is the catalogue. Each row is one field:

  • Name is the identifier used in references and in saved reports. Lower-case letters, digits and underscores. Changing it breaks reports that use it, so choose it once.
  • Label is what people see. Leave it empty and a readable one is made from the name.
  • Group puts the field into a section of the picker. Group order on the table above controls the order of those sections; anything not listed follows alphabetically.
  • Kind is the dimension type or the measure kind. See Field kinds.
  • SQL is the expression over the table's columns. Usually just the column name. It may be an expression (CASE WHEN on_time THEN 1.0 ELSE 0.0 END), and it may refer to other fields of the same table as ${other_field}.
  • Timeframes, for a time dimension: the column's own type (date, datetime, timestamp) and which timeframes to offer.
  • Format: a number pattern. See Number formats.
  • Hidden keeps a field out of the picker while leaving it usable inside ${references}. That is how a helper expression is defined once and reused without cluttering the list.
  • Drill is the list of dimensions a value of this field drills into.

4. Test it. Test compiles and runs a query that touches every visible field of the table at once and shows five rows and the SQL. It is the fastest way to find a typo in an expression, and it proves a derived table's SQL before anybody depends on it.

Be aware of what it costs on a warehouse. Every field means every column, so on BigQuery a Test reads the whole table's columns once. Five rows do not make it cheap: BigQuery bills for the columns a query touches, not the rows it returns. It carries the same billed-bytes cap as everything else, and it is a button you press deliberately, but do not lean on it.

5. Save. Saving validates the whole document and refuses with every problem at once rather than the first one: unknown measure kinds, references to fields that do not exist, a measure built out of a dimension, a reference that loops, two fields with the same name. The problems name the table and the field, so they are usually a one-line fix.

The catalogue document

A catalogue is a JSON file, which means it can be copied, diffed, put in version control and reviewed. The editor writes it for you; this is what it writes.

The catalogue shipped with Explore is a complete worked example: one local table over a Parquet file, with counts, sums, averages, a distinct count, two rates and two measures built out of other measures.

{
  "version": 1,
  "name": "Shipments (sample)",
  "description": "A sample catalogue over the sample shipments file, so Explore can be tried without a warehouse. Owner: whoever installed this. Edit or delete it freely.",
  "connection": {"kind": "local"},
  "tables": [
    {
      "id": "shipments",
      "label": "Shipments",
      "description": "One row per parcel shipped, January 2024 to August 2026. Synthetic sample data.",
      "source": "../samples/shipments.parquet",
      "groups": ["Order", "Route", "Carrier", "Service", "Money"],
      "defaults": {"limit": 500},
      "fields": [
        {"name": "order_date", "label": "Order date", "group": "Order", "type": "time", "sql": "order_date", "datatype": "date",
         "timeframes": ["date", "week", "month", "quarter", "year", "day_of_week", "month_of_year"],
         "description": "The day the order was placed."},
        {"name": "ship_date", "label": "Ship date", "group": "Order", "type": "time", "sql": "ship_date", "datatype": "date",
         "timeframes": ["date", "week", "month", "quarter", "year"]},
        {"name": "channel", "label": "Channel", "group": "Order", "type": "string", "sql": "channel", "description": "Web, App or Marketplace."},
        {"name": "category", "label": "Category", "group": "Order", "type": "string", "sql": "category"},
        {"name": "items", "label": "Items in parcel", "group": "Order", "type": "number", "sql": "items"},
        {"name": "origin_country", "label": "Origin country", "group": "Route", "type": "string", "sql": "origin_country"},
        {"name": "destination_country", "label": "Destination country", "group": "Route", "type": "string", "sql": "destination_country",
         "description": "ISO code of the delivery country.", "drill": ["carrier", "service_level"]},
        {"name": "destination", "label": "Destination", "group": "Route", "type": "string", "sql": "destination_name",
         "drill": ["carrier"]},
        {"name": "region", "label": "Region", "group": "Route", "type": "string", "sql": "region", "drill": ["destination_country"]},
        {"name": "carrier", "label": "Carrier", "group": "Carrier", "type": "string", "sql": "carrier", "drill": ["service_level"]},
        {"name": "service_level", "label": "Service level", "group": "Carrier", "type": "string", "sql": "service_level"},
        {"name": "on_time", "label": "Delivered on time", "group": "Service", "type": "yesno", "sql": "on_time"},
        {"name": "returned", "label": "Returned", "group": "Service", "type": "yesno", "sql": "returned"},
        {"name": "transit_days", "label": "Transit days", "group": "Service", "type": "number", "sql": "transit_days"},
        {"name": "weight_kg", "label": "Weight (kg)", "group": "Order", "type": "number", "sql": "weight_kg"},

        {"name": "shipments", "label": "Shipments", "group": "Order", "measure": "count", "description": "Number of parcels."},
        {"name": "total_items", "label": "Total items", "group": "Order", "measure": "sum", "sql": "items", "format": "#,##0"},
        {"name": "revenue", "label": "Revenue (EUR)", "group": "Money", "measure": "sum", "sql": "merchandise_value_eur", "format": "€#,##0",
         "description": "Merchandise value of the parcels."},
        {"name": "shipping_charged", "label": "Shipping charged (EUR)", "group": "Money", "measure": "sum", "sql": "shipping_charged_eur", "format": "€#,##0"},
        {"name": "carrier_cost", "label": "Carrier cost (EUR)", "group": "Money", "measure": "sum", "sql": "carrier_cost_eur", "format": "€#,##0"},
        {"name": "duties", "label": "Duties (EUR)", "group": "Money", "measure": "sum", "sql": "duties_eur", "format": "€#,##0"},
        {"name": "shipping_margin", "label": "Shipping margin (EUR)", "group": "Money", "measure": "number",
         "sql": "${shipping_charged} - ${carrier_cost}", "format": "€#,##0", "description": "What was charged for shipping minus what the carrier cost."},
        {"name": "average_order_value", "label": "Average order value (EUR)", "group": "Money", "measure": "number",
         "sql": "${revenue} / NULLIF(${shipments}, 0)", "format": "€#,##0.00"},
        {"name": "average_transit_days", "label": "Average transit days", "group": "Service", "measure": "avg", "sql": "transit_days", "format": "0.0"},
        {"name": "on_time_rate", "label": "On-time rate", "group": "Service", "measure": "avg",
         "sql": "CASE WHEN on_time THEN 1.0 ELSE 0.0 END", "format": "0.0%"},
        {"name": "return_rate", "label": "Return rate", "group": "Service", "measure": "avg",
         "sql": "CASE WHEN returned THEN 1.0 ELSE 0.0 END", "format": "0.0%"},
        {"name": "average_weight", "label": "Average weight (kg)", "group": "Order", "measure": "avg", "sql": "weight_kg", "format": "0.00"},
        {"name": "distinct_destinations", "label": "Countries served", "group": "Route", "measure": "count_distinct", "sql": "destination_country"}
      ]
    }
  ]
}

That document publishes one table with 15 dimensions and 13 measures, and this is how it reads in the picker:

FieldLabelKindDefined as
order_dateOrder datedimension, time (date)order_date, at date, week, month, quarter, year, day_of_week, month_of_year
ship_dateShip datedimension, time (date)ship_date, at date, week, month, quarter, year
channelChanneldimension, stringchannel
categoryCategorydimension, stringcategory
itemsItems in parceldimension, numberitems
origin_countryOrigin countrydimension, stringorigin_country
destination_countryDestination countrydimension, stringdestination_country
destinationDestinationdimension, stringdestination_name
regionRegiondimension, stringregion
carrierCarrierdimension, stringcarrier
service_levelService leveldimension, stringservice_level
on_timeDelivered on timedimension, yesnoon_time
returnedReturneddimension, yesnoreturned
transit_daysTransit daysdimension, numbertransit_days
weight_kgWeight (kg)dimension, numberweight_kg
shipmentsShipmentsmeasure, countevery row
total_itemsTotal itemsmeasure, sumitems
revenueRevenue (EUR)measure, summerchandise_value_eur
shipping_chargedShipping charged (EUR)measure, sumshipping_charged_eur
carrier_costCarrier cost (EUR)measure, sumcarrier_cost_eur
dutiesDuties (EUR)measure, sumduties_eur
shipping_marginShipping margin (EUR)measure, number${shipping_charged} - ${carrier_cost}
average_order_valueAverage order value (EUR)measure, number${revenue} / NULLIF(${shipments}, 0)
average_transit_daysAverage transit daysmeasure, avgtransit_days
on_time_rateOn-time ratemeasure, avgCASE WHEN on_time THEN 1.0 ELSE 0.0 END
return_rateReturn ratemeasure, avgCASE WHEN returned THEN 1.0 ELSE 0.0 END
average_weightAverage weight (kg)measure, avgweight_kg
distinct_destinationsCountries servedmeasure, count_distinctdestination_country

Three things in it are worth copying into your own catalogue.

A rate as an average of ones and zeros. on_time_rate is an avg over CASE WHEN on_time THEN 1.0 ELSE 0.0 END, formatted 0.0%. That is the correct way to carry a percentage: the database averages the flags, so every level of grouping and every filter gets the true rate for what it covers.

Arithmetic over measures. average_order_value is a number measure, ${revenue} / NULLIF(${shipments}, 0). It refers to two other measures, so it is recomputed rather than averaged, and the NULLIF keeps a group with no shipments from dividing by zero.

Drill paths. region drills to destination_country, which drills to carrier and service_level. That is a hierarchy expressed as three small statements rather than one structure, and it means a click on a region leads somewhere sensible.

The source is a relative path, so the catalogue and its file travel together. In a warehouse catalogue the same field would read "source": "analytics.curated.shipments".

Derived tables, which is where a join lives

Explore has no join planner and will not grow one. A query is one SELECT over one table, which is what makes the SQL tab readable and the cost predictable.

A derived table is the answer. In the catalogue editor, From SQL takes a SELECT statement - with as many joins, unions and subqueries as the question needs - and publishes its result as a table. Fields are then defined over the columns that statement produced, exactly as they would be over a physical table, and every query Explore compiles wraps it:

SELECT ... FROM (
  SELECT o.order_id, o.order_date, c.country, o.value_eur
  FROM orders o JOIN customers c ON c.id = o.customer_id
) AS t
WHERE ...
GROUP BY ...

The join is written once, by somebody who knows the data, and reviewed like any other code. Nobody exploring can get it wrong, and the warehouse's own optimiser sees the whole statement, so a filter still reaches the underlying tables.

Two practical notes. A derived table has no stored size, so the cost guard's fall-back estimate cannot see it; the dry run still can. And if the same join is wanted by more than one catalogue, it belongs in a view in the warehouse rather than pasted twice.

Shared measures

A measure is defined in the catalogue and nowhere else. A report picks a measure; it can never define one.

This is not a limitation, it is the product. The moment two people can each write their own version of "revenue", the two numbers in the two reports differ, and every meeting about the difference is lost time. One definition, in one file, with an owner.

It is also why a measure may be built out of other measures. ${revenue} / NULLIF(${shipments}, 0) is a number measure: it is not an aggregate itself, it is arithmetic over aggregates, so it is recomputed correctly at every level of grouping and under every filter. Rules the editor enforces:

  • A number measure refers to measures. Referring to a dimension is a mistake and is refused, with the suggestion to make it a sum or avg over an expression instead.
  • Any other measure kind aggregates dimensions or raw columns, never another measure. A sum of a sum is not a thing.
  • References may not loop, directly or through a chain.
  • NULLIF(x, 0) is the idiom for a denominator. Without it, a group whose denominator comes to zero fails with a division error instead of leaving an empty cell.

Publishing a catalogue

There is no catalogue server. Publishing is copying a file to a folder other people can read.

  1. Author the catalogue on your own machine. It is saved under Documents\Binse Data Analytics & Dashboards\Catalogues.
  2. Copy the .json file to a shared folder.
  3. Each person adds that folder once, under Settings then Catalogue folders.

From then on the catalogue appears in their list, marked read-only, with the path it came from. They can explore it, save reports over it and download from it. They cannot change it, and neither the edit form nor the delete button will act.

Copy to my catalogues takes an editable copy into their own Documents folder. That is the supported way to adapt somebody else's catalogue: a copy with its own name, which nobody mistakes for the shared one. The two then have separate lives, which is honest - there is no merge, and pretending otherwise would be worse.

Updating a published catalogue is overwriting the file on the share. The list is read fresh every time it is asked for, so people pick up a change the next time they load the page. A catalogue that somebody has open keeps working; their next query uses the new definitions.

A catalogue file that cannot be read - bad JSON, or a field with a typo - would otherwise simply vanish from the list, so the home page says which file failed and why.

Connecting to BigQuery

BigQuery is the warehouse Explore was built for. No live BigQuery project has answered a query from Explore yet: its SQL is checked as text and its driver options are checked without connecting, so treat it as unproven rather than proven until that run has happened.

The connection string is bigquery://project/dataset, for example bigquery://acme-analytics/curated. That dataset becomes the default, so a table published as shipments is looked for there; a table in another dataset is published as dataset.table, or project.dataset.table to reach another project entirely. Two options may be added: bigquery://project/dataset?location=EU when your data is not in the multi-region the driver would guess, and ?credentials=C:\path\key.json for the rare case of a service-account file.

Signing in. With no credentials option the driver uses Google's Application Default Credentials. On a normal laptop that means running this once, in a terminal:

gcloud auth application-default login

A browser window asks you to sign in to Google and the credentials are left where the driver finds them. There is nothing to type into Binse and no key is ever written by Binse.

Queries run as you. Every query Explore sends carries your Google identity, so Google's IAM decides what you may read. If you cannot see a table in the BigQuery console, you cannot see it here either, and the error you get is Google's. This is the whole access-control story: Binse adds none of its own, has no accounts, and cannot grant anybody anything.

Two things follow. Two people opening the same report may legitimately see different numbers, or one of them an error, because they have different grants. And a catalogue is not a permission: it says what is published, not who may read it.

The driver is adbc-driver-bigquery, which ships with the installer. A machine without it says so and offers the pip install line rather than failing at query time.

Warehouses and dialects

A catalogue points at one connection, and the engine compiles its queries into that connection's dialect. You never write the SQL, but it is worth knowing which of these has been proved.

ConnectionDialectConnection string looks likeNeedsTried
Local files (DuckDB)duckdba folder of .parquet, .csv or .xlsx filesnothing extraexercised
SQLitesqlitesqlite:///C:/path/to/file.dbnothing extraported from the Dashboards compiler, unconfirmed
SQL Server / Azure SQLmssqlmssql://user:${BINSE_DB_PASSWORD}@server.database.windows.net:1433/databasepip install connectorxported from the Dashboards compiler, unconfirmed
PostgreSQLpostgrespostgresql://user:${BINSE_DB_PASSWORD}@host:5432/databasepip install connectorxported from the Dashboards compiler, unconfirmed
MySQL / MariaDBmysqlmysql://user:${BINSE_DB_PASSWORD}@host:3306/databasepip install connectorxported from the Dashboards compiler, unconfirmed
Snowflakesnowflakesnowflake://user:${BINSE_DB_PASSWORD}@account/database/schema?warehouse=WHpip install adbc-driver-snowflakeported from the Dashboards compiler, unconfirmed
BigQuerybigquerybigquery://project/datasetpip install adbc-driver-bigqueryexercised
Databricksdatabricksdatabricks://token:${BINSE_DB_TOKEN}@host?http_path=/sql/1.0/warehouses/xxxpip install databricks-sql-connectorported from the Dashboards compiler, unconfirmed
Other database (ODBC)standardDriver={ODBC Driver 18 for SQL Server};Server=host;Database=db;UID=user;PWD=${BINSE_DB_PASSWORD}pip install pyodbcported from the Dashboards compiler, unconfirmed

Ported-but-unconfirmed means the compiler writes that dialect's SQL and the output is checked by tests, but no real server of that kind has answered a query. No warehouse has. The one engine that has answered real queries is DuckDB, reading files on your own machine.

The dialect table is a port of the one Dashboards uses for pushdown, so both products truncate a week to the same Monday and quote a name the same way.

Median is the one measure kind that is not available everywhere. Where a dialect has no median the engine trusts, it refuses to compile rather than quietly returning a different statistic.

DialectMedian
duckdbyes
bigqueryyes
sqliterefused, in these words: median is not available on sqlite; use avg, or a derived table that computes it
mssqlrefused, in these words: median is not available on mssql; use avg, or a derived table that computes it
postgresyes
mysqlrefused, in these words: median is not available on mysql; use avg, or a derived table that computes it
snowflakeyes
databricksyes
standardyes

The cost guard

BigQuery bills for the bytes a query reads. A wrong click on a large table is a real cost, so Explore guards it at both ends, and the two guards do different things.

Ask above (byteGuardGb, 1 GB by default) is Binse asking you. Before a query runs against BigQuery, the engine works out what it would read: a dry run through the driver where the driver answers, and the table's stored size as the fall-back. Above the threshold you get "Run anyway?" with the figure. Say yes and it runs; the answer applies to that query only.

Refuse above (maxBytesBilledGb, 10 GB by default) is Google refusing on your behalf. It is passed to the driver as a cap on billed bytes, and BigQuery declines to start a query that would exceed it instead of running it and charging you. Nothing in Binse can be clicked past it: raise the setting, or write a cheaper query.

Both exist because they fail differently. The first can be dismissed by a person in a hurry, which is exactly what you want for a query you know is worth it. The second cannot, which is what you want protecting the account from a mistake nobody noticed. Setting either to 0 turns it off.

The cap applies to every query the engine sends, not only the ones you press Run for: the value list behind a filter picker and the catalogue editor's Test read the same table and cost the same money.

The estimate is worked out once per query per session and remembered, so opening the SQL tab, then running, does not pay for two dry runs. The fall-back estimate - the table's stored size - overstates a query that reads only two columns, because BigQuery bills by column. That is the safe direction to be wrong in.

None of this applies to a local catalogue: DuckDB over a file on your own machine costs nothing and is not guarded.

Caching

A result is kept in memory for 10 minutes by default, keyed by the exact SQL. Change the chart type, switch to the table and back, open the SQL tab: none of that re-queries the warehouse. Change a filter and it does, because the SQL is different.

A cached result is labelled cached beside the row count, so you always know whether you are looking at a fresh answer.

Run without cache, under the More menu, forces a re-run. Use it when the underlying table has just been reloaded. The cache is also cleared whenever settings are saved or a catalogue is changed, since either could alter what a query means.

Set the cache to 0 minutes to turn it off. The trade is money and patience against freshness: on a warehouse that lands new rows once a night, ten minutes is nothing.

Number formats

A field may carry a format, a small Excel-like pattern that decides how its numbers are printed everywhere: the table, the chart, the tooltips and the totals. It is set once in the catalogue, so a currency is a currency in every report over that table.

A pattern is an optional prefix, a numeric core, an optional %, and an optional suffix. 0 means a digit that is always shown, # a digit shown only when it is there, , asks for thousands separators, and the digits after the point set how many decimals. A % multiplies the value by a hundred as well as adding the sign, so a rate is stored as 0.912 and shown as 91.2%.

Pattern1234.5-1234.50.912
01235-12351
0.01234.5-1234.50.9
0.001234.50-1234.500.91
#,##01,235-1,2351
#,##0.001,234.50-1,234.500.91
0%123450%-123450%91%
0.0%123450.0%-123450.0%91.2%
€#,##0€1,235-€1,235€1
#,##0 kg1,235 kg-1,235 kg1 kg
(none)1,234.5-1,234.50.91

Two things the table shows that are easy to miss. A percent pattern is for a value stored as a fraction, which is why 0.912 reads as 91.2% and why 1,234.5 under a percent pattern reads as an absurd number rather than a mistake in the arithmetic. And the minus sign goes in front of any prefix, so a negative amount reads -€1,235 rather than €-1,235.

With no pattern, numbers get thousands separators and up to two decimals. Whole numbers stay whole. A missing value is never formatted; it is drawn as ∅.

The formatting is done by Binse rather than by the browser's locale, so the same pattern produces the same text on every machine, which is what makes a saved report reproducible.

Settings

Settings on the home page holds five things:

  • Catalogue folders: the shared folders to read read-only catalogues from. Your own folder is listed underneath so you know where your files are.
  • Ask above (GB): the size of query that makes Binse ask before it runs.
  • Refuse above (GB): the size BigQuery itself will not run at all.
  • Default row limit: kept here for a new query. Be aware that the number a new query actually starts at today comes from the table's own default in the catalogue, or 500 when it sets none, so a table's default is the place to change it.
  • Cache (minutes): how long a result is reused.

Settings are yours and this machine's. They are not part of a catalogue and are not shared, which is right: a cost guard is about your budget and your patience.

Limits

WhatDefaultWhere it comes from
Rows a query returns500A table's own default in the catalogue, else this. The Limit box changes it for one query.
Most rows a query may return5,000Fixed. Explore is for questions, not extracts.
Ask before reading more than1 GBSettings. 0 never asks.
Refuse above10 GBSettings. 0 is no cap.
A result is reused for10 minutesSettings. 0 turns the cache off.
Most rows one download may write100,000Settings. The ceiling on All results, and on a custom count.
Distinct pivot values before a warning40Fixed. It is a warning, not a refusal.
Values offered by a filter picker50Fixed, and at most 500. Search to narrow.
Rows read to suggest fields20Fixed. Used to spot an identifier column.
Rows a catalogue Test returns5Fixed. It still reads every column.

Keyboard shortcuts

There are few of them on purpose. These are every key the app listens for.

KeysWhereWhat it does
Ctrl + EnterExplore and the report viewerRun the query. Cmd + Enter on a Mac keyboard.
EnterIn a filter boxRun the query.
EnterIn a dialogThe dialog's main action: save, add, apply.
Enter or SpaceOn a field in the pickerTick or untick it.
Enter or SpaceOn one of your own fieldsOpen it for editing.
DeleteOn one of your own fieldsRemove it from the report.
-> and <-On a date field in the pickerUnfold and fold its timeframes.
/The home pageJump to the search box.
F1 or ?AnywhereOpen this guide.
EscAnywhereClose the dialog, popover, menu or guide that is open.
Tab and Shift + TabIn a dialogMove through its controls; focus stays inside the dialog.

Everything else is a click. Nothing runs a query by accident, which is the shortcut that matters most on a warehouse that bills by the byte.

What Explore does not do

The honest list, so nothing is a surprise.

  • BigQuery is the only warehouse that has been exercised. The other dialects are ported from the Dashboards pushdown compiler and compile correctly, but no real database has confirmed them. Treat them as untested until yours says otherwise.
  • The catalogue editor's Test reads every column once. It is a real query over every visible field. On BigQuery that is the whole table's columns, and it is not free.
  • There is no exact byte count before a query's first run unless the driver's dry run answers. Otherwise the guard falls back to the table's stored size, which overstates a narrow query.
  • Median is not available on every dialect. Where it is not, the engine refuses with the reason rather than compiling something that would quietly be a different statistic. See Warehouses and dialects.
  • There is no LookML import. A catalogue is authored fresh from the handful of tables people actually use, with the suggester doing the first draft.
  • There is no query-time join planner. A derived table is the join, written once in SQL.
  • There is no access control of Binse's own. Google's IAM, or the file permissions on a local file, and nothing else.
  • Nothing is extracted. Explore never copies a warehouse table to your machine. The one way rows land anywhere is the explicit Add to Dashboards hand-off, and what lands is the aggregate.

Troubleshooting

"Pick at least one field." The query has nothing in it. Tick a dimension or a measure.

The driver for this connection is not installed. The catalogue points at a backend whose Python driver is missing on this machine. The message names the pip install line. Until then the table cannot be queried; a local catalogue always works.

"The connection this catalogue uses has been removed." The catalogue names a connection profile that no longer exists here. Open the catalogue and choose another connection, or re-create the profile with the same name.

A catalogue is missing from the home page. Look for the warning at the top: a file that could not be read is listed with the reason. Bad JSON is usually a trailing comma; a validation problem names the table and the field.

"Run anyway?" on every query. The estimate exceeds the ask above threshold. Either the table really is that big, or the dry run could not answer and the fall-back is using the whole table's stored size. Narrow the query, or raise the threshold in Settings.

A query is refused by BigQuery with a bytes-billed error. That is the refuse above cap doing its job. Raise it in Settings if the query is genuinely worth it.

A number looks wrong. Open the SQL tab and read the measure. Nine times in ten the field is a different thing from what its label suggested, and the fix belongs in the catalogue.

A rate total does not match the average of the column. It should not. See Totals.

"N rows of more". The row limit truncated the result. See The row limit.

A pivot is unreadable. Too many pivot values. Use a coarser timeframe, filter the pivoted field, or put it back in the rows.

A filter says it is not a date filter. The message lists the forms that work. The usual cause is a day-first date: write 2026-03-15, not 15/03/2026.

"median is not available on …". That dialect has no median the engine trusts. Use an average, or compute it in a derived table.

A report is marked missing. Its catalogue or table has gone or been renamed. Restore the catalogue, or rebuild the report over the new table.

"Read-only here." The catalogue came from a shared folder. Use Copy to my catalogues.

Nothing happens when I tick a field. That is correct: nothing runs until you press Run or Ctrl + Enter. Nothing you do to a query costs money by accident.

The page says the UI is not built. A source checkout with no web\dist. Run npm run build in binse-explore\web.

The user guide will not open. The engine serves docs/user-guide.html. If it is not there, run py -3.14 -X utf8 docs/build_docs.py.

Glossary

TermMeaning
CatalogueA JSON file listing the tables that may be explored, their fields and their measures. The unit of publishing.
Cost guardThe pair of thresholds that ask before, and refuse above, a size of query on BigQuery.
Derived tableA table published as a SELECT statement rather than a physical table. Where a join lives.
DialectThe flavour of SQL a connection speaks. The compiler writes it; you never do.
DimensionA field you group by: a country, a carrier, a month.
DrillReplacing the current dimensions with the ones a field drills into, filtered to the value you clicked.
Dry runAsking BigQuery what a query would read without running it.
Local catalogueA catalogue whose tables are files on your own machine, read by DuckDB.
MeasureA number computed over each group: a count, a sum, an average, or arithmetic over other measures.
Number measureA measure defined as arithmetic over other measures, so a rate is recomputed rather than averaged.
PivotTurning the values of a dimension into columns.
ProfileA saved connection to a database, shared with the other Binse workspaces.
ReportA saved query, opened read-only with the filters its author left open.
Row limitThe maximum number of rows a query returns. Not a display setting.
TimeframeThe grain a date is grouped at: date, week, month, quarter, year, or a part such as day of week.
TruncatedThe row limit cut the result: there were more rows.