All questions
Question 1
A transaction table has one row per purchase. A customer-history table stores a new row whenever a customer's segment changes, with effective start and end dates. Joining the tables on CustomerID alone causes older and newer transactions to appear under multiple segments.
What is the most appropriate way to troubleshoot and correct this duplication?
- Match each transaction to the customer-history row whose effective date range contains the transaction date. (correct answer)
- Use an inner join so customer-history records without corresponding transactions are removed.
- Keep the most recently loaded customer-history row for every CustomerID before joining.
- Hide the historical segment fields so Tableau does not display the duplicated categories.
Explanation: When you see a question about joining tables that change over time — like customer segments, pricing tiers, or account statuses — you're dealing with a slowly changing dimension (SCD). The core challenge is matching each transaction not just to the right customer, but to the right version of that customer's record at the time of the transaction.
A simple join on CustomerID alone ignores time entirely. If a customer has three historical segment rows, every transaction multiplies into three rows — one per segment record — causing the duplication described in the passage. The fix is a date-range join: match each transaction to the customer-history row where the transaction date falls between the effective start and end dates. This is exactly what A does, and it's the only approach that correctly aligns each transaction with its historically accurate segment.
B misses the point entirely — an inner join removes unmatched rows but does nothing to resolve the one-to-many relationship causing duplication. You'd still get inflated records for customers with multiple history rows. C sounds reasonable but is fundamentally wrong for historical analysis; keeping only the most recent segment row would misclassify past transactions that occurred under a different segment. That corrupts your data rather than fixing it. D is the worst option — hiding fields in Tableau doesn't fix the underlying duplicate rows in the data source. Your aggregates (sums, counts) would still be inflated; you'd just be hiding the evidence.
As a study tip: whenever a question involves a dimension table that stores history with date ranges, your instinct should be "date-range join," not a simple key match.
Question 2
Orders has one row per order. OrderLines has several rows per order, and Returns has several rows per order. OrderLines and Returns are each physically joined to Orders using OrderID. For an order with three line rows and two return rows, the resulting data contains 3×2=6 rows.
What is the primary structural cause of the six-row result?
- The two child tables form a fanout because their rows are independently matched through the same order key. (correct answer)
- The Orders table contains an unmatched record that is preserved once for every child table.
- Tableau automatically unions child-table rows whenever both joins use the same parent key.
- The return rows are duplicated because an inner join always preserves all rows from both inputs.
Explanation: When two child tables are both joined to the same parent, you need to think about what happens structurally at the row level — this question is testing your understanding of join fanout (also called a many-to-many multiplication effect).
Here's the core logic: Orders is the parent. OrderLines adds 3 rows for the order; Returns adds 2 rows. When both are joined to Orders via OrderID, the database has no natural way to pair the child rows with each other — so every OrderLines row gets matched with every Returns row, producing 3×2=6 rows. This is fanout: two independent child relationships intersecting through the same key create a Cartesian-style multiplication. Answer A correctly names this — the child tables are independently matched through the same parent key, which is precisely what causes the multiplication.
B is wrong because it describes an unmatched (null-preserving) record, which is a concept from outer joins, not from the duplication caused by two populated child tables multiplying against each other.
C is wrong because Tableau does not automatically union child rows. A union stacks rows vertically; it doesn't govern how two separate joins interact. This answer confuses union with join behavior.
D is wrong on two counts: inner joins do not preserve all rows from both inputs (that's an outer join), and duplication here stems from the two-child fanout structure, not from a property of inner joins alone.
Study tip: Whenever you see multiple child tables joined to the same parent, immediately ask yourself whether fanout could inflate row counts — this is one of the most common data modeling pitfalls tested on the Tableau exam. Question 3
A left table contains 80,000 rows and exactly one row per OrderID. After a left join to a status table, the joined data contains 126,000 rows. COUNTD(OrderID) in the joined data is still 80,000.
Which conclusion is best supported by these results?
- The left table itself must contain duplicate OrderID values that were hidden before the join.
- Exactly 46,000 orders have no matching status row and were replaced by null values.
- Some orders match multiple status rows, but the left join has retained every original OrderID. (correct answer)
- Some original orders were lost, and an equal number of new OrderID values replaced them.
Explanation: When you see row counts change after a join, your first instinct should be to ask why — specifically, whether rows were gained, lost, or duplicated. Here, the left table has 80,000 rows and the joined result has 126,000, meaning 46,000 extra rows appeared. That's the key signal.
A left join never drops rows from the left table — it always preserves every left-side record. So those 46,000 additional rows came from the right (status) table matching multiple times against the same OrderID values. Meanwhile, COUNTD(OrderID) = 80{,}000 confirms every original order is still present and no new OrderIDs were introduced. This makes C the correct conclusion: some orders matched multiple status rows, but every original OrderID survived the join intact.
A is wrong because the passage explicitly states the left table has exactly one row per OrderID — no hidden duplicates existed before the join. B misreads the math; 46,000 is the number of extra rows created by multi-row matches, not the count of unmatched orders. Unmatched orders would appear as single rows with null status values, not add rows. D contradicts both what left joins do (they never remove left-side rows) and what COUNTD confirms — no OrderIDs were lost or replaced.
A useful rule of thumb: if post-join row count exceeds the left table, suspect one-to-many matches on the right side. If it equals the left table, it's one-to-one (or unmatched rows filling with nulls). COUNTD on your join key is your best diagnostic to confirm nothing was lost. Question 4
System A treats customer codes as case-sensitive and contains separate customers with codes ab and AB. System B also contains separate lookup rows for ab and AB. To address failed matches, a developer changes the join calculation on both sides to UPPER(CustomerCode). The joined row count then increases sharply.
What most likely caused the increase?
- The uppercase calculation changed the join to left outer and preserved every lookup row twice.
- The uppercase calculation converted the join into a union and appended both code variants as new rows.
- The uppercase calculation forced unmatched codes to null, causing each null to match every non-null code.
- The uppercase calculation collapsed distinct codes to one value, creating many-to-many matches within that value. (correct answer)
Explanation: When a join uses a calculated field like UPPER(CustomerCode), you need to think carefully about how that transformation affects the cardinality of your match — specifically, how many rows on one side can match how many rows on the other.
Here's what happens in this scenario: System A has two distinct codes, ab and AB. System B also has two distinct rows, one for ab and one for AB. Before the UPPER() fix, these codes fail to match across systems (case mismatch). After applying UPPER() on both sides, both ab and AB collapse to the single value AB. Now every row in System A that evaluates to AB can match every row in System B that also evaluates to AB. That's a many-to-many join: 2 rows × 2 rows = 4 matched rows instead of 0 or 1. This fan-out is what causes the sharp row count increase, making D the correct answer.
A is wrong because UPPER() has no effect on join type — it cannot convert an inner join into a left outer join. Join type is a separate configuration. B is wrong because UPPER() is a row-level transformation, not a structural operation like UNION, which stacks datasets vertically. C describes a real Tableau behavior where null keys match other nulls, but UPPER() on a non-null string never produces null — it simply uppercases the value.
As a study tip, whenever you see a normalization function (UPPER, LOWER, TRIM) applied to a join key, immediately ask yourself: "Could this collapse multiple distinct values into one, creating a many-to-many relationship?" That fan-out is one of the most common sources of unexpected row inflation in Tableau joins.
Question 5
Two files contain the same transaction columns, one for the prior year and one for the current year. Each customer can have many transactions in each file. The files are physically joined on CustomerID, causing every prior-year transaction for a customer to combine with every current-year transaction for that customer.
Which correction best matches the intended data structure?
- Aggregate both files by CustomerID and join them to preserve each original transaction row.
- Use a left join so only prior-year transactions determine the number of resulting rows.
- Add Transaction Amount to the join so transactions with equal values are paired together.
- Union the files so transactions are appended, then identify the year from the source or date. (correct answer)
Explanation: When you see two files with the same structure representing different time periods, the critical question is: do you want to combine rows side by side, or stack rows on top of each other? A join combines rows horizontally by matching keys — but when each customer has multiple transactions in both files, a join creates a Cartesian product. For example, a customer with 3 prior-year rows and 4 current-year rows produces 12 combined rows, inflating your data and distorting any analysis.
The right fix is a Union, which appends rows vertically. D is correct because unioning the two files preserves every original transaction as its own row, and you can distinguish the year using either a source field Tableau automatically generates or a date column already in the data. This matches the actual structure: a single list of transactions across both years.
A is tempting but wrong — aggregating by CustomerID before joining collapses all transaction-level detail, so you'd lose the granularity that makes transaction data useful in the first place. B misunderstands what join types control: left, right, and inner joins determine which matching keys are kept, not how many rows result from a multi-row match. The Cartesian inflation still occurs with a left join. C attempts to reduce the explosion by adding a second join condition, but transaction amounts aren't meaningful pairing logic — this creates arbitrary matches and still doesn't reflect the true data structure.
As a study tip, train yourself to ask: "Am I combining columns, or stacking rows?" Joins = columns side by side; Unions = rows stacked. When files share identical columns across time periods, a Union is almost always the right tool.
Question 6
A physical join has the option to match null join values enabled. The left table contains many rows with a null DeviceID, and the right table also contains many rows with a null DeviceID. After enabling the option, the extract size increases dramatically.
Which explanation and corrective action are most appropriate?
- All null DeviceID rows on both sides can match one another; exclude null-key records from the join or replace them with genuinely discriminating key values. (correct answer)
- Each null DeviceID matches exactly one arbitrarily selected row on the other side; sort both tables so that the selected row becomes deterministic and consistent.
- Null DeviceID rows are appended through internal union behavior rather than matched; change the physical join from left outer to inner to stop the appending.
- Tableau consolidates all null-key records into one shared placeholder row per table; aggregate the joined measures after the join to restore the correct level of detail.
Explanation: When Tableau's "match null join values" option is enabled, it treats NULL as equal to NULL during the join. This sounds helpful, but it creates a serious cardinality explosion — and that's exactly what this question tests.
Here's why: in standard SQL, NULL ≠ NULL, so null-key rows never match. But with this option enabled, every null DeviceID row on the left matches every null DeviceID row on the right. If the left table has 1,000 null rows and the right has 1,000 null rows, you suddenly get 1,000,000 joined rows — a classic cross-join effect. The extract balloons because of this many-to-many null matching behavior. Answer A correctly identifies this root cause and prescribes the right fix: either filter out null-key records before the join or replace nulls with meaningful, discriminating values that join properly.
Answer B is wrong because Tableau doesn't select a single arbitrary row per null — it matches all nulls against all nulls. There is no "one deterministic row" behavior, and sorting changes nothing about this.
Answer C is wrong because no internal union is occurring. The growth comes from join row multiplication, not appending. Switching from left outer to inner join would not resolve a null matching explosion.
Answer D is wrong because Tableau does not consolidate null-key rows into a single placeholder. Aggregating after the join wouldn't fix the underlying data explosion — it would just mask it.
As a study tip: whenever you see "extract size increases dramatically" in a join scenario, immediately think about cardinality — specifically whether a many-to-many relationship has been accidentally created.
Question 7
A Sales table contains one row per transaction and includes ProductCode and Region. A Price table contains one row per ProductCode–Region combination. ProductCode values are reused across three regions. After the tables are physically joined using only ProductCode, each sales transaction matches three price rows.
Which change most directly corrects the row explosion while retaining the applicable regional price?
- Change the physical join from an inner join to a left join on ProductCode.
- Add Region to the join so both ProductCode and Region must match. (correct answer)
- Remove duplicate ProductCode values from the Sales table before creating the join.
- Aggregate the joined data by ProductCode after connecting to the two tables.
Explanation: When you see a question about unexpected row multiplication after a join, your first instinct should be to examine the granularity of the join condition — specifically, whether the join keys fully define a unique match between tables.
Here, the Price table is structured at the ProductCode–Region grain, meaning each ProductCode appears three times (once per region). Joining on ProductCode alone tells Tableau to match every sales row to all three regional price rows, tripling your row count. The fix is to make the join condition as specific as the data: joining on both ProductCode and Region ensures each sales transaction matches exactly one price row — the correct regional price. That's why B is correct.
A is wrong because switching from an inner join to a left join changes which rows are kept, not how many matches are made per row. A left join on ProductCode alone still produces three matches per transaction; you'd just retain unmatched sales rows too.
C is wrong because the duplication isn't caused by duplicate ProductCodes in Sales — it's caused by multiple rows in the Price table sharing the same ProductCode across regions. Removing data from Sales doesn't fix the structural mismatch in the join condition.
D is wrong because aggregating after the join collapses the inflated rows after the damage is done. You'd likely mix or average prices across regions rather than retrieve the applicable one, producing incorrect metrics rather than correcting the join logic.
The key pattern: row explosion in joins always points to an incomplete join key. Ask yourself whether your join condition matches the true grain of the lookup table.
Question 8
Accounts contains one row per AccountID. Contacts contains multiple rows for some AccountID values. A left join from Accounts to Contacts produces duplicate account measures. An analyst proposes changing the join to an inner join.
What result should be expected if the analyst makes only that change?
- Matched accounts will still repeat, while accounts without contacts will be removed. (correct answer)
- Matched accounts will become unique, while accounts without contacts will remain as nulls.
- All accounts will become unique because inner joins select one matching contact row.
- All contact rows will remain, but repeated account measures will automatically be aggregated.
Explanation: When working with join types in Tableau, the key distinction to understand is that the join type controls which rows are included, not whether duplicates are eliminated. Duplicates arise from the one-to-many relationship between tables — and changing from a left join to an inner join does nothing to resolve that structural issue.
Here's why: in a left join from Accounts to Contacts, every account appears at least once, and accounts with multiple contacts repeat once per contact row. An inner join simply removes accounts that have no matching contacts — it still preserves every matched row, including all the duplicates. So if an account has three contacts, it still appears three times. Answer A captures this precisely: matched accounts continue to repeat, but unmatched accounts (those with no contacts) are now excluded entirely rather than appearing with null contact values.
Answer B is wrong because it reverses the logic — inner joins don't make matched accounts unique; they only drop unmatched ones. Nulls don't "remain" for unmatched accounts because those rows are removed entirely. Answer C is a common misconception: inner joins do not magically select one representative contact row per account. They return all valid matches. Answer D is wrong because Tableau doesn't automatically aggregate measures based on join type — aggregation is controlled separately through your view's level of detail and calculated fields.
As a study tip, always separate two distinct problems: which rows are included (controlled by join type) versus duplicate row inflation (caused by data granularity). Changing the join type solves the first problem, not the second.
Question 9
A product dimension has multiple records per ProductID because it contains active and inactive versions. A data source filter retaining only records where Status equals Active appears to eliminate duplicated sales after the dimension is joined to a fact table.
Before accepting this as a reliable correction, what should the developer verify?
- Every inactive dimension row has at least one matching record in the sales fact table.
- Every ProductID has exactly one active dimension row at the join's effective level of detail. (correct answer)
- The filter is displayed to worksheet users so they can restore inactive dimension rows.
- The physical join is changed to full outer so inactive products remain available as nulls.
Explanation: Whenever a dimension table has multiple rows per key, your first concern should be granularity: does the join produce one-to-one or one-to-many matches? If a single ProductID maps to more than one dimension row at join time, every matching fact row gets duplicated — inflating aggregates silently and dangerously.
Filtering to Status = Active feels like a clean fix, but it only works if each ProductID has exactly one active row. That's what answer B is asking you to confirm. If two active rows exist for the same ProductID (perhaps an overlapping effective-date range or a data quality issue), the duplication problem survives the filter. The join still fans out, and your sales totals are still wrong. Verifying one-active-row-per-ProductID is the only way to trust that the filter genuinely resolves the granularity mismatch.
Answer A is a distraction — whether inactive rows have matching fact records is irrelevant to whether the active filter eliminates fan-out on the rows you kept. Answer C conflates data governance (transparency to end users) with data correctness; even if users could see the filter, that doesn't validate that the data is accurate. Answer D misunderstands the problem entirely — switching to a full outer join would introduce more unmatched rows, not resolve duplication; outer joins don't fix granularity issues caused by duplicate dimension keys.
The study tip here: always trace the grain. Before trusting any filter as a duplication fix, ask yourself "after filtering, is there still a one-to-one relationship between dimension key and fact row?" If not, the filter is cosmetic, not corrective.
Question 10
Sales contains daily transaction rows. Quotas contains one row per salesperson and month. A physical join on Salesperson and Month repeats the monthly quota on every transaction. Users must analyze individual transactions and also calculate accurate quota totals across salespeople and months.
Which data-modeling change best preserves both analytical requirements without relying on a view-specific workaround?
- Use AVG(Quota) in every worksheet while leaving the physical join unchanged.
- Add Transaction Date to the join even though Quotas has no daily date field.
- Relate Sales and Quotas as logical tables using Salesperson and Month. (correct answer)
- Convert the physical join to a full outer join on Salesperson and Month.
Explanation: When you see a question about fan traps or duplicated measures caused by joins, think about Tableau's logical layer vs. physical layer distinction. The core problem here is that joining Sales and Quotas physically causes quota values to repeat across every transaction row, making SUM(Quota) overcount dramatically.
The cleanest fix is C — relating Sales and Quotas as logical tables. Tableau's relationship model keeps the two tables separate at the physical level and generates context-aware queries depending on what fields are on the viz. When you analyze transactions, Tableau queries Sales. When you analyze quotas, it queries Quotas. When you need both, it federates them correctly using the Salesperson and Month keys — no duplication, no workaround required.
Each distractor has a specific flaw worth understanding. A attempts to correct overcounting with AVG(Quota), which works only when every salesperson has the same number of daily transactions each month — a fragile assumption that breaks with real, uneven data. B introduces a nonsensical join condition: Quotas has no daily date field, so joining on Transaction Date would either fail or create a massive cross-join, worsening the fan trap rather than solving it. D switches to a full outer join, which changes which rows are included (preserving unmatched rows from both sides) but does nothing to prevent quota duplication on matched rows — the overcounting problem remains entirely intact.
Study tip: Whenever a question describes a fact table joined to an aggregate/summary table, that's your signal that a Tableau relationship (not a join) is likely the correct modeling choice. Relationships are designed precisely for this many-to-one mismatch scenario.