Tuesday, January 23, 2024

Querying MongoDB Documents

MongoDB provides an efficient environment for storing document data at scale. However, most of your interactions with MongoDB will not be related to storing data but rather to querying data. The better you understand how to retrieve the data you need, the more effectively you can interact with MongoDB and support your data-driven applications.

To help you get started, this article introduces you to various ways you can query document data in a MongoDB database. I demonstrate how to use both MongoDB Shell and the MongoDB Compass GUI when interacting with your collection data.

Keep in mind, however, that building MongoDB queries is an extensive and many-faceted topic, one that can easily justify multiple articles (if not a book or two). In this article, I introduce you to the basics, with the goal of helping you create a foundation on which you can continue to build. The better you understand the concepts presented here, the more easily you’ll be able to move onto more advanced topics.

Note: For the examples in this article, I used MongoDB Atlas and MongoDB Compass. The last section of this article—“Appendix: Preparing your MongoDB environment”—provides information about how to set up your environment and includes a link to a .json file that you’ll need to download if you want to try out these examples for yourself.

Querying documents in a MongoDB collection

When working in MongoDB Compass, you have two options for finding documents in a collection. You can use MongoDB Shell, which is embedded in Compass, or you can use the graphical components of the Compass interface. I plan to demonstrate both approaches.

The graphical features in Compass are useful when learning about MongoDB or when you want to run ad hoc queries. Compass also makes it easier to view the documents returned by your queries. However, you’ll likely want to work in the shell as well, especially as you gain experience with MongoDB. The shell can be faster and more efficient for some operations, and it enables you to run shell scripts.

By learning both approaches, you can see first-hand how the shell commands and GUI features relate to each other, which can lead to a better understanding of both approaches. You’ll also be able to better decide which approach is best for you from one situation to the next as you become more comfortable with retrieving MongoDB data.

When running queries in the shell, you’ll often use a find statement that specifies which documents to include in the results and which fields to include in the documents. A find statement is actually a call to the find method, which is one of the method’s available to a collection object. The following syntax shows the basic elements that make up a find statement:

db.collection.find( { filter }, { project } ).options;

A find statement includes both required and optional components. Here’s a breakdown of what each one means:

  • db. System variable for referencing the current database and accessing the properties and methods available to the database object.
  • collection. Placeholder. The name of the target collection.
  • find. The find method, which is one of the methods available to the collection object for retrieving documents from that collection. For this article, you will be using reports as the collection name.
  • filter. Placeholder. An optional parameter that determines which documents are returned from the collection. If the parameter is omitted or passed in as an empty document ({}), MongoDB returns all documents from the collection. Some MongoDB documentation refers to this element as the query, rather than filter.
  • projection. Placeholder. An optional parameter that determines which document fields are included in the results. If the parameter is omitted, MongoDB includes all fields in the returned documents.
  • options. Placeholder. One or more optional settings that can be included in a find statement to better refine the query.

From this syntax, you can see that the simplest find statement is one that returns all documents in a collection and all fields within those documents. In other words, the statement includes no optional components, as shown in the following simplified syntax:

db.collection.find();

Before you run a find statement in MongoDB Shell, you should first ensure that you’re working within the context of the target database. If you’re not, you should run a use statement that specifies that database, which in this case, is weather. The weather database contains the reports collection, both of which are discussed in the appendix. The following use statement changes the context to the weather database:

use weather;

To run this command, type or paste the statement at the shell’s command prompt and then press Enter. If the shell is not displayed in Compass, click the >_MONGOSH link at the bottom left corner of the interface. This will expand the bottom window and display the shell’s command prompt, where you can now run the USE and other MongoDB statements.

After you run your USE statement, you can then run the following find statement, which returns all documents in the reports collection:

db.reports.find();

The collection contains 100 documents; however, the shell displays only 20 documents at a time. To view the next set of documents, you must enter it. (The it command stands for iterate.) The following figure shows the find statement and a portion of the first document returned by the query.

As you can see, the figure shows only a small portion of the results, which points to the fact that returning a lot of large documents to the shell can get quite unwieldy. As you progress through this article, you’ll learn various ways to refine your query to get more precise results.

In the meantime, you might find it useful to view only the number of documents that a query would return, rather than returning all those documents. For this, you can use the count option, which you tag onto the end of you statement, as in the following example:

db.reports.find().count();

To include the count option, add a period after the find method’s closing parenthesis and then specify the option name, followed by its own set of parentheses. Now the statement will return the value 100, rather than the documents themselves.

If you want to view the collection’s documents in the Compass GUI instead of the shell, you need only open the collection in the main window. Opening a collection is comparable to running a find statement without any of the optional parameters. One of the biggest advantages to using the GUI is that you have three different options for displaying the documents: List View, JSON View, and Table View, which I discussed in the first article in this series.

Adding a basic filter to your MongoDB query

Now that you understand how to run a find statement, you can start adding filters to your queries to control which documents to include in your results. At its most basic, a filter consists of a field name and a field value. The field name comes first and the value follows. The two are separated by a colon and enclosed in curly brackets to define them as an embedded document.

For example, the following find statement includes a filter that specifies callLetters as the field and GTOT as the target value:

db.reports.find( { "callLetters": "GTOT" } );

The statement will now return only those documents whose callLetters field has a value of GTOT. As it turns out, only one document meets this search condition. The following figure shows part of the document as it appears in MongoDB Shell. Notice that the callLetters field has a value of GTOT.

If you want to return the same results in the Compass GUI, you must take a slightly different approach to defining your filter parameter. Start by locating the Filter text box that sits above the main GUI window (Just over the Add Data and Export Data buttons). In this box, type or paste the following code snippet:

{ "callLetters": "GTOT" }

This is the same filter parameter that you defined in the previous Find method call. As before, the filter is an embedded document that specifies the callLetters field with GTOT as the value.

After you add the code to the Filter text box, click the Find button, which is to the right of the text box. Compass will then display the single document, as shown in the following figure.

Notice how much easier it is to view the document in the Compass GUI than in the shell. One thing I want to point out, however, is that you must use the correct upper and lower cases when specifying field and value names, whether using the shell or the graphical features. For example, the following find statement uses CallLetters (initial cap) rather than callLetters, so the statement returns no documents:

db.reports.find( { "CallLetters": "GTOT" } );

I also want to point out the findOne method, which you can use in place of the find method when working with single documents. As its name suggests, the findOne method returns only one document. The following example demonstrates how this works:

db.reports.findOne( { "callLetters": "GTOT" } );

The statement will return the same results as the earlier example because only one document satisfies the search condition. However, if you use the findOne method for a query that would otherwise return multiple documents, MongoDB will return only the first document, based on the natural order of the documents on the disk.

In this case, the findOne method would be comparable to using the find method with the limit option, as in the following example:

db.reports.find( { "callLetters": "GTOT" } ).limit(1);

The limit option specifies the maximum number of documents that the query should return. Because that argument is 1, only the first document is returned, based on the natural order of the documents on disk. The document is the same one returned by the previous findOne statement. For the most part, you should use a findOne statement only when you’re working with single documents.

Basing your query on an array

In the previous examples, the filter section (the embedded document) references a string field. However, you can also specify other types of fields in your filter, including arrays and embedded documents. For example, the following find statement filters the documents based on the sections array and the UG2 value in that array.

db.reports.find( { "sections": "UG2" } );

When you base your filter on an array, you need only supply the field name and value, just like you saw with the string field. You don’t have to be concerned with where the value is positioned in the array. The statement returns all documents in which the sections array contains the value UG2. Seven documents meet this condition.

You can also achieve the same results in the Compass GUI by entering the following code snippet into the Filter text box and then clicking Find:

{ "sections": "UG2" }

Compass displays the seven returned documents in the main window, as you saw in the previous examples. If you view the values in the sections array in each document, you’ll see that they include the UG2 value.

Now let’s recast the preceding find statement as a findOne statement and run the new statement in the shell:

db.reports.findOne( { "sections": "UG2" } );

This example is identical to the previous one except that it uses the findOne method. As a result, it returns only the first document. You can achieve the same results by running the following find statement, which includes the limit option:

db.reports.find( { "sections": "UG2" } ).limit(1);

You can also achieve these results in the Compass GUI by entering the following code snippet into the Filter text box:

{ "sections": "UG2" }

In addition, you must type 1 in the Limit text box in the query form, as shown in the following figure. To access this form, click the Options link at the far right of the interface, above the main window. After you define the Limit option, click the Find button. MongoDB now returns only a single document. Notice that the document’s sections array includes the UG2 value.

You’re not limited to searching on only one value in an array. You can search for multiple values by creating an embedded array for the filter’s value. For example, the following find statement returns documents whose sections array contains either the UG2 value or the AY1 value (but not both):

db.reports.find( { "sections": [ "UG2", "AY1" ] } );

As it turns out, the reports collection includes no documents that contain only one of the two values. However, if you want to return documents that include either one or both values, you can use the $in operator as part of the field value:

db.reports.find( { "sections": { $in: [ "UG2", "AY1" ] } } );

As with many MongoDB operators, when you use the $in operator, you define an embedded document as the filter’s field’s value. The embedded document includes the $in operator as the embedded document field name and an array as the field value. The array contains the target values.

The $in operator returns all documents that include any of the specified values. In this case, it will return any documents that contain the UG2 value, the AY1 value, or both. As a result, the find statement now returns 93 documents.

In some cases, you might want to return only those documents that contain all the specified array values, in which case, you can use the $all operator rather than $in, as in this following example:

