Saturday, September 23, 2023

Analyze and Tune SQL Server Statistics

\Over the years, SQL Server Statistics have been discussed in countless blog posts, articles, and presentations, and I believe that they will remain a core topic for a while when speaking about performance. Why is that? Well, if we were to consider the equivalent of Maslow’s hierarchy of needs for database performances, statistics would be a foundational needs of a database system (namely the “physiological” needs) as they are one of the most fundamental pieces of information needed to build execution plans.

That being said, accurate statistics are not likely to help in all situations: you can think of poorly written queries, table-valued parameters (TVP), incorrect model assumptions in the cardinality estimator (CE), parameter sensitive plans (PSP) and so on. But I believe that it is worth it to check if they are “accurate enough” to avoid having a blind spot in this area.

In this article, we assume that the reader is familiar with the basics of SQL Server statistics, so we will focus on a question that seems simple at first glance: are statistics “accurate enough”? Should you need more information before diving in this article, feel free to have a look at these great articles: the basics and problems and solutions.

If you have already searched a bit on this topic, it is possible that the answer you have most often found to this question can be summarized by “it depends” combined with “test on your systems”. Not bad advice, but some people have pushed it a bit further, and one of the best approaches that I would recommend has been published by Kimberley L. Tripp. It put me on track a couple of months ago, and I have been lucky enough to be allowed to work on this topic as part of my current role as performance engineer.

We will start by trying to understand what “accurate enough” means, what are the main factors affecting statistics accuracy, then we will explain and demonstrate a process to analyze and try to tune statistics. In the last section we will see how you could give it a try, what to do when the sampling rate cannot be increased or when it will not increase statistics accuracy, and finally how modern versions of SQL Server may help to tackle this problem.

What is “accurate enough”?

KIS (keep it simple!) To keep it simple, we can consider that statistics are accurate enough when they are not the root cause of significant discrepancies between the estimates and the actuals leading to the creation (and possibly caching) of poorly performing execution plans, which may also consume much more resources than required. With “accurate enough” statistics, we increase our chances of building execution plans for realistic data volumes, so the plan trees chosen (e.g. sequence, operators) and the resources granted (e.g. memory) are more likely to deliver optimal performances.

Accuracy importance is relative

It is important to keep in mind that a workload that is likely to seek or scan very large ranges of data is typically less sensitive to inaccurate statistics: the larger the range, the more “smoothed out” the inaccuracies. On the other hand, a workload that is looking for discrete data points is more likely to be sensitive to inaccurate statistics. Also, as mentioned in the introduction, there are a number of situations where the accuracy of the statistics may not matter much:

  • when the query is so simple that it gets a trivial plan
  • when using a table-valued parameter (TVP) – which do not have statistics, at the very best we have a cardinality estimates (estimated number of rows)
  • when using local variables or the query hint OPTIMIZE_FOR_UNKNOWN
  • when incorrect model assumptions are used by the cardinality estimator (CE)

So yes, despite your efforts to build accurate statistics, you may end up with estimates based on the density vector or fixed guesses – and the outcome will not necessarily be bad! And when parameter sensitive plans (PSP) arise, you may get the perfect plan for the parameters used at compile time, but unfortunately these parameters may not be representative of most of the calls so the plan built for them may perform poorly for most of the calls.

Factors affecting statistics accuracy

In this section I will look at a few of the things that can affect how accurate statistics are for any given table.

Write activity

We can use the columns “last_updated” and “modification_counter” exposed by the Dynamic Management Function (DMF) sys.dm_db_statistics_properties to identify the date and time the statistics object was last updated and the total number of modifications for the leading statistics column (the column on which the histogram is built) since the last time statistics were updated. We can also rely on a mix of technical knowledge, business knowledge and experience to identify when an update of statistics could be worth it (e.g. after an ETL load, at the end of the year-end closing…), in addition to automatic and/or scheduled updates.

However, as tempting as it may appear, updating statistics often to keep them synchronized with the updates performed on the database should be considered with caution because:

  • it has a cost (CPU, IOs, tempdb)
  • it may degrade the statistics accuracy (e.g. when using the default sampling rate whereas the last update has been performed during an index rebuild – meaning with full scan)
  • it may invalidate cached execution plans, and it might be difficult to predict if the new plan will be as “good” as the previous one
  • by default, automatic updates of statistics are synchronous so the query optimizer will wait for the statistics to be (re)computed to (re)compile the execution plan, hence delaying query execution (can be asynchronous but to use with caution)
  • the default CE does a better job at estimating the number of rows beyond the histogram than the legacy CE, the legacy CE could leverage the trace flags 2389 / 2390 to help in some situations (and both CE could leverage 4139!)

So, having the automatic updates of statistics enabled plus a scheduled update process (e.g. weekly) is generally a good practice, but it is definitely not a “one-size-fits-all”. And finally, should you want to dive deeper into the impact of this factor which will not be covered in this article, you can have a look at this excellent article from Erin Stellato.

Sampling rate

From the DMF sys.dm_db_statistics_properties mentioned above, an important bit of information is exposed in the “rows_sampled” column. This is the total number of rows sampled for statistics calculations. By default, this number is primarily based on the table size, as explained in this MS blog archive SQL Server Statistics: Explained and demonstrated by Joe Sack in Auto-Update Stats Default Sampling Test. So, by default, the sampling rate (rows_sampled/rows*100) ranges from 100% (full scan) for tables smaller than 8MB to less than 1% for large tables.

The algorithm defining the number of rows to sample has been designed so on most of the systems StatMan (internal program in charge of computing statistics) should produce the best statistics possible with as few resources as possible (CPU, IOs, tempdb) and as fast as possible. As you can guess, it is a very challenging task but it does a great job as this works quite well for the vast majority of statistics.

Data pattern

Then, the sampling rate alone does not dictate the accuracy of the statistics. A low sampling rate may be perfectly acceptable for data that can have only few distinct values (e.g. status code, active status, etc) or data that are evenly distributed (e.g. a column with an identity property) but it is likely to generate inaccurate statistics for data that can have a lot of values (e.g. thousands of customers) or unevenly distributed data (e.g. if the sales activity has huge variations). The challenge of getting accurate estimates for specific data points could be summarized as below:

So, when focusing on specific data points, even if statistics are updated with full scan the estimates may be completely wrong. To be more specific, if there are no updates on the underlying data the details of the statistics (density vectors and histogram steps properties) will be perfectly accurate, but it may not be enough. As we are limited to 201 steps (200 for actual values, plus 1 for nulls) in the histograms, the dispersion around the mean (the “average_range_rows”) may be extremely important. Said differently, the actual number of rows matching a specific value in the range of such steps may vary widely.

Analyze and Tune Statistics

Now that we have introduced the core factors affecting statistics accuracy, we will see how we could automatically evaluate the accuracy of statistics (analysis phase) and if we can improve the accuracy of statistics – when it is worth it and possible – by using a custom sampling rate (tuning phase). A stored procedure named sp_AnalyzeAndTuneStatistics will implement the analysis and tuning logic. In this section, we will outline how it works, its input parameters and perform a demo.

Note that the algorithm will skip graph tables as there are some differences in the internals that make it beyond the scope of this article.

The code for this stored procedure can be downloaded in .zip format here from the Simple-Talk site.

How it works

Let’s start with the analysis phase. From a high-level point of view, this phase gets the statistics eligible for an analysis in the current database and loops on each of them to:

  • Update the statistic (using the default sampling rate)
  • Based on the representation in the histogram (found using DBCC SHOW_STATISTICS), analyze the base table to compute actual equal_rows, actual average_range_rows and coefficient of variation in the range (dispersion around average_range_rows)
  • Count the number of steps having a difference factor above the maximum difference factor passed as input parameter (by comparing estimated and actual equal_rows and average_range_rows) and the number of steps having a coefficient of variation above the maximum coefficient of variation passed as input parameter
  • Log the results

So, at the end of the analysis phase, we can review the logs to find out for each statistic analyzed how many steps are above the thresholds passed as input parameters. These steps will be qualified as “not compliant” (with the thresholds) from now on.

Then, when the mode “tuning” is enabled (see the statement block related to “IF @p_TryTuning=1” in the procedure), a nested loop is activated in the loop on each statistic, so after the initial analysis:

  • the statistic is updated with a larger sampling rate
  • a new analysis is executed
  • if the new number of steps not compliant drops quickly enough, the nested loop is repeated, otherwise the tuning phase stops

The goal is basically to identify the “low hanging fruit”, meaning the statistics which accuracy may experience an improvement with a limited increase of the sampling rate. But how do we assess if the number of steps not compliant drops quickly enough? We will use the concept of slope. The steeper the slope, the faster an improvement is expected, meaning the faster the number of steps not compliant have to drop after each increase of the sampling rate. The steepness of the slope will be set by the input parameter named “slope steepness”: the higher it is, the steeper the slope (and vice-versa).

So, the formulas to compute the maximum number of steps not compliant for a given sampling rate use a parameter (prefixed with “@p_”) and a couple of variables computed during the execution (prefixed with “@v_”):

  • @v_NbStepsExceedingDifferenceFactorInitial: the initial number of steps having a difference factor exceeding @p_MaxDifferenceFactor (see Input parameters below), using the default sampling rate
  • @v_NbStepsExceedingCoVInitial: the initial number of steps having a coefficient of variation exceeding @p_MaxCoV (see Input parameters below), using the default sampling rate
  • @p_SlopeSteepness: the slope steepness (see Input parameters below)
  • @v_SamplingRateAssessed: the sampling rate assessed
  • @v_DefaultSamplingRate: the default sampling rate

And are computed this way:

  • For the maximum number of steps not compliant in terms of difference factor
FLOOR ( @v_NbStepsExceedingDifferenceFactorInitial / 1 
+ ( @p_SlopeSteepness 
    * ( @v_SamplingRateAssessed - @v_DefaultSamplingRate ) 
    * ( @v_SamplingRateAssessed - @v_DefaultSamplingRate ) 
    / 10000 ) )
  • For the maximum number of steps not compliant in terms of coefficient of variation
FLOOR ( @v_NbStepsExceedingCoVInitial / 1 
+ ( @p_SlopeSteepness 
    * ( @v_SamplingRateAssessed - @v_DefaultSamplingRate ) 
    * ( @v_SamplingRateAssessed - @v_DefaultSamplingRate )  
    / 10000 ) )

Note: the significance used for the function FLOOR is 1

You can see below a representation of the maximum number of steps not compliant acceptable for @p_SlopeSteepness = 25 (default value), @v_DefaultSamplingRate = 10 and @v_NbStepsExceedingDifferenceFactorInitial = 50:

And the same representation for p_SlopeSteepness = 100:

As you can see, the steeper the slope, the faster the number of steps not compliant have to drop.

Input parameters

Let’s now have a look at the input parameters of the stored procedure so it can be clearer how the process works to gather the information on statistics.

@p_MaxDifferenceFactor

