Did you know you can add the same physical table in OneLake multiple times to the same Direct Lake semantic model?
Let’s say you have two tables in a Fabric Lakehouse. One is a fact table called Sales:
…and the other is a dimension table called Date:
Note that the Sales fact table has two date columns, OrderDate and ShipDate.
If you create a DirectLake semantic model using the Web Editor and add these two tables you could rename the Date table to Order Date and build a relationship between it and the OrderDate column on the Sales table:
What about analysing by Ship Date though? You could create a physical copy of the Date table in your Lakehouse and add that to the model, but there’s another option.
If you connect to the model using a tool like Tabular Editor, duplicate the Order Date table and rename the new table Ship Date:
You then have two tables in your semantic model connected to the same physical table in your Lakehouse, and you can create a relationship between this new table and the Sales table:
…and then use the new dimension in your reports:
This is handy for handling role-playing dimensions, which are most often Date dimensions but may be other types of dimension too. Indeed, I’ve sometimes found the need to add the same fact table to a model more than once. The benefit of only having one physical copy is reduced refresh time, lower storage costs (although for most role playing dimensions the savings will be negligible) and simpler ETL.
On my LinkedIn feed this morning I saw an amazing demo from Brian Julius showing how you can use AI to generate code for Python visuals in Power BI, immediately followed by this video from Marco Russo warning us not to believe all the hype about AI. I guess I, like most people, am waiting to see what turns out to be useful in the real world as a result of all of the current investment in AI. Certainly every Power BI customer I talk to these days is very interested in Copilot and the focus always seems to be using Copilot to understand the data shown on a report or to ask questions about the data in a semantic model. That’s understandable because these are the features that will benefit the largest number of users. However, I think the often-overlooked Copilot in DAX Query View is where you’ll get the most value today.
I have to admit that I’m a relatively late convert to DAX Query View, probably because I’ve been a hardcore DAX Studio user for so long. The ease with which you can write, test and update the definitions of measures in your model with DAX Query View is what lured me over and once I was there I started testing its Copilot features too. The documentation gives you the basics of what is possible with Copilot in DAX Query View but it turns out it’s capable of a lot more. So what are the practical benefits of Copilot in DAX Query View?
Writing measures
DAX is incredibly powerful but it’s also hard to learn. Up until recently if you were struggling to write a DAX measure you’d probably go to your favourite search engine, search for blog posts or questions on forums that seemed to describe what you wanted to achieve, copy some code from them and hope you can adapt it to your own needs. Nowadays more and more people are using LLMs to write DAX and while this doesn’t replace the need to learn the language (don’t throw away that copy of “The Definitive Guide to DAX” yet) it is a faster way to get working code than using search engines alone. Copilot in DAX Query View gives you this experience more conveniently right inside Power BI Desktop and with none of the security and privacy concerns about using a free public AI service. It also knows about the structure of your semantic model without you needing to explicitly pass that information in through your prompts, leading to better quality results.
Let’s see an example. Consider the following model:
In DAX Query View, the following Copilot prompt
Write a measure that returns Order Amount for the employee Gabi and show that measure in a query broken down by product
…returns the following measure definition and DAX query:
DEFINE
MEASURE 'Orders'[Gabi's Order Amount] =
CALCULATE(
[Order Amount],
'Employee'[Employee Name] = "Gabi"
)
EVALUATE
SUMMARIZECOLUMNS(
'Product'[Product Name],
"Gabi's Order Amount", [Gabi's Order Amount]
)
As you can see, it’s successfully generated the measure definition I wanted and also written a DAX query that shows the results. I’ll admit it’s not the most complex measure in the world but all the same I know plenty of users find Calculate() hard to understand and would find this very helpful. I will also admit that the quality of the results is heavily dependent on whether you have followed best practices for Power BI semantic model design.
Explain DAX topics and apply them to your code
Looking at the measure Copilot just wrote you might think “should I be using the KeepFilters() function here?”. Entering the prompt:
Explain the DAX KeepFilters function
…gives you a nice explanation of what the KeepFilters() function does:
That’s great, but I could get that from a search engine. What is impressive is that the explanation includes a version of the measure above rewritten to use KeepFilters():
Rewrite the "Gabi's Order Amount" measure so that it uses the DAX KeepFilters function
…and see the rewritten version side-by-side with the original:
…so you can see what exactly has changed and decide whether to accept the change or not.
Explain DAX code
Taking over the ownership of a semantic model that someone else built and understanding their code is tough. Copilot in DAX Query View can help here too. Let’s say you have the following measure in your model:
Avg Order Amount =
//Chris said to use the Divide() function
//to avoid possible division by zero
DIVIDE(
[Order Amount],
[Order Count]
)
The prompt:
Explain the avg order amount measure
…gives you an explanation of what the measure, and all of the measures it references, does:
It takes code comments as well as the code itself into account and although at the time of writing it doesn’t look at the description properties of your tables, columns or measures it will soon.
Suggest improvements to your measures
Let’s say you have a measure with the following definition:
Order Amount Apples =
CALCULATE(
[Order Amount],
FILTER('Product', 'Product'[Product Name]="Apples")
)
An experienced Power BI developer will read this and immediately hear an (Italian accented) voice in their head saying “Filter columns not tables!” even if in this case the query optimiser will ensure this measure performs well. Most Power BI developers will not see a problem though, and while I’m fairly sure that Tabular Editor’s best practices analyser would also pick up the problem not everyone knows about or is allowed to use Tabular Editor.
In DAX Query View you can enter the prompt:
Optimise the Order Amount Apples measure and explain the suggested changes
…and get a pretty good summary of how the measure could be improved to avoid the use of the Filter() function:
Conclusion
Copilot in DAX Query View is not magic. It’s not perfect and it will hallucinate or make bad suggestions occasionally, although I didn’t cherry-pick any of its output while writing this post and it is getting better and better at an impressive rate. It will not replace all the other tools and techniques you use today to write DAX and it will not put Marco and Alberto out of a job. But there’s a saying inside Microsoft that “Copilot is not a pilot”, which sums up nicely how to think about it: Copilot won’t do your job for you but will help you do your job faster and better. As these examples demonstrate, Copilot in DAX Query View can provide a significant productivity boost, especially for less experienced Power BI developers. If you’re the Power BI admin for a large organisation with a lot of self-service usage then I think that productivity boost more than justifies the cost of the Fabric capacity required to run it.
[And yes, before you comment, I know that the fact Power BI Copilot is only available for F64/P1 and above capacities and has geographic restrictions isn’t ideal, but trust me we’re working on ways to make it more affordable and more widely available. We want as many people as possible to use it but we can’t give it away for free. If you’re comparing this with a free service then remember the old saying: if a product is free then you are the product. You may be ok with that but your employer’s lawyers may not be.]
If you’re performance tuning a DirectQuery mode semantic model in Power BI, one of the first things you’ll want to do is look at the SQL that Power BI is generating. That’s easy if you have permissions to monitor your source database but if you don’t, it can be quite difficult to do so from Power BI. I explained the options for getting the SQL generated in DirectQuery mode and why it’s so complicated in a presentation here, but I’ve recently found a new way of doing this in Power BI Desktop (but not the Service) that works for some M-based connectors, for example Snowflake.
The trick is to use Profiler and the M Data Provider Events/Execute Source Query event. To demonstrate this I created a DirectQuery semantic model in Power BI Desktop using Snowflake as a source. I then connected SQL Server Profiler to Power BI Desktop (the easiest way to do this is to install it as an External Tool as described here, although you can connect by finding the diagnostics port as described here) and selected the following events:
I then added a card visual to my report that contained a simple SUM measure.
Here are the events generated in Profiler:
As you would expect there’s a single DirectQuery Begin/End event pair generated because a single SQL query is sent to Snowflake by the card visual. However, the TextData column shows the following SQL – which is definitely not Snowflake SQL:
SELECT SUM([t0].[fare_amount])
AS [a0]
FROM [NYCTAXIDATA] AS [t0]
This is because for some data sources, Power BI emits TSQL and the connector used then converts this to the actual dialect of SQL used by the source database.
This is where the Execute Source Query event comes in. The TextData column for this event shows the actual SQL sent to Snowflake for this query, which is:
select sum("fare_amount") as "C1"
from "NYCTAXIDB"."PUBLIC"."NYCTAXIDATA"
Interestingly this event is generated after the Query End event for the whole query, but you can associate this event with its parent DAX query by matching the value in the RequestId column with the RequestId for the other events generated by the query.
I’ve been playing around with Power BI Copilot a lot recently, and following on from my recent posts on tuning Copilot by editing the Linguistic Schema and setting the Row Label and Key Column properties in this post I want to delve into some of the advanced properties of relationships. I won’t pretend to understand everything that’s going on here – I’m still learning all this – but I thought it would be useful to share what I’ve found out so far.
These properties are described in this YouTube video from the Q&A Modeling Bootcamp series that I referred to in my Linguistic Schema post. They available to set in Power BI Desktop on the Relationships pane of the Q&A Setup dialog by clicking on the “Add to relationship” dropdown box” when creating a new relationship:
For my testing I created a semantic model containing a single table called Journeys. Each row in this table represents a leg of a journey; the journeys are identified by the values in the Journey column, each leg is identified by the values in the Stop column. When a person starts a journey there is a row with Stop = 1 and Stop Type = Start; they may then call in on one or more towns, indicated by Stop Type = Call; and when the journey ends there is a row with Stop Type = End. The Mode column contains the mode of transport, the date represents the date of the leg of the journey, and there is a Booked column which tells you when the journey was booked.
For example, in this data Chris started a car journey in London on January 1st, called in on Oxford, then finished his journey in Birmingham on January 2nd.
There’s also a measure called Journey Count which does a distinct count on the journey column:
Journey Count = DISTINCTCOUNT(Journey[Journey])
Now Copilot does a pretty good job with this table without any tuning. For example, the prompt
Whose journeys started in London?
is correctly interpreted most of the time as “Show the person for journeys where Town is London and the Stop is 1”:
This is a great example of where the LLM adds extra smarts that aren’t present in the old Q&A feature. But some similar questions don’t get answered so well. For example:
Whose journeys ended in Glasgow?
…gives this result:
Not only is the number shown irrelevant the answer is wrong, because Helen also ended a journey in Glasgow. So some tuning is needed.
Here’s where I’m going to show what I did and how it fixed the problem but I want to make it clear that there may be better ways of doing this. I’m sure this time next year I’ll look back at this post and cringe and my ignorance but hey, we all have to start somewhere!
I added several relationships to the model, for example this Verb relationship to indicate that People end Journeys in Towns on Dates where Stop Type is “End”.
In this case the People column indicates the subject, the verb is “end” and the objects is the Journeys table. There is a prepositional phrase “in” to indicate that journeys end in towns. Journeys end on the date given in the Date column. The fact that a journey has ended is indicated by a condition that the Stop Type column contains the value “End”. The prepositional phrase, Time and Condition were added with the “Add to relationship” dropdown.
After this I got the following, correct response to the prompt
Whose journeys ended in Glasgow?
Here’s another example. Consider the prompt:
Who passed through Birmingham?
The original model gives the following, reasonable response:
But let’s say that when we write “passed through” we only mean Stop Type = Call, so journeys like the one Chris made that start or end in Birmingham don’t count.
With the following relationship:
…you not only get the correct result:
…you can ask questions like:
Who passed through Birmingham and on which dates?
As you can see, the “Add to relationship” dropdown box adds even more ways to configure relationships and even more ways to tune Copilot. Knowing your English grammar and parts of speech is going to help a lot here – and this is something that may not come easily to someone working in tech. I like this kind of challenge though. Who knows, maybe the ability to set up a Linguistic Schema will be as important as the ability to write DAX for Power BI developers soon?
The logs for your on-premises data gateway contain a lot of information that is useful for troubleshooting and performance tuning. The contents of the logs are documented here and there a several great solutions out there for reporting on their contents: there’s a Microsoft-provided template and Rui Romano’s more sophisticated monitoring solution here (there’s also another, even better community solution being worked on that will be publicly available soon). To do any troubleshooting or performance tuning, though, you need to link the data generated by Power BI in Log Analytics or Profiler traces to the data in the gateway logs. How can you do this?
Let’s take the refresh of an Import mode semantic model that uses an on-premises SQL Server database as a source as an example. If you have Log Analytics connected to your workspace you can run a KQL query something like this to find the events generated by your semantic model refresh by Power BI:
PowerBIDatasetsWorkspace
| where TimeGenerated > ago(1hr)
| where OperationName in ("CommmandBegin", "CommandEnd", "ProgressReportBegin", "ProgressReportEnd")
//| where OperationName in ("ExecutionMetrics")
| project OperationName, OperationDetailName, EventText, TimeGenerated, XmlaRequestId
| order by TimeGenerated asc
All the events associated with the refresh command will have the same value in the XmlaRequestId column. If you’re using a Profiler trace to capture the same information the RequestID column will contain the same value.
If you then export the gateway logs and look in the Query Execution Report log file for example (I did it with Power Query – it’s just a CSV file) you can match the XmlaRequestId value from Log Analytics to the value in the RequestId column in the log file:
In this example my semantic model contains two tables sourced from an on-premises SQL Server database, so there are two entries in the Query Execution Report log file, one for each table. Once you’ve found the matching rows in the Query Execution Report log file then you can find all kinds of useful information such as how much time (if any) it took to read data from the source or to spool data to disk. You can do the same thing with the other gateway log files: for example the Open Connection Report will tell you how long it took to open any connections to the data source.
If you have a DirectQuery model connected to an on-premises data source then you do the same thing: all the events associated with a DAX query (such as the Query Begin/End events) will all have the same XmlaRequestId, and again all you need to do is find that value in the RequestId column in the gateway logs.
A few weeks ago I wrote a post on how to improve the results you get from Power BI Copilot by editing the Linguistic Schema. As I mentioned, though, there are in fact lots of different ways that you as a Power BI semantic model developer can improve the results you get from Copilot and in this post I’ll show you another one: setting the Row Labels and Key Columns properties on a table.
To illustrate this, I built a semantic model with one table in called Customers. Here are the contents:
There are two important things to notice here:
The Preferred Mode Of Address column contains the full name of the customer. Let’s say there is a business rule that specifies that whenever we display a customer in a report, we need to use the Preferred Mode Of Address column.
Some customers have the same name, and individual customers are identified by their CustomerID (this is not a slowly changing dimension so there is one row per customer).
Now, let’s give Power BI Copilot the following prompt:
show customers by age
This is interpreted as “Showing customers sorted by age”, which is fair enough, and here’s the output:
There are a few things wrong here, but the first one to fix is that it’s showing customers by their first name and not the Preferred Mode Of Address column.
We can fix this by going to the Model View pane in Power BI Desktop, selecting the table itself, and setting the Row Label property on it to the Preferred Mode Of Address column:
The Row Label property of a table has been around since the beginning of Power BI but is very rarely used. For tables that represent entities (such as customers) it indicates which column should be used as the name of the entity.
After making this change, the same prompt (which is interpreted in the same way) now returns the following:
This is better because we’re now showing customers by Preferred Mode Of Address. However there’s another problem: the oldest customer is shown as Fred Blogs with an age of 54. There is no single customer called Fred Blogs with an age of 54 though – there are two different customers called Fred Blogs who are 48 and 6 years old respectively, and Power BI has treated them as the same person and summed their ages.
In order to get Power BI to understand that individual customers are distinguished by the CustomerID column, we need to set the Key Column property on the table to CustomerID:
For tables that represent entities, the Key Column property tells Power BI which column uniquely identifies each entity.
After making this change, the prompt now gives us the result we’re looking for:
As you can see, there are now two separate bars for the two customers called Fred Bloggs – there is one bar for each unique Customer ID.
In conclusion it’s a good idea to set these two properties on most, if not all, dimension tables in your semantic model if you’re planning on using Copilot.
[Thanks to Roseanne Levasseur for telling me about the importance of these properties with regard to Copilot]
As you probably know, in Power BI Direct Lake mode column data is only loaded into memory when it is needed by a query. I gave a few examples of this – and how to monitor it using DMVs – in this blog post from last year. But which columns are loaded into memory in which circumstances? I was thinking about this recently and realised I didn’t know for sure, so I decided to do some tests. Some of the results were obvious, some were a surprise.
Test semantic model and methodology
For my tests I loaded the following tables of data into a Fabric Lakehouse:
…and created a Direct Lake custom semantic model that looked like this:
Before every test I refreshed the model to make sure there was no data in memory (be aware that in the future it may be that refreshing a model does not purge all column data from memory). Then, for each test, I ran a single DAX query from DAX Studio and afterwards ran the following query to see whether the dictionaries for each column were resident in memory (again, see my previous post for background):
Note that I’m using one of the new DAX Info functions, INFO.STORAGETABLECOLUMNS(), instead of the older DMV syntax I was using last year; I think the DAX Info functions are a lot more convenient to use.
[I also looked at whether column segments were paged into memory using the INFO.STORAGETABLECOLUMNSEGMENTS() but found that its results were consistent with INFO.STORAGETABLECOLUMNS() so I didn’t include its results in this post and assumed that if a column’s dictionary was resident in memory, so were the associated column segments. It’s also worth mentioning that the latest versions of DAX Studio have some great new functionality in the Model Metrics for telling you which columns are resident in memory in a Direct Lake model]
Here’s what the query above returned immediately after a refresh, before any other queries had been run:
The DICTIONARY_ISRESIDENT column tells you whether a column’s dictionary is resident in memory. Each table in a Direct Lake model (as in an Import mode model) has a hidden column called RowNumber, and for a Direct Lake model this column is always resident in memory. As you can see, all other columns are not resident in memory at this point.
Counting rows in a table
For my first test I created a measure that counted the rows in the Sales table:
Order Count = COUNTROWS('Sales')
I then ran a query that returned just the result of this measure:
I then ran my query to see what had been loaded into memory, and…
…nothing had changed! No new column data had been loaded into memory at all. I assume this is because Power BI can resolve this query using the RowNumber column from the Sales table.
I then added the Order_Status column from the Sales table to the query:
EVALUATE
SUMMARIZECOLUMNS(
Sales[Order_Status],
"Order Count", [Order Count]
)
ORDER BY
Sales[Order_Status] ASC
After this query, the Order_Status column was resident in memory as you might expect, but no other column was:
Relationships
Instead of using the Order_Status column, I then looked at the impact of using a column from a different table. I created a query that showed Order Count by Customer Name:
EVALUATE
SUMMARIZECOLUMNS(
Customer[Customer_Name],
"Order Count", [Order Count]
)
ORDER BY
Customer[Customer_Name] ASC
After this, the Customer_Name column was in memory along with the two CustomerID columns used in the relationship between the Customer and Sales table:
Any time your query references columns in different tables, the columns used in the relationships between those tables must also be resident in memory.
Measures
I then defined another measure:
Sales Amount = SUM(Sales[Sales_Amount])
And ran the following query:
EVALUATE
SUMMARIZECOLUMNS(
Sales[Order_Status],
"Sales Amount", [Sales Amount]
)
ORDER BY
Sales[Order_Status] ASC
After this, the Order_Status and Sales_Amount columns were resident in memory:
No surprises here: as you would expect, if a column is referenced by a measure then it needs to be resident in memory.
Measures that use relationships also work as expected. I created the following measure:
Order Count Citrus =
CALCULATE([Order Count], 'Product'[Category]="Citrus")
…and the following query:
EVALUATE
SUMMARIZECOLUMNS(
Customer[Customer_Name],
"Order Count Citrus", [Order Count Citrus]
)
ORDER BY
Customer[Customer_Name] ASC
After this query, all the columns used in the measure and all the relationships needed by the query and the measure were resident in memory:
Order Count Status X V1 =
CALCULATE([Order Count], 'Sales'[Order_Status]="X")
Order Count Status X V2 =
CALCULATE([Order Count], FILTER('Sales', 'Sales'[Order_Status]="X"))
My first query used the first of these measures, the efficient version:
EVALUATE
SUMMARIZECOLUMNS(
Sales[Order_Status],
"Order Count Status X V1", [Order Count Status X V1]
)
ORDER BY
Sales[Order_Status] ASC
After this query the Customer_Name column, the two Customer_ID columns and the Order_Status column were resident in memory:
But what about the second, inefficient version of the measure?
EVALUATE
SUMMARIZECOLUMNS(
Customer[Customer_Name],
"Order Count Status X V2", [Order Count Status X V2]
)
ORDER BY
Customer[Customer_Name] ASC
The same columns were resident in memory:
I don’t think this means this version of the measure is any less efficient than it is in Import mode, just that the two versions of the measure need the same columns to be resident in memory to run.
Conclusion
Knowing which columns your query needs to have resident in memory is important for two reasons in Direct Lake mode: it helps understand query performance, because loading column data into memory takes time (Teo Lachev published some test results in a blog post a few days ago which show this clearly); it also helps you understand model memory usage and where you stand relative to the memory limits for the capacity SKU you’re using. As these results show it’s better to test to see which columns your Power BI reports need to have resident in memory rather than rely on guesswork.
It is also possible to use the DISCOVER_CALC_DEPENDENCIES DMV (or the INFO.CALCDEPENDENCY function) to see which tables, columns, measures and relationships are referenced by a DAX query as I described here; I’ve been playing around with this for Direct Lake too, and will report my findings in a future blog post.
In the past I have blogged about how important the number of connections from a Power BI DirectQuery semantic model to a data source is for performance. It’s also true that in some cases opening a connection, or some of the operations associated with opening a connection, can be very slow. As a result it can be useful to see when your semantic model opens connections to a data source, and you can do this with Log Analytics.
To show this I published a DirectQuery semantic model to a workspace with Log Analytics enabled and opened a report with one table visual on it. Because the model and report had only just been published I could be sure there were no existing connections back to the data source used by the DirectQuery model. I then ran the following KQL query:
PowerBIDatasetsWorkspace
| where TimeGenerated>ago(1hr)
| where OperationName in ('Notification', 'QueryBegin', 'QueryEnd', 'DirectQueryBegin', 'DirectQueryEnd')
| project TimeGenerated, OperationName, OperationDetailName, EventText, DurationMs
| order by TimeGenerated asc
Here’s the output:
Some things to note:
I have highlighted in red the events of type Notification and subtype OpenedConnection. These events are fired when the semantic model opens connections back to the data source. There are three of these events so three connections are being opened.
There is one DAX query generated for the single visual on the report page. As you would expect there is a single pair of QueryBegin/QueryEnd events.
After the QueryBegin event there are two DirectQueryBegin/DirectQueryEnd events, which mean that for this DAX query Power BI ran two SQL queries against the source.
Immediately before these two DirectQueryBegin/DirectQueryEnd pairs, and after the QueryBegin event, there are two Notification events which indicate that two connections are being opened to run these queries.
Before the DAX query runs, another connection is opened and there is a DirectQueryBegin/DirectQueryEnd pair that is not associated with a DAX query that I have highlighted in blue. The EventText column contains M code rather than SQL code, and a look at this M code shows that it uses the DirectQueryCapabilities.From function which is marked for “internal use only” in the docs. The DirectQueryEnd event from this pair has a duration of 3 seconds which shows that in this case it’s quite expensive. This query is the Power Query engine understanding the DirectQuery capabilities of the source and one of the side effects of this is that it needs to read metadata from the source, which can be quite slow sometimes. Doing things like disabling relationship columns in the connector and, for some sources like Snowflake, connecting with an identity or with a security role that can only see the tables or views used by the model (as mentioned here), can make this a lot faster.
For some reason the Notification events associated with opening connections do not appear in Profiler traces run on models in the Service, although they do appear if you run a trace on a model in Power BI Desktop. This is why I’ve used Log Analytics here.
Power BI pools connections once they are opened and keeps them alive for a certain amount of time and it also caches data source metadata after it has retrieved it, so as a result you may not see the events I have highlighted here when you run a report.
[Thanks to Curt Hagenlocher and Akshai Mirchandani for some of the information in this post]
Recently I was working with a customer using DirectQuery mode and where all traffic to the data source had to go through an on-premises data gateway for security reasons. They noticed that report performance got worse when traffic went through the gateway and this was particularly true when Power BI generated SQL queries that returned hundreds of thousands of rows. Looking in the gateway logs we found that spooling (see here and here for more details on spooling, and here for information on monitoring spooling in the logs) was taking place, so I advised the customer to set the StreamBeforeRequestCompletes property on the gateway to true. This eliminated the delays from spooling on the gateway (I’m told it helps performance inside the model too) and resulted in some substantial improvements in report performance. As a result, if you’re using DirectQuery and a gateway, you should experiment with the StreamBeforeRequestCompletes property to see if it can help you too.
That said, if you’re using DirectQuery mode you should try to avoid situations where Power BI generates SQL queries that return a large number of rows such as table visuals with scrollbars. Regardless of whether you’re using a gateway or not, moving large amounts of data from your data source to your semantic model can be slow – even if your data source tells you the SQL query itself is fast. You can use Performance Analyzer in Power BI Desktop (see here) to find out how many rows the SQL queries Power BI generates return and how long it takes to read that data.
Here’s another one of my occasional posts about books I’ve been sent free copies of. Full disclosure: as always, these aren’t reviews as such, they’re more like free publicity in return for the free books and I don’t pretend to be unbiased; also the Amazon UK links have a affiliate code in that gives me a kickback if you buy any of these books.
What am I doing covering an AI book here? Lisa is an ex-colleague of mine at Microsoft and I respect her opinions. Also, I suspect like a lot of you, I have mixed feelings about the current AI boom: I can see the value in AI but I can also see the vast amount of hype and the obviously ridiculous claims being made. More than anything I see senior executives talking confidently about a subject I’m sure they don’t understand, and that is clearly a big problem. This book aims to help solve that problem by providing a practical guide to AI for non-technical leaders, in the form of a series of case studies and interviews with entrepreneurs and C-level people in the AI space. This is a very readable book – Lisa has talked to a lot of interesting, knowledgeable people – and the format makes it a lot more palatable for the target audience of your boss’s boss’s boss than your average tech book. As a technical person who isn’t by any means an AI expert I also enjoyed reading it.
Spend any time on public Power BI forums and you’ll see a lot of questions from people who want to know how to start a career in Power BI or get tips for Power BI interviews; as a result I’m sure there’s a big market for a book like this. It’s a mix of technical topics (the type that you might be asked about in a technical interview for a Power BI job) and non-technical advice such as how to network on LinkedIn, negotiate salaries and acecpt or reject a job offer. That might seem a bit of a strange combination but it works and the advice is both detailed and very sensible, so as a result I would have no hesitation in recommending this to anyone trying to get a job as a Power BI developer.