Author: Bhanoji Duppada
Edition: First Edition, 2026
Website: learnhub101.com
This work was created with AI assistance under the direction and editorial supervision of Bhanoji Duppada. All technical content has been reviewed, verified, and edited by the author. The author retains full copyright and creative responsibility.
Copyright ยฉ 2026 Bhanoji Duppada. All rights reserved.
No part of this publication may be reproduced or transmitted without prior written permission of the author.
By Bhanoji Duppada
Your Complete Guide to Acing Clinical SAS Programming Interviews
This book is the result of 11+ years of experience in clinical SAS programming across multiple pharmaceutical companies and CROs. Every question in this book has been asked in real interviews --- from entry-level programmer screenings to senior lead assessments at top pharma companies including Pfizer, Novartis, Roche, AstraZeneca, and leading CROs like IQVIA, PPD, and Syneos Health.
The 200 questions are organized from foundational concepts to advanced scenarios, covering every topic area you'll encounter: Base SAS, PROC SQL, macro language, CDISC standards (SDTM and ADaM), TLF programming, regulatory submissions, and the emerging R transition. Each answer goes beyond the textbook definition to include the practical context interviewers are really testing for.
What makes clinical SAS interviews unique is their dual focus: interviewers assess both your programming proficiency and your understanding of the regulatory environment. A brilliant SAS programmer who doesn't understand why TRTEMFL matters to a medical reviewer will struggle in pharma interviews. Conversely, someone who memorized the ADaM Implementation Guide but can't write a clean merge will fail the technical screening. This book bridges both worlds.
Whether you're a fresh graduate entering the industry, a SAS programmer moving into clinical trials, or a senior programmer preparing for a lead role, this book gives you the confidence to walk into any interview fully prepared.
The clinical SAS job market has evolved significantly. Five years ago, interviews focused almost exclusively on SAS syntax and CDISC knowledge. Today, interviewers increasingly ask about:
This book addresses all these dimensions while maintaining deep coverage of traditional SAS programming competencies that remain the foundation of every clinical programmer's toolkit.
The 200 questions in this book aren't meant to be read passively. Each question represents a real interview scenario, and your preparation should mirror how you'll perform under pressure.
If you're a beginner (0-2 years): Start with Chapters 1-3 (Base SAS, SQL, Macros). These form the technical foundation every interviewer expects. Spend at least two weeks on these chapters before moving forward. Practice writing code by hand --- many companies still use whiteboard or paper-based coding exercises.
Then read Chapter 4 (SDTM) and Chapter 5 (ADaM) to understand the clinical domain. At the entry level, interviewers don't expect you to have mapped dozens of domains, but they do expect you to explain the purpose of SDTM and articulate why ADaM exists as a separate standard.
If you're mid-level (2-5 years): Focus on Chapters 4-7 (CDISC standards, TLF, Regulatory). Interviewers at this level expect you to discuss domain mapping decisions, derivation logic, and submission processes --- not just syntax. You should be able to explain why you chose a particular EPOCH derivation approach or how you handled a non-standard baseline definition.
Work through the scenario questions even if they seem basic. Mid-level interviews often revisit fundamental concepts but expect sophisticated answers. When asked "What is LOCF?" a beginner can define it; a mid-level programmer should discuss when LOCF is appropriate, its limitations, and alternative imputation methods.
If you're senior (5+ years): Go straight to Chapter 8 (Scenario Problems), Chapter 9 (R Transition), and the advanced chapters (11-26). Senior interviews focus on debugging complex scenarios, making architectural decisions, and leading teams through technology transitions.
Prepare for questions about mentoring junior programmers, handling disagreements with statisticians or data managers, and making judgment calls when specifications are ambiguous. Technical excellence is assumed at this level; interviews assess leadership and problem-solving under ambiguity.
For every level: Don't just memorize answers. Understand the why behind each answer. Interviewers can tell the difference between someone who memorized a definition and someone who has actually debugged a TRTEMFL derivation at 2 AM before a submission deadline.
Consider this example from a real interview:
/* Interview Question: What's wrong with this TRTEMFL derivation? */
data adae;
set adae;
if ASTDT >= TRTSDT and ASTDT <= TRTEDT then TRTEMFL = 'Y';
else TRTEMFL = 'N';
run;
A memorized answer might say "it looks correct." An experienced programmer would identify multiple issues:
A complete answer might look like this:
/* Improved TRTEMFL derivation with proper date handling */
data adae;
set adae;
/* Treatment-emergent: AE starts on or after first treatment date */
/* and on or before last treatment date (or ongoing if TRTEDT missing) */
if ASTDT >= TRTSDT then do;
if TRTEDT ne . then do;
if ASTDT <= TRTEDT then TRTEMFL = 'Y';
else TRTEMFL = ''; /* Post-treatment: leave blank per company convention */
end;
else TRTEMFL = 'Y'; /* Ongoing treatment: assume treatment-emergent */
end;
else if ASTDT = . then do;
/* Missing AE start date: apply company-specific rules */
/* Often set to 'Y' if AE end date is during treatment */
if AENDT >= TRTSDT then TRTEMFL = 'Y';
end;
else TRTEMFL = ''; /* Pre-treatment events */
run;
This depth of thinking is what separates candidates who get offers from those who don't.
Every interviewer has criteria they're evaluating, even if they don't state them explicitly. Throughout this book, you'll find "What They're Really Testing" boxes that reveal the underlying assessment:
Answer:
The DATA step processes data through two phases: compilation and execution.
Compilation phase: SAS reads your code top-to-bottom, creates the Program Data Vector (PDV) --- a row of memory that holds one observation --- and identifies all variables, their types, and lengths. No data is read yet.
Execution phase: SAS loops through input data one observation at a time: 1. Reset PDV variables to missing (except those with RETAIN) 2. Read one observation from the input dataset via SET/MERGE/INPUT 3. Execute each statement in sequence 4. At the bottom, implicitly write the PDV to the output dataset (OUTPUT) 5. Return to step 1 for the next observation
/* This behavior explains why RETAIN is needed */
data running_total;
set sdtm.ae;
by USUBJID;
retain ae_count; /* Without RETAIN, ae_count resets to . each iteration */
if first.USUBJID then ae_count = 0;
ae_count + 1; /* The sum statement (+=1) also implicitly retains */
run;
Why interviewers ask this: Understanding the PDV separates programmers who memorize syntax from those who understand WHY their code works. Every "weird" SAS behavior --- RETAIN, FIRST./LAST., automatic OUTPUT --- traces back to the PDV loop.
Scoring guide: - 5/5: Explains both phases, mentions PDV, gives clinical example - 3/5: Says "SAS reads one row at a time" without PDV details - 1/5: Can't explain the execution model
Answer:
Both subset data, but they operate at different stages:
WHERE applies during data reading (before the observation enters the PDV). It's faster because SAS never loads unwanted records.
IF applies during execution (after the observation is in the PDV). It's slower because SAS reads every record, then decides whether to keep it.
/* WHERE: Faster --- reads only severe AEs from disk */
data severe_ae;
set sdtm.ae;
where AESEV = "SEVERE";
run;
/* IF: Slower --- reads ALL AEs, then drops non-severe */
data severe_ae;
set sdtm.ae;
if AESEV = "SEVERE";
run;
When you MUST use IF (WHERE won't work): - When referencing a variable created in the same DATA step - When using FIRST./LAST. variables - When using automatic variables (N, ERROR) - When the condition involves functions not supported by WHERE
/* WHERE can't reference computed variables */
data flagged;
set sdtm.ae;
duration = input(AEENDTC, yymmdd10.) - input(AESTDTC, yymmdd10.) + 1;
if duration > 30; /* Must use IF --- duration doesn't exist at WHERE stage */
run;
/* WHERE can't reference FIRST./LAST. */
data first_ae;
set sdtm.ae;
by USUBJID;
if first.USUBJID; /* Must use IF --- FIRST. is computed during execution */
run;
Clinical context: For large datasets like LB (laboratory, often 100K+ records), using WHERE instead of IF can cut processing time by 50% or more. Always use WHERE when possible.
Answer:
All three read data into the PDV, but they serve different purposes:
SET: Reads one or more datasets sequentially (stacking) or one at a time.
/* Stack (append) datasets */
data all_ae;
set ae_study1 ae_study2 ae_study3;
run;
/* Result: all records from study1, then study2, then study3 */
MERGE: Combines datasets side-by-side by matching on BY variables.
proc sort data=dm; by USUBJID; run;
proc sort data=ae; by USUBJID; run;
data dm_ae;
merge dm(in=a) ae(in=b);
by USUBJID;
if a and b; /* Inner join */
run;
UPDATE: Applies transaction records to a master dataset. Only changes non-missing values.
/* Master has original data, transaction has corrections */
data corrected;
update master transaction;
by USUBJID;
run;
/* Missing values in transaction DON'T overwrite master --- unlike MERGE */
Clinical use cases: - SET: Stacking AE data from multiple data cuts, combining SDTM domains across studies for ISS/ISE - MERGE: Adding ADSL treatment/population info to every SDTM domain, the most common operation in ADaM programming - UPDATE: Applying data corrections from sponsors, updating patient identifiers after data management queries
The critical MERGE trap: MERGE with many-to-many BY groups doesn't produce a Cartesian product --- it does a sequential match that gives unexpected results. Use PROC SQL for many-to-many joins.
Answer:
RETAIN prevents the PDV from resetting a variable to missing at the top of each iteration. Normally, every variable except those from SET/MERGE gets reset to missing.
data cumulative;
set sdtm.ae;
by USUBJID;
retain running_count 0; /* Starts at 0, carries forward */
running_count + 1; /* The sum statement also retains */
run;
FIRST. and LAST. are temporary boolean (0/1) flags created when you use a BY statement in a DATA step:
proc sort data=sdtm.ae; by USUBJID AEDECOD AESTDTC; run;
data ae_processed;
set sdtm.ae;
by USUBJID AEDECOD;
/* FIRST.USUBJID = 1 when a new subject begins */
/* LAST.USUBJID = 1 when a subject's last record is reached */
/* FIRST.AEDECOD = 1 for first occurrence of each AE per subject */
/* Clinical: Count AEs per subject */
retain ae_count;
if first.USUBJID then ae_count = 0;
ae_count + 1;
/* Clinical: Flag first occurrence (AOCCFL in ADaM) */
if first.AEDECOD then AOCCFL = "Y";
else AOCCFL = "";
/* Clinical: Keep one record per subject (for subject-level summaries) */
if last.USUBJID; /* Only output the last record of each subject */
run;
Critical rules: 1. Data MUST be sorted by the BY variables before using FIRST./LAST. 2. FIRST./LAST. are created for EVERY variable in the BY statement 3. They're temporary --- they don't appear in the output dataset 4. A record can be both FIRST.X = 1 and LAST.X = 1 (when there's only one record per group)
Clinical applications:
- AESEQ derivation: if first.USUBJID then AESEQ = 0; AESEQ + 1;
- Baseline flagging: Last pre-dose record: sort descending, then if first.PARAMCD
- Occurrence flags: AOCCFL = "Y" when first.AEDECOD per subject
- Subject-level data: if last.USUBJID then output; to create one row per subject
Answer:
SAS has exactly two variable types:
Character (CHAR): Stores text. Maximum length 32,767 bytes. Stored as-is. Numeric (NUM): Stores numbers AND dates. Default length 8 bytes (stores 15+ significant digits). Dates are stored as days since Jan 1, 1960.
Clinical pitfalls:
/* Pitfall 1: USUBJID is character --- never do math on it */
/* WRONG: */ if USUBJID > 100;
/* RIGHT: */ where USUBJID = "XYZ-001-0100";
/* Pitfall 2: Dates in SDTM are CHARACTER (ISO 8601) */
/* AESTDTC = "2025-03-15" is character! */
/* Must convert to numeric for date math: */
adt = input(AESTDTC, yymmdd10.); /* Now adt = 23816 (numeric) */
format adt date9.; /* Displays as 15MAR2025 */
/* Pitfall 3: Leading zeros */
/* Numeric 001 becomes 1. Use character for subject IDs with leading zeros. */
SUBJID_N = 001; /* Stored as 1 */
SUBJID_C = "001"; /* Stored as "001" */
/* Pitfall 4: Comparing character numbers */
/* "9" > "10" in character comparison (sorts by first character) */
/* Always convert to numeric for proper comparison */
When type matters most in clinical programming: - SDTM dates (--DTC) are always CHARACTER - ADaM dates (ADT, ASTDT) are always NUMERIC - AVAL is NUMERIC, AVALC is CHARACTER (same value, two types) - Population flags (SAFFL, ITTFL) are CHARACTER ("Y" or blank)
Answer:
LENGTH: The storage size of a variable in bytes. For character variables, this is the maximum string length. For numeric, it's almost always 8 bytes.
data dm;
length USUBJID $40 SEX $1 RACE $60; /* Set before first use */
set raw.demographics;
USUBJID = catx("-", STUDYID, SITEID, SUBJID);
run;
FORMAT: Controls how a value is DISPLAYED. It does not change the stored value.
format ADT date9.; /* 23816 displays as "15MAR2025" */
format PCT 5.1; /* 45.333 displays as "45.3" */
format TRTDURD comma8.; /* 1234567 displays as "1,234,567" */
INFORMAT: Controls how RAW INPUT is READ into a variable. Used with INPUT statement or INPUT() function.
/* INFORMAT in INPUT statement */
data raw;
input @1 SUBJID $8. @10 AE_DATE mmddyy10.;
/* $8. reads 8 characters; mmddyy10. reads "03/15/2025" as numeric date */
datalines;
0001 03/15/2025
;
run;
/* INFORMAT in INPUT() function --- most common in clinical */
ADT = input(AESTDTC, yymmdd10.); /* Read ISO date "2025-03-15" */
Clinical rule of thumb: - Set LENGTH before first reference (prevents truncation) - Apply FORMAT in final output step (controls display in TLFs) - Use INFORMAT/INPUT() for character-to-numeric date conversions
Answer:
N counts the current iteration of the DATA step loop. It starts at 1 and increments by 1 for each observation read. It is NOT the observation number in the output dataset (because IF statements skip output).
/* _N_ as a row counter */
data first_100;
set sdtm.ae;
if _N_ <= 100; /* Keep first 100 records only */
run;
/* _N_ for debugging --- shows which record caused an issue */
data checked;
set sdtm.lb;
if LBSTRESN = . and LBORRES ne "" then
put "WARNING: Missing numeric result at _N_=" _N_ USUBJID= LBTESTCD= LBORRES=;
run;
ERROR is set to 1 when SAS encounters a data error (invalid data, division by zero, etc.):
data cleaned;
set raw.labs;
/* Attempt numeric conversion --- _ERROR_ flags bad values */
LBSTRESN = input(LBORRES, ?? best.); /* ?? suppresses error messages */
if _ERROR_ then do;
put "NOTE: Non-numeric lab result: " USUBJID= LBTESTCD= LBORRES=;
_ERROR_ = 0; /* Reset to prevent error propagation */
end;
run;
Neither N nor ERROR appears in the output dataset --- they exist only during DATA step execution. You can reference them in IF/WHERE statements but can't keep them without assignment to another variable.
Q8-Q15: Arrays (processing multiple variables), DO loops (generating sequences), Functions vs CALL routines, PROC SORT options (NODUPKEY, NODUP, DUPOUT), KEEP/DROP/RENAME dataset options vs statements, IN= variable behavior, LENGTH statement placement rules, Variable type conversion with PUT() and INPUT()...
Answer:
PROC SQL uses set-based logic (process all rows at once), while the DATA step uses row-by-row logic (process one observation at a time through the PDV). This fundamental difference affects when you should use each.
Use PROC SQL when:
- You need many-to-many joins (DATA step MERGE fails for this)
- You need to count distinct values (COUNT(DISTINCT USUBJID))
- You need to create macro variables from data (SELECT INTO :)
- You're writing complex subqueries or correlated queries
- You need to combine aggregation with detail in one query
Use DATA step when: - You need row-by-row processing with RETAIN or FIRST./LAST. - You need to create multiple output datasets from one input - You need complex conditional logic that spans multiple records - You need to read raw text files (INPUT statement)
/* PROC SQL: Count distinct subjects with AEs per treatment --- one step */
proc sql;
select TRT01A, count(distinct USUBJID) as N_WITH_AE
from adam.adae
where SAFFL = "Y" and TRTEMFL = "Y"
group by TRT01A;
quit;
/* DATA step equivalent --- requires PROC SORT + BY + FIRST. --- three steps */
proc sort data=adam.adae(where=(SAFFL="Y" and TRTEMFL="Y"))
out=_ae nodupkey; by TRT01A USUBJID; run;
proc freq data=_ae noprint; tables TRT01A / out=_counts; run;
/* _counts has the same result but took more code */
Clinical rule: Use DATA step for dataset creation (SDTM mapping, ADaM derivation). Use PROC SQL for analysis queries (subject counts, macro variables for TLF headers, summary statistics).
Answer:
The SELECT INTO : syntax creates macro variables directly from query results. This is the most common PROC SQL technique in clinical TLF programming.
/* Single value */
proc sql noprint;
select count(distinct USUBJID) into :N_TOTAL trimmed
from adam.adsl
where SAFFL = "Y";
quit;
%put Total safety subjects: &N_TOTAL;
/* Multiple values --- one per group (Big N for table headers) */
proc sql noprint;
select count(distinct USUBJID) into :N1 trimmed, :N2 trimmed, :N3 trimmed
from adam.adsl
where SAFFL = "Y"
group by TRT01A
order by TRT01A;
quit;
/* Now &N1 = Placebo count, &N2 = Drug 100mg count, &N3 = Drug 200mg count */
/* Concatenated list --- all unique values into one macro variable */
proc sql noprint;
select distinct PARAMCD into :ALL_PARAMS separated by " "
from adam.advs;
quit;
%put Parameters: &ALL_PARAMS;
/* Result: SYSBP DIABP PULSE TEMP WEIGHT */
The TRIMMED option (SAS 9.4+) removes leading/trailing blanks. Without it, numeric values have leading spaces from default formatting. Always use TRIMMED.
Clinical application: Every TLF program starts with Big N:
title "Table 14.1.1: Demographics (Placebo N=&N1 | Drug N=&N2 | Total N=&N3)";
Answer:
/* INNER JOIN: Only subjects who have BOTH demographics AND adverse events */
proc sql;
create table ae_with_demo as
select a.USUBJID, a.AETERM, a.AESEV, b.AGE, b.SEX, b.TRT01A
from sdtm.ae as a
inner join adam.adsl as b
on a.USUBJID = b.USUBJID;
quit;
/* Subjects with no AEs are excluded */
/* LEFT JOIN: All ADSL subjects + their AEs (if any) */
proc sql;
create table all_subjects_ae as
select a.USUBJID, a.TRT01A, b.AETERM, b.AESEV
from adam.adsl as a
left join sdtm.ae as b
on a.USUBJID = b.USUBJID
where a.SAFFL = "Y";
quit;
/* Subjects with no AEs have NULL for AETERM/AESEV */
/* FULL OUTER JOIN: All records from both datasets */
proc sql;
create table full_join as
select coalesce(a.USUBJID, b.USUBJID) as USUBJID, a.AETERM, b.CMTRT
from sdtm.ae as a
full join sdtm.cm as b
on a.USUBJID = b.USUBJID;
quit;
/* ANTI JOIN (using NOT EXISTS): Subjects who do NOT have AEs */
proc sql;
create table no_ae_subjects as
select *
from adam.adsl as a
where not exists (
select 1 from sdtm.ae as b
where b.USUBJID = a.USUBJID
);
quit;
/* Useful for: "How many safety subjects had zero adverse events?" */
The DATA step MERGE equivalent of each join:
- INNER JOIN = merge a(in=x) b(in=y); by KEY; if x and y;
- LEFT JOIN = merge a(in=x) b(in=y); by KEY; if x;
- FULL OUTER JOIN = merge a b; by KEY; (no IF statement)
- ANTI JOIN = merge a(in=x) b(in=y); by KEY; if x and not y;
Answer:
A correlated subquery references the outer query --- it executes once per row of the outer query. It's the SQL equivalent of "look up a value for this specific record."
Most important clinical use: Finding baseline values
/* Find the last pre-dose lab value for each subject + test */
proc sql;
create table baseline_labs as
select a.*
from sdtm.lb as a
inner join adam.adsl as b
on a.USUBJID = b.USUBJID
where input(a.LBDTC, yymmdd10.) <= b.TRTSDT
and input(a.LBDTC, yymmdd10.) = (
/* Correlated subquery: finds max date for THIS subject + test */
select max(input(c.LBDTC, yymmdd10.))
from sdtm.lb as c
where c.USUBJID = a.USUBJID /* Correlates to outer query */
and c.LBTESTCD = a.LBTESTCD /* Same test */
and input(c.LBDTC, yymmdd10.) <= b.TRTSDT /* Pre-dose */
);
quit;
Other clinical uses: - Finding the worst post-baseline severity per subject - Identifying the closest scheduled visit to each unscheduled visit - Flagging subjects whose lab values exceed 3ร upper limit of normal
Performance warning: Correlated subqueries can be slow on large datasets because the inner query runs once per outer row. For LB datasets with 500K+ records, consider using a DATA step with PROC SORT and FIRST./LAST. instead.
Answer:
SAS-specific extension. In standard SQL, you can't reference a computed column in the same SELECT's WHERE or HAVING clause. CALCULATED lets you do this:
proc sql;
create table ae_summary as
select TRT01A,
count(distinct USUBJID) as N_AE,
(select count(distinct USUBJID) from adam.adsl
where TRT01A = a.TRT01A and SAFFL="Y") as N_TOTAL,
calculated N_AE / calculated N_TOTAL * 100 as PCT format=5.1
from adam.adae as a
where TRTEMFL = "Y" and SAFFL = "Y"
group by TRT01A
having calculated N_AE > 5; /* Only show groups with > 5 subjects */
quit;
Without CALCULATED, you'd need to repeat the full expression:
having count(distinct USUBJID) > 5; /* Verbose alternative */
Interview tip: Mention that CALCULATED is a SAS extension --- it doesn't work in Oracle, PostgreSQL, or other databases. If your company migrates to R/dbplyr, this syntax won't transfer.
Answer:
Many-to-many relationships occur when both datasets have multiple records per key. Example: one subject has 5 AEs AND 3 concomitant medications. A DATA step MERGE gives unpredictable results (5 records, not 15).
/* WRONG: DATA step MERGE for many-to-many */
data ae_cm;
merge ae(in=a) cm(in=b);
by USUBJID;
if a and b;
run;
/* WARNING: Subject 001 has 5 AEs and 3 CMs โ you get 5 records, NOT 15 */
/* CORRECT: PROC SQL produces the full Cartesian product */
proc sql;
create table ae_cm as
select a.USUBJID, a.AETERM, a.AESTDTC, b.CMTRT, b.CMSTDTC
from sdtm.ae as a
inner join sdtm.cm as b
on a.USUBJID = b.USUBJID;
quit;
/* Subject 001: 5 AEs ร 3 CMs = 15 records (correct Cartesian product) */
When do many-to-many joins actually occur in clinical work? - Checking if any concomitant med was taken during an AE period - Linking AEs to concomitant medications for narrative writing - Joining protocol deviations with AE records for safety analysis
Best practice: Almost always add a date window to prevent full Cartesian products:
proc sql;
create table ae_cm_overlap as
select a.USUBJID, a.AETERM, b.CMTRT
from sdtm.ae as a
inner join sdtm.cm as b
on a.USUBJID = b.USUBJID
and input(b.CMSTDTC, yymmdd10.) <= input(a.AEENDTC, yymmdd10.)
and input(b.CMENDTC, yymmdd10.) >= input(a.AESTDTC, yymmdd10.);
quit;
/* Only CMs overlapping with each AE period */
Q14-Q22: UNION/INTERSECT/EXCEPT set operations, EXISTS vs IN performance, GROUP BY with HAVING, self-joins for finding adjacent visits, NULL handling (COALESCE, CASE), creating macro variable lists, PROC SQL table creation vs DATA step, dictionary tables for metadata, PROC SQL vs PROC MEANS/FREQ for summary statistics...
Answer:
The SAS macro language is a text-processing layer that generates SAS code before compilation. It has its own variables (%LET, &var), functions (%SCAN, %EVAL), and control flow (%IF, %DO). The macro processor resolves all macro references first, then passes the generated code to the SAS compiler.
Why clinical programmers need macros:
Without macros, you'd copy-paste the same 50-line SDTM mapping code for each of 15 domains, changing only the domain name and variable list. With macros, you write it once:
/* Without macros: 15 copies of this code */
proc sort data=sdtm.ae; by USUBJID AESTDTC; run;
data sdtm.ae;
set sdtm.ae; by USUBJID;
if first.USUBJID then AESEQ = 0; AESEQ + 1;
run;
/* With macros: one definition, 15 calls */
%macro add_seq(dsin=, domain=, sortby=);
proc sort data=&dsin; by USUBJID &sortby; run;
data &dsin;
set &dsin; by USUBJID;
if first.USUBJID then &domain.SEQ = 0;
&domain.SEQ + 1;
run;
%mend;
%add_seq(dsin=sdtm.ae, domain=AE, sortby=AESTDTC AETERM);
%add_seq(dsin=sdtm.cm, domain=CM, sortby=CMSTDTC CMTRT);
%add_seq(dsin=sdtm.vs, domain=VS, sortby=VSTESTCD VSDTC);
/* ... 12 more domains ... */
Interview tip: Mention that a senior programmer's macro library is their most valuable professional asset. Over a career, you accumulate 50-100 macros that cover everything from data import to submission QC.
Answer:
Both create macro variables, but at different stages:
%LET runs during macro processing (before SAS code compiles). It assigns a fixed text value:
%let STUDY = XYZ-001;
%let CUTDATE = 2025-06-30;
title "Study &STUDY --- Cutoff: &CUTDATE";
CALL SYMPUTX runs during DATA step execution. It creates macro variables from data values:
/* Create macro variable from data --- can't use %LET for this */
data _null_;
set adam.adsl(obs=1);
call symputx('STUDY_TITLE', STUDYID, 'G'); /* 'G' = global scope */
run;
%put Study: &STUDY_TITLE;
Critical scope difference:
- %LET always creates a global macro variable (unless inside %LOCAL)
- CALL SYMPUTX third argument controls scope:
- 'G' = global (available everywhere)
- 'L' = local (only inside current macro)
- Default (no argument) = local if inside a macro, global otherwise
Clinical trap:
%macro get_counts;
proc sql noprint;
select count(*) into :N trimmed from adam.adsl where SAFFL="Y";
quit;
/* INTO : creates LOCAL by default inside a macro */
%put Inside macro: N=&N; /* Works: 150 */
%mend;
%get_counts;
%put Outside macro: N=&N; /* WARNING: N not resolved! */
/* Fix: Use CALL SYMPUTX with 'G', or %GLOBAL N before the query */
Answer:
Macro quoting "hides" special characters from the macro processor so it doesn't interpret them prematurely. This is the most confusing topic in SAS macros --- and the most commonly tested.
%STR --- Quotes text at compile time. Hides semicolons, commas, and unmatched quotes.
%let WHERE_CLAUSE = %str(where SAFFL = "Y" and AGE > 65;);
/* Without %STR, the ; would end the %LET early */
%NRSTR --- Like %STR but also hides & and %. Prevents macro resolution.
%let CODE = %nrstr(%put Hello &NAME;);
/* Stored literally as "%put Hello &NAME;" --- &NAME not resolved */
%BQUOTE --- Quotes text at execution time. Resolves macros first, then quotes the result.
%let TERM = %bquote(Nausea/Vomiting);
/* The / would normally be interpreted as division */
%SUPERQ --- The nuclear option. Prevents ANY resolution of the referenced macro variable.
%let RAW = HEART&LUNG;
%let SAFE = %superq(RAW);
/* &LUNG is NOT resolved --- stored as literal "HEART&LUNG" */
When you actually need quoting in clinical work:
/* Problem: Macro value contains special characters */
%let AE_TERM = %str(Headache, moderate); /* Comma would break macro call */
%let FILE_PATH = %str(C:\Studies\XYZ-001\output); /* Backslashes */
%let WHERE = %str(AETERM = "Nausea/Vomiting"); /* Slash + quotes */
Interview answer: "I use %STR most often in clinical work for paths and WHERE clauses that contain special characters. %BQUOTE is needed when the special characters come from resolved macro variables rather than literal text."
Answer:
Three system options control macro debugging:
options mprint; /* Shows generated SAS code after macro resolution */
options mlogic; /* Shows macro logic execution (IF/DO/WHILE evaluation) */
options symbolgen; /* Shows each macro variable resolution */
MPRINT is the most useful --- it shows the actual SAS code that was generated. When your output is wrong, MPRINT reveals exactly what SAS executed.
options mprint;
%macro count_ae(pop=SAFFL);
proc sql;
select count(distinct USUBJID)
from adam.adae
where &pop = "Y" and TRTEMFL = "Y";
quit;
%mend;
%count_ae(pop=SAFFL);
/* MPRINT output in log:
MPRINT(COUNT_AE): proc sql;
MPRINT(COUNT_AE): select count(distinct USUBJID)
MPRINT(COUNT_AE): from adam.adae
MPRINT(COUNT_AE): where SAFFL = "Y" and TRTEMFL = "Y";
MPRINT(COUNT_AE): quit;
*/
Other debugging techniques:
/* %PUT for tracing variable values */
%macro process_domain(domain=);
%put NOTE: [process_domain] Starting &domain;
/* ... processing ... */
%put NOTE: [process_domain] Completed &domain (&SYSERR);
%mend;
/* Check if macro variable exists */
%if %symexist(MY_VAR) %then %put MY_VAR = &MY_VAR;
%else %put WARNING: MY_VAR not defined;
/* Check macro variable value */
%put _user_; /* Lists all user-defined macro variables */
%put _global_; /* Lists all global macro variables */
%put _local_; /* Lists all local macro variables (inside a macro) */
Production practice: Always turn debugging OFF before production runs:
options nomprint nomlogic nosymbolgen;
Answer:
Compiled macros (%MACRO/%MEND) are defined in your current session. They exist only as long as SAS is running:
%macro hello(name=);
%put Hello &name;
%mend;
%hello(name=Bhanu);
Autocall macros are stored as individual .sas files in a designated directory. SAS finds and compiles them automatically the first time they're called:
/macros/autocall/
โโโ chklog.sas /* Contains %macro chklog ... %mend; */
โโโ demo_table.sas
โโโ ae_summary.sas
โโโ compare_datasets.sas
/* Tell SAS where to find autocall macros */
options sasautos=("/macros/autocall" sasautos);
/* SAS automatically finds and compiles chklog.sas when you call it */
%chklog(indir=/logs/adam); /* No need to %INCLUDE first */
Stored compiled macros (third option) are pre-compiled and stored in a SAS catalog. They load faster than autocall but can't be edited without recompilation:
/* Store compiled macro */
libname maclib "/macros/compiled";
options mstored sasmstore=maclib;
%macro chklog(indir=) / store source;
/* ... macro code ... */
%mend;
Clinical programming standard:
- Development: Inline %MACRO/%MEND or %INCLUDE
- Production: Autocall library (one file per macro, version-controlled)
- Validated environment: Stored compiled macros (IT-managed, change-controlled)
Q19-Q28: Macro parameters (keyword vs positional), %SYSFUNC (calling SAS functions in macro), %EVAL vs %SYSEVALF (integer vs floating-point math), macro arrays (%SCAN loop pattern), nested macros, macro error handling (%SYSERR, %SYSRC), conditional compilation (%IF vs DATA step IF), creating utility macro libraries, macro documentation standards for pharma...
This is a free sample of the first 3 chapters. The complete guide continues with dozens more chapters of production-ready code, CDISC standards, and FDA submission practices.