Maximum factor of difference accepted between estimates and actuals, to assess equal_rows and avg_range_rows accuracy in each step of the histograms. If the factor of difference of the step for equal_rows or avg_range_rows is above the value of this parameter, the step is considered “not accurate enough” (not compliant). It is computed by dividing the largest value (estimated or actual) by the smallest value (estimated or actual), to consider underestimations and overestimations similarly.

Example 1:

the estimated number of rows equal to the range high value (equal_rows) is 100

the actual number of rows equal to the range high value (equal_rows) is 1000

=> the factor of difference for equal_rows is 10

Example 2:

the estimated average number of rows per distinct value in the range (avg_range_rows) is 1000

the actual average number of rows per distinct value in the range (avg_range_rows) is 100

=> the factor of difference for avg_range_rows is 10

This parameter has a default value of 10. It could be considered large, but this is by design as we want to focus on significant discrepancies. Of course, it is not great to have an actual number of rows that is larger or smaller by a factor of 6 or 8 but is it much more likely to cause performance issues when it is larger or smaller by a factor above 10.

@p_MaxCoV

Maximum coefficient of variation accepted in the histogram ranges, to assess the dispersion around avg_range_rows. It is expressed as a percentage and has a default value of 1000. This default value could also be considered large but this is by design as we want to focus on significant discrepancies.

@p_SchemaName, @p_ObjectName, @p_StatName

If you want to filter by the schema, object, and/or statistic name, this parameters let you. By default the value is NULL which is not applying a filter.

@p_TryTuning

To activate the tuning phase where statistics are actually updated, By default this is set to 0 and will not make changes to statistics.

@p_SamplingRateIncrement

Used only when @p_TryTuning=1. Increment to use when attempting to tune the sampling rate, with a default value of 10.

@p_SlopeSteepness

Used only when @p_TryTuning=1. See above for a detailed definition. The steeper the slope, the faster the number of steps above @p_MaxDifferenceFactor and @p_MaxCoV must drop after each increase of the sampling rate. It has a default value of 25.

@p_PersistSamplingRate

Used only when @p_TryTuning=1. To set to 1 to persist the optimal sampling rate identified. It has a default value of 0.

Note: truncates will reset the sampling rate persisted and before SQL Server 2016 SP2 CU17, SQL Server 2017 CU26 or SQL Server 2019 CU10 the index rebuilds will also reset the sampling rate persisted.

@p_UseNoRecompute

Used only when @p_TryTuning=1. To set to 1 to mark the statistics with norecompute to disable automatic updates of the statistic. For instance, when you can demonstrate that scheduled updates of the statistic are enough to maintain query performance. It has a default value of 0.

@p_IndexStatsOnly

To set to 0 to include auto created and user created statistics. It has a default value of 1.

Warning: Generally not recommended, as the analysis may be very slow and consume much more resources (intense scan activity).

@p_ShowDetails

To set to 1 to display the details for each round of the assessment so the stored procedure returns some datasets that may come in handy to push the analysis further:

  • when statistics are retrieved, the list of statistics that will be analyzed
  • for each analysis round, the value of some variables (most of them are also logged) and the histogram with estimates, actuals, difference factors and coefficient of variation for each step
  • at the end, the logs with the details in “log_details”

It has a default value of 0.

Demo

In this section I will demonstrate how the statistic tuning works using some object that I will generate. As the data population method used is not fully deterministic, you may get slightly different results when executing this demo.

The code is in the aforementioned file here in two parts, with an intermediate step to create the stored procedure: demo files.

Environment

We will use a database hosted on a SQL Server 2022 Developer Edition instance, that contains a table named Sales.OrderHeader, to simulate multiple data patterns (see previous section) on the column SubmittedDate:

  • limited number of distinct values (scenario 1)
  • large number of distinct values with a relatively linear distribution (scenario 2)
  • low number of distinct values with an unevenly distribution (scenario 3)
  • large number of distinct values with an unevenly distribution (scenario 4)

In the first 3 scenarios, the table size will be relatively identical so the default sampling rate will be relatively stable for these data patterns. Then we will use a stored procedure named dbo.sp_AnalyzeAndTuneStatistics to analyze and, when necessary, try to tune the sampling rate to increase the statistics accuracy.

Quick word about indexing: the table Sales.OrderHeader is a clustered index (clustering key on an auto-incremented integer) and a nonclustered index will be created on the column SubmittedDate. So, it is the statistics created for this index that will be analyzed. It is important to note that the index does not affect the accuracy of the statistics, its primary purpose is to speed up the analysis.

The SQL scripts to create the stored procedure dbo.sp_AnalyzeAndTuneStatistics, populate the database and execute the demo are available on GitHub.

Scenario 1 – limited number of distinct values

In this scenario, there is a limited number of distinct values (184 distinct submitted dates – from July 2022 to December 2022) and the distribution of the data is relatively balanced (between 40’000 and 60’000 order headers per day):

EXEC sp_spaceused N'Sales.OrderHeader';

SELECT COUNT(DISTINCT SubmittedDate)
FROM     Sales.OrderHeader;

;WITH CTE_01 AS
(
        SELECT  SubmittedDate
                        ,COUNT(*) AS [Count]
        FROM            Sales.OrderHeader
        GROUP BY        SubmittedDate
)
SELECT MIN([Count]) AS MinCount
         ,MAX([Count]) AS MaxCount
FROM     CTE_01;
SELECT    so.[name] AS [table_name]
        ,st.[name] AS [stats_name]
        ,st.has_persisted_sample
        ,st.no_recompute
        ,stp.last_updated
        ,stp.modification_counter
        ,stp.persisted_sample_percent
        ,stp.[rows]
        ,stp.rows_sampled
        ,stp.steps
        ,COALESCE(ROUND((CAST(rows_sampled AS real)
                    /CAST([rows] AS real)*100),2),0) 
                                     AS last_sampling_rate
FROM    sys.objects so
INNER JOIN    sys.stats st
ON so.[object_id] = st.[object_id]
CROSS APPLY sys.dm_db_stats_properties
                         (st.[object_id],st.stats_id) stp
WHERE    OBJECT_SCHEMA_NAME(so.[object_id]) = N'Sales';

This will return something close to:

We can see that the statistics are up-to-date, and have been computed with a sampling rate of 2.11%. Then launch an analysis of the statistic:

EXEC [dbo].[sp_AnalyzeAndTuneStatistics]
      @p_SchemaName=N'Sales'
      ,@p_ObjectName=N'OrderHeader'
      ,@p_StatName=N'IX_Date'
      ,@p_ShowDetails=1

It completes after a couple of seconds, and we can use column “log_details” in the last dataset returned to get the logs:

{
        "schema_name": "Sales"
        ,"object_name": "OrderHeader"
        ,"stats_name": "IX_Date"
        ,"initial_steps":184
        ,"initial_sampling_rate":2.11
        ,"initial_sampling_rate_is_default":1
        ,"initial_steps_exceeding_difference_factor":0
        ,"initial_steps_exceeding_cov":0
        ,"details":
        [       
        {"round_id":1,"steps":184,"sampling_rate":2.11
            ,"is_default":1
            ,"steps_exceeding_difference_factor":0
            ,"steps_exceeding_cov":0
            ,"round_duration_ms":1790}
        ]
        ,"summary": "Analysis of current histogram completed"
        ,"assessment_duration_ms":1790
}

Based on this synthesis, we can see that all the steps are “compliant” with the thresholds used for

@p_MaxDifferenceFactor("initial_steps_exceeding_difference_factor":0) 
   and @p_MaxCov ("initial_steps_exceeding_cov":0).

And as we have passed the value 1 to the parameter @p_ShowDetails of the stored procedure, we can use the histogram dataset returned by the stored procedure to build a representation:

 

The X axis represents the histogram steps, the Y axis represents the value of the factor of difference and the value of the coefficient of variation. So, with the default value of 10 for @p_MaxDifferenceFactor and 1000 for @p_MaxCov, we can confirm that all the steps are “compliant” with the thresholds used:

  • the difference factor for equal_rows, obtained by considering the greatest value between [equal_rows estimated divided by equal_rows actual] and [equal_rows actual divided by equal_rows estimated] fluctuates between 1 and less than 4, which makes sense as the distribution of the data is quite linear
  • the difference factor for average_range_rows, obtained by considering the greatest value between [average_range_rows estimated divided by average_range_rows actual] and [average_range_rows actual divided by average_range_rows estimated] is always 1, which makes sense as the ranges are empty (value set to 1 by StatMan, not 0)
  • the coefficient of variation is always 0, because the ranges are empty

Knowing that we are in scenario 1, which is based on the easiest data pattern to deal with, it is not surprising to get these results with just the default sampling rate. The estimates based on this statistic will be extremely accurate, so we are good and we can move on to the next scenario.

Scenario 2

In this scenario, there is a limited number of distinct values (184 distinct submitted dates – from July 2022 to December 2022) and the distribution of the data is not balanced (between 1 and 100’000 order headers per day):

We can see that the statistics are up-to-date, and have been computed with a sampling rate of 2%. Then launch an analysis of the statistic and review the logs:

{
        "schema_name": "Sales"
        ,"object_name": "OrderHeader"
        ,"stats_name": "IX_Date"
        ,"initial_steps":174
        ,"initial_sampling_rate":2
        ,"initial_sampling_rate_is_default":1
        ,"initial_steps_exceeding_difference_factor":10
        ,"initial_steps_exceeding_cov":10
        ,"details":
        [
         {"round_id":1,"steps":174,"sampling_rate":2,
          "is_default":1,"steps_exceeding_difference_factor":10,
          "steps_exceeding_cov":10,"round_duration_ms":1983}
        ]
        ,"summary": "Analysis of current histogram completed"
        ,"assessment_duration_ms":1983
}

Based on this synthesis, we can see that this time some steps are not “compliant” with the thresholds used for

@p_MaxDifferenceFactor 
         ("initial_steps_exceeding_difference_factor":10) 
  and @p_MaxCov ("initial_steps_exceeding_cov":10)

Let’s use histogram dataset returned by the stored procedure to figure out which steps are not compliant (because of average_range_rows_difference_factor AND coefficient of variation) and correlate this with the logs:

As we can see, for these steps the average_range_rows estimated is 1, meaning on average 1 order header submitted per day on the days that are part of the range. But actually, there have been on average between 500 and 9611 order headers submitted per day on the days that are part of the range. In this scenario it looks like StatMan did not detect the order headers that have been submitted on the days that are part of these ranges.

And indeed, the probability to miss out dates where only a few thousands of order headers have been submitted is high as that there may be days with up to 100’000 order headers submitted and that the sampling is low, at 2%.

Let’s check for instance on the 6th of October 2022:

SELECT   TOP(8000)
         *
FROM     Sales.OrderHeader
WHERE    SubmittedDate = '20221006'
ORDER BY CustomerId;

The actual plan for this query is:

Looking at the execution plan, we can confirm the estimation was completely wrong, and we get a spill on the sort operator as not enough memory was requested / granted. Of course, the performance impact is limited on such simple queries, but as the complexity of the query grows (e.g. joins with multiple other large tables) it may become a serious problem. And if a plan based on such estimates is cached and reused by subsequent executions of the same code / stored procedure, it may impact multiple processes and users.

