cswork / libraryOpen practice workspace

Articles

SAS Hash Object Interview Questions: Failed Lookups, Duplicate Keys, and Memory Trade-Offs

Practice SAS hash object interviews: trace failed FIND calls, clear retained outputs, choose duplicate-key policies, and compare memory with sorted merges.

SAS hash object interview questions often turn on one misleading row: the key is absent, yet the output still contains a plausible region and fee. A strong answer follows the program data vector, checks the method return code, and defines how many output observations each input should produce.

The central rule is to clear lookup outputs before each lookup, preserve the incoming key, and choose duplicate handling before loading the hash. Then estimate memory for the actual stored items rather than assuming that an in-memory lookup is always the fastest safe choice.

This guide uses official Base SAS documentation and original interview exercises. It describes conventional DATA step hash objects, not DS2 or CAS behavior, and makes no claim about a particular employer's question frequency.

What does a successful FIND actually change?

The official FIND method reference defines zero as success and nonzero as failure. A successful lookup copies the associated hash data into its DATA step variables. The key variables must have compatible types with the defined key.

That is an update to working variables, not the automatic construction of a fresh joined row. If a lookup fails, your program must decide which values it will output. Checking rc identifies the missing match; it does not itself express a left-join replacement policy.

For a membership test, the official CHECK method checks whether a key exists without updating the data variables. Replacing FIND with CHECK therefore cannot fix an enrichment that needs region or fee. Explain which operation your answer requires: existence, one retrieved record, or all associated records.

A useful opening clarification is: “Should unmatched events remain in the result, and should one customer produce one label or several?” Those decisions determine both control flow and expected row counts.

Trace the failed lookup before changing the code

Our original lookup data has customer 10 in North with fee 8, and customer 20 in South with fee 12. The event stream contains keys 10, 99, 20, 99, in that order. Key 99 is absent from the lookup.

Suppose lookup variable attributes are established with an unexecuted SET statement and only initialized during hash creation. Variables associated with SET have automatic retention behavior under the official RETAIN statement rules. The event SET reads only the event key, so it does not replace the lookup outputs.

For that specific setup, failing to clear outputs on later iterations gives this expected trace:

Incoming keyFIND resultBroken outputCorrect left-enrichment output
10SuccessNorth, 8North, 8
99FailureNorth, 8, inheritedMissing, missing
20SuccessSouth, 12South, 12
99FailureSouth, 12, inheritedMissing, missing

The defect is especially dangerous because the inherited values look valid. A nonmissing-field test could accept them, while a total fee calculation silently includes money for customers that never matched.

Do not turn this example into “every variable always retains its value.” The variable's origin matters. Ordinary assignment-created variables normally reset between iterations unless another retention rule applies. Read the declaration and input statements before predicting state; the SAS DATA step guide develops that foundation separately.

Write the reset in the right place

Here is a complete small fixture and a left-enrichment implementation. The SAS snippet is checked against the documented interfaces; it has not been executed in a SAS runtime here. Its expected output is the four rows in the corrected column above.

data customers;
  length region $5;
  input cust_id region $ fee;
  datalines;
10 North 8
20 South 12
;
run;

data events;
  input cust_id;
  datalines;
10
99
20
99
;
run;

data enriched;
  if 0 then set customers(keep=cust_id region fee);
  if _n_=1 then do;
    declare hash h(dataset:'customers');
    h.defineKey('cust_id');
    h.defineData('region','fee');
    h.defineDone();
    call missing(cust_id,region,fee);
  end;
  set events;
  call missing(region,fee);
  rc=h.find();
  matched=(rc=0);
  output;
  keep cust_id region fee matched;
run;

Notice the two different initialization jobs. The first branch creates the object once and establishes initial state. The later CALL MISSING handles each observation, after reading the incoming event but before attempting its lookup.

Do not clear cust_id after SET events. That would erase the search key. Also avoid clearing every variable indiscriminately: a broader program might carry an accumulator or event amount that must survive until output.

For an inner enrichment, use if rc=0 then output; instead of unconditional output. In this fixture, that deliberately produces two observations. For a left enrichment, four observations are correct, with an explicit matched indicator distinguishing an absent customer from a customer whose stored fee is itself missing.

Duplicate keys are a result contract

Now change the lookup to two records for customer 10: North/8 followed by West/12. Give the event stream two occurrences of customer 10. Before choosing a constructor option, predict the output grain.

Official hash object concepts document these DATASET-load choices: by default the first occurrence of a duplicate key is kept; duplicate:'replace' retains the last loaded occurrence; duplicate:'error' reports duplicate-key errors in the log. Do not describe a logged error as a guaranteed application-level abort policy without checking the surrounding job.

With multidata:'yes', multiple data items can be associated with a key. Our original fixture then has these possible contracts:

Requested resultDeliberate implementationMatched output count
First loaded label per eventDefault duplicate handling2
Last loaded label per eventDUPLICATE replace2
Every label per eventMULTIDATA plus traversal4