db.reports.find( { "sections": { $all: [ "UG2", "AY1" ] } } );

The find statement now returns only six documents, which are the only ones that contain both the UG2 and AY1 values.

You can also use the $in or $all operator when setting up your queries in the Compass GUI. For example, you can duplicate the results of the preceding example by entering the following code snippet into the Filter text box:

{ "sections": { $all: [ "UG2", "AY1" ] } }

After you click Find, Compass returns the six documents and displays them in the main window. As before, you can view the values in the sections array in each document to verify that they each include both the UG2 and AY1 values.

Basing your query on an embedded document

When working with a MongoDB collection, you’ll likely want to base your queries on embedded documents, just like you saw with arrays. For instance, the documents in the reports collection contain the airTemperature field, which is an embedded document. One of the fields in the airTemperature document is value, which takes a numerical value. You can base your statement’s filter on the value field and its value, as in the following example:

db.reports.find( { "airTemperature.value": 25 } );

Similar to what you’ve seen in previous examples, the filter parameter is an embedded document that specifies the field and its target value, which is 25. Notice that the field is specified as a two-part name. The first part is the airTemperature field, and the second part is the value subfield. As a result, the statement returns only those documents whose airTemperature.value field has a value of 25, which turns out to be six documents.

To return the same results in the Compass GUI, you should enter the same filter (embedded document) in the Filter text box and then click Find, just like you did in previous examples:

{ "airTemperature.value": 25 }

The Compass main window should now display the same six documents that were returned by the previous find statement. You can also drill into the returned documents to verify that they meet the search condition.

In some cases, you might need to dig even deeper into the hierarchy of embedded documents. For instance, the reports collection includes documents that contain the wind field, which is an embedded document. One of the fields in the embedded document is direction, also an embedded document. The direction field, in turn, contains the angle field, which takes a numerical value.

Suppose you want to retrieve only those documents whose angle value is 300. The following find statement demonstrates how to achieve this:

db.reports.find( { "wind.direction.angle": 300 } );

When specifying the angle field in your filter, you must qualify the name with the wind field, then the direction field, and finally the angle field. In this case, the angle field must have a value of 300 for the document to be returned. Three documents meet this search condition.

As before, you can achieve the same results in the Compass GUI by entering the statement’s filter into the Filter text box and then clicking Find:

{ "wind.direction.angle": 300 }

The reports documents also contain the pastWeatherObservationManual field, which is an array. The array contains an embedded document, and the embedded document contains the period field, which is also an embedded document. One of the fields in the period document is value.

You can query the value field by again qualifying the name, as you saw before, even though the top-level field is an array. For example, the following find statement retrieves those documents whose value field has a value of 3:

db.reports.find( { "pastWeatherObservationManual.period.value": 3 } );

In this case, the statement returns 10 documents, but you can mix up your queries in other ways. For instance, you can query arrays embedded in documents and even in other arrays.

Specifying fields in a MongoDB query

The examples up to this point showed you different methods of controlling which documents to include in your search results, but you can also control which fields to include in those documents. (Or in other words, what fields to include in your search results.)

To specify the fields, you must add a projection parameter to your statements. The parameter, which follows the filter parameter, is also defined as an embedded document. Within this document, you specify one or more field names, along with a status code that indicates whether to include or exclude the field.

For example, the following find statement defines a projection parameter that includes the airTemperature and dewpoint fields:

db.reports.find( 
  { "callLetters": "GTOT" },
  { "airTemperature": 1, "dewPoint": 1 }
);

For each included field, the projection parameter specifies a value of 1, which means that the two fields should be included in the results. None of the other fields will be included in this output.

The following figure shows the statement and the single document it returns, as they appear in MongoDB Shell. Notice that the results include the _id field along with the fields you specified. MongoDB includes the _id field automatically unless you specifically tell it not to, which I’ll demonstrate in just a bit.

If you want to achieve the same results in the MongoDB GUI, start by clicking the Options link to open the query form, as you saw earlier. Then, in the Filter text box, add the following code snippet:

{ "callLetters": "GTOT" }

This is just like you defined the filter in the previous examples. The form’s Filter text box serves as the counterpart to the filter parameter in a find statement. Next, enter the following snippet in the Project text box and click Find:

{ "airTemperature": 1, "dewPoint": 1 }

Similar to the Filter text box, the Project text box serves as the counterpart to the projection parameter in a find statement. The following figure shows the query form with the Filter and Project text boxes completed, along with the returned document.

In some cases, you might want to return all the documents but still limit which fields are returned. You can do this by including an empty filter parameter—a set of curly brackets ({})—in the position where you would normally define the parameter. For example, the following find statement is similar to the previous example except that it returns all 100 documents:

db.reports.find( {}, { "airTemperature": 1, "dewPoint": 1 } );

Notice that the first argument in the find method is the filter parameter, which is simply a set of empty curly brackets. The second argument defines the projection parameter.

As noted earlier, the previous two find statements returned the _id field along with the requested fields. However, there might be times when you want to omit this field from your results. If this is the case, you should include the _id field in your projection parameter, but set its value to 0, rather than 1, as in the following example:

db.reports.find( 
  { "callLetters": "GTOT" },
  { "airTemperature": 1, "dewPoint": 1, "_id": 0 }
);

The _id field is somewhat of an anomaly, in terms of how it’s used in a projection. Normally, the parameter’s specified columns must all be set to the same value, either 1 for inclusion or 0 for exclusion (exclusion would return all fields except the excluded ones).

The only exception to this is the _id field. As you can see in this example, it’s okay to define it with a different setting than the other fields.

Adding options to a MongoDB query

Earlier in the article, you saw examples of find statements that included options tagged onto the end. For example, you learned how to use the count option to return the number of documents, rather than the actual documents. You also learned about how to use the limit option to restrict the number of returned documents to the specified amount.

Another useful option is sort, which lets you control how the returned documents are ordered. When you use this option, you include an embedded document as the option’s argument. The embedded document specifies the names of one or more fields on which to base the ordering, along with a code that indicates the sort order. For instance, the following find statement specifies that the returned documents should be sorted by the callLetters field values, in ascending order:

db.reports.find( 
  { "sections": "UG2" },
  { "callLetters": 1, "airTemperature": 1 }  
).sort( { "callLetters": 1 } );

The callLetters field in the sort option is assigned a value of 1, which indicates ascending order. If you want them sorted in descending order, you should specify -1. The following figure shows the statement and several of the returned documents. The statement returns seven documents in all.

You can duplicate this statement in the Compass GUI, as you saw with other examples. Once again, expand the query form and enter the filter and projection in the Filter and Project text boxes, respectively. Then, in the Sort text box, enter the following snippet and click Find:

{ "callLetters": 1 }

The following figure shows the filled-out query form and part of the results returned by the query.

When defining the sort option, you can specify multiple fields with different sort orders. For example, the following find statement sorts first on the visibility.distance.value field, in ascending order, and then on the airTemperature.value field, in descending order:

db.reports.find( 
  { "sections": "UG2" },
  { "callLetters": 1, "visibility": 1, "airTemperature": 1 }  
).sort( { "visibility.distance.value": 1, "airTemperature.value": -1 } );

The find statement returns seven documents. You can achieve the same results in the Compass GUI by entering the following code snippet in the query form’s Sort box:

{ "visibility.distance.value": 1, "airTemperature.value": -1 }

Another available option is skip, which lets you specify the number of rows to skip when returning documents. According to MongoDB documentation, this capability can be useful when you want to paginate your results. The following find statement provides an example of the skip option, which specifies that five documents should be skipped:

db.reports.find( 
  { "sections": "UG2" },
  { "callLetters": 1, "airTemperature": 1 }  
).sort( { "callLetters": 1 } ).skip(5);

Normally, this statement would return seven documents, but now it returns only the last two. If you want to get these same results in the Compass GUI, you must specify the number of documents to bypass in the Skip text box in the query form, as shown in the following figure.

MongoDB also lets you stack options at the end of a find statement. For instance, the following find statement includes both the skip option and the collation option, which specifies en as the locale value:

db.reports.find( 
  { "sections": "UG2" },
  { "callLetters": 1, "airTemperature": 1 }  
).sort( { "callLetters": 1 } ).skip(5).collation( { locale: "en" } );

The collation option, which is tagged onto the statement after the skip option, lets you choose which collation to use for the documents returned by the find statement. In this case, the en collation is specified, which represents English.

You can also define the collation in the Compass query form. For instance, to achieve the same results as those in the previous example, you would enter the following snippet in the Collation text box, along with the other code.

{ locale: "en" }

The following figure shows the query form after all the text boxes have been filled out, including the Collation text box. The figure also shows the two documents retuned by the query.

There are plenty of other options from which to choose when building your queries. You can learn more about the available options in the MongoDB article Cursor Methods. The options are referred to as cursor methods because the find statement returns a cursor object, and the methods build off that object, which is why you can call them in your find statements.

Working with operators in a MongoDB query

MongoDB also supports the use of operators to help you better refine your queries. For example, you can use comparison operators in your filters as part of the field values, as in the following find statement:

db.reports.find( 
  { "airTemperature.value": { $lte: 0 } },
  { "callLetters": 1, "airTemperature": 1 }  
).sort( { "callLetters": 1 } );

The statement includes the $lte (less than or equal to) operator and specifies 0 as the operator’s value. When you include an operator in your filter parameter, you add it as an embedded document in which the operator is the field name and its value is the target comparison value. In this case, the statement returns all documents whose airTemperature.value field has a value less than or equal to 0. Seven documents meet this search condition.