So, would a higher sampling rate help in this scenario? Let’s figure out by calling the stored procedure with the parameter @p_TryTuning set to 1:

EXEC [dbo].[sp_AnalyzeAndTuneStatistics]
        @p_SchemaName=N'Sales'
        ,@p_ObjectName=N'OrderHeader'
        ,@p_StatName=N'IX_Date'
        ,@p_TryTuning=1
        ,@p_ShowDetails=1;

And get the logs:

{
        "schema_name": "Sales"
        ,"object_name": "OrderHeader"
        ,"stats_name": "IX_Date"
        ,"initial_steps":174
        ,"initial_sampling_rate":2
        ,"initial_sampling_rate_is_default":1
        ,"initial_steps_exceeding_difference_factor":10
        ,"initial_steps_exceeding_cov":10
        ,"details":
        [
        {"round_id":1,"steps":174,"sampling_rate":2
        ,"is_default":1
        ,"steps_exceeding_difference_factor":10
        ,"steps_exceeding_cov":10,"round_duration_ms":2110}
        ,{"round_id":2,"steps":183,"sampling_rate":10
                   ,"is_default":0,
                   ,"steps_exceeding_difference_factor":1
        ,"steps_exceeding_cov":1,"round_duration_ms":2803}
        ,{"round_id":3,"steps":184,"sampling_rate":20
                   ,"is_default":0
                  ,"steps_exceeding_difference_factor":0
        ,"steps_exceeding_cov":0,"round_duration_ms":3970}
        ]
        ,"optimal_steps":184
        ,"optimal_sampling_rate":20
        ,"optimal_sampling_rate_is_default":0
        ,"optimal_steps_exceeding_difference_factor":0
        ,"optimal_steps_exceeding_cov":0
        ,"summary": "Optimal sampling rate identified 
                                     (NbRound(s):3)"
        ,"assessment_duration_ms":8883
}

We can notice that there have been 3 rounds to assess tuning opportunities:

  • the initial round, with the default sampling rate and the results we already know

{“round_id”:1,”steps”:174,”sampling_rate”:2,”is_default”:1,”steps_exceeding_difference_factor”:10,”steps_exceeding_cov”:10,”round_duration_ms”:2110}

  • a second round, with a sampling rate of 10%, led to a drop (from 10 to 1) of the number of steps not compliant with the thresholds
{"round_id":2,"steps":183,"sampling_rate":10,"is_default":0,
"steps_exceeding_difference_factor":1,
"steps_exceeding_cov":1,"round_duration_ms":2803}
  • a third round, with a sampling rate of 20%, led to the removal of all the steps not compliant
{"round_id":3,"steps":184,"sampling_rate":20,
"is_default":0,"steps_exceeding_difference_factor":0,
"steps_exceeding_cov":0,"round_duration_ms":3970}

As the results obtained with a sampling rate of 20% led to the removal of all the steps not compliant, the optimization process stopped and considered that the optimal sampling rate is 20%.

Scenario 3

In this scenario, there is a large number of distinct values (1826 distinct submitted dates – from January 2018 to December 2022) and the distribution of the data is relatively balanced (between 4’000 and 6’000 order headers per day):

We can see that the statistics are up-to-date, and have been computed with a sampling rate of 2.13%. Then launch an analysis of the statistic with tuning attempt and review the logs:

{
        "schema_name": "Sales"
        ,"object_name": "OrderHeader"
        ,"stats_name": "IX_Date"
        ,"initial_steps":200
        ,"initial_sampling_rate":2.13
        ,"initial_sampling_rate_is_default":1
        ,"initial_steps_exceeding_difference_factor":18
        ,"initial_steps_exceeding_cov":18
        ,"details":
        [
        {"round_id":1,"steps":200,"sampling_rate":2.13
                ,"is_default":1
                ,"steps_exceeding_difference_factor":18
                ,"steps_exceeding_cov":18
                ,"round_duration_ms":8423}
      ,{"round_id":2,"steps":189,"sampling_rate":10
                ,"is_default":0
                ,"steps_exceeding_difference_factor":5
                ,"steps_exceeding_cov":5,"round_duration_ms":8843}
      ,{"round_id":3,"steps":191,"sampling_rate":20
                ,"is_default":0
                ,"steps_exceeding_difference_factor":3
                ,"steps_exceeding_cov":3
                ,"round_duration_ms":10267}
      ,{"round_id":4,"steps":198,"sampling_rate":30
                ,"is_default":0
                ,"steps_exceeding_difference_factor":0
                ,"steps_exceeding_cov":0
                ,"round_duration_ms":10803}
        ]
        ,"optimal_steps":198
        ,"optimal_sampling_rate":30
        ,"optimal_sampling_rate_is_default":0
        ,"optimal_steps_exceeding_difference_factor":0
        ,"optimal_steps_exceeding_cov":0
        ,"summary": "Optimal sampling rate identified
                                         (NbRound(s):4)"
        ,"assessment_duration_ms":38336
}

We can notice that there have been 4 rounds to assess tuning opportunities:

  • the initial round, with the default sampling rate and 18 steps not compliant with the thresholds
{"round_id":1,"steps":200,"sampling_rate":2.13,"is_default":1,
"steps_exceeding_difference_factor":18,
"steps_exceeding_cov":18,"round_duration_ms":8423}

The explanation is similar to scenario 2 “…StatMan did not detect the order headers that have been submitted on the days that are part of these ranges.” but the root cause is slightly different: the probability to miss out dates is high because there are a lot of distinct dates and that the sampling is quite low, at 2.13%.

  • a second round, with a sampling rate of 10%, that led to a drop (from 18 to 5) of the number of steps not compliant with the thresholds

{"round_id":2,"steps":189,"sampling_rate":10,"is_default":0,
"steps_exceeding_difference_factor":5,"steps_exceeding_cov":5,
"round_duration_ms":8843}

  • a third round, with a sampling rate of 20%, led to another drop (from 5 to 3) of the number of steps not compliant with the thresholds

{"round_id":3,"steps":191,"sampling_rate":20,"is_default":0,
"steps_exceeding_difference_factor":3,"steps_exceeding_cov":3,
"round_duration_ms":10267}

  • a fourth round, with a sampling rate of 30, led to the removal of all the steps not compliant

{"round_id":4,"steps":198,"sampling_rate":30,"is_default":0,
"steps_exceeding_difference_factor":0,"steps_exceeding_cov":0,
"round_duration_ms":10803}

As the results obtained with a sampling rate of 30% led to the removal of all the steps not compliant, the optimization process stopped and considered that the optimal sampling rate is 30%.

Scenario 4

In this scenario, there is a large number of distinct values (1826 distinct submitted dates – from January 2018 to December 2022) and the distribution of the data is not balanced (between 1 and 100’000 order headers per day):

We can see that the statistics are up-to-date, and have been computed with a sampling rate of 0.5%, which is lower than in the previous scenarios as the table is larger (around 8 GBs). Then launch an analysis of the statistic with tuning attempt and review the logs:

{
        "schema_name": "Sales"
        ,"object_name": "OrderHeader"
        ,"stats_name": "IX_Date"
        ,"initial_steps":199
        ,"initial_sampling_rate":0.5
        ,"initial_sampling_rate_is_default":1
        ,"initial_steps_exceeding_difference_factor":9
        ,"initial_steps_exceeding_cov":8
        ,"details":
        [
        {"round_id":1,"steps":199,"sampling_rate":0.5
                ,"is_default":1
                ,"steps_exceeding_difference_factor":9
                ,"steps_exceeding_cov":8,"round_duration_ms":19776}
        ,{"round_id":2,"steps":196,"sampling_rate":10
                ,"is_default":0
                ,"steps_exceeding_difference_factor":23
                ,"steps_exceeding_cov":1
                ,"round_duration_ms":30074}
        ]
        ,"optimal_steps":196
        ,"optimal_sampling_rate":0.5
        ,"optimal_sampling_rate_is_default":1
        ,"optimal_steps_exceeding_difference_factor":9
        ,"optimal_steps_exceeding_cov":8
        ,"summary": "Default sampling rate is enough 
                                       (NbRound(s):2)"
        ,"assessment_duration_ms":49850
}

We can notice that there have been 2 rounds to assess tuning opportunities:

  • the initial round, with the default sampling rate and 9 steps not compliant with the thresholds

{"round_id":1,"steps":199,"sampling_rate":0.5,"is_default":1,
"steps_exceeding_difference_factor":9,"steps_exceeding_cov":8,
"round_duration_ms":19776}

Let’s use the histogram dataset returned by the stored procedure to investigate a bit more, by sorting the data by avg_range_rows_difference_factor (descending):

  • a second round, with a sampling rate of 10%, led to an increase (from 9 to 23) of the
  • |

{"round_id":1,"steps":199,"sampling_rate":0.5,"is_default":1,
"steps_exceeding_difference_factor":9,"steps_exceeding_cov":8,
"round_duration_ms":19776}

Let’s use the histogram dataset returned by the stored procedure to investigate a bit more, firstly by sorting the data by avg_range_rows_difference_factor (descending) to get the step exceeding the difference factor for this metric:

Secondly, we can sort the data by equal_rows_difference_factor (descending), to get the 22 steps exceeding the difference factor for this metric:

In this scenario, this metric should not be considered as an actual issue: as there may be up to 100’000 order headers per day, it is not that bad to build (and possibly cache) a plan for an estimated number of a couple of dozen of thousands of order headers.

Considering that there are 1826 distinct dates in the table, having this discrepancy on only 22 dates could safely be ignored. So, to conclude this scenario, the default sampling rate is likely to be enough, especially considering the size of the table (cost of maintaining the statistic).

What’s next?

Give it a try. Now that we have demonstrated a process to automate the analysis of statistics on synthetic data, you may want to evaluate the accuracy of statistics in your databases. Should you want to give it a try in your environment, please keep in mind that the methodology presented is usable at your own risks, without any guarantee.

With that disclaimer in mind, here is a skeleton of methodology you can follow:

  • execute the stored procedure on a copy of a production database (preferably without data updates during the analysis), with @p_TryTuning=1 (e.g. on a server where you restore production backups on a regular basis to make sure that you can actually restore your backups)
EXEC [dbo].[sp_AnalyzeAndTuneStatistics]
        ,@p_TryTuning=1;

Reminder: this stored procedure will update statistics

  • review the result of the assessment with this query
SELECT log_desc
      ,log_date
      ,JSON_VALUE(log_details,'$.schema_name')
      ,JSON_VALUE(log_details,'$.object_name')
      ,JSON_VALUE(log_details,'$.stats_name')
      ,JSON_VALUE(log_details,'$.initial_sampling_rate')
      ,JSON_VALUE(log_details,
                    '$.initial_sampling_rate_is_default')
     ,JSON_VALUE(log_details,
             '$.initial_steps_exceeding_difference_factor')
     ,JSON_VALUE(log_details,'$.initial_steps_exceeding_cov')
      ,JSON_VALUE(log_details,'$.optimal_sampling_rate')
      ,JSON_VALUE(log_details,
                    '$.optimal_sampling_rate_is_default')
         ,JSON_VALUE(log_details,
               '$.optimal_steps_exceeding_difference_factor')
      ,JSON_VALUE(log_details,'$.optimal_steps_exceeding_cov')
      ,JSON_VALUE(log_details,'$.summary') AS [summary]
      ,log_details
