I remember reading about lazy evaluation for DAX variables the first time several years back in the book “The Definitive Guide to DAX” from Marco Russo & Alberto Ferrari. I also remember being very confused about the concept back then. It took some time and research in order to understand the concept a little better.
If you find yourself struggling with understanding lazy evaluation as well, I invite you to read this post.
In order to better visualize certain aspects that will assist in explaining lazy evaluation, I have chosen to use Python instead of DAX to explain the concept. At the end I will briefly get back to lazy evaluation for DAX variables.
Let’s start with why lazy evaluation is interesting. Imagine having a huge dataset and limited memory (which is actually always the case).
You want to save values from this dataset that match a certain condition. However, you run into memory issues: you do not have enough memory in the tool you have available for computing the results.
Instead of storing the results in a list, which is loaded completely into memory, you could opt for a generator object instead. A generator object only stores the metadata of the object which leads to a much smaller size. Below is an example of this:
import sys
# Create a range object
r = range(1_000_000)
print("Size of range object:", sys.getsizeof(r)) # Small size, e.g., 48 bytes
# Create a list from the range
l = list(r)
print("Size of list object:", sys.getsizeof(l)) # Much larger size, 8.000.056 bytes
# Create a generator object from the range
g = (a for a in r)
print("Size of list object:", sys.getsizeof(g)) # Small size, 192 bytesThe snippet of code below gives a more elaborate explanation of what exactly lazy evaluation does.
import re
import sys
# So imagine you have this huge dataset containing sales and you only want to have sales with apples:
huge_dataset = [{1: '10 apples', 2: '3 pears', 3: '30 apples', 4: 'ad infinitum'}]
# Create regex object in order to extract all values with 'apples' from the dictionary in the list below
regex_apples = re.compile('.*apples', re.IGNORECASE)
# Using list comprehension, you can filter out the apples and store them in a list:
# Variable apples_sales stores all the filtered results in memory
apples_sales = [sale for sale in huge_dataset[0].values() if re.match(regex_apples, sale)] # re.match returns a Match object when it finds something, and returns None when nothing is found. When a match is found, a Match object will be returned and this object will be interpreted as 'truthy' in the if-statement. When there is no match, or with other words, when no Match object is returned (resulting in "None"), the condition wil be 'falsy' in the if-statement.
# This would however, load the entire huge_dataset in memory. If you do not want abuse your memory in such a manner, you could opt for a Generator object instead.
# Generator objects use lazy evaluation which means that they do not load the entire content of the object into memory.
# Variable apples_sales_gen stores a Generator object which only contains the "metadata" in memory, meaning the condition (if-statement) and the iterable object
apples_sales_gen = (sale for sale in huge_dataset[0].values() if re.match(regex_apples, sale))
# Processing only starts when you explicitly iterate over the Generator object
print(next(apples_sales_gen)) # Only the first item of the Generator object is printed: 10 apples
print(next(apples_sales_gen)) # Only the second item of the Generator object is printed: 30 apples
# The list stores all the filtered results, so this will show the entire list:
print(apples_sales) # All items in the list apples_sales will be printed: ['10 apples', '30 apples']
# You can see the generator's object reference (it has not been consumed yet):
print(apples_sales_gen) # Outputs something like: <generator object <genexpr> at 0x0000020D2DE935A0>As for how to apply this knowledge to lazy evaluation and DAX variables: variables that are declared in DAX are only evaluated when used in the RETURN statement.