Another useful operator is $in, which you’ve already seen in action. The operator matches any of the values included in the embedded array. For instance, the following find statement returns any document whose airTemperature.value field has a value of 5, 10, 15, or 20:

db.reports.find( 
  { "airTemperature.value": { $in: [ 5, 10, 15, 20 ] } },
  { "callLetters": 1, "airTemperature": 1 }
).sort( { "callLetters": 1 } );

You can also include multiple operators in your queries. The following find statement contains both the $gt (greater than) operator and the $lt (less than) operator:

db.reports.find( 
  { "airTemperature.value": { $gt: -1, $lt: 1 } },
  { "callLetters": 1, "airTemperature": 1 }  
).sort( { "callLetters": 1 } );

The statement returns only those documents whose airTemperature.value field has a value between -1 and 1. Only seven documents match this search condition.

Not all operators are comparison operators. For instance, MongoDB also supports element operators. One such operator is $exists, which returns documents that either have or don’t have the specified field. The following example demonstrates how this works:

db.reports.find( 
  { 
    "airTemperature.value": { $gt: 5 },
    "skyConditionObservation": { $exists: false } 
  },
  { "callLetters": 1, "airTemperature": 1 }  
).sort( { "airTemperature.value": -1 } );

The filter parameter defines two search conditions, which are separated by a comma. The first uses the $gt (greater than) operator to return only those documents whose airTemperature.value field has a value greater than 5. The second condition uses the $exists operator to check whether the skyConditionObservation field exists. Because the operator’s value is set to false, the results will include only those documents that do not contain this field. Only two documents meet both conditions.

Getting started with MongoDB queries

I’ve covered a lot of territory in this article, yet what I’ve shown you here represents only a small sampling of the many ways you can query a MongoDB collection. Not surprisingly, there is much more that you can do with your queries, especially when it comes to defining the filters, whether using MongoDB Shell or the Compass GUI.

I highly recommend that you check out the MongoDB documentation to learn more about each aspect of building a query. A good place to start is with the topic db.collection.find(). The better you understand how the find statement works, the more equipped you’ll be to move onto to other aspects of querying MongoDB data.

Appendix: Preparing your MongoDB environment

For the examples in this article, I used MongoDB Atlas for storing the data and MongoDB Compass to work with that data. This is the same approach I used for the previous articles in this series. The first of these articles explains how to set up the environments.

The examples in this article are based on a collection that I created from the sample_weatherdata database available through MongoDB Atlas. If you want to try out these examples for yourself, you’ll need to download the document from this site and save as weather.json file to a folder that you can access from within MongoDB Compass. You’ll then need to take the following steps to import the data into your collection:

  1. In MongoDB Compass, connect to your cluster on MongoDB Atlas, if you’re not already connected.
  2. In the left panel, click the Create database button (plus sign), which is located to the right of the Databases node.
  3. In the Create Database dialog box, type weather in the Database Name text box, type reports in the Collection Name text box, and click Create Database.
  4. After the database and collection have been created, select the reports collection node in the left panel if it’s not already selected.
  5. In the main window, click the Add Data drop-down arrow and click Import JSON or CSV file.
  6. When your system’s file manager window appears, navigate to the folder that contains the weather.json file, select the file, and click Select. Compass will load the data into the reports collection.

That’s all you need to do to try out the examples in this article. I plan to go into more detail about both importing and exporting data later in the series.

The post Querying MongoDB Documents appeared first on Simple Talk.



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

Friday, January 19, 2024

The Importance of Retention

It is always an afterthought. New objects are created that start off small and current. New feature development takes over and the recently architected data structures become old news. Over time, data grows and suddenly a previously small table contains millions or billions of rows.

Is all that data necessary? How long should it be retained for? If there is no answer to this question, then the actuality may be “Forever”, or more honestly “No one knows for sure.”

Retention takes on many forms and this article dives into ways in which data can be managed over time to ensure that it is fast, accurate, and readily available.

Note: the code for this article can be found here.

The Common Scenario

A new feature has been added and it requires verbose logging, which will provide troubleshooting information, as well as useful usage data that can be crunched in the future. The following is how this table might look:

CREATE TABLE dbo.OrderChangeLog
(       OrderChangeLogID INT NOT NULL IDENTITY(1,1) 
        CONSTRAINT PK_OrderChangeLog PRIMARY KEY CLUSTERED,
        OrderID INT NOT NULL,
        CustomerID INT NOT NULL,
        ModifiedDateTime DATETIME NOT NULL,
        ModifiedByPersonID INT NOT NULL,
        ChangeRequestAPIDetails VARCHAR(MAX) NOT NULL,
        BatchNumber INT NOT NULL,
        ColumnChanged VARCHAR(128) NOT NULL,
        OldValue VARCHAR(MAX) NULL,
        NewValue VARCHAR(MAX) NULL
);

If you have ever worked with a log table, then some of this will no doubt look familiar. Alongside some basic information as to who made the change and when, there is extensive detail documenting exactly what happened and why. Given that there are three strings of MAX length, a row could conceivably be quite large.

In development, this data set starts off small. Orders are created, modified, completed, and deleted, and the log table accumulates some data that spans the lifecycle of the software release. This typically would be a few weeks or months. Once released, the new feature performs equally well, and the development team moves on to new projects.

Six months later, complaints come in that making changes to orders is slow. The issue is tracked back to this log table containing what has grown to be over a billion rows of verbose data, consuming hundreds of gigabytes of space (or in reality, much more maybe?). To further complicate this problem, some new reports were written that expose this data to end-users. Prior to this, the log data existed solely for internal troubleshooting and posterity.

Now what?

This is the problem. Since no one defined the lifecycle of this data, the administrator will likely be hesitant to just delete considerable amounts of it. What if it was required for some regulatory reason? Note that this is really common with logging type data, but it pertains to data of all types. Do you need an order for a cheeseburger from 1966 in your database? Or for a toaster in 2008? Of for that matter, any item sold last week? The latter is probably more likely to be “yes”, but it isn’t necessarily so.

What is Retention?

This diagram illustrates (with a pinch of humor) what happens to data that grows forever unchecked. The purpose of retention is to carefully architect how data is stored so that it remains highly available, even as it grows in size and complexity over time.

There are many techniques that can be used to make data more manageable as it grows. Some require organizational support to implement, whereas others are completely technical in nature, and can be completed via data engineering or development. The following are a wide variety of strategies that can be used to improve data usability, even when it grows to an enormous size.

Create a Retention Period

A retention period specified how long data will be maintained in its current form. Oftentimes, (and ideally in all cases,) data is only needed as-is for a set period, after which it can be deleted, moved, compressed, archived, or something else.

I firmly believe that when data is architected that has any chance of growing large, a retention period should be defined up-front and documented. A retention period can be “Forever”. This is 100% acceptable. The knowledge that data will be maintained as-is forever provides valuable information. That knowledge can be used to ensure that those data structures are built with a future of big data in mind. (Of course, forever typically just means that data growth is not tremendous so we will leave it to the future to deal with!)

Retention periods can be any amount of time and will vary greatly based on the type of data being discussed. Some common examples are:

  • Temporary Data: This includes session data, temporary report parameters, temporary filter properties, etc…This data is needed for a specific purpose, after which it is disposable. This type of data can be stored in temporary objects, memory-optimized tables, or other structures that are automatically dropped when their purpose is fulfilled.
  • Short-Term Data: Data that is needed for minutes or hours and is critical for operational or short-term application needs. A retention period for this sort of data would be either a short time span, or when processes using the data are complete. This data likely needs to be durable but might not need to be. It may also be tolerant of delayed durability.
  • Mid-Term Data: This is data that needs to be retained for days or weeks. It may be part of ETL processes, log tables, email metadata, or other processes that are important, but in no way long-term or permanent in nature. This data may be tolerant of compression and other space-saving strategies.
  • Long-Term Data: Data that should be retained for months or years. Not only can it get quite large, but it may consist of a mix of OLAP and OLTP processes. Depending on usage, it will likely benefit from a combination of different data architecture strategies. Because it can get large, it is helpful to understand if the RPO and RTO for this data is the same, regardless of its age.
  • Indefinite/Forever Data: Some data needs to be saved forever. This may be because users may need to go back and validate old data. It may be due to legal or contractual obligations. Regardless of reason, this is data that can grow massive and needs to be managed in a way that ensures that is stays usable. Oftentimes, this data will be analytic in nature, but this will not always be the case.
  • Defining retention periods up-front not only ensures a more appropriate data architecture, but it also avoids inconsistencies. Without a policy, it is possible that an operator may delete old data in response to a performance problem, user request, or some other unusual evet. Once this begins to happen, then the retention period for that data becomes a giant question mark:

Data mysteries are typically not fun to tackle. This specific type of mystery is one that will end up poorly documented if anyone bothers to document it at all. The result is that this informal data retention event lives on only in the brain of an operator, destined to be forgotten or misinterpreted.

A Note on RPO, RTO, and Availability

Data retention is directly linked to the concepts of RPO and RTO. Availability is equally important to anyone that will be consuming data in any form. Therefore, there is value in briefly reviewing what these terms mean.

RPO: Recovery Point Objective. In the event of a disaster, data incident, outage, or any other unusual hardware or software problem, RPO describes the maximum amount of data that will be lost.