FROM   dbo.[Log]
WHERE  log_desc LIKE N'dbo.sp_AnalyzeAndTuneStatistics %'
AND      ISJSON(log_details)=1;

Note: you can filter on “JSON_VALUE(log_details,'$.optimal_sampling_rate_is_default')<>'1‘” to list only the statistics for which a custom sampling rate has been identified

  • still on your copy of the production database, activate the query store, execute a simulation of the production workload, deploy the custom sampling rates with the option PERSIST_SAMPLE_PERCENT=ON, re-execute the workload and compare the results (e.g. query performance, resource usage)

WARNING: updating statistics does not guarantee that the execution plan(s) relying on these tables / statistics will be recompiled. Typically, if the modification counter is 0 when the statistics are updated, it won’t cause a recompile. It is however possible to force a recompilation of the plans relying on a table by calling sp_recompile with the table name as parameter.

  • to evaluate the gains with the custom sampling rate, the most reliable technique is to use the query store and limit the scope of the analysis to the query having execution plans relying on the statistics that have been modified (available in the execution plans since SQL 2017 and SQL 2016 SP2 with the new cardinality estimator – see SQL Server 2017 Showplan enhancements)
  • to evaluate the overhead of the custom sampling rate you can:
    • (on production) monitor the automatic updates on these statistics over a period of time using an Extended Event (XE) session to capture the events “sqlserver.auto_stats” (filter by table and statistic if needed)
    • on your copy of the production database, start an Extended Event (XE) session to capture the events “sqlserver.sql_statement_completed” reporting the IOs/CPU consumed with the default sampling rate and with the custom sampling rate (using UPDATE STATISTICS command)
    • compute the overhead of the custom sampling rate ([cost with custom sampling rate – cost with default sampling rate])
    • multiply the overhead of the custom sampling rate by the number of automatic updates that have been identified by the XE session, plus the number of updates that are triggered explicitly by the application and IT maintenance scripts
    • (on production), compare the overhead obtained with the cost of all the queries (use sys.query_store_runtime_stats, filtered on the period of time where you have monitored the automatic updates of statistics)

Note: this technique will only give you an indication, not a guarantee (e.g. if the statistics updates occur on a timeframe where you server capacity is almost saturated, updating with a higher sampling rate during this timeframe could create more problems than it may solve)

  • (on production) deploy the custom sampling rates using the UPDATE STATISTICS command with the option PERSIST_SAMPLE_PERCENT=ON and after a period of time compare the results (see above)

Lastly, whatever the methodology that you choose, make sure to document your tests and the changes implemented. It will make your life easier to explain it / do a handover and you may want to re-assess these changes after a period of time (e.g. as the database grows or shrinks over time). And of course, keep me posted when you find bugs or improvements 🙂

Other options

Sometimes, you may find out that statistics are the root cause of significant discrepancies between the estimates and the actuals leading to the creation (and possibly caching) of poorly performing execution plans, but unfortunately increasing the sampling rate is not possible (e.g. too costly) or it does not help much (depending on the data pattern). In such situations, there are a couple of options that you may consider:

  • filtered statistics, filtered indexes and partitioned views (each underlying table has its own statistics) may come in handy to “zoom” on some ranges of data (spoiler alert: they come with some pitfalls, see the presentation by Kimberley L. Tripp)
  • archiving data in another table and purging data are also valid options that could be helpful for maintenance and statistics accuracy; restricting the number of rows “online” is likely to lead to a higher (and more stable) sampling rate and the number of distinct values to model in the histogram steps may be reduced as well
  • partitioning will primarily help for maintenance if you use incremental statistics (not enabled by default), but the estimates will still be derived from a merged histogram that covers the whole table (see this excellent article from Erin Stellato); this is one of the reasons why you may prefer partitioned views
  • the sampling rate is primarily based on the table size, so if enabling compression (row or page) reduces the number of pages, then the default sampling rate may increase; similarly, the fill factor also impacts the number of pages (the lower it is, the higher the sampling rate may be)

Intelligent Query Processing

SQL Server Intelligent Query Processing (IQP) is a set of features that have been introduced progressively, and some of them may help to mitigate incorrect estimations (and not only the ones extrapolated from the statistics). We will not detail them (Erik Darling and Brent Ozar have already explained a lot of details – and pitfalls!) but it is important to mention them and keep them in mind when evaluating the update of your SQL services. Starting with SQL 2017:

Then SQL 2019 added another set of features:

And more recently with SQL 2022:

These features are great improvements that contribute to the popularity of SQL Server in the relational database engines market, and they will most likely be improved and enriched over time. However, I believe that we should not rely solely on these kinds of improvements (or trace flags, hints, plan guides, query store plan forcing and so on) to fix estimation issues.

Conclusion

In this article, we have discussed the concept of “statistics accuracy”, which is relative and depends on multiple factors, then the main factors that affect statistics accuracy and we have demonstrated a process to automatically evaluate and, when possible, tune statistics.

The key takeaway is that the data pattern has a major impact on the accuracy of the statistics, and you can relatively easily modify the sampling rate, the frequency of the statistics updates, use hints and trace flags, but it is much more challenging to act upon the data and its modelisation (e.g. with partitioned views, archiving, purging) to increase statistics accuracy. It is also important to keep in mind that some changes may require modifications of the workload / applications to be leveraged (e.g. filtered statistics, filtered indexes or partitioned views).

The good news is, if you are actually facing issues related to estimations, that there are solutions, and some of them are delivered out of the box with modern versions of SQL Server. Additionally, you can investigate on your own systems with the methodology suggested, so you may gain some useful insights about your statistics. Finally, I hope that you found this article interesting, and that you have learned something!

 

The post Analyze and Tune SQL Server Statistics appeared first on Simple Talk.



from Simple Talk https://ift.tt/XHVlxyp
via

Wednesday, September 20, 2023

Microsoft Fabric: Checking and Fixing Tables V-Order Optimization

Download the notebook used on this blog

I explained in a previous article how the Tables in a lakehouse are V-Order optimized. We noticed this configuration depends on our settings, which can be enabled or not.

One question remains: How could we check if the tables are V-Order optimized or not?

The tables we will use in this example are the same provided by Microsoft as a sample and we described in details how to load them in our article about lakehouse.

You can check more details about V-ORDER Optimization on this link

Checking tables configuration

We can use the following PySpark script to check the configuration of the tables in our lakehouse:

import pyarrow.dataset as pq
import os
def show_metadata(delta_file_path,tablename):
 

 # Print schema metadata

 print(f"\nSchema Properties for : {tablename}")

 print("--------------------")

 schema_properties = pq.dataset(delta_file_path).schema.metadata

 if schema_properties:

     for key, value in schema_properties.items():
         print(f"{key.decode('utf-8')}: {value.decode('utf-8')}")

 else:

    print("No schema properties found.")


 # Test the function with a path to your delta file#

full_tables = os.listdir('/lakehouse/default/Tables')

for table in full_tables:
  
  show_metadata('//lakehouse/default/Tables/' + table,table)

A screenshot of a computer Description automatically generated

This script is inspired on a script provided by a user in the Fabric Community forums.

What we should notice about how this code works:

  • We have a procedure defined, called show_metadata.
  • The procedure receives an array of table names as a parameter.
  • In order to generate the array dynamically, we use os.listdir. In this way, the script works for all tables in the lakehouse.
  • The show_metadata procedure is called in a loop for each one of the tables. Mind the standard path for the tables in the lakehouse.
  • Inside the procedure, we generate a dataset and use the properties .schema.metadata to read the properties of the table.
  • We make a loop on the table properties to show each one.
  • The table properties need to be decoded from UTF-8

The image below shows a piece of the result after executing this script, highlighting the V-ORDER property on the tables:

A close-up of text Description automatically generated

Using the script to test the configurations

Using this script, we can test the default configuration in different situation, and we will discover the following:

  • The default configuration for spark sessions is to use V-ORDER when writing tables unless we explicitly disable it.
  • This makes the V-ORDER also default when using the Convert-To-Table UI, unless we disable it on workspace levell (Why would we do such thing?)

Fixing a table without V-ORDER

Considering V-ORDER is default on spark sessions in Fabric, it will not be common to have tables not using V-ORDER.

But let’s create one in this situation and discover how to fix it when needed.

If the table dimension_stock_item already exists in your demo environment, you will need to drop it before running this script.

A simple spark SQL statement solves it:

%%sql
drop table dimension_stock_item

We can use the following pyspark code to create a table without the v-order optimization:

from pyspark.sql.types import *

spark.conf.set("spark.sql.parquet.vorder.enabled", "false")

def loadFullDataFromSource(table_name):

    df = spark.read.format("parquet").load('Files/' + table_name)

    df.write.mode("overwrite").format("delta").save("Tables/" + table_name)

full_tables = [

   'dimension_stock_item'

]

for table in full_tables:

    loadFullDataFromSource(table)

A screenshot of a computer program Description automatically generated

We should mind the following about this pyspark script:

  • We have a loadFullDataFromSource procedure to make the load in a default way. It works unless the table has some specific partitioning.
  • We disable the v-order configuration for the session on the start of the script.
  • We create an array of the tables, but it has only one table. It’s only a standard way to build it.
  • We make a for loop on the array, calling the loadFullDataFromSource procedure. Once again, it’s only a standard way to build it, since in our example it’s only for one table.

Once the import is executed with the V-ORDER disabled, we can execute again the script to check the Tables metadata and we may notice the lack of V-Order property on this table. The image below illustrates this:

Fixing the lack of V-ORDER

We need to fix this problem without loading the entire table again. In order to do so, we can use the OPTMIZE statement. This statement can reorganize the PARQUET files to avoid the small file size problem and apply the V-ORDER when this one is missing.

It’s very simple:

%%sql
OPTIMIZE dimension_stock_item VORDER;

A white rectangular object with black text Description automatically generated

We can execute the script to check the table’s metadata again and there it is: The table will have the V-ORDER enabled.

A computer screen shot of text Description automatically generated

Disclaimer

On the documentation, the OPTIMIZE is used to reorganize the files and apply the V-ORDER. The ALTER TABLE, also present on the documentation, is used to set a TBLPROPERTY which should ensure future writes in the table use V-ORDER.

TBLPROPERTY seems to have no relation with the metadata extracted by the script illustrated here. The TBLPROPERTY could be set and the metadata not, and the opposite.

Microsoft Fabric is still in preview, many of these details are still going to change and the documentation may not be precise.

Conclusion

All the defaults lead to the V-ORDER always being enabled in lakehouse (data warehouses are a completely different story). But it’s important to be able to check it every time a different scenario appears.

 

The post Microsoft Fabric: Checking and Fixing Tables V-Order Optimization appeared first on Simple Talk.



