 Welcome to today's Postgres Conference webinar, Functions, Use and Misuse. We're joined by Henrietta Dombrovskaya, Director of Analytics at Brokerx, who will discuss how functions are executed and are using functions effects performance, how to use functions with user-defined types, the advantage and disadvantages of using functions in OPT and OLAP environments, and how to use functions with Dynamics SQL. My name is Lindsay Hooper. I'm one of the Postgres Conference organizers, and I'll be your moderator for this webinar. A little bit about your speaker. So Hetty is a database researcher and developer with over 35 years of academic and industrial experience. She holds a PhD in computer science from the University of St. Petersburg in Russia, and at present she's the Director of Data Analytics at Brokerx, a local organizer of the Chicago PostgresQL user group, an active community member and a frequent speaker at Postgres conferences, an author of PostgresQL query optimization book, and many, many other titles. So welcome to you, Hetty. So with that, I'm going to hand it off. Take it away. All right. Thank you, Lindsay. Hello, everybody. Thank you so much for attending my presentation. As Lindsay just noted, everything is recorded, so I really appreciate people taking time and attending in person because everybody needs audience. You know, even silent audience is still better than that. So today's topic is functions and procedures, use and misuse. And indeed, functions and storage procedures are in some sense, I think, most controversial objects in PostgresQL because I think at least 50% of the time they are used like in the wrong place and not used in the right place. So as it was mentioned, the examples which I will use coming from the book Postgres query optimization, so just 10 seconds of self-promotion. Book is still there available in Amazon, in a paperback, and in electronic. And all the examples are available on the open source Postgres Air database. So you should have this link and like if you care, you can just, you know, get this code from your screens. So this is like full-size database. You can download it and use for all sorts of different things. You can test pretty much everything on this database. So again, the examples will be based on the tables from this database, and I will like mention briefly what they are, but hopefully most examples will be like self-explanatory. So we will start with functions and how we're using it. So why functions? Like as I said, functions are most underused and misused objects in PostgresQL, from my perspective. So why? Because everybody started from some programming language and everybody knows how to write functions in programming language. So does it help to write Postgres functions? Actually, it does not help to write Postgres functions. So let's take a closer look at why and how Postgres functions are different from functions in programming languages. So first of all, there are two types of functions. First are internal functions, which are actually pretty much like your functions in C or Java whatever you want. You can do like mathematical functions, string functions, time functions, so those are kind of something which are really familiar. And then there are user defined functions, which can be written in query languages like functions written in SQL, there can be C functions, or like on C or C like languages. And there are other functions, which can be written on procedural languages. For example, like on PL SQL, you can do Python functions and many other functions. So in today's presentation, we'll be talking solely about functions, which are written in Postgres programming procedural languages, PLPG, SQL. So all the functions, which are written in PLPG, SQL can include any SQL operator, any control structures, if case loops, et cetera, and it can include calls to other functions. So for start, let's look at one example of how to create functions. To create function, we use create function operator. So what we have here, that is the name of the function. So the name of the function is texting numeric. Then it has parameter. The parameter is called input text and it's of type text. Then they're saying it returns integer and it is written on PLPG SQL language. And all the rest is the function body, so starts from body and from body. And that is like a very simple thing, which is basically converting text to integer. And the first question you might ask, why we need to create a separate function for this simple operation? Doesn't Postgres have ways to convert text to numeric? Absolutely. And actually several ways to convert text to numeric. So you can do past, you can do like convert to numeric, you can convert to number, like all sorts of things. But what happens if the text that you pass is not something which is convertible to number? So if it's not like one or 100 or whatever, what it's like A1. So what will happen? All of this system supplied conversion function will arrow, it will generate Postgres arrow. And then the whole SQL where you are applying this function will collapse and you do not want it. So you want to do something in exception. So what to do when the parameter which is past does not represent numeric. So in this function, we said, okay, do nothing, return null. So this allows us to operate on some strings and if it converts to integer grade, if nothing return null. Actually, that might be not always a good idea to use this type of functions, but at least that's the purpose. And we will see in a little bit why it might not be always a good idea. So now really quickly, because that is everything you can see in documentation. So I will go through the next couple of slides really quickly. So function might have named parameters or position parameters. So we can define it by name as you have input text or we can just have parameter one, parameter two, et cetera. Parameters might be specified as in, out or in, out, although most of the time we use in parameters without out. And then we just return function, result of the function like what it returns. Also function, there is such a thing as function overload, which means you can define the function with the same name and with different types of different sets of parameters, different number of parameters, different part of parameters. So you may call the same function like here again, we are using for this air database in the function num passengers, calculates the number of passengers. It can be on the flight ID. So we can pass flight ID, return number of passengers on the flight. We can also return number of passengers, which we departed from this specific airport on the specific date. Or we can do it by the flight, flight number, which is text field. So everything here will be actually fine, except off, we cannot change the return type. So when we try to change return type, so we had integer, integer, integer, and then we said ops, numeric and this will return an error because all the functions which we use for overload, if we have the same name parameters might be different, but it should return the same type. So that's like all the exception. Another thing which is worth mentioning, it's nested all the quoting because that's absolutely wonderful for the switcher as quite often you need to have string within string, like example here, you want to pass an error message and you need to have the text error, you need to have quote inside. For example, here, a record can be updated. So you need hands with quote. So if you do not have a special notation for handling this, you always need to double this quote, like the triple this quote and whatever. So postgres allows nested dollar quoting. So every double dollar sign means, might mean the beginning of the string and then matching the double dollar sign means the end of the string. And also you can put any identifier in between. So if you look at the function body, function body is also a text. So it's passed to the create function as a text. So we have here dollar function dollar. That's like the end of the text of the function. And the beginning was also function. And inside you can have multiple other dollar quarters and they all will be nested properly. So you can have string within string within string. It's actually extremely helpful, one of the best features for this. So this is said, what we just mentioned. So the body of the function is passed when function is created as the string, which means it can be pretty much anything. And all the very, very basic syntax checks are performed when function is created. So this has been said, when you create function, like say you did it in Oracle, for example, or if I can sign base, most of the time assume, OK, create function, function created successfully. That means function works. So with Postgres, it means virtually nothing. So the only thing that means that you have the right matching the number of if, then, and if, or like case and case. So all the very basic checks, there might be lots of errors which are not identified when function is created. Again, that is an example from Postgres Air Database. So we create this function number of passengers departed from this airport on this departure date. We run create, it created successfully, right? Now we want to execute it. So now we're trying to execute it and create return successfully. Great. We do execute, select number of passengers departed from Ahara on UI7. What we're getting? We're getting an error message. Column airport code does not exist. So what? Yeah, because actually we set airport code in the function, but actually the correct column name was departure airport. So Postgres didn't even check whether we reference correct columns for this table. Then, OK, fix this. OK, it's not airport code is departure airport run again. Column pday does not exist. Yes, because the parameter was called not pday, but it was called p departure date and again, when function was created, Postgres didn't check. Should it? No, it doesn't cut again. We pass it as a body of the function was a string and these details were not checked. Okay. So in contrast to other databases, if you have experience of creating functions as well, anywhere else, functions in Postgres are stored in form of the source code. No, they're not really compiled. Function is created. It doesn't mean it is compiled. It is stored as a source code. Functions are interpreted during execution and even when function is executed only when the execution path reaches this specific command, this command will be analyzed and prepared statements will be created and it will be prepared statements will be reused in the same session. So if you run the same function multiple times in the same session, but otherwise it won't be it will start all over again. So that means that you might not discover some errors in your function for a while. So that means that when you test the functions, you need to pay special attention. You tested all the cases because if you have multiple case statements, for example, and one of the case statements was never reached, you might never even know your function has an error. So that's kind of warning for the functions. Also, when you create, again, that's what we're talking today about PLPGSECO functions, no execution plan is created. So no checks of existence of tables, columns, or other functions are performed, whatever is referenced in the function body. And you might even know whether it works or not. So just be very careful while using functions. It's not what you expect from the programming language experience. Other thing, both these functions are atomic. So that means that you cannot initiate a begin a commit or rollback transaction inside the function. And again, for people who come from other databases, it's huge disappointment. So we will talk a little bit at the end of this session. It is done with post-restorated processes, but functions are atomic. And also you can never explain function. If you are used to running explain to see how your SQL will work. You cannot explain the function like the only thing it will give you. Okay, explain. It will say, okay, that is like the result. So if you want to know what path execution path is chosen, you need to actually run explain plan for specific statements inside the function with specific parameters. So that is important. And again, the reason originally for this atomic set is because original purpose of the function was to be able to select them in the select list. So for example, you can do this call this function. When you select something from this passengers, right? So here you have a select text to integer as passport number, select text to date, the expiration date from the passenger passport table. So that is how we can use functions. And now I want to tell you why it's not always a good idea. Precisely because of this, that functions are atomic. And it might be not so pronounced when you use functions in the OTP environment, when you select at any given moment or the small number of rows. But it might be more pronounced when you do it for the large data volume. So here we have this function. Again, create function number of passengers on specific flight. Awesome, right? So we select count from the passengers who were registered for specific flight. Great. Now, we want to use this function in select. And so what we're doing, we are doing so for each flight, which departed from Ahara on July 5th, between July 5th and July 15th, we are selecting flight ID and the number of passengers. So legit, OK, that's what we want. We want to reuse the function. So the execution time for this statement will be three and a half seconds. You might say it's not the end of the world, but honestly, that is something alarming for online application. Three and a half seconds just selecting a number of passengers. So if you instead of calling function, you will perform this calculation inside select. So you'll do select flight, count as number of passengers, and you will do this grouping inside select. The execution time is actually less than a second. So by this difference, the difference is precisely because function is atomic and optimization of the function is not included in the optimization of the whole sequel. So in this case, function will be executed each time it is called separately. So that is so-called optimization fence. And that is something we just need to have in mind. So not always using functions, especially in longer queries is a good idea. Now, you might ask, OK, so that is custom function. What about simple transformation? This, you know, I always knew it's problematic. And for Postgrease Air, we built this table passenger passport specifically to show why it might be a problem on the large data volumes. So this table was created as a know-how in many systems when people do not know where to put new data. They use something like custom types, like custom defined types, special types. And they put everything in this big table with like type and qualify, etc. So that was how we built this passenger passport table. So there are lots, lots of different information in kind of key value format. So that huge table, I think, like 15 million rolls, something to mess up. So you just do select passenger ID, passport number, passenger expiration date from passenger passport, OK? So 50 million records, 41 second execution time. Just pure select, select everything from this table. Now we are selecting. So by doing simple Postgrease transformations with no exception. So we can put passport number to numeric, passport expiration date to date. Execution time is two minutes. And if we call the user defined function text to integer and text to date, the execution time is nine minutes. So that's how much difference it makes. So again, that is something to be aware. Sometimes it's not important. Sometimes it's important. Just these numbers, I think, are very illustrative on what might happen when the whole functions in the select list. Because otherwise nothing changing. We do not join this table with anything. So the select execution plan will be the same in all of these three cases. But the execution time will be very different. So now after all this, I already thought like, how horrible it can be? Is there any way functions can improve performance? Actually, they can and we just need to use them differently. Hey, and that's what we are going to show. But again, I'm keeping reiterating on this because in programming languages, you often use functions just to like code. Like, you know, separating, encapsulating the pieces of code which you need to reuse. And it's not always a good idea with database functions. Right? So we can this function, but we need to utilize slightly different approach. No code, even if we do code factoring, it will be in a little bit different place. So in order to show one of the good usages of post-bis functions, let's look at the user defined types. So I can talk about these defined types will be another class, another presentation. But now I will just briefly mention it. So Postgres allows to define user defined types, which can be as simple as create domain or can create an emulator or can create a range type or like any base type. So those are all simple types. What we want to look at today are user defined composite types. And many of you guys did it in your life. So we can create the composite type, which will be like record type. For example, here, we are creating the boarding pass record, which has like boarding pass ID, booking lag, flight number, et cetera, first, last name, seat number. So that's how your boarding pass looks like. You know, we created this, what we say, a database in the midst of pandemics. So it was a little bit nostalgic. Nobody would fly. Now people kind of like start to get there. So now people start to remember again what are boarding passes, what are connections, what are slides with false flags. So now it's like, not that foreign as a year ago. Some composite user defined type. So what we're going to do with this and what it has to do with functions. All right. So we can create functions which return composite types. And what we have here, we have like very simple function, which returns all boarding passes for the specific flight. So printing all boarding passes for all passengers. So it returns set of boarding pass records. And you look at the border of the function, it's very simple. It returns the query, select all this information we just described. And then when we execute this function, it returns the whole result set. So if you look at the result of the function, it will be like the same as you would select things from the tables. Like no, no difference from a cycle process. So why it is better? OK, you select, run select or run functions was the difference. Why it's better? So several reasons. First of all, there are parameters. So we do not need to write SQL each time. We can just pass different parameters. In this case, we can print boarding passes for any flight. Also, there may be more complicated function for it. There may be more things than just select and I won't go into depth. Here it has, and like I said, it's like whole separate topic, how you can interpret different parameters differently. Then the other important thing, it will create different type of dependencies. And I will tell about in a second and functions will allow you to execute dynamic SQL. So let's look at all this other things. So functions are not dependent. So why it is important? Imagine you have a system where you created multiple views, materialized views and something, and then you need to modify one of the underlying tables. Or God forbid, you'd like to drop and create something else. So what this would not allow you unless you drop cascade all dependent objects and they may have all dependent objects and in real systems, like in many reporting systems with views and materialized views, modifying just one element might result in cascade dropping of like 60, like 90, I had the situation when we had to recreate 95 objects when something underneath was modified. So that is because these objects are dependent and progress holds these dependencies. Functions, as we said, functions kind of do not know what is inside the function. So if you reference any table, so like if instead of materialized views, for example, or something, you're used to reference the tables, your reference other functions, they do not know whether you did something or not. So for this function, so what we're doing here, we're issuing a booking, a boarding pass for specific passenger, right? So we are actually creating this boarding pass and then we are executing query, select a star from boarding pass and the path with ID. So when this function is created, technically this function, the reference should not even exist and it would not check. And if we change something with this function, if we want to create, replace, modify, it would not cascade drop this function. So again, in systems where such situations occur, it's very convenient because like, okay, we know what we're doing, okay? And we need to know what we're doing. So otherwise actually if something significant changes and functions wouldn't be cascade drop, you'll find everything and you'll try to execute it. But in many cases, it's more convenient because you can recompile and modify all these components. However, the functions will create different types of dependencies and that will be dependencies on the types. So types, that's something we'll hopefully talk a little bit, maybe not. Okay, so the other thing which we want to talk about, functions and security. So by default, okay, all objects and database have permission so people can have like, select and sort of delete and add some other permissions on the objects. So functions, if you do not do anything, they do not have any extra security permissions on their own. By default, functions, permissions are public, which means that, okay, anybody can execute any function providing they have usage on the scheme of a function is created. And the only way how permissions will be checked is when, okay, so I'm a user. So if I have all the permissions on the underlying objects which are inside functions, so if I am granted this select, this insert, this update, then I can execute it. And if I do not have it, then I cannot execute. So the security on the function, by default, that's executed the final will be, that's literally what permissions this user have. So the situation we often want to address, you know, everybody have very important business users in the organization, right? And this very important business users always tells us, we need to see production data because otherwise we cannot make our important business decisions. And you have kind of like two options, either to give them access to the database, which, you know, you might not always be confident that important business user has all the capabilities to run SQL, which won't bring the database down. I think many of us have these experiences when very powerful business user brings the system down, this I had multiple times, so that's what we can do. So we are creating function, which is the security definer. So what it means, a user to whom we will grant permission to execute this function, then this path execution, while they are like in the function body, they will have the same permissions as the user who created this function. And then say, okay, so we are like our user, like we are DBA user in our database. So we create this function, critical function, and we do whatever we need, when nobody are doing it in the right way and nothing crazy, nobody is going to drop database or like data board information. Then we will revoke execution and dysfunction from public. That means that nobody except of the user whom we designate will be able to execute this function. And then we grant execute on this function to our very important business user. So this way, they actually can execute something critical on production data, but they won't be able to do whatever they want on production data. Only what we allow them, and basically in the way how exactly we allow them to do this. So that is actually a very powerful way of doing this. And that is something which we strongly recommend to you. Okay, next, a function with the word return. So again, for the longest time, both of these did not have procedures. Both of these only had functions. And functions technically were supposed to be like used in select statements. And in many cases, we still wanted to package some set of operations inside of like some procedural model. And for this, we used functions with word return. So function which returns nothing. For example, here we're doing like function cancel flight. It will do lots of things. It will change the scheduler. It will unknown all the boarding passes, et cetera, but it does not return anything. It's like cancel flight, okay, done. So this function will return nothing. And the other, like the other way what we're doing here, we can execute it when we execute function inside other functions. We also do not necessarily need to process return of the function. So one way is doing word function. The other way is perform operator. So we can do perform operator inside others. And then we just would not capture the return result. So that's like other way of doing this. So that was all with functions. And then finally, you know, we'll do like 10 years, whatever later, more process introduce procedures. So now as again with postgres, with same with Oracle, with Cybase, with Microsoft SQL Server, now postgres also has a procedure. So what's the difference? Okay, actually lots of things are the same, but many things are different. That's a good part. So to create procedure, we use create procedure and it's same as that function. We have procedure name, procedure parameters. Only it does not need return type because procedure does not return anything. You can define the output parameters, but again, that will be parameters. Procedure does not return anything. And you do not do select from procedure, you call procedure. So you can, for example, execute your call cancel flight. So we do not need to create function with return. You can just create procedure, cancel flight. But that's not kind of the best part because, okay, we can use void. What's the difference? So most important thing is transaction management. Finally, I can tell you a person who came to postgres from Oracle, that was like my huge disappointment, like, ah, where are my transactions? How I can manage transactions? How I can do like rollbacks? How I can commit intermediate results? Not like the next version, next version. So I remember like for seven years, like after each post conference, I was coming to one of the major contributors and asked like, so when they're going to have them? So then finally, yeah, we got them. So in storage procedures, you actually can do conditional commits. And like one of the good parts is here, for example, when you do massive updates or massive inserts, you can commit intermediate results because otherwise you have this like huge process which runs forever and takes all the temp, like all the buffers and everything, then when it fails and everything fails. So at least you can commit stuff in the middle. That's a good part. And many other reasons why committing partially inside the function is the great thing. And the other thing which functions allow is the exception processing, like multiple exception processing. So again, here what we have, we have several execution blocks within storage procedure. So we can have multiple begins, multiple ends. And for each of these blocks, we can define exception and like for example, how I use it with great success when we like literally have like lots and lots of things in one procedure. And when we raise exception, at least we know in which part of the procedure is exception was raised because otherwise you can go on and on and like, oh my gosh, when did it happen? So this way that allows you to identify directly and precisely even when the exception is the same where exactly it happens. So again, very, very, very useful tool. I think that again, procedures are also kind of underused just because then you and for many years people actually use the word functions instead of procedures. So for me that was also like kind of change of mindset. Okay, do you need to return something? If you don't need to return something just create procedures or create functions because that was also like force of habit. So I'm like encouraging people using sort of procedures when they do not need to return results. All right, and now we are up to the most fun part. And again, I could potentially talk about this forever but I am not because like if it will be separate topic I will just outline a little bit about the usage of dynamic SQL. So it is probably the main purpose of using function storage procedures to be able to execute SQL dynamically. So what is dynamic SQL? For example, like this, so we have a text. So SQL is text and what we are doing we are building the select statement here. So we assign this text variable the text of the SQL statement. Select count from booking, we are booking reference blah, blah, blah. And then we execute this SQL and we return the result of this execution. So that is as easy as this but like why it is so important because you can actually generate different SQL within the same function of storage procedure and execute something different depending for example on input parameters. Now, why it works better in Postgres? Why it looks better than parameters? So that's actually mental block for many people who come from different database systems. And I often had to work with people who come for example from Microsoft SQL Server and from Oracle in all database text books tell you do not use dynamic SQL always store parameterized statements because they optimize better. So that is works like kind of opposite with Postgres. Okay, so why it works opposite in Postgres? Because the execution plans again, there are some exceptions to all of this statement but by default most of the time execution plans are not cashed even for prepared statement. So that means like in Oracle you executed it's like sits there in the shared memory and each time the same query is running it's like here is the plan execute. So it's not the case in Postgres. Optimization always happens right before execution. And it happens like later than all in all other systems. So in Postgres, again, except for few exceptions the optimization is done for this particular SQL statement with this particular value. So for example, whether this particular set of parameters will produce massive result or small result for which of the parameters or which of the conditions will be the most restrictive condition kind of Postgres does not know in advance it would optimize for this particular values. So because of that, using dynamic SQL it's just better and more reliable especially actually for all TP systems because then the very better chances that each and single query will be executed the right way this very moment. So as I said for all TP I'm almost not touching it because it's huge separate topic but I want to show you something which actually like kind of less frequent typing probably and I find it very useful technique. So dynamic queries for a lab because with a lab so the problem I see with a lab systems people often say we don't need to optimize anything because those are reporting queries and like whenever they will finish whatever and that's what I mentioned in September when I had a presentation about long queries and after full scan. Long queries still we still want to optimize long queries. And that's one of the examples of kind of like a little bit counterintuitive optimization of long queries and that's like modeled from very real situation which I had with like working with marketing analytics. So that's a function very like okay, normal function. So what we are returning, we are returning a category. So the input parameter is integer age and we're saying, okay, so age under two, it's infant. So from two to 12 as a child, from 12 to 35 as an adult and else senior. So like function which you can easily imagine is needed for the like our airlines database. And then it is absolutely fine when we are doing it like for one record for a couple of records. Now remember we created this like huge tables they're like specifically for the purpose. Okay, model how it will work with long analytical twins. So it has lots of messages, like several million messages. So if we have here limit again this example is taken from the book, we had limit like literally so that it won't run forever. So we were just, you know, kind of making it more safe. So we select for each passenger age category and the execution time when the coldest function is at 25 seconds. And if we instead of calling this function like inside the select we are doing this case statement then the execution time was nine seconds. So like almost three times faster when they do not call this function. The function does not even do any select, you know, it's like ridiculous, right? Its function doesn't perform any select it performs data transformation, but it's still counted. So kind of our first advice was like do not call your function, do not categorize your things while you are selecting just do this, but then like, okay. So we actually want to have something like categories and functions because we use it not just in this query we use it in like 20 or 50 or 100 other queries. So how we can still do this and like how we can still make it reusable. Okay, here how you can make it reusable. So we can use dynamic function and look what we're doing here. So this dynamic function generates a part of SQL statement. It does not calculate anything. So this function again takes the age as a like a parameter because it will be some column in the table and it generates the part of SQL. So it returns a string. So remember how, okay. The board was doing a yellow cell body, okay. And the body and returns part of code, not the value. And what we're doing then? Anywhere when we need to use this age category that's what we're doing. So we are selecting passenger ID, then we concatenate. So here is the dynamic query. So we execute query. Passenger ID include this part with case dynamic category as age category and then we execute the statement. So we use function to generate a part of SQL. And this actually again, we faced it in like more serious environment where we were able to improve performance of statements like approximately four times, but that was like from an hour to 15 minutes. So it was actually a huge help. So that is like one of the example, okay. So again, that's like again, it is more pronounced when we have large datasets and we need to use almost the same thing. Again, as everything else, it's not the universal solution. And in some cases people still might want to opt doing something else, but it's one of the things I just wanted to illustrate how it works in pretty much like unexpected way. And there's still way more things to talk about functions and about user defined types and how to build that functions with like miracles, but maybe it will be an autotopic of other talk. And this concludes this presentation and thank you so much for your participation and any questions yet. Beautiful. Thanks so much, Hedy. Yes, we have two questions that have come in. The first is you mentioned that functions can't have explain. What about stored procedures? Can we see an execution plan? No, no, same story. Yeah, we can look that inside them. Easy. And the second question is a bit long. So if you want to pull up chat so that you can see it as well, I'll still read it out. Let me see, let me see. Related, oh gosh, okay. Related to explain. You mentioned that the only real way to deal with this is to explain the statement that is inside the function. But sometimes that's extremely difficult if the statement is dependent on the variables, values, results, from previous statements in the function. Yeah. Actual public support. Yes, yeah. Okay, no, there is again, there is no universal solution. So I can tell you some techniques which I use with functions. It's like debugging functions is not fun and that's why sometimes I'm saying, you know what, if it's not super critical, just do not use. But if you actually debug the function with Dynamics SQL, the results are beautiful, reliable, and it's really like, I think the only way to ensure robust performance of the application. But what are you? So first of all, like, you know, if you use a PitchetApp unit test, sorry, yeah, PitchetApp test, you can actually build. So yes, and sometimes you actually need to test just parts of the function. Again, I can talk a lot about this like testing technique. Yeah, most of the time you might need to put intermediate like printouts, like raise notice. And yeah, there is like no way unfortunately to compile. So you need to test separate parts. That's what I was doing. I was like passing, like pass parameter, captured to generated SQL. And then you need to take generated SQL and execute it. And then most of the time you are able to figure out what exactly is running slow, for example, what exactly does not slow. But you know what I might, okay, maybe we should have another presentation about how to test it. I never had it formalized. I mean, I have several like tricks how to do this, but yeah, it's like I don't have universal solution, yeah. Sorry. Wonderful. Okay, those seem to be our two questions. All right. So with that, Hedy, thank you as always. We always love having you on and at the conference in person and online for these webinars. And I want to thank all of our attendees for spending a little bit of their day with us. So with that, have a great rest of your afternoon and I hope to see you on future PostgreSQL conference webinars. Thank you.