While many executives will want the RPO for their applications to be near-zero, maintaining that minimal amount of data loss is very expensive. In reality, RPO is based on operational processes such as replication, Availability Groups, snapshots, or backups.

RTO: Recovery Time Objective. In the event of a disaster, data incident, outage, or any other unusual hardware or software problem, RTO describes the maximum amount of time needed for a system to return to its normal operation.

To briefly summarize the above descriptions: RTO is how long users have to wait for an outage to end, whereas RPO is how much data could be lost via that outage.

Retention Strategies

There are a wide variety of strategies that can be used to convert unwieldy and large data into manageable data. The table provided earlier in this article will be used to demonstrate each option. The column ModifiedDateTime will be used to evaluate any changes made via retention. The following are many common strategies:

Deletion of Data

The simplest form of retention is to delete data that is outside of the retention period. When an organization decides that data is no longer needed in any form, then deleting it provides relief by reducing data size and maintaining that reduction indefinitely. If retention of 12 months was implemented against the ModifiedDateTime column, then a recurring job could execute that deletes any data older than 12 months.

The following code example would be one way to accomplish this task:

DELETE TOP (1000) OrderChangeLog
FROM dbo.OrderChangeLog
WHERE OrderChangeLog.ModifiedDateTime <= 
                   DATEADD(MONTH, -12, GETUTCDATE());

WHILE @@ROWCOUNT > 0
BEGIN
        DELETE TOP (1000) OrderChangeLog
        FROM dbo.OrderChangeLog
        WHERE OrderChangeLog.ModifiedDateTime <= 
                    DATEADD(MONTH, -12, GETUTCDATE());
END;

The code above deletes 1000 rows from OrderChangeLog and then continues to do so, as long as rows are being deleted. The batch size is customizable and should be based on data size. 1000 rows is arbitrary and that number can be adjusted lower for a table with very wide rows or higher for a table with narrow rows. Also consider the impact on the transaction log as each DELETE statement will be logged. Therefore, deleting one million rows at one time will generate a larger transaction that could cause log growth and delays while being committed.

Archiving Data

Some organizations no longer require older data for production use but want to maintain it somewhere for posterity. Often data is retained for legal or contractual reasons. Sometimes it is retained “just in case”. Regardless of reason, archival of data allows it to be removed from its source location and moved somewhere else.

A benefit of data archival is that while the production data source may be housed on expensive hardware, the archival destination will usually have less stringent requirements. Archived data can often be treated as cool/cold data, rather than as time-sensitive/mission-critical application data. This translates directly into resource and cost savings. The destination for archived data may not even need to be a database server. It could be a file server or some other easy-to-manage location.

This code illustrates one way to archive data older than 12 months, including the creation of the archive table:

CREATE TABLE dbo.OrderChangeLogArchive
(       OrderChangeLogID INT NOT NULL 
          CONSTRAINT PK_OrderChangeLogArchive 
          PRIMARY KEY CLUSTERED WITH (DATA_COMPRESSION = PAGE),
        OrderID INT NOT NULL,
        CustomerID INT NOT NULL,
        ModifiedDateTime DATETIME NOT NULL,
        ModifiedByPersonID INT NOT NULL,
        ChangeRequestAPIDetails VARCHAR(MAX) NOT NULL,
        BatchNumber INT NOT NULL,
        ColumnChanged VARCHAR(128) NOT NULL,
        OldValue VARCHAR(MAX) NULL,
        NewValue VARCHAR(MAX) NULL
);
GO
DELETE TOP (1000) OrderChangeLog
OUTPUT  DELETED.OrderChangeLogID,
                DELETED.OrderID,
                DELETED.CustomerID,
                DELETED.ModifiedDateTime,
                DELETED.ModifiedByPersonID,
                DELETED.ChangeRequestAPIDetails,
                DELETED.BatchNumber,
                DELETED.ColumnChanged,
                DELETED.OldValue,
                DELETED.NewValue
INTO dbo.OrderChangeLogArchive
WHERE OrderChangeLog.ModifiedDateTime <= 
                 DATEADD(MONTH, -12, GETUTCDATE());

WHILE @@ROWCOUNT > 0
BEGIN
        DELETE TOP (1000) OrderChangeLog
        OUTPUT  DELETED.OrderChangeLogID,
                        DELETED.OrderID,
                        DELETED.CustomerID,
                        DELETED.ModifiedDateTime,
                        DELETED.ModifiedByPersonID,
                        DELETED.ChangeRequestAPIDetails,
                        DELETED.BatchNumber,
                        DELETED.ColumnChanged,
                        DELETED.OldValue,
                        DELETED.NewValue
        INTO dbo.OrderChangeLogArchive
        WHERE OrderChangeLog.ModifiedDateTime <= 
                 DATEADD(MONTH, -12, GETUTCDATE());
END

Note that the IDENTITY attribute was removed from OrderChangeLogID, as this value will be taken directly from the source table. Therefore, there is no need to modify or create a value for it. Using DELETE coupled with OUTPUT DELETED allows for data to be deleted from the source table and inserted into the archive table in a single step, which simplifies code and allows the operation to be more atomic than if we were to write these as separate steps. Lastly, the archive table is page compressed, to save space. Since it presumably will not by modified via UPDATE statements, this is a safe way to save storage, memory, and speed up data access.

If this table were to be exceptionally large, there may also be value in partitioning it. This would allow for more efficient maintenance operations, if sheer table size became problematic. The archive table does not need to reside in the same database or even the same server. Some organizations prefer archived data to be sent to reporting/analytics environments, to a data lake, or some other less expensive environment. This decision would be based on who needs the data and how often it would be needed.

Delete Specific Dimensions

Sometimes historical data is important, but not all parts of it are needed once enough time has passed. For example, consider the table discussed earlier:

CREATE TABLE dbo.OrderChangeLog
(       OrderChangeLogID INT NOT NULL IDENTITY(1,1) 
        CONSTRAINT PK_OrderChangeLog PRIMARY KEY CLUSTERED,
        OrderID INT NOT NULL,
        CustomerID INT NOT NULL,
        ModifiedDateTime DATETIME NOT NULL,
        ModifiedByPersonID INT NOT NULL,
        ChangeRequestAPIDetails VARCHAR(MAX) NOT NULL,
        BatchNumber INT NOT NULL,
        ColumnChanged VARCHAR(128) NOT NULL,
        OldValue VARCHAR(MAX) NULL,
        NewValue VARCHAR(MAX) NULL
);

Within this table are three VARCHAR(MAX) columns that can potentially be very large. The remaining columns are relatively small and limited in size. A common retention scenario occurs when the logging of changes is critical, but the extensive details are no longer needed after some period. In this table, the column ChangeRequestAPIDetails is likely to be the largest column as it will contain some sort of detail regarding API calls. Whether stored as XML, JSON, test, or some other format, let us consider the scenario where this is by and far the widest column in the table by a significant margin.

For an organization where knowledge of change is critical, but complete detail becomes unneeded after a year, a retention process could be implemented that sets the column ChangeRequestAPIDetails to NULL, blank, or some other trivial value that indicates that the data was removed for retention purposes.

By clearing out the largest column(s) in a table, a significant amount of space can be saved, especially if the table is also the target of compression. The result is a table that still provides extensive history but omits detail that does not need to be saved forever.

A process to do this would be very similar to retention processes introduced earlier:

UPDATE TOP (1000) OrderChangeLog
        SET ChangeRequestAPIDetails = NULL
FROM dbo.OrderChangeLog
WHERE OrderChangeLog.ModifiedDateTime <= DATEADD(MONTH, -12, GETUTCDATE())
AND OrderChangeLog.ChangeRequestAPIDetails IS NOT NULL;

WHILE @@ROWCOUNT > 0
BEGIN
        UPDATE TOP (1000) OrderChangeLog
                SET ChangeRequestAPIDetails = NULL
        FROM dbo.OrderChangeLog
        WHERE OrderChangeLog.ModifiedDateTime <= 
                     DATEADD(MONTH, -12, GETUTCDATE())
        AND OrderChangeLog.ChangeRequestAPIDetails IS NOT NULL;
END;

This is another effective way to reduce the footprint of historical data when specific columns are quite large and are no longer needed once outside of a prescribed retention period.

Data Compression

When data is older and less often used, compression is an excellent way to save on storage space, memory, and often speed up queries against it. While a full discussion of row and page compression is out-of-scope for this article, Microsoft has extensive documentation on the implementation and best practices for each: https://learn.microsoft.com/en-us/sql/relational-databases/data-compression/data-compression?view=sql-server-ver16

So long as archived data is expected to not be updated often (or ever), page compression would be the most likely candidate to use on it. Page compression can reduce the footprint of repetitive data by 50%-75% or sometimes more. Compression ratios are based on the data types, values, and repetitiveness of the underlying data. For administrators or developers interested in estimating the impact of compression prior to implementation, they may use the system stored procedure sp_estimate_data_compression_savings.

Columnstore compression (and columnstore archive compression) are also viable alternatives for compressing archived data. There are more caveats as to which data profiles are ideal for columnstore compression. Ideally, columnstore compressed data should be narrower, repetitive, and somewhat similar to data warehouse-style data. That being said, it is intended for analytic data. Columnstore indexes will underperform on highly transactional data. More info on columnstore indexes can be found in this hands-on series on SimpleTalk.

Partitioning a Table

Partitioning allows a table to be physically split up by a specific dimension or set of dimensions, allowing each partition to be managed separately. This allows for faster and easier maintenance operations, as well as potentially improved performance.