from Simple Talk https://ift.tt/cO0y2a3
via

Tuesday, September 19, 2023

Alternatives To SQL-2022 Built-in function GREATEST

If you haven’t already heard, SQL 2022 has introduced a new built-in system function called GREATEST. Simply put, it is to a set of columns, variables, expressions etc. what the MAX function is to a set of values (i.e., rows) of a single column or expression. The opposite of GREATEST function is LEAST function which returns the smallest value from the supplied list of columns, variables, constants etc. GREATEST & LEAST can be considered a pair, just as MIN/MAX functions are.

So, on a SQL Server 2022 instance, you can simply call GREATEST function with list of columns, variables, or constant values to get the highest value out of all of them. Here is an example with list of constants:

SELECT GREATEST(500, 10, 3, 50, 200, 8) [Greatest],
       GREATEST(10, 500, 3, 50, 200, 8) [Greatest2],
       GREATEST(200, 10, 3, 50, 500, 8) [Greatest3]

This will return:

A screenshot of a computer Description automatically generated

In the same manner,

I can also pass a list of columns to it:

with cte as
(
select 
      DB_NAME(database_id) db_name,
      max(last_user_seek) last_user_seek,
      max(last_user_scan) last_user_scan,
      max(last_user_lookup) last_user_lookup,
      max(last_user_update) last_user_update
from sys.dm_db_index_usage_stats
group by DB_NAME(database_id)
)
select 
      *,
      GREATEST(last_user_seek, last_user_scan, 
      last_user_lookup,last_user_update) last_access_time_cte
from cte;

This returns the following:

And while it is not as easy to read, note that you can pass in expressions as well, as you can get the same result by pushing the GREATEST function up into the primary query too:

select DB_NAME(database_id) db_name, 
       max(last_user_seek) last_user_seek, 
       max(last_user_scan) last_user_scan, 
       max(last_user_lookup) last_user_lookup, 
       max(last_user_update) last_user_update, 
       GREATEST(max(last_user_seek), 
                max(last_user_scan), 
                max(last_user_lookup), 
                max(last_user_update)) AS last_access_time_cte 
from sys.dm_db_index_usage_stats 
group by DB_NAME(database_id);

Isn’t that a nifty feature?

This rest of this article, focuses on how to achieve the same functionality in pre-SQL Server 2022 versions of SQL Server. Same approaches can be used to mimic the functionality of LEAST function as well.

I recently had to remind a client of mine that we are past the halfway mark of year 2023 that they still didn’t have a single SQL Server 2022 server, even though their SQL Server licenses covered it. I would not be surprised if there are many organizations that may still have a sizable number of SQL Servers pre-SQL 2022.

I personally though, am looking forward to when the next release of SQL Server will come out, whether it would be in 2024, 2025, or 2026? Hopefully Microsoft doesn’t make us wait beyond the year 2026.

In this article I am going to demonstrate 2 viable alternative solutions to find the largest value of a group of values in your queries (not including writing a very complicated CASE expression!)

Table Value Constructor

To start with, I will discuss using a Table Value Constructor (TVC) as I think it is easier to write and understand. TVC is very similar to the VALUES clause that you have used with the INSERT INTO statements to add new data into a table. You are maybe already aware that you can insert multiple rows in a single INSERT INTO statement:

declare @customer table  (id int identity, name varchar(100));
insert into @customer(name)
values
       ('tom'),
       ('jerry'),
       ('brian');
 select * from @customer;

This returns:

id          name
----------- ----------

1           tom
2           jerry

3           brian

Similarly, you can use a VALUES clause to construct a derived table. Here is an example of using it with constants:

SELECT [Greatest] = 
(
    SELECT MAX([Greatest])  FROM
          (VALUES (500),(1000),(3),
                   (50), (200),(8))
             AS derived_table([Greatest])
);

This will return the greatest of values in that derived table:

A screenshot of a computer Description automatically generated

Similarly, you can use list of columns, variables, expressions etc. 

So, using the VALUES construct here, I will construct a derived table to convert values from multiple columns into values/rows for a single derived column, last_access_time, sort its results in descending order (from highest to lowest) then select the first row’s value to get the highest (i.e. GREATEST) value.

with cte as
(
select
       DB_NAME(database_id) db_name,
       max(last_user_seek) last_user_seek,
       max(last_user_scan) last_user_scan,
       max(last_user_lookup) last_user_lookup,
       max(last_user_update) last_user_update
from sys.dm_db_index_usage_stats
group by DB_NAME(database_id)
)
select
             *,
             last_access_time_derived = 
                    (select top 1 last_access_time from
                    (values
                           (last_user_seek),
                           (last_user_scan),
                           (last_user_lookup),
                           (last_user_update))
                           derived_table(last_access_time) 
                     order by last_access_time desc
                    )
from cte;

This will return something like the following (depending on your actual server’s utilization):

Note: depending on the kind of server you are using this on, it may have NULL values for some of the values. For example, on the editor’s computer, it returned:

So, just like MAX, it does not return NULL unless all scalar expressions are NULL. This method is by no means as easy to work with as the GREATEST function, but it is generally straightforward to implement if needed.

It looks complicated without the built-in GREATEST function, doesn’t it? Well, I think the next method, UNPIVOT, I will show you is even more complicated. That’s just my opinion, the opinions of others in general may vary.

Unpivot

The idea with using an unpivot is to take values from multiple columns and turns them into rows for a single column. In other words, it can be used to convert a set of columns into a row. If you are familiar with the TRANSPOSE function in excel, unpivot is very similar. It’s the opposite of the PIVOT statement, which takes values for a single column from multiple rows and turns each value into multiple columns.

Here is an example of how you can use UNPIVOT to convert columns into rows (note that if you have not executed any queries in the other databases since a reboot, no data may be returned by this query):

/*
The DMV sys.dm_db_index_usage_stats returns the operational stats on 
each index in every database that have had any read and/or write 
activity since the last restart of the sql instance. It returns 
one row for each index. However, by use of the UNPIVOT clause, I
am converting it's four columns with "last_*" names, into rows so
instead of a single row for each index, it returns multiple rows, 
one row for each column that has a not-null value. 
The where clause filters out:
1. System databases i.e. where database_id > 4
2. Non-clustered indexes i.e where index_id <= 1
   Essentially, when index_id is 0 the table is a heap i.e. 
                                   it has no clustered index
                when index_id is 1 the table has a clustered 
                                       index and is not a heap
*/
SELECT TOP 10
        DB_NAME(database_id) [db_name],
        OBJECT_NAME(object_id, database_id) [object_name],
        [column_name],
        [column_value]
FROM sys.dm_db_index_usage_stats
        UNPIVOT ([column_value]
                     FOR [column_name] IN 
                                (
                                   last_user_seek, 
                                   last_user_scan, 
                                   last_user_lookup, 
                                   last_user_update
                                                )
                ) unpv
WHERE index_id <= 1
AND database_id > 4
ORDER BY [db_name], [object_name], [column_name];

The values for the four columns last_user_lookup, last_user_scan, last_user_seek and last_user_update in the sys.dm_db_index_usage_stats are now showing as rows.

And I just want to know the highest value from the 4 columns, I can just use the MAX function and slightly modify the query to add the GROUP BY clause.

SELECT TOP 10
        DB_NAME(database_id) [db_name],
        OBJECT_NAME(object_id, database_id) [object_name],
        [last_accessed] = MAX([column_value])
FROM sys.dm_db_index_usage_stats
        UNPIVOT ([column_value]
                     FOR [column_name] IN 
                                                (
                                                        last_user_seek, 
                                                        last_user_scan, 
                                                        last_user_lookup, 
                                                        last_user_update
                                                )
                                        ) unpv
WHERE database_id > 4
GROUP BY database_id, object_id
ORDER BY [db_name], [object_name];

This returns the following on my server.

To extend the example further, I want to know if/when the last time a table in the the database was accessed. This query is longer than it may need to be because I also want to display the individual values for the 4 columns as well as the MAX/highest value among them.

USE  MSDB;

with cte as
(
SELECT
        [last_user_seek]   = MAX(last_user_seek),
        [last_user_scan]   = MAX(last_user_scan),
        [last_user_lookup] = MAX(last_user_lookup),
        [last_user_update] = MAX(last_user_update)
FROM sys.dm_db_index_usage_stats AS i
WHERE i.database_id = DB_ID()
)
 SELECT
             [Database] = DB_NAME(),  
             *
FROM cte,
(
       SELECT MAX([last_access_time]) [last_access_time]
       FROM
             (
                 SELECT
                     [last_user_seek]   = MAX(last_user_seek),
                     [last_user_scan]   = MAX(last_user_scan),
                     [last_user_lookup] = MAX(last_user_lookup),
                     [last_user_update] = MAX(last_user_update)
                 FROM cte
             ) p
       UNPIVOT 
             ([last_access_time] FOR [Column] IN
                    (
                      last_user_seek,
                      last_user_scan,
                      last_user_lookup,
                      last_user_update)
                    )  AS unpvt
) unpvt;

This returns:

As you can see, the using UNPIVOT can take more lines of code and can be quite a bit complicated. As you will see later in this article, per the performance test I performed, it is also slower than the Table Value Constructor method.

I have tested these syntaxes in SQL Server versions going back to 2012. PIVOT and UNPIVOT were first introduced in SQL 2005 so the syntax’s I presented in this article should still work there.

Performance Test

How is the query performance between the TVF and Unpivot methods, or with the new greatest function?

To find that out, I am going to generate some random data. The following SQL script creates a table with name random_data_for_testing and inserts 10 million rows into it.

SET NOCOUNT ON;
if object_id('random_data_for_testing', 'U') is not null 
        drop table random_data_for_testing;
GO
create table random_data_for_testing
(
        id int identity primary key,
        value_01 float default substring(cast(rand() 
                              as varchar(20)), 3, 15),
        value_02 float default substring(cast(rand() 
                              as varchar(20)), 3, 15),
        value_03 float default substring(cast(rand() 
                              as varchar(20)), 3, 15),
        value_04 float default substring(cast(rand() 
                              as varchar(20)), 3, 15),
        value_05 float default substring(cast(rand() 
                              as varchar(20)), 3, 15),
        value_06 float default substring(cast(rand() 
                              as varchar(20)), 3, 15),
        value_07 float default substring(cast(rand() 
                              as varchar(20)), 3, 15),
        value_08 float default substring(cast(rand() 
                              as varchar(20)), 3, 15),
        value_09 float default substring(cast(rand() 
                              as varchar(20)), 3, 15),
        value_10 float default substring(cast(rand() 
                              as varchar(20)), 3, 15)
);
GO
declare @commit_size int = 100000; 
declare @max_rows int = 10000000; 
declare @counter int = 1;
declare @commit_counter int = 0;
declare @max_commit_count int = @max_rows / @commit_size; 
while @counter <= @max_rows
begin
        if @@TRANCOUNT = 0 BEGIN TRAN
        
        insert into random_data_for_testing default values
        set @counter = @counter + 1
        IF @@TRANCOUNT > 0 and @counter % @commit_size = 0
        BEGIN
                set @commit_counter = @commit_counter + 1
                raiserror('Committing %i of %i transactions....', 10, 
             1, @commit_counter, @max_commit_count) with nowait
                commit;
        END