“Last loaded” is a physical order rule, not a latest-effective-date rule. If West/12 was loaded second but has an older effective date, replacement chooses the older business record. An interview answer should define validity, ordering, and tie resolution before constructing a one-record-per-key lookup.

If duplicates violate the input contract, detect and handle them as a data-quality failure. Silently selecting one record changes the problem. If duplicates represent valid history, selecting the latest record without reference to the event's effective date can also be wrong.

How do you retrieve every item under a key?

For a multidata hash, one FIND is the start of retrieval, not a complete one-to-many join. The official FIND_NEXT reference provides traversal of subsequent data items for the current key.

The following fragment assumes h was initialized with multidata:'yes' and the same key/data definitions. It implements matched-only output:

set events;
call missing(region,fee);
rc=h.find();
do while (rc=0);
  output;
  rc=h.find_next();
end;

Each successful retrieval writes one observation before advancing. If the initial lookup fails, the loop writes none. To preserve unmatched events, add a separate failed-initial-lookup branch that outputs the already-cleared variables once. Do not output a final unsuccessful traversal result as though it were another match.

For two events and two stored labels, expect four matched observations. Add one unknown event and a left-style unmatched branch: now expect five total observations, not three. Count matched data items separately from distinct keys, because those quantities drive different assertions and memory estimates.

Unless your contract establishes ordering, validate the multiset of matching records rather than pretending traversal order is a business ranking. A separate sort or selection rule should express a required latest-first result.

Estimate memory before recommending a hash

Official ITEM_SIZE documentation describes an item estimate that includes key/data variables and some internal information. Multiplying it by NUM_ITEMS approximates storage, but excludes initial overhead and certain alignment costs. It is not a promise about the process's peak resident memory.

Consider an original sizing example: two million stored items at an assumed measured ITEM_SIZE of 64 bytes require about 128 million bytes, or 122 MiB, before excluded costs. The number 64 is an exercise assumption, not a benchmark for your schema.

Include the input/output working state, other hash objects, and simultaneous jobs in the budget. If multidata triples the items per customer, estimating from distinct customer count understates storage. Wide declared character fields also deserve scrutiny even when displayed values are short.

Project only the necessary lookup columns and filter irrelevant records before loading. Reducing data must preserve the selected duplicate/effective-date policy; filtering away an inconvenient duplicate is not a valid performance optimization.

A version-specific caution comes from SAS Problem Note 67620, which discusses changed default HASHEXP sizing in SAS 9.4M7 and associated memory behavior. Check the installed release rather than quoting one universal bucket default. Increasing buckets does not repair incorrect output state.

Hash lookup or sorted merge: what should you compare?

Our recommendation is to compare plans under the same semantic contract. A hash can avoid sorting a large event stream when a suitably narrow lookup fits memory. A sorted plan may be attractive when inputs already have the required order or the lookup cannot fit safely.

For unique lookup keys and many event rows, ask about lookup size, input ordering, available memory, sort spill space, and repeated reuse. Benchmark complete elapsed time and resource use, including hash loading or any required sorts. Timing only the FIND call omits the cost that could decide the plan.

Be careful with duplicate keys on both inputs. A DATA step match-merge is not automatically the Cartesian multiplication produced by a SQL many-to-many join. Changing to MERGE can change output cardinality even if both programs use the same key. State the requested grain and verify it independently.

A useful interview response is: “The unique-key hash gives one output per event, preserves input event order in this implementation, and needs the selected lookup in memory. If the memory budget fails, I will choose an alternative that preserves that contract and include sorting costs.” That connects performance evidence to correctness.

Rehearse the failure cases with targeted questions

QuestionSAS rehearsal focus
Implement Left Join Using Python Dictionaries EfficientlyPreserve unknown events and clear satellite outputs
Reason About Composite Join Keys and Predicate PlacementDefine the complete key before the hash lookup
Compute join counts and window ranksDistinguish two events from four duplicate-key matches
Explain Hash Map Collisions and Operation ComplexitySeparate typical costs from guarantees and memory limits
Externally Sort a 500 GB CSV by One Column with 16 GB of RAMCompare a sorted alternative and budget spill passes

In a rehearsal, first predict the four-row trace without running code. Then change one requirement: keep all duplicate labels, reject duplicates, or preserve unknown keys. Explain which branch and row-count assertion changes. Finally, estimate memory using item count and declared payload width.

Before calling the answer finished, check an unknown first key, a miss after a hit, consecutive misses, duplicate lookup keys, duplicate event keys, and a matched record with missing data. Each case distinguishes a real contract from a plausible happy path.

The goal is an explanation that remains correct when the interviewer changes the data.

Sources and Further Reading

Research checked October 4, 2026. Documentation covers the cited Base SAS releases; original traces are explanatory fixtures, not a proprietary SAS runtime benchmark or candidate report.