In a table that blends OLTP and OLAP, oftentimes the recent data is the target of most transactional operations, whereas the older data is the target of analytic operations (if it is even accessed often).

Relating to retention, the most significant benefits of partitioning are:

  • The ability to compress each partition differently. This allows active data to remain uncompressed whereas less active data can be row or page compressed.
  • Partition truncation and range splits/merges can be used to quickly delete old data via truncation. The details of how to automate sliding partition ranges are out of scope for this article, but there is significant documentation on partitioning, as well as how to configure dynamic partition ranges. Microsoft’s documentation is a great place to start: https://learn.microsoft.com/en-us/sql/relational-databases/partitions/partitioned-tables-and-indexes?view=sql-server-ver16

Partition elimination allows queries against a partitioned table to automatically skip reading data from partitions that do not match a filter criteria against the partition column(s). This is typically not the primary reason to partition a table, but it can provide a significant performance boost for tables that have a limited set of active data and an extensive set of inactive data, such as tables that are targeted with retention policies.

Partitioned tables require some components to be used:

  • Partition Function: Defined the boundaries that will be used to divide data up based on a key column or columns.
  • Partition Scheme: Determines which filegroups will be used to store data based on the details of the partition function.
  • Database Filegroups and Files: Data in a partitioned table can be stored in any filegroup or file, but the ideal way to do so is to have a separate filegroup/file for each partition. This allows each partition to be managed separately without impacting other data.

The following is a simple example of how to partition the table introduced earlier in this article:

ALTER DATABASE WideWorldImporters 
     ADD FILEGROUP WideWorldImporters_2022_fg;

ALTER DATABASE WideWorldImporters 
     ADD FILEGROUP WideWorldImporters_2023_fg;

ALTER DATABASE WideWorldImporters 
     ADD FILEGROUP WideWorldImporters_2024_fg;

ALTER DATABASE WideWorldImporters 
     ADD FILEGROUP WideWorldImporters_2025_fg;

ALTER DATABASE WideWorldImporters 
     ADD FILEGROUP WideWorldImporters_2026_fg;

ALTER DATABASE WideWorldImporters 
  ADD FILE 
(NAME = WideWorldImporters_2022_data, 
    FILENAME = 'C:\SQLData\WideWorldImporters_2022_data.ndf',
         SIZE = 100MB, MAXSIZE = UNLIMITED, FILEGROWTH = 500MB)
TO FILEGROUP WideWorldImporters_2022_fg;

ALTER DATABASE WideWorldImporters 
  ADD FILE
 (NAME = WideWorldImporters_2023_data, 
    FILENAME = 'C:\SQLData\WideWorldImporters_2023_data.ndf',
         SIZE = 100MB, MAXSIZE = UNLIMITED, FILEGROWTH = 500MB)
TO FILEGROUP WideWorldImporters_2023_fg;

ALTER DATABASE WideWorldImporters 
  ADD FILE
 (NAME = WideWorldImporters_2024_data, 
    FILENAME = 'C:\SQLData\WideWorldImporters_2024_data.ndf',
         SIZE = 100MB, MAXSIZE = UNLIMITED, FILEGROWTH = 500MB)
TO FILEGROUP WideWorldImporters_2024_fg;

ALTER DATABASE WideWorldImporters 
  ADD FILE
 (NAME = WideWorldImporters_2025_data, 
    FILENAME = 'C:\SQLData\WideWorldImporters_2025_data.ndf',
         SIZE = 100MB, MAXSIZE = UNLIMITED, FILEGROWTH = 500MB)
TO FILEGROUP WideWorldImporters_2025_fg;

ALTER DATABASE WideWorldImporters 
  ADD FILE
 (NAME = WideWorldImporters_2026_data, 
    FILENAME = 'C:\SQLData\WideWorldImporters_2026_data.ndf',
         SIZE = 100MB, MAXSIZE = UNLIMITED, FILEGROWTH = 500MB)
TO FILEGROUP WideWorldImporters_2026_fg;
GO
CREATE PARTITION FUNCTION   
   OrderChangeLogPartitioned_years_function (DATE)
AS RANGE RIGHT FOR VALUES
('2023-01-01', '2024-01-01', '2025-01-01', '2026-01-01');
GO
CREATE PARTITION SCHEME fact_order_BIG_CCI_years_scheme
AS PARTITION OrderChangeLogPartitioned_years_function 
TO (WideWorldImporters_2022_fg, WideWorldImporters_2023_fg, 
    WideWorldImporters_2024_fg, WideWorldImporters_2025_fg, 
    WideWorldImporters_2026_fg);
GO
CREATE TABLE dbo.OrderChangeLogPartitioned
(  OrderChangeLogID INT NOT NULL IDENTITY(1,1) 
      CONSTRAINT PK_OrderChangeLogPartitioned 
           PRIMARY KEY CLUSTERED,
   OrderID INT NOT NULL,
   CustomerID INT NOT NULL,
   ModifiedDateTime DATETIME NOT NULL,
   ModifiedByPersonID INT NOT NULL,
   ChangeRequestAPIDetails VARCHAR(MAX) NOT NULL,
   BatchNumber INT NOT NULL,
   ColumnChanged VARCHAR(128) NOT NULL,
   OldValue VARCHAR(MAX) NULL,
   NewValue VARCHAR(MAX) NULL
) ON fact_order_BIG_CCI_years_scheme (ModifiedDateTime);

The code above performs the following tasks:

  1. Creates 5 filegroups to logically store data from the partitioned table. These can be shared with other partitioned tables as time periods overlap.
  2. Creates 5 files to physically store data for the filegroups created above.
  3. Creates a partition function to split the data up logically based on date ranges.
  4. Creates a partition schema to determine where data should be stored based on the details of the partition function.
  5. Creates a table on the partition scheme, rather than on PRIMARY.

When data is inserted into the partitioned table, it will be stored in the data file corresponding to the value of ModifiedDateTime automatically. From here, there is a wide variety of options that open up, allowing partitions to be manipulated independently of each other, like this:

TRUNCATE TABLE dbo.OrderChangeLogPartitioned 
       WITH (PARTITIONS(1));

ALTER INDEX PK_OrderChangeLogPartitioned 
   ON dbo.OrderChangeLogPartitioned REBUILD PARTITION = 4 
      WITH (DATA_COMPRESSION = PAGE);

Being able to truncate a partition allows retention to be applied to a table without the need to delete rows and consume time and transaction log space. Being able to perform index maintenance and only target a current partition (rather than historical data) will reduce the time and resources needed to perform that maintenance.

Turn Old OLTP Data into OLAP Data

Oftentimes, as data transitions from being hot to warm and then to cool/cold, its purpose shifts from transactional to analytic in nature. What used to be data that was frequently inserted, updated, and deleted, is now read-only and only a small fraction of it is used at any one time.

In these scenarios, consider storing this data as analytic data, rather than as transactional. This provides more flexibility to you to choose an analytic storage solution that maximizes compression, read speeds, and whatever indexing/optimizations support the reads that will occur against it. In this case, the data will be used in fundamentally different ways. Therefore, it can be stored in a fundamentally different way.

If the data needs to remain in SQL Server, consider columnstore indexes or page compression to maximize space/memory savings. Also consider removing any unneeded covering indexes that used to support transactional activities, but now are no longer applicable.

If the data can leave SQL Server, then consider whatever solutions your organization already has available for large, relatively static analytic data. Solutions such as Synapse Analytics, Fabric, a data warehouse, Azure Databricks, and others are all potential options. Which you choose will be based on your own skill set and the tools available to your organization.

Other Options

These are some of the more common retention methods and their application in SQL Server. There are many other ways to manage retention, including third-party applications, hardware solutions, hosted data management platforms, and more. Choose the retention plan that fits your environment best, as it may be a single option discussed here or a combination of many.

A Note on Retention Process Timing and Impact

Any process that manipulates large amounts of data should be isolated to the safest time possible to run. This may be overnight, early in the morning, on a weekend, or during some other quiet time. Retention should not negatively impact the systems they service.

In a highly transactional environment, changes should occur at times when the fewest users are trying to access the impacted data. Any data manipulation should be batched to avoid locking underlying objects and blocking other users from accessing them. In most OLTP systems, this ideal time is late in the evening or early in the morning. In geo-located or distributed/global systems, retention timing can be customized based on the time zone or other usage details.

In an analytic environment, retention changes should occur either as part of data load processes or at times when they will not conflict with data loads. Similarly, if retention could impact analytics, then those processes should run when as few analytic processes are expected to run. Since administrators generally have more control over OLAP environments, finding a good time to implement retention processes will often be simpler than in buys transactional environments.

The Magic of Retention Policies

Understanding retention methods is helpful, but without a formal agreement between business stakeholders and those responsible for its data, retention will not exist in a meaningful way.

A retention policy is an official contract between a business, its users, its employees, and any other stakeholders that determines how long data will be retained. The details of this policy might be simple, or they may be nuanced and complex.

For example, an organization might decide that any transactional data older than 10 years can be deleted forever. This retention policy sounds quite simple and would be documented so that all stakeholders have access to it and understand it. However, you do have to be careful with things like product data, which, unlike transactional data that records Bob from Hoboken purchasing a red shelf set, may have a lifecycle greater than 10 years.

Retention policies are often referenced in contracts, data privacy agreements, or other terms-of-service agreements. Note that determining what data is 10 years or older might be complex:

In this image, it becomes clear that some technical decision-making will be required to determine exactly what “10 years of retention” means, but this is a question that can be answered with some patience and diligence.

If the question is asked: “How long is this data kept for?”, the answer should be, “it is in the retention policy” (or something similar). Without a policy, no one knows for sure. Without a policy, the answer can be murky:

  • “We keep this data forever. I think(?)”
  • We keep this data for 2 years. This undocumented process handles it!
  • We keep the data forever, but sometimes delete an arbitrary amount when performance issues arise.
  • We keep this data for 1 year. Except on this one legacy server where it is 2 years. And for these 10 customers where it is 3 years. Our subject-matter expert knows all of this, so it is OK.

Any conversation remotely like these should leave us feeling a bit queasy. The uncertainty is the problem and the solution is a policy to remove the need for guesswork. With a policy, the answer to “How long is this data kept for?” will be one of two possible answers:

  • It is in the retention policy: right here.
  • It is not in the retention policy. We will decide on a retention period and add it to the policy.

A retention policy is the final step to solving the problem introduced in this article – so do not skip it! In general, organizations may be hesitant to apply retention at first, but once they understand that this is not as hard as it seems, they’ll warm up to the idea and become more open to it over time.

Keep in mind that a retention policy can state that certain data is retained indefinitely. This is 100% acceptable. As long as the organization acknowledges this fact and will ensure that the hardware and software are available to support the size of forever-data, then it is OK. Having the policy that states this is far superior to having no policy and *thinking* that the data lasts forever when it may or not actually be accurate.

Conclusion

Retention is an important dimension of data architecture, development, and operations that allows data size and scope to remain manageable over time. Simple questions, research, and answers can help shape the retention periods for different objects and help ensure that the time period that data is retained for is aligned with its purpose within an application.

Retention policies formalize retention and ensure that retention details are communicated effectively to any stakeholders that may need to understand them. Retention without a retention policy often takes the form of unofficial, undocumented, or confusing processes that are known to some people, but not to others. A policy removes confusion and replaces it with fact.

As a bonus, being the author and enabler of a retention policy presents a leadership and collaboration opportunity that can be helpful to one’s career. These are all opportunities to improve an organization’s data, which will ultimately ensure that processes that use that data are more effective. This includes everything from applications to reports to AI. Good data ensures these applications are effective, whereas excessively large or confusing data can impede that progress.

 

The post The Importance of Retention appeared first on Simple Talk.



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

Wednesday, January 17, 2024

Fabric and Self-Service: Control who can use

We can say Fabric is the evolution of the Power BI environment.

Power BI is a self-service environment, and so is Fabric. This allows the implementation of very interesting architectures, which will be the subject of future videos and articles.

However, it’s not something free-and-easy, and it shouldn’t be. Using Fabric Admin Portal (or Power BI Admin Portal? It’s starting to be confusing) you can control who will have access to create Fabric objects.

A screenshot of a screen Description automatically generated

We have the following possibilities:

  • Leave it enable for the entire organization. It will become free-and-easy, I don’t recommend.
  • Specify the groups which can create Fabric objects
  • Specify groups which can’t create fabric objects

In general, the best option is to define “who can”, instead of “who can’t”. I would love to hear about exception scenarios for this.

Fabric is integrated with Azure Active Directory (ops… Entra IDs). In this way this permission control integrates with your organization permissions and you can define which teams can create Fabric objects by creating Entra groups (Active Directory groups).

The fact this ability fits with perfection on the purposes of a Data Mesh implementation is a subject for future articles and videos

 

The post Fabric and Self-Service: Control who can use appeared first on Simple Talk.



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

Tuesday, January 16, 2024

Working on Technology Off the Clock

I have always spent time learning relational databases on my own, non-work time. As the years have passed, I have spent less of my own free time on technical learning and writing for many reasons, but still, I spend plenty of my own time learning about the database technology that Codd started.

While I never actually kept count, I have spent at least thousands of hours blogging, writing books, learning, and just playing around with SQL Server features to figure out how they work. I have also worked with some very competent people who only spent 40 hours a week working with the same technology.

I am not here to bash those making a great living just working at your day job. In some ways, this blog is more here to celebrate those who do a great job just during their day job. Over the years, I have written many blogs about What Counts for a DBA. In those years, I probably came across as thinking you were total slackers. It was what I knew and what I loved. All of the people I knew who were great were doing the same things.

In my early years, I spent just unthinkable amounts of time writing/learning. My first book took 20 hours or more each week for almost a year. I bought a very early smartphone to do a solid part of the writing on the go. (I still do this to this day, though the phone I am writing on now has many more temptations than the old green screen Palm device did!) I wrote at my daughter’s sports events, doctor’s visits, concerts, and even once while working the PowerPoint for a wedding. Sleep was commonly joked about but rarely done to any satisfactory level.

The company I worked for always appreciated my effort, but never required it. The skills I learned doing the writing were often useful in my daily work, but never once was it required. Spending lots of your free time being required by your employer is one of the main arguments I see people talk about when they are pessimistic about doing extra-curricular learning. “This is something you are doing for your company.” I never thought of it that way. I thought of it as doing something that I wanted to do that was (mostly) kind of fun.

Do I regret it? I do have regrets. While I never let it crowd out my family/spiritual life, yet I did let it take over the rest of my life. I pretty much stopped exercising, I stopped eating food that didn’t come from a bag with a character on the side, and sleep became something that lasted far less time than the surgeon general would have praised me for. If I had any advice for someone reading this blog, it would be to take care of your health first, then keep learning.

What about yourself? Do you have any feelings about treating work like a hobby? Drop your opinions in the comments!

The post Working on Technology Off the Clock appeared first on Simple Talk.



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

Making Accessibility Part of the Design Process – Part 2

We live in the height of the digital age, where the digital space has become a thriving community, with every person craving a great yet personalized experience. In this era, there is one centralized truth with undeniable clarity: Accessibility is no longer a mere option; it is the cornerstone that will lead to the creation of a truly welcoming community.

In this second part of our series on accessibility design, we will discuss essential topics that lie at the heart of crafting accessible digital environments. We’ll begin by throwing more light on three of the five fundamental visual patterns vital to your designs:

  • Color Contrasting
  • Font Sizing
  • Labelling and Iconography

As we explore accessibility, it’s essential to recognize that it isn’t limited to addressing the needs of individuals with disabilities; it’s about shaping a digital terrain where everyone thrives. It’s about breaking barriers and creating an online environment that empowers and enriches the lives of all individuals.

It’s important to note that certain sections of our discussion may pose a challenge, as we discuss the intricate process of creating accessible web pages and applications. While there may not be actual lines of code within the document, our primary focus is on unraveling the fundamental concepts that drive web development.

So, whether you’re here to satisfy your curiosity, deepen your tech knowledge, or champion digital inclusivity, you’re in the right place.

Exploring Some of the Basics of Accessibility about Visual Patterns of Accessibility

In our previous article, we provided a brief introduction to these concepts. In this section, we will delve deeper into each aspect, gaining a comprehensive understanding of the specific WCAG 2 standards that must be met for each of these criteria.

Color Contrasting

For color contrasting, the minimum conformance level to be met is AA. (As noted in the previous entry, the scale goes from A to AAA.) Web design at this conformance level should have text or images of text with a contrast ratio of at least 4.5:1 and/or 3.0:1 for larger texts. The second conformance level to be met is AAA, which is a higher level compared to level AA. At AAA, texts or images of text should have a contrast ratio of at least 7.0:1 and/or 4.5:1 for larger texts. Larger texts are usually classified as fonts with font-weight greater than 24 pixels (regular) and 18 pixels (for bold texts).

These conformance levels when used during design, ensure that there is adequate differentiation in the visual elements (i.e., foreground text and background).

During design and building, you should endeavor to test for the conformance level and optimize your work accordingly. Here are some practical ways to measure and ensure color contrast: 

  • Online Contrast Checkers: There are many online tools and contrast checkers that can help you evaluate the contrast between two colors. These tools often provide a pass/fail assessment based on WCAG guidelines. Some popular ones include: 
  • Browser Extensions: Some browser extensions can analyze color contrast on web pages. These extensions can provide accessibility assessments, including color contrast checks. Some common ones are:
  • Color Pickers with Contrast Info: Some color picker tools, like the one in Adobe Photoshop or online color pickers, provide contrast information between selected colors. This can be helpful for designers and developers. 
  • Contrast Ratio Color Grids: Some design systems and frameworks offer predefined color palettes with contrast ratios already calculated. For example, the Material Design system provides such color palettes to ensure accessibility. 
  • Design Tools: Graphic design software like Adobe XD and Figma often have built-in accessibility features and plugins that can help you check color contrast as you design. 
  • Manual Calculation: If you want to calculate the contrast ratio manually, you can use the formula provided by WCAG. The formula is:
    (L1 + 0.05) / (L2 + 0.05)
    In the next section, I will demonstrate how this is done manually, which is great to at least have some idea of what the tools do.

Calculating color contrasting manually

The formula is as stated:

Contrast Ratio = (L1 + 0.05) / (L2 + 0.05).