end
if @@TRANCOUNT > 0 COMMIT;

My first test query is to do a test to get the highest value among the 10 columns (named value_01 to value_10 in the test table), using all 3 methods. I ran the script multiple times to make sure there are no physical reads or read-ahead reads with any of the queries so we can compare just the duration, logical IO and CPU as accurately as possible.

SET NOCOUNT ON;
SET ANSI_WARNINGS OFF;
GO
PRINT '---------------------------------------------'
PRINT '********* Test using the function Greatest...'
PRINT '---------------------------------------------'
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
GO
SELECT MAX(GREATEST((value_01),(value_02),(value_03),
                     (value_04),(value_05),(value_06), 
                     (value_07),(value_09),(value_10))) 
                                AS using_greatest_function
FROM random_data_for_testing;

SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;
GO
PRINT '---------------------------------------------'
PRINT '********* Test using the TVC method..........'
PRINT '---------------------------------------------'
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
GO
SELECT MAX(greatest_value) using_tvf FROM
(
        SELECT greatest_value = 
                (SELECT MAX(greatest_value) 
                 FROM (VALUES(value_01),(value_02),(value_03),
                             (value_04),(value_05),(value_06),
                             (value_07),(value_09),(value_10)
                                        ) derived_table(greatest_value)
                )
                        FROM random_data_for_testing
) a;

SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;
GO
PRINT '---------------------------------------------'
PRINT '********* Test using the Unpivot...'
PRINT '---------------------------------------------'
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
GO
SELECT MAX(greatest_value) using_unpivot FROM random_data_for_testing
UNPIVOT  ([greatest_value] FOR [Column] IN (value_01, value_02, 
                                            value_03 , value_04, 
                                            value_05,value_06, 
                                            value_07,value_09,
                                                 value_10)) AS unpvt

SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;
GO

Results:

Here are the key stats from the STATISTICS IO and STATISTICS TIME:

********* Test using the function Greatest...
Table 'random_data_for_testing'. Scan count 5, logical reads 118295
SQL Server Execution Times:
CPU time = 4405 ms, elapsed time = 1381 ms.

********* Test using the TVC method..........
Table 'random_data_for_testing'. Scan count 5, logical reads 117343
SQL Server Execution Times:
CPU time = 12000 ms, elapsed time = 3332 ms.

********* Test using the Unpivot...
Table 'random_data_for_testing'Scan count 5, logical reads 117179
SQL Server Execution Times:
CPU time = 17219 ms, elapsed time = 4813 ms.

The amount of IO (logical reads) is almost identical among the three methods. But notice the difference in the CPU time and Elapsed time values for each. The elapsed time for the function GREATEST is 1381 ms, for the TVC it is 3332 ms and for UNPIVOT it is 4813 ms.

Below is the screenshot of the actual execution plan and as you can see the query cost as relative to the batch, cost of function GREATEST is 12%, TVC is 34% and the UNPIVOT is 54%.

As you can see from the thickness of the pipe going from right to left, when a query needs to process a relatively great deal of rows, using the build in function gives the best performance. That shouldn’t be a surprise. If that wasn’t the case, somebody at Microsoft made a big booboo! However, between the TVF and Unpivot, TVF is faster.

Note: This by no means a scientific test. Generally, though I have seen TVF to be faster than Unpivot.

What if the query is using a very selective filter condition on a clustered primary key on a single, integer column? This kind of queries are usually ideal queries in “almost” any conditions, even if it includes columns with LOB data types like XML, VARBINARY(MAX) etc.
Just to be sure, let’s look at the following example:

SET NOCOUNT ON; 
SET ANSI_WARNINGS OFF; 
GO 

declare @id int = 100100; 
PRINT '---------------------------------------------'; 
PRINT '********* Test using the function Greatest...'; 
PRINT '---------------------------------------------';
SET STATISTICS IO ON; 
SET STATISTICS TIME ON;

SELECT *,
        using_greatest_function = 
                          GREATEST(value_01,value_02,value_03,value_04, 
                                   value_05,value_06,value_07,value_08,
                                   value_09, value_10                                                   ) 
FROM random_data_for_testing 
WHERE id = @id;

SET STATISTICS IO OFF; 
SET STATISTICS TIME OFF; 
PRINT '---------------------------------------------' 
PRINT '********* Test using the TVC method..........' 
PRINT '---------------------------------------------'

SET STATISTICS IO ON; 
SET STATISTICS TIME ON;  

SELECT  *, using_tvf = ( 
                SELECT MAX(greatest_value) 
                FROM  (VALUES (value_01),(value_02),(value_03), 
                              (value_04),(value_05),(value_06),  
                              (value_07),(value_08),(value_09),                           
                              (value_10)) AS derived_table(greatest_value)                       ) 
FROM random_data_for_testing 
WHERE id = @id;

SET STATISTICS IO OFF; 
SET STATISTICS TIME OFF; 

PRINT '---------------------------------------------'; 
PRINT '********* Test using the Unpivot...'; 
PRINT '---------------------------------------------';
SET STATISTICS IO ON; 
SET STATISTICS TIME ON; 

SELECT t.*, p.using_unpivot 
FROM random_data_for_testing t
        INNER JOIN  ( SELECT id, MAX(greatest_value) using_unpivot 
                      FROM random_data_for_testing 
                      UNPIVOT  ([greatest_value] FOR [Column] IN  
                                  (value_01,value_02,value_03, value_04, 
                                   value_05, value_06, value_07, value_08,
                                   value_09, value_10)) AS unpvt 
                       WHERE id = @id group by id ) p 
               on p.id = t.id;

SET STATISTICS IO OFF; 
SET STATISTICS TIME OFF;

This returns three identical result sets:

And quite similar execution times as well:

********* Test using the function Greatest...
Table 'random_data_for_testing'. Scan count 0, logical reads 3
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 0 ms.

********* Test using the TVC method..........
Table 'random_data_for_testing'. Scan count 0, logical reads 3
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 0 ms.

********* Test using the Unpivot...
Table 'random_data_for_testing'. Scan count 0, logical reads 6
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 5 ms.

With the following actual execution plans:

The entire script finishes in sub-sub-second. Still, at a microsecond (a millionth of a second) level, you can see that the Unpivot performs the worst.

Summary

In this article, I have demonstrated the new GREATEST and LEAST value functions in SQL Server 2022 and have show that in general testing, these functions are very fast. However, as not everyone is using SQL Server 2022, I also demonstrated a method using Table Value Constructors that give you similar functionality (with much less straightforward code), as well as a method using the often disregarded UNPIVOT function.

 

The post Alternatives To SQL-2022 Built-in function GREATEST appeared first on Simple Talk.



from Simple Talk https://ift.tt/MeO7CUY
via

Monday, September 18, 2023

SQL Server Row Level Security Deep Dive. Part 4 – Integration, Anti-patterns, and Alternatives

One of the primary reasons to implement RLS is to facilitate reporting and ease the administrative burden. This section covers some considerations for using RLS with the primary Microsoft reporting engines and gives you an idea of things to look for in your reporting engine. Some anti patterns and alternatives to RLS are also examined.

Power BI

Power BI integrates directly with RLS when using direct query access. Most department level implementations of Power BI will likely use the built in caching, which makes performance tuning much easier. In larger enterprise level implementations of Power BI, RLS may fit the requirements. Nothing special is needed to integrate it with Power BI, other than direct queries.

RLS is also available as a separate mechanism in Power BI. This allows data cached by Power BI to also be controlled by RLS. If RLS is used in the database tier, it must also be implemented in the Power BI tier when using caching. This results in a redundant implementation, but data is protected at all levels. It also allows the caching benefits of Power BI to be used while still using RLS.

It is recommended to use the same RLS rules in both the database and Power BI. This will reduce the amount of troubleshooting and ensures the business requirements are met. This is a good thing to keep in mind when designing your RLS implementation. Business users won’t understand the architectural implications of a design, so it is your job to help this as much as possible. Even less technical users on the team or those not as familiar with SQL and RLS will need guidance. A simple design that meets the requirements is usually the best design choice.

SQL Server Reporting Services (SSRS)

SSRS connections using pass through authentication to SQL Server use RLS seamlessly. SSRS has internal caching mechanisms in the SSRS temp database, but it doesn’t impact the data returned to users, unlike the Power BI caching mechanism. As with all uses of RLS, any type of access to the data is protected when hitting it via SSRS. If the connection string for a report uses a service account, it will not be protected by RLS. To be more precise, it will be protected by RLS, but all users of the report will have the same access and that access will correspond to the authorization of the service account.

Application Integration

It is generally a bad idea to have users directly access transactional systems, but applications are able to use RLS via a few different methods. The SESSION_CONTEXT can be set or a specific user can be designated using EXECUTE AS USER syntax. This is one of the methods used in the WideWorldImporters sample. Using RLS for application security can simplify some development tasks, but may not be worth the extra effort. If your application requires additional layers of security, such as screen level access or if on-screen functionality differs depending on the current user, adding RLS may be redundant. If the application is simple and requires immediate access to data for reports, RLS may fit the need. If used in a transactional system, be sure to test scenarios for administrators and service accounts for your API layer. Performance tuning is important for any RLS application, but it becomes critical for user facing applications.

General considerations

Service accounts with elevated access bypass RLS for report users. In this scenario, users have the same access as the service account. This can be a feature or a bug depending on your implementation. This is related to the caution about cached data. Cached data used by some report engines will effectively bypass RLS if a service account is used. There are generally additional security mechanisms in these report engines that allow user access to be controlled, but it must be considered. Testing should include report access for different users and database level access if appropriate.

Anti Patterns

There are instances when RLS isn’t a good choice for a project. This will vary, and there are always exceptions, but if you find yourself using RLS with one of the following patterns you should stop and consider if it is the correct choice. When working outside of normal patterns it is always a good idea to have enough reasons to be able to defend your decisions.

Transactional systems

Direct user access to transactional systems is a poor choice in most scenarios. Using RLS doesn’t change this pattern. Potential issues with blocking, untested queries, excessive I/O and CPU, security concerns, extra exposure to the database are all potential issues.

RLS can be used in transactional systems without granting direct access to the system. This can be used as a way to implement restricted security without much of the custom coding typically associated with restricted access. It depends on the specific business needs, but an API layer can set the executing user via SESSION_CONTEXT() and as a consequence, different rows can be returned. This wouldn’t work for all scenarios and would start to get more complicated as actual pages or functionality needs to be restricted, but it’s an interesting pattern. An example of this is given in the WideWorldImporters sample database and associated code.

Databases without direct user access

Databases that don’t allow direct user access aren’t likely candidates for RLS, since the whole point of row level security is direct access control. There are patterns for RLS that would work in these scenarios, but they are generally managed at an API level.

Less experienced teams

