Forum Discussion

AlB's avatar
AlB
Community Champion
5 years ago
Solved

Column values change when loading into model

Hi all, See attached file for support. We build a simple table in the query editor (M code below). Note we are creating a custom column "Value" with randomly generated numbers. In the query edito...
  • Smauro's avatar
    5 years ago

    Hi AlB 

     

    I'll try to explain a little bit.

    No matter the queries we write, in the end we have the PQ Engine to do the work for us. In order to make this engine fast enough, there have been written a whole lot of optimisations. So, it will do some steps together, it will rid of some all the way because they are deemed not needed, etc. That's why it's a functional language: it starts from the result and works its way up using only parts that it is actually going to need. And this is probably why the "DataMashup" has the Mashup in its name.

    Some of these optimisations give birth to "bugs" like the one you came accross.

     

    Using:

     

        #"Added Custom" = Table.AddColumn(#"Changed Type", "Value", each Number.RandomBetween(0,1000), type number),

     

    The PQe will "understand" that the row values are not needed: We ask for a number, we do not make use of any row value, and then return it. So, when mashing this thing up, it will actually compute Number.RandomBetween(0,1000) once and then add it in every row.

     

    It is a bit smarter than that of course, we cannot "trick" it to use the row values if they're not needed:

     

        #"Added Custom" = Table.AddColumn(#"Changed Type", "Value", each Number.RandomBetween(0,1000) + [CustomerId] - [CustomerId], type number),

     

    will also never use the rows' values.

     

    To actually see this happening in PQ Editor one can use a Transformer function:

     

    Table.TransformColumns(#"Added Custom",{{"Value", each Number.RandomBetween(0,1000), type number}})

     

    with which every row should be filled by the same number.

     

    OK, now we need to actually force it to calculate this thing for every row. And this is why Index or Buffer are commonly used:

    i) By indexing after the random number generation, the PQe actually has to go through every row, look at its values, save their position and values and then go on.

    ii) By buffering, it loads the whole thing into memory, so it actually computes the random numbers once again for every row, but also keeping all that.

     

    There's another solution not really talked about, which is for me the fastest one I know of: Replacing. Table.ReplaceValues actually reads every row and discards it, being a bit faster than indexing. These two steps should work as expected:

     

        #"Added Custom" = Table.AddColumn(#"Changed Type", "Value", each null, type number),
        #"Replaced Value" = Table.ReplaceValue(#"Added Custom",null,each Number.RandomBetween(0, 1000),Replacer.ReplaceValue,{"Value"})

     

     

    Last, you can avoid all that by using List.Random, which is actually made to compute different random numbers. However, afterwards you'll need to zip it with your data and expand etc etc, or add an index and transform the index to the corresponding list's random value. All that taking some valueable calculation time. But take those two steps:

     

        #"Zip with List Random" = List.Zip({#"Changed Type"[CustomerId], List.Transform(List.Random(List.Count(#"Changed Type"[CustomerId])), each Value.Multiply(_, 1000))}),
        #"List to Table" = Table.FromList(#"Zip with List Random", (x) => x, {"CustomerId", "Value"})
    in
        #"List to Table"

     

     

    Hope it makes some sense now.

    Best,

    Spyros

  • Smauro's avatar
    Smauro
    5 years ago

    Hi AlB 

     

    I'm glad you found this helpful. I've read a lot about how the mechanics work, but this documentation is found mostly on forums, by reading responses by people actually involved in the engine's developement (I am not one of them 🙂 ). There's a good series by Ben Gribaudo about M, and it does meddle a little bit with these topics (lazy evaluation) but not so much.

     

    OK, now to explaining this last bit. The query preview actually computes some things and has its own buffer, which is the one helping us to see if what we're doing actually works. When we're previewing the results, they are buffered in the DataMashup so we can see and work with them, we can see how each step is computed by clicking on them etc.  (btw this is why a good optimisation when working with large data models is disabling background preview).

     

    In a way, the preview is a bit less lazy (less optimisations in our case) than the actual query plan, in regards to trying to put the steps in order, letting you look at these steps etc. So, Number.RandomBetween(0,1000) gets evaluated and shown.

     

    However, when the queries are applied, the queries are not looked at individually but get reevaluated all together, compressed and put into the Tabular model's Storage Engine (VertiPaq). The preview's results are not used. These optimisations are a lot better - we see that sometimes while working with very complicated queries taking about the same time for 1000 rows (preview) and for the actual evaluation. One such optimisation is assuming idempotence for every function: given the same arguments the function will return the same result. With this, Number.RandomBetween(0,1000) is evaluated once and the result is subsequentl added in every row.

     

    Of course, with static data we see no difference, but with random data we do.