Let’s break down each component of this formula:

  • Contrast Ratio: This is the final result you’re trying to calculate. The contrast ratio represents the difference in luminance between two colors. It is often used in web design and graphic design to ensure text and content are easily readable against background colors.
  • L1 (Relative Luminance of the Lighter Color): L1 represents the relative luminance of the lighter of the two colors in the comparison. It’s the luminance value for the color you want to ensure is readable.
  • L2 (Relative Luminance of the Darker Color): L2 represents the relative luminance of the darker of the two colors in the comparison. It’s the luminance value for the background or surrounding color.
  • 0.05 (Constant Value): This is a constant value (0.05) added to both L1 and L2 in the formula. It’s a small constant that’s used to prevent the contrast ratio from reaching zero in cases where one of the colors has very low luminance. It ensures that even in cases of low luminance, there’s still some level of contrast. By adding 0.05 to both L1 and L2, you ensure that even if one color has a luminance of 0 (absolute black) or is very close to 0, the contrast ratio doesn’t become undefined, which is important for accessibility considerations.

As stated earlier, based on the WCAG 2 guidelines for color contrasting, the minimum level of conformance to be met is AA. Web design at this conformance level should have text or images of text with a contrast ratio of at least 4.5:1 and/or 3.0:1 for larger texts. The second conformance level to be met is AAA, which is a higher level compared to level AA. At AAA, texts or images of text should have a contrast ratio of at least 7.0:1 and/or 4.5:1 for larger texts. Larger texts are usually classified as fonts with font-weight greater than 24 pixels (regular) and 18 pixels (for bold texts).

Calculation of Luminance (L1 and L2)

Luminance is a measure of the perceived brightness of a color. There are various ways to calculate luminance, but one commonly used formula is the relative luminance formula, which is part of the Web Content Accessibility Guidelines (WCAG) for web design. This formula converts a color to a grayscale value based on the sRGB color space. The relative luminance (Y) of a color in the sRGB color space can be calculated using the following formula:

  • Y = 0.2126 * R + 0.7152 * G + 0.0722 * B

This formula calculates the luminance of a color by considering its red, green, and blue components, taking into account the varying sensitivity of the human eye to different colors.

  • Y is the relative luminance.
  • R (Red Component): R represents the red component of the color. In the formula, R should be a normalized value between 0 and 1, where 0 means no red, and 1 means full intensity red.
  • G (Green Component): G represents the green component of the color. Similar to R, G should be a normalized value between 0 and 1, where 0 means no green, and 1 means full intensity green.
  • B (Blue Component): B represents the blue component of the color. Again, B should be a normalized value between 0 and 1, where 0 means no blue, and 1 means full intensity blue.
  • The coefficients (0.2126, 0.7152, and 0.0722) are specific weights assigned to each color component to simulate the human eye’s sensitivity to different colors. These weights are derived from the CIE 1931 standard, which is a mathematical model of human color vision. The human eye is more sensitive to green light, moderately sensitive to red light, and less sensitive to blue light, which is why these coefficients are used to approximate the perceived luminance.

Worth Noting:

To calculate the luminance of a color in this manner, you’ll need to ensure that the RGB values are in the sRGB color space and are normalized to a range of 0 to 1.

So, for example, pure white (255, 255, 255) should be divided by 255 to get the normalized RGB values (1, 1, 1), while pure black (0, 0, 0) would be (0, 0, 0).

Examples of Colors with Good Contrast Ratio:

  • White Text (RGB: 255, 255, 255) on Black Background (RGB: 0, 0, 0):
          
    • L1 (White Text) = 1
    • L2 (Black Background) = 0
    • Contrast Ratio = (1 + 0.05) / (0 + 0.05) = 1.05 / 0.05 = 21
    • White text on a black background meets the WCAG standard with a contrast ratio of 21.
  • White Text (RGB: 255, 255, 255) on Crimson Background (RGB: 255, 8, 74):

    • L1 (White Text) = 1
    • L2 (Crimson Background) = 0.2126 * 255/255 + 0.7152 * 8/255 + 0.0722 * 74/255 ≈ 0.1587
    • Contrast Ratio = (1 + 0.05) / (0.1587 + 0.05) ≈ 1.05 / 0.2087 ≈ 5.03
    • White text on a crimson background meets the WCAG standard with a contrast ratio of 5.03.
  • Cream Text (RGB: 255, 253, 208) on Dark Red Background (RGB: 139, 0, 0):
     
    • L1 (Cream Text) = 0.2126 * 255/255 + 0.7152 * 253/255 + 0.0722 * 208/255 ≈ 0.8911
    • L2 (Dark Red Background) = 0.2126 * 139/255 + 0.7152 * 0/255 + 0.0722 * 0/255 ≈ 0.0881
    • Contrast Ratio = (0.8911 + 0.05) / (0.0881 + 0.05) ≈ 0.9411 / 0.1381 ≈ 6.81
    • Cream text on a dark red background meets the WCAG standard with a contrast ratio of 6.81.

Examples of Colors with Bad Contrast Ratio

  • Light Gray Text (RGB: 200, 200, 200) on a White Background (RGB: 255, 255, 255):
     
    • L1 (Light Gray Text) = 0.7152 * 200/255 + 0.2126 * 200/255 + 0.0722 * 200/255 ≈ 0.7376
    • L2 (White Background) = 1
    • Contrast Ratio = (0.7376 + 0.05) / (1 + 0.05) ≈ 0.7876 / 1.05 ≈ 0.75
    • The contrast ratio of 0.75 is significantly lower than the WCAG requirement of 4.5:1, indicating that light gray text on a white background does not meet the accessibility standard.
  • Dark Blue Text (RGB: 0, 0, 128) on a Black Background (RGB: 0, 0, 0):
    • L1 (Dark Blue Text) = 0.7152 * 0/255 + 0.2126 * 0/255 + 0.0722 * 128/255 ≈ 0.1804
    • L2 (Black Background) = 0
    • Contrast Ratio = (0.1804 + 0.05) / (0 + 0.05) ≈ 0.2304 / 0.05 ≈ 4.608
    • While this combination barely meets the WCAG minimum requirement of 4.5:1, it is considered barely acceptable. It is not a color combination that you should generally choose to use, which is why I included it in the not acceptable section.
  • Light Yellow Text (RGB: 255, 255, 128) on a Pale Yellow Background (RGB: 255, 255, 192):
     
    • L1 (Light Yellow Text) = 0.7152 * 255/255 + 0.2126 * 255/255 + 0.0722 * 128/255 ≈ 0.9279
    • L2 (Pale Yellow Background) = 0.7152 * 255/255 + 0.2126 * 255/255 + 0.0722 * 192/255 ≈ 0.8632
    • Contrast Ratio = (0.9279 + 0.05) / (0.8632 + 0.05) ≈ 0.9779 / 0.9132 ≈ 1.068
    • The contrast ratio of 1.068 is lower than the WCAG requirement of 4.5:1, indicating that light yellow text on a pale yellow background does not meet the accessibility standard.
  • Red Text (RGB: 255, 0, 0) on Blue Background (RGB: 0, 0, 255):
     
    • L1 (Red Text) = 0.2126 * 255/255 + 0.7152 * 0/255 + 0.0722 * 0/255 ≈ 0.2126
    • L2 (Blue Background) = 0.2126 * 0/255 + 0.7152 * 0/255 + 0.0722 * 255/255 ≈ 0.0722
    • Contrast Ratio = (0.2126 + 0.05) / (0.0722 + 0.05) ≈ 0.2626 / 0.1222 ≈ 2.15
    • Red text on a blue background does not meet the WCAG standard with a contrast ratio of 2.15, as it falls below the minimum requirement.

Font Sizing

Unlike color contrasting, there is no formula, specific font size or weight that has been given. However, it is generally recommended not to use small fonts. Generally, small fonts are fonts less than 24px (when regular) and 18px (when bold).

Ideally you should not go beyond 8px and your hierarchy of pixels should be in increments of 8 (i.e., 8px, 16px, 24px, 32px, 40px, 48px, etc.). This allows for variation in text sizes and allows for a proper hierarchy to be achieved, which is encouraged by the guidelines. It is also pertinent to know that body text should not be smaller than 16px and 8px should be used sparingly. Most people will go for 12px instead of 8px, which is acceptable since smaller sizes than 12px are illegible on some smaller devices.

To fix the issue of illegibility, the guidelines have an AA level of conformance to be met. This conformance requires that, except for captions and images of text, the text should be resizable without the need for assistive technology, up to a 200 percent increase in size. Despite this increase in size, the text should also be able to maintain its content and functionality. A liquid layout, that adapts the text by resizing and reflowing it as necessary to fit the on-screen display, is also recommended.

Furthermore, concerning the scalability of text, the guidelines recommend that text be sized in percentages, em units, or with named fonts (e.g., larger, smaller). This allows for the adaptability of text (Understanding Success Criterion 1.4.4 | Understanding WCAG 2.0, 2008).

Visual Presentation of Text

Based on the AAA conformance level, text blocks should be made visually pleasant, with the aid of certain available mechanisms, aimed at enhancing user customization and readability.

These mechanisms adhere to specific guidelines:

  • User Customization: Users should have the option to choose their preferred foreground and background colors, allowing for the personalization of the reading experience.
  • Optimal Width: The width of the text blocks should be limited to 80 characters or glyphs (40 if using CJK characters) per line. This restriction prevents long lines of text, which present a challenge when being read, especially on wide screens.
  • Text Alignment: Text should not be justified. Justification sometimes creates uneven spacing between words, negatively impacting readability.
  • Line and Paragraph Spacing: Within paragraphs, the leading line spacing should be set to at least space-and-a-half, providing enough vertical space between lines for better readability. Furthermore, the spacing between paragraphs should be at least 1.5 times larger than the line spacing, further enhancing the visual separation between paragraphs.
  • Horizontal Scrolling Prevention: When resizing the text to 200 percent, users should not need to scroll horizontally to read a line of text on a full-screen window. This is aimed at preventing the inconvenience of having to move horizontally to read complete lines.