Setup and long-term maintenance and troubleshooting can be challenging with RLS. Teams that are inexperienced with SQL Server may not be a good choice for implementing RLS. All teams can be trained, but there are some teams that focus on development or business groups that also manage a database. There isn’t a checklist to determine if a team can manage RLS, but experience of the team and available support after implementation are key factors in the decision process.

Multi-tenant systems

I would recommend against using RLS as the security mechanism for multi-tenant transactional client systems. Access for these systems will generally not be direct data level authorization. If users are allowed to directly query the data, they can impact the performance of other clients. If implemented properly (no direct access and no ad-hoc queries), data won’t be exposed, but it does make maintenance and administration more difficult.

Data warehouses used by multiple affiliated departments or groups work well with RLS and are generally more tolerant of potential issues. Typically, reporting and ad-hoc queries are also less sensitive to performance variations. Properly modeled data also helps with performance. The table design can take RLS into account and include the RLS columns in the base design, greatly decreasing performance impacts. Take special care with security and consider additional audits if the data is especially sensitive.

Considering the potential for side-channel attacks, I wouldn’t recommend RLS for direct access with multiple external clients, even in a data warehouse scenario. If you do go down this path, be sure contracts support this and clients are aware, as some require separate physical servers with no chance of errors.

Staging / load databases

Staging or load databases are a common ETL pattern. A staging database is used to house data as it is prepared for the final warehouse. It is a landing zone where data can be manipulated and analyzed before it hits the production system. The same functionality can be achieved with schemas and locked down security in the production database, but a staging database offers some advantages. The entire database can be locked down to include only developers and the ETL service account, backups in the primary system will be smaller, performance can be tuned separately for each database (i.e., separate tiers in Azure or separate drives on a physical server), and no RLS is needed at this layer if users are properly restricted. This can ease the performance load involved with ETL, help testing, make reloads easier, and improve data analysis and profiling without impacting production data. There are almost no scenarios where end users would be allowed access to the staging database. The data in staging databases, or even just staging tables, tends to have more chaotic data. This data may not be curated to the degree necessary to implement RLS. RLS is not a good fit for staging databases. It isn’t even a good fit for staging tables. These databases or staging tables should be excluded from direct user access. Only ETL service accounts, developers and testers need access to these database objects. RLS requires extra work, configuration and testing. Removing that requirement for a load process can reduce the number of errors in the development process and during loads. When service accounts are changed it is easy to have ETL scenarios resulting in duplicates, excess deletes, or general errors. Having a locked-down zone without this requirement makes each part of the process easier.

Other databases or systems that don’t need RLS

SQL Server has robust security structures and mechanisms with great flexibility. Standard security structures and design should be considered first. In fact, security requirements are a first part of any SQL database design and should be considered at each step of the process and with each business requirement. The standard security structures, such as system roles, application roles, schema level security, execute as, etc. should all be evaluated and applied to meet requirements first. They require virtually no coding and minimal testing after initial setup and validation. Programming objects such as stored procedures, views and functions can also be used as a security layer, but they do require more testing and maintenance.

Small user base

Systems with a small user base likely don’t require RLS to keep them secure. There are some exceptions, but small systems should be kept uncomplicated when possible. A simple view layer or stored procedures usually will meet the needs of these systems. Smaller systems generally mean smaller support systems and that should be accounted for in the design.

There are multiple ways to bypass the need for RLS. Some are easier than others, but most are more administrative or developer intensive than using RLS. Whenever choosing a solution, it’s always a good idea to at least consider other options. I include this advice for all database design decisions down to the level of a single column definition. You should be able to defend each choice in a database design and RLS is no different. Considering the security and performance implications and the development overhead, it is especially true for RLS. This isn’t an inclusive list of alternatives, but some common solutions.

Alternatives to RLS

There are multiple ways to bypass the need for RLS. Some are easier than others, but most are more administrative or developer intensive than using RLS. Whenever choosing a solution, it’s always a good idea to at least consider other options. I include this advice for all database design decisions down to the level of a single column definition. You should be able to defend each choice in a database design and RLS is no different. Considering the security implications and the development overhead, it is especially true for RLS. This isn’t an inclusive list of alternatives, but some common solutions.

Multiple databases

 For multiple clients or user groups, creating a separate database for each client can be a viable option. Separate databases can be an especially tempting method when there are regulatory issues involved or if data is stored for competing companies. Care must still be taken with this method. In a server environment, it is easy for a single login to access multiple databases. Administrators of multi-tenant systems should be familiar with the security considerations for their particular system. Additional and regular audits are recommended to ensure everything is configured correctly.

Creating a separate database for each group or set of users requiring access can be a tedious process and difficult to maintain. Objects need to be synchronized, bugs found in one database likely impact the other databases, performance related issues need to be synchronized, users need to be maintained, and all administrative tasks need to be configured. Patterns and templates should be established for creating new databases, objects, and maintenance tasks. DevOps deployments with a common code base also make this easier.

Devops

A database project deployed via standard DevOps methods is recommended to keep multiple databases with the same objects synchronized. This includes objects such as indexes and constraint names. It is common in projects with duplicated databases for different clients to have custom objects in each database. These should also be managed in a database project, allowing for automatic deployments. Separate branches or even projects can be used for these customizations. Whatever method is used to maintain each database, consistency is critical. Methods need to be created to synchronize not only objects, but to maintain users, configure administrative tasks and possibly implement maintenance tasks.

Multiple reports

Another method to provide different data based on users is to create different queries and reports for each distinct set of users. This offers some simplicity in the initial design, but long-term maintenance can be more complicated or at least tedious. This is a very straight-forward solution that is easily understood by less-technical members of the team. Once a base report is designed, it is very simple to copy that report and change the items specific to a group of users.

If using this design pattern for your project, there are a few things that need to be addressed. The primary concern is how to replicate or copy reports and keep them synchronized. The initial creation of reports is relatively easy, but future changes are the challenge. It is easy to put a rule in place that all reports in a group must be updated at the same time, but harder to ensure that it happens. I don’t have a great solution for this problem. Development can be chaotic, especially when there are team changes or when a high-priority bug is fixed. Anyone with data experience knows that some issues only happen with particular sets of data. They might be outliers or they might be errors. In a critical situation, the group of users, meaning one particular report, will be fixed first. Ideally the other reports based on the same template should then be updated, but it may not always happen. Code reviews, automated tools and testing are the common ways to manage this.

The next item for copied reports is determining the testing strategy. A testing strategy is needed for all development tasks, but this one can be deceptively difficult. The assumption might be that all reports based on the same core report are the same other than some WHERE clauses. But there is no guarantee that all reports are synchronized, so a thorough testing pattern must be established. There are some programmatic methods to decrease this burden, but those methods must be created and followed for every report. This can include creating a template for each group of reports or doing a diff on each group of reports. But an extreme amount of rigor must be employed in development to allow testing shortcuts. It can be useful to bring the test team into the conversation early in the development phases to ensure their requirements are considered.

Another consideration for creating multiple reports is the method for exposing those reports to end users and general organization. This depends on the report engine or custom method for creating reports. This also can be the primary method for securing the data, so be sure the method you choose is correct for your architecture.

Custom code in SQL 

SQL Server objects are a very common method for restricting access to data and are often considered a security mechanism when used in this way. Almost any SQL object can be used in this way. Common methods are listed below.

Stored procedure logic

Only allowing access to data through stored procedures is a common security mechanism for direct SQL access. Any logic check or lookup can be performed in the stored procedure. It is also possible to create different stored procedures for different groups of users. If groups of stored procedures are used to limit access, you will want to organize those procedures in a way that makes it easier to manage them. Creating a schema for each group and creating procedures in those schemas greatly simplifies the management. This approach can also be layered with the reporting strategy so that each report for a particular group hits different stored procedures in their designated schema.

Views

Creating different views for groups of users is another effective way to control access to data. As with stored procedures, it is useful to group views in some way. Naming conventions or different schemas are both useful. I like to create a different schema for specific users or groups. This simplifies administration greatly since I only need to apply security to the schema. This also makes is easy to tell what is happening in the database just by looking at the schemas. Views can be combined with stored procedures and used by the reporting layer.

Triggers

Triggers can’t be used with SELECT statements, but they can be used to control INSERT, UPDATE, and DELETE statements. They can also be used with views using the INSTEAD OF option. The use case for this is likely small, but it’s worth considering if DML (INSERT, UPDATE, DELETE) is needed with the solution. An INSTEAD OF trigger can be combined with stored procedures or views to manage the SELECT portion of the requirements.

This isn’t a solution I would focus on developing and is an anti-pattern. A more robust solution is to focus on automating ETL processes and eliminating the need for direct user updates.

Scalar Functions / Table Valued Functions

Functions, both scalar and TVF can be used to limit the data returned to users. On the surface, this can be a tempting pattern. You could create a function that limits data in a way similar to RLS and use it in all of your stored procedures. If you only allow access via stored procedures, data is locked down and it is a simpler solution than RLS.

The main issue with this design pattern is that functions have a tendency to be performance killers. You might encounter hidden RBAR (row-by-agonizing-row), which is similar to a hidden cursor. Using a scalar function is generally tied to using it in the WHERE clause or in a JOIN. This also causes performance issues. This alone is a good enough reason to avoid functions. Security misconfigurations are also a possibility and are discussed in the next section.

Disadvantages of Custom Code

Even though it must be tested and validated, an advantage of RLS is that it is within a standard framework and portions of it can be validated with data management views (DMV). RLS is non-prescriptive, but there are requirements that must be met to implement RLS. And you will generally follow a standard pattern for RLS within a database project. This allows automated validation and makes it easier to perform code reviews and object validation.

This is in contrast to custom code. As with RLS, it must be tested thoroughly, but there aren’t generic patterns that can be discussed. It all depends on the individual implementation. There will be standards that are followed, but each user group will have a different set of criteria. This makes it challenging to automate validation.

As mentioned, custom code must be tested extensively. Since new objects are created for each user group, the coding and testing effort can be extensive. Not only does each new object or report need to be tested, each group added also creates a set of database objects or reports that need validation. This adds a maintenance burden to the developer team and to the test team.

Performance must be carefully managed when using custom code. This is no different from any solution, including RLS, but there are some methods that might appear tempting to use that can cause issues. If generic objects are created to manage access and ease management, especially functions, they can cause hidden RBAR in the query plan.

Another potential issue is related to misconfigurations. This is subjective, but I think there is a greater chance of security gaps when using custom objects than with RLS. If security administrators aren’t aware of the custom code strategy getting used, they could grant direct access to tables using db_datareader. This would allow users to bypass the SQL objects. I have seen administrators try to troubleshoot RLS issues by granting db_datareader and even db_owner role access. With RLS, no extra data is returned. With SQL objects, this type of misconfiguration leads to all data getting exposed.

The other thing to keep in mind is that users often find a way around limitations. If data is so restricted that users don’t use the system, they will probably find the data in another system or use an extract. This can actually make the overall system less secure. Discuss needs with users and make sure the solution works for users while still keeping the data secure.

Application logic

Letting an application control access to the data is the standard pattern for modern architectures, so this isn’t much of an alternative. It is probably more realistic to call RLS the alternative to this pattern, but it is included here for the sake of completeness. There are many ways that applications control access to data. For larger enterprise applications, this is usually table-based controls, security groups, or token-based access of some type. If users don’t need direct access to the database, this is usually a safe choice.

Summary

RLS is a great solution for some projects, but previous methods for differentiating access might be a better fit for some projects. Any method selected, including RLS, need to be thoroughly tested and the code needs to be managed. Each method has challenges and the specific needs of the project will dictate what is needed. Usually there are multiple viable options and the general technical stack used and team skills will be a big driver of what is implemented.

RLS seamlessly integrates with most report engines with some caveats. It is still the responsibility of the development team to ensure that data is protected if external teams and report engines access the data. The primary concern with external access is caching and sharing of data that effectively bypass RLS if the same standards aren’t enforced. Replicating RLS in the destination system or locking down the data to authorized users are common ways to enforce security in any destination system.

RLS doesn’t fit all scenarios and there is a fair amount of administrative overhead in the design, implementation, and maintenance of RLS. It may not fit transactional systems, staging databases, small systems or systems without administrative support. Security and proper database design should take these factors into consideration.

Next sections on RLS:

Part 5 – RLS Attacks

Part 6 – Mitigations to RLS Attacks

 

The post SQL Server Row Level Security Deep Dive. Part 4 – Integration, Anti-patterns, and Alternatives appeared first on Simple Talk.



from Simple Talk https://ift.tt/1sCtvrw
via

Saturday, September 16, 2023

How to Set up Jenkins CI/CD on Kubernetes Cluster using Helm

Jenkins is an open-source multi-platform software for continuous integration/continuous delivery and deployment (CI/CD) in DevOps. It is one of the most common CI/CD tools. Jenkins uses CI/CD pipelines to automate the software development and deployment workflows. Jenkins uses plugins to integrate with DevOps tools such as Docker and Kubernetes.

Jenkins plugins can also be used to integrate with cloud providers such as Google Cloud, AWS, and Microsoft Azure. If you want to integrate any DevOps tool with Jenkins, you have to install its plugin in the Jenkins controller platform. In this tutorial, you will learn how to set up Jenkins CI/CD on Kubernetes Cluster using Helm. Helm is one of the easiest ways of installing an application on the Kubernetes Cluster.

Prerequisites

For you to follow this tutorial on how to Setup Jenkins CI/CD on Kubernetes Cluster using Helm, you must:

  • Have Docker set up and understand how to use Docker:
  • Understand Kubernetes. (There is some free training on the Kubernetes site)

How to Setup Jenkins CI/CD on Kubernetes Cluster using Helm

Helm is an open-source platform that was created by DeisLabs. Helm is used as the package manager for Kubernetes. Many developers use Helm to install any Kubernetes application to the Kubernetes Cluster. Helm automates the process of deploying a Kubernetes application to the Kubernetes Cluster. Helms bundles a Kubernetes application into a single package known as a Helm chart. Developers will then take this Helm chart and install them on the Kubernetes Cluster.

A Helm chart contains all the Kubernetes YAML files for deploying a Kubernetes application. Once you install the Helm chart on the Kubernetes Cluster, it will create the Kubernetes components such as Kubernetes Deployment and Service on the Kubernetes cluster. You can create your Helm chart using Helm or you can reuse other people’s Helm charts.

Helm has an official Helm Chart repository called ArtifactHub] where developers can push and publish their Helm Charts. In this repository, you can easily find Helm charts for any Kubernetes application and reuse them. For complex applications like Jenkins, it is best to use the official Jenkins Helm Chart. Many Developers have contributed to the Jenkins Helm chart and it has all the Kubernetes YAML files. It will save us time in creating our custom Jenkins Helm chart. In this tutorial you will search and download the official Jenkins Helm Chart from ArtifactHub. We will then install the official Jenkins Helm Chart on the Kubernetes Cluster using Helm. Let’s install Helm.

Installing Helm

There are several ways to install Helm which are covered in the official Helm installation guide. You can check out the official Helm installation docs here. In this article, we will cover the online way, but there are other ways to install it.

Helm is a package manager for Kubernetes that makes it easy to install and manage Helm charts. A Helm chart is a collection of Kubernetes resources that can be used to deploy an application or service.

Before you install Helm, you must configure/create a Kubernetes Cluster. We will install Helm on the configured Kubernetes Cluster. There are various Kubernetes Kubernetes Clusters that you can configure. You can check out here the different types of Kubernetes Cluster. Cloud providers like Google Cloud, AWS, and Microsoft Azure have a dedicated Kubernetes Cluster that you can configure and use. In this tutorial, we will not use any of the cloud providers, instead, we will use a local Kubernetes called Minikube that comes with Docker. As long as you have Docker running you can easily configure Minikube. To configure and start Minikube, follow these steps:

Start Docker

Once you have started Docker, it is now easy to start Minikube.

Configure and start Minikube

To configure and start Minikube, run this command in your shell:

minikube start

The output should be similar to:

Now that we have configured and started our Kubernetes Cluster (Minikube), let’s install Helm. There are different ways of installing Helm on your specific operating system. There are other ways of installing Helm on your machine. In this tutorial I will show you how to install Helm on Linux, macOS, and Windows.

You can go through the official Helm installation guide to see the other ways of installing Helm on your specific machine.

Installing Helm on Linux

To install Helm on Linux, open your Linux terminal and execute this command:

sudo apt-get install helm

Installing Helm on Windows

To install Helm on Windows, open your Git bash terminal and execute this command:

choco install kubernetes-helm

Installing Helm on macOs

To install Helm on macOs, open your terminal and execute this command

brew install helm

Helm Installation Output

I have installed Helm on my Windows operating system as shown in the image below:

To check if Helm is running in your Kubernetes cluster, run this in your shell in your shell:

helm

The command should output the following in your terminal:

After you have installed Helm on your specific operating system, the next step is to search for the official Jenkins Helm Chart on the ArtifactHub.

Searching for the Official Jenkins Helm Chart

You will search the official Jenkins Helm chart on ArtifactHub. ArtifactHub is a web-based application that enables finding, installing, and publishing Helm charts.

You can click on this link https://artifacthub.io/ to open ArtifactHub.

You can then search for Jenkins Helm Chart as shown below:

After finding the official Jenkins Helm Chart on ArtifactHub, you need to add the Jenkins chart repository on your local machine.

To add the Jenkins chart repository on your local machine, run this command in your shell:

helm repo add jenkins https://charts.jenkins.io

It will download and add the official Jenkins Helm Chart repository. To update this repository and get the latest version of the Helm Chart, run this command in your terminal:

helm repo update

This command will get the latest version of Jenkins Helm Chart by default. For more information, go here:

Installing the Official Jenkins Helm Chart on the Kubernetes Cluster using Helm

Step One: Run this command in your shell

helm install myjenkins jenkins/jenkins

This command will deploy the Jenkins CI/CD on the Kubernetes cluster. The name of the Jenkins Helm Chart release is myjenkins. There are different versions and releases for the Jenkins Helm chart that you can use. In this article you will use myjenkins since it is the release that has all the configurations for running the CI/CD pipeline.

Helm will use the Kubernetes YAML files in the Helm and create the Kubernetes components (Deployment and Service). We have reused the official Jenkins Helm Chart to set up the Jenkins CI/CD on the Kubernetes Cluster.

Output is shown in the image below:

To start using the Jenkins CI/CD running on the Kubernetes Cluster, follow these steps:

Step Two: Get your ‘admin’ password for Unlocking Jenkins

To get the admin password, run this command in your terminal. You have to execute the first command found in the output above to get the admin password.

kubectl exec --namespace default -it svc/myjenkins -c jenkins – 
/bin/cat /run/secrets/chart-admin-password && echo

Admin Password Output:

Step Three: Get the Jenkins URL

To get the Jenkins URL, run the following shell command:

kubectl --namespace default port-forward svc/myjenkins 8080:8080

Output:

This command will expose the Jenkins container instance on port 8080. You can go to http://127.0.0.1:8080 to access the Jenkins CI/CD

Step Four: Unlock Jenkins with Admin Password

To unlock Jenkins with an admin password, open http://127.0.0.1:8080 in your browser to access your running Jenkins container instance as shown in the image below:

Copy the initial administrator password and paste it into the password field. It will unlock the running Jenkins container instance. You can now finish setting up Jenkins.

Step Five: Finishing setting up Jenkins CI/CD

To finish setting up Jenkins CI/CD follow these steps:

Customize Jenkins

You will install the suggested plugins to customize Jenkins as shown in the image below. Select Install suggested plugins to allow the installation of the necessary plugins. The plugins to be installed are shown in the image below:

After a few minutes (depending on your internet speed), it will install the suggested plugins. This step will install plugins such as:

  • Git Plugin
  • Blue Ocean
  • Simple Theme
  • Performance Publisher
  • Dashboard View
  • Maven Integration
  • Folders
  • Bootstrapped-multi-test-results-report
  • Pipeline Utility Steps

These are the basic plugins that Jenkins requires to run. For more information on the suggested plugins that will be installed, check out this article that explains these plugins in detail.

Jenkins will use the plugins to integrate with other software as shown in the image below.

Creating First Jenkins Admin User

You need to create a first Jenkins Admin user who will have control over the Jenkins platform. The Admin user will perform all actions/tasks in the Jenkins platform like starting CI/CD pipelines and managing Jenkins.

After installing all the basic plugins shown above, you will be redirected to another page for you to create your first admin user. This page is shown in the image below:

You can input all the admin user details and then click the `Save and Continue` button. You will then be redirected to another page as shown below:

Adding Jenkins URL

You can add the Jenkins URL as shown below. This is the same URL http://127.0.0.1:8080 where Jenkins is running.

You can click `Save and Finish`. Your Jenkins platform will be running and ready to use. You will be redirected to another page as shown below:

Start using Jenkins

To start using Jenkins click on the `Start using Jenkins` button as shown in the image below:

You can now start using Jenkins to create all your Jenkins CI/CD pipeline for automating the software development and deployment workflows. You will have access to the Jenkins Dashboard as shown below:

You have successfully set up Jenkins CI/CD on Kubernetes using Helm. You can access the Jenkins Dashboard.

Conclusion

In this tutorial, you have learned how to set up Jenkins CI/CD on Kubernetes Cluster using Helm. We used Helm because it’s one of the easiest ways of installing an application on the Kubernetes Cluster. This tutorial shows you how to install Helm on Linux, macOS, and Windows. It also shows you how to search for the Official Jenkins Helm Chart.

We also installed the official Jenkins Helm Chart on the Kubernetes Cluster using Helm.

Additionally, this tutorial shows you how to get your ‘admin’ password for unlocking Jenkins and how to login into Jenkins. In the end, we finished setting up Jenkins CI/CD on Kubernetes using Helm and accessed the Jenkins Dashboard. Hope this tutorial helps you in getting started with Jenkins.

The post How to Set up Jenkins CI/CD on Kubernetes Cluster using Helm appeared first on Simple Talk.



from Simple Talk https://ift.tt/KImj6sx
via