Text Spacing

The conformance level to be met for text spacing is level AA. In content created using markup languages, an optimal visual presentation of text should be ensured by the implementation of specific text style properties. By incorporating the following guidelines, content can maintain its integrity and functionality while allowing users to customize their reading experience:

  • Line Height: The line spacing (line height) should be at least 1.5 times the font size. This provides sufficient vertical space between lines and enhances readability.
  • Spacing after Paragraphs: The spacing after paragraphs should be at least 2 times the font size. This allows for clear visual separation between paragraphs, making navigation and comprehension easier.
  • Letter Spacing: The letter spacing, also known as tracking, should be at least 0.12 times the font size. Proper letter spacing enhances legibility and readability, especially for persons with visual impairments or reading difficulties.
  • Word Spacing: This should be set to at least 0.16 times the font size. This improves the flow and readability of the text.

There are some exceptions, which apply to human languages and scripts that do not use one or more of these text style properties in their written form. In such cases, adherence to the properties applicable to the specific language and script combination is sufficient to meet the requirements of the conformance level (Understanding Success Criterion 1.4.12: Text Spacing | WAI | W3C, 2008).

Labelling and Iconography

Concerning labelling and iconography, the guide first speaks about 2 things that meet the level of conformance AA (Understanding Success Criterion 2.4.6: Headings and Labels | WAI | W3C, 2023). These are:

  • The Importance of Clear and Descriptive Headings and Labels: Clear and descriptive headings and labels, help users easily find the information they are looking for and understand the relationships between different parts of the content. Descriptive labels give users more information about the content, which helps users identify specific components within the content. This provides a user-friendly and accessible experience.
  • Length of Labels and Headings: Although labels and headings are required, the guide does not necessarily require them to be lengthy. If a suitable cue for finding and navigating the content effectively can be provided by a single word or character, then it can suffice.

Furthermore, all components of the user interface websites – such as form elements, links, and components generated by scripts, should be programmatically accessible. This complies with conformance level A. The aim of this is to make sure that user interface elements such as the name, role, states, properties, and values can be programmatically determined and set. This allows assistive technologies and user agents to understand and communicate these components effectively to users with disabilities. However, already existing HTML controls meet this level of conformance. Thus, this applies mostly to custom user interface components, ensuring that they are inclusively accessible (Understanding Success Criterion 4.1.2: Name, Role, Value | WAI | W3C, 2008).

Placeholder Text Vs Helper Text

Placeholder text serves as a temporary guide, often faint or italicized, within a form field, offering an example of the expected user input. It’s a space-efficient way to prompt users without permanently occupying the field. Once users begin typing or interacting, the placeholder text usually disappears. While suitable for brief hints, it isn’t a substitute for explicit instructions and may have limitations in accessibility and user understanding.

In contrast, helper text provides additional information or instructions displayed above, alongside, or below a form field. Unlike placeholder text, helper text remains visible after user engagement, offering detailed guidance and ensuring clarity throughout the interaction. It is valuable for comprehensive information, preventing user confusion, and meeting accessibility standards by providing explicit instructions about required input.

With the rise of minimalism, form labelling faces challenges. Some designers favour placeholder texts over helper texts for aesthetic reasons, but this may compromise accessibility. Placeholder texts, while visually appealing, can create difficulties for users who forget the required information once the example disappears.

To navigate the challenges posed by minimalism in form labelling, adhering to the Web Content Accessibility Guidelines (WCAG) 2 is paramount. Let’s explore the essential criteria and best practices for achieving conformance at different levels, emphasizing the importance and reasons for each:

  1. 1. Conformance to Level A – Criteria 1.3.1 (Info and Relationships): At the foundational Level A, the goal is to make information, structural elements, and relationships programmatically accessible or available in text format. This is crucial because:
  2. Accessibility: By utilizing proper semantic HTML elements and ensuring tables are appropriately labelled, you enhance the accessibility of your content, allowing assistive technologies to interpret and present information effectively.
  3. User Understanding: Incorporating ARIA roles and attributes provides additional context to users, improving their understanding of the content and its structure.
  4. Conformance to Level AA – Criteria 2.4.6 (Headings and Labels): Elevating the standard to Level AA emphasizes the clarity of headings and labels within the content. The reasons for this emphasis include:
  5. Enhanced User Experience: Descriptive and informative headings facilitate an enhanced user experience by providing clear navigation cues. Users can easily understand the content and navigate through it.
  6. Usability: Explicit associations between label elements and input fields improve usability, especially for users relying on assistive technologies. It ensures a seamless and intuitive interaction.
  7. Conformance Level A – Criteria 3.3.2 (Labels or Instructions): At the foundational Level A, clear labels or instructions are imperative when user input is required. The importance of this lies in:
  8. User Guidance: Clear and concise labels offer users guidance, reducing the likelihood of errors and enhancing the overall user experience.
  9. Accessibility: Helpful instructions or cues during form completion make the interaction more accessible, particularly for users who may require additional guidance.
  10. Conformance Level A – Criteria 4.1.2 (Name, Role, Value): Effective communication at Level A involves ensuring user interface components have identifiable names and roles that can be programmatically determined. The significance of this includes:
  11. Programmatic Accessibility: Proper identification and labelling with semantic HTML elements and ARIA roles ensure programmatic accessibility. This is crucial for assistive technologies to convey information effectively to users.
  12. User Understanding: Assigning appropriate attributes like “name” and “value” ensures that users can understand and interact with interface components, contributing to a more inclusive user experience.
  13. Programmable Setability of States, Properties, and Values: This is also in conformance to Level A of Criteria 4.1.2 (Name, Role, Value). Based on this, it is important that if users can change certain things like settings, features, or values, they should be able to do it using a program or tool.

The importance of these criteria is underscored by:

  • Programmatic Control: Allowing users to modify states, properties, and values programmatically ensures that individuals who rely on assistive technologies have full control over the interactive elements on the website. This is fundamental for a diverse user base with varying needs.
  • Real-Time Feedback: By notifying user agents and assistive technologies of any changes made to these components, the web environment becomes more dynamic and responsive. Users, including those with disabilities, receive real-time feedback, fostering a more engaging and accessible interaction.
  • Enhanced User Autonomy: Programmatically settable components empower users, enabling them to customize their experience based on personal preferences and accessibility requirements. This contributes to a sense of autonomy and inclusivity.

Achieving optimal accessibility involves a thoughtful balance between placeholder texts and helper texts in form design. While placeholder texts offer a temporary guide for user input, their exclusive use can pose challenges in meeting crucial accessibility criteria. Adhering to Web Content Accessibility Guidelines (WCAG) 2 underscores the importance of making information programmatically accessible and providing clear guidance. Helper texts, with their permanent visibility, play a vital role in meeting these criteria, and ensuring users receive comprehensive information. Therefore, the collaborative use of placeholder and helper texts emerges as a best practice, combining the benefits of guidance during input and persistent labelling for a more inclusive and user-centric digital environment.

Conclusion

In conclusion, our exploration of accessibility in digital design has unveiled the intricate yet indispensable aspects of color contrasting, font sizing, and the nuanced world of text presentation and spacing. As we’ve delved into the specifics of these visual patterns, we’ve demystified the technicalities and emphasized their critical role in creating an inclusive digital space for all.

It’s crucial to remember that accessibility is not merely a checkbox to be ticked; it’s a commitment to fostering a digital landscape where everyone, regardless of ability, can navigate, engage, and thrive. Our journey through the visual patterns of accessibility has provided insights into the meticulous considerations that go into making websites and applications truly welcoming and usable for a diverse audience.

As we eagerly anticipate Part 3 of our series, where we will unravel the complexities of navigation and flow, and delve into the importance of regular testing, we invite you to continue this journey with us. Navigation is the lifeline of user experience, and in our next instalment, we’ll guide you through the principles that ensure seamless movement and interaction within digital spaces. Moreover, we’ll underscore the significance of continuous testing in maintaining accessibility standards, adapting to evolving technologies, and addressing emerging challenges.

Whether you’re a designer, developer, or an advocate for digital inclusivity, our commitment remains unwavering — to empower you with the knowledge and tools to shape a digital world where no one is left behind. So, stay tuned for the next chapter in our series, where we unravel the secrets of navigation, flow, and the indispensable role of regular testing in the pursuit of digital accessibility excellence. Together, let’s build a digital future that truly belongs to everyone.

References

Understanding Success Criterion 1.4.12: Text Spacing | WAI | W3C. (2008, December 11). Retrieved July 17, 2023, from https://www.w3.org/WAI/WCAG22/Understanding/text-spacing.html

Understanding Success Criterion 1.4.4 | Understanding WCAG 2.0. (2008, December 11). Retrieved July 17, 2023, from https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-scale.html

Understanding Success Criterion 1.4.8: Visual Presentation | WAI | W3C. (2008, December 11). Web Accessibility Initiative. Retrieved July 17, 2023, from https://www.w3.org/WAI/WCAG22/Understanding/visual-presentation.html

Web Content Accessibility Guidelines (WCAG) 2.2. (2023, May 17). https://www.w3.org/TR/WCAG22/

 

The post Making Accessibility Part of the Design Process – Part 2 appeared first on Simple Talk.



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