SQL Server Analytical Toolkit: Using Windowing, Analytical, Ranking, and Aggregate Functions for Data and Statistical Analysis 1st Edition Angelo Bobak 2024 Scribd Download
SQL Server Analytical Toolkit: Using Windowing, Analytical, Ranking, and Aggregate Functions for Data and Statistical Analysis 1st Edition Angelo Bobak 2024 Scribd Download
com
OR CLICK HERE
DOWLOAD NOW
https://ebookmass.com/product/fixed-income-mathematics-analytical-and-
statistical-techniques-5th-edition-frank-j-fabozzi/
ebookmass.com
https://ebookmass.com/product/tietz-fundamentos-de-quimica-clinica-e-
diagnostico-molecular-7a-edition-carl-a-burtis/
ebookmass.com
Big Data in Astronomy: Scientific Data Processing for
Advanced Radio Telescopes 1st Edition Linghe Kong (Editor)
https://ebookmass.com/product/big-data-in-astronomy-scientific-data-
processing-for-advanced-radio-telescopes-1st-edition-linghe-kong-
editor/
ebookmass.com
https://ebookmass.com/product/fortran-for-scientists-and-
engineers-4th-edition-stephen-j-chapman/
ebookmass.com
https://ebookmass.com/product/on-the-origin-of-evolution-tracing-
darwins-dangerous-idea-from-aristotle-to-dna-john-gribbin/
ebookmass.com
https://ebookmass.com/product/process-safety-and-big-data-sagit-
valeev/
ebookmass.com
https://ebookmass.com/product/shopping-and-the-
senses-1800-1970-serena-dyer/
ebookmass.com
Calculus: Early Transcendentals 8th Edition eBook
https://ebookmass.com/product/calculus-early-transcendentals-8th-
edition-ebook/
ebookmass.com
SQL Server
Analytical
Toolkit
Using Windowing, Analytical, Ranking,
and Aggregate Functions for Data and
Statistical Analysis
—
Angelo Bobak
SQL Server Analytical
Toolkit
Using Windowing, Analytical,
Ranking, and Aggregate Functions
for Data and Statistical Analysis
Angelo Bobak
SQL Server Analytical Toolkit: Using Windowing, Analytical, Ranking, and Aggregate
Functions for Data and Statistical Analysis
Angelo Bobak
Hastings On Hudson, NY, USA
v
Table of Contents
vi
Table of Contents
vii
Table of Contents
viii
Table of Contents
ix
Table of Contents
x
Table of Contents
xiii
Table of Contents
Index������������������������������������������������������������������������������������������������������������������� 1035
xiv
About the Author
Angelo R. Bobak is a published author with more than three
decades of experience and expertise in the areas of business
intelligence, data architecture, data warehouse design, data
modeling, master data management, and data quality using
the Microsoft BI Stack across several industry sectors such as
finance, telecommunications, engineering, publishing, and
automotive.
xv
About the Technical Reviewer
Alicia Moniz is a leader in Data & AI at Microsoft, an
organizer for Global AI Bootcamp – Houston Edition, and
a #KafkaOnAzure Evangelista and prior was a three-time
Microsoft AI MVP. She is an active supporter of women in
technology and volunteers her time at events that help make
AI technology accessible to the masses. She is a co-author of
the Apress publication Beginning Azure Cognitive Services:
Data-Driven Decision Making Through Artificial Intelligence
along with fellow Microsoft MVPs Matt Gordon, Ida Bergum,
Mia Chang, and Ginger Grant. With over 14 years of experience in data warehousing
and advanced analytics, Alicia is constantly upskilling and holds more than 12 in-
demand IT certifications including AWS, Azure, and Kafka. She is active in the Microsoft
User Group community and enjoys speaking on AI, SQL Server, #KafkaOnAzure, and
personal branding for women in technology topics. Currently, she authors the blog
HybridDataLakes.com, a blog focused on cloud data learning resources, and produces
content for the YouTube channel #KafkaOnAzure.
xvii
Introduction
Welcome to my book, SQL Server Analytical Toolkit.
What’s this book about?
This is a book on applying Microsoft SQL Server aggregate, analytical, and ranking
functions across various industries for the purpose of statistical, reporting, analytical,
and historical performance analysis using a series of built-in SQL Server functions
affectionately known as the window functions!
No, not window functions like the ones used in the C# or other Microsoft Windows
application programming. They are called window functions because they implement
windows into the data set generated by a query. These windows allow you to control
where the functions are applied in the data by creating partitions in the query data set.
“What’s a partition?” you might ask. This is a key concept you need to understand to
get the most out of this book. Suppose you have a data set that has six rows for product
category A and six rows for product category B. Each row has a column that stores sales
values that you wish to analyze. The data set can be divided into two sections, one for
each product category. These are the partitions that the window functions use. You can
analyze each partition by applying the window functions (more on this in Chapter 1).
We will see that the window in each partition can be further divided into smaller
windows. The mechanism of a window frame allows you to control which rows in
the partition are submitted to the window function relative to the current row being
processed. For example, apply a function like the SUM() function to the current row being
processed and any prior rows in the partition to calculate running totals by month. Move
to the next row in the partition and it behaves the same.
The book focuses on applying these functions across four key industries: sales,
finance, engineering, and inventory control. I did this so that readers in these industries
can find something they are familiar with in their day-to-day job activities. Even if you
are not working across these industries, you can still benefit by learning the window
functions and seeing how they are applied.
Maybe you want to interview for a developer role in the finance sector? Or maybe
you work in engineering or telecommunications or you are a manufacturer of retail
products. This book will help you acquire some valuable skills that will help you pass the
job interview.
xix
Introduction
Although you could perform these functions with tools like Power BI, performing
these functions at the SQL level precalculates results and improves performance so that
reporting tools use precalculated data.
By the way, there are many books out there on SQL Server and window (or
windowing) functions. What’s so different about this book?
Approach
This book takes a cookbook approach. Not only are you shown how to use the functions,
but you are shown how to apply them across sales, finance, inventory control, and
engineering scenarios.
These functions are grouped into three categories, so for each industry use case we
look at, we will dedicate a chapter to each function category:
• Aggregate functions
• Analytical functions
• Ranking functions
For each function, a query is created and explained. Next, the results are examined
and analyzed.
Where applicable the results are used to generate some interesting graphs with
Microsoft Excel spreadsheets, like creating normal distribution charts for sales data.
Appendix A contains descriptions and syntax for these functions in case you are not
familiar with them, so feel free to examine them before diving into the book.
Key to mastering the concepts in this book is understanding what the OVER() clause
does. Chapter 1 starts off by defining what the OVER() clause is and how it is used with
the window functions.
Several diagrams clearly explain what data sets, partitions, and window frames are
and how they are key to using the window functions.
Each of the industries we identified earlier has three dedicated chapters, one for each
of the window function categories. Each chapter provides a specification for the query to be
written, the code to satisfy the specification, and then one or more figures to show the results.
The book is unique in that it goes beyond just showing how each function works; it
presents use case scenarios related to statistical analysis, data analysis, and BI (BI stands
for business intelligence by the way).
xx
Introduction
The book also makes available all code examples including code to create and load
each of the four databases via the publisher's Google website.
Lastly, just enough theory is included to introduce you to statistical analysis in case
you are not familiar with terms like standard deviation, mean, normal distribution, and
variance. These are important as they will supply you with valuable skills to support your
business users and enhance your skills portfolio. Hey, a little business theory can’t hurt!
Appendix B has a brief primer on statistics, so make sure to check it out in case these
topics are new to you. It discusses standard deviation, variance, normal distribution,
other statistical calculations, and bell curves.
Back to the window functions. These functions generate a lot of numerical data. It’s
great to generate numbers with decimal points but even more interesting to graph them
and understand what they mean. A picture is worth a thousand words. Seeing a graph
that shows sales decreasing month by month is certainly worth looking at and should
raise alarms!
You can also use the Excel spreadsheets to verify your results by using the
spreadsheets’ built-in functions to make sure they match the results of your queries.
Always test your data against a set of results known to be correct (you might just learn
a little bit about Microsoft Excel too!). The spreadsheets used in this book will also be
available on the publisher's Google website.
The book includes tips and discussions that will take you through the process of
learning the SQL Server aggregate, ranking, and analytical functions. These are delivered
in a step-by-step approach so you can easily master the concepts. Data results are
analyzed so that you can understand what the function does and how the windows are
used to analyze the data work.
Expectations
Now that you know what you are in for, what do I expect from you?
Not much really, at a high level.
I expect you to be an intermediate to advanced SQL Server developer or data
architect who needs to learn how to use window functions. You can write medium-
complexity queries that use joins, understand what a CTE (common table expression) is,
and be able to create and load database tables.
You could also be a tech-savvy business analyst who needs to apply sophisticated
data analysis for your business users or clients.
xxi
Introduction
Lastly, you could be a technology manager who wants to understand what your
development team does in their roles as developers. All will benefit from this book.
In conclusion, you need
In case you do not know how to use SSMS, there are many excellent YouTube videos
and sites that can show you how to use this tool in a short amount of time.
You can also check out my podcast “GRUMPY PODCAST 01 NAVIGATING SSMS” at
www.grumpyolditguy.com under the TSQL Podcasts menu selection in the menu bar.
Note You can download the code from the publisher’s Google website at
https://github.com/Apress/SQL-Server-Analytical-Toolkit.
xxii
Introduction
Very simple and easy to read. It is clear by the names used that the query retrieves
departmental information, specifically the department identifier, the manager identifier,
and the manager’s last name. Notice the name of the table. There is no doubt as to what
the table contains.
That’s it! Let’s begin our journey into window functions.
xxiii
CHAPTER 1
1
© Angelo Bobak 2023
A. Bobak, SQL Server Analytical Toolkit, https://doi.org/10.1007/978-1-4842-8667-8_1
Chapter 1 Partitions, Frames, and the OVER( ) Clause
data set can also be a partition by itself, and a window can be defined that uses all rows
in the single partition. It all depends on how you include and define the PARTITION BY,
ORDER BY, and ROWS/RANGE window frame clauses.
These conditions can be specified with the OVER() clause. Let’s see how this works.
• Seven years later, aggregate functions with support for the ORDER BY
clause were introduced in 2012.
• Support for window frames (which we will discuss shortly) was also
introduced in 2012.
2
Chapter 1 Partitions, Frames, and the OVER( ) Clause
The capability of the window functions has grown over the years and delivers a rich
and powerful set of tools to analyze and solve complex data analysis problems.
Each of the subsequent chapters will create and discuss queries for these categories
for four industry-specific databases that are in scope for this book. Please refer to
Appendix A for syntax and descriptions of what each of the preceding functions does if
you are unfamiliar with them or need a refresher on how to use them in a query.
3
Chapter 1 Partitions, Frames, and the OVER( ) Clause
SELECT OrderYear,OrderMonth,SalesAmount,
SUM(SalesAmount) OVER(
PARTITION BY OrderYear
ORDER BY OrderMonth ASC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS AmountTotal
FROM OverExample
ORDER BY OrderYear,OrderMonth
GO
Between a set of parentheses after the OVER keyword, three other clauses can be
included such as PARTITION BY, ORDER BY, and ROWS or RANGE clauses (to define the
window frame that presents the rows to the function for processing).
Even if you have an ORDER BY clause in the OVER() clause, you can also include the
usual ORDER BY clause at the end of the query to sort the final processed result set in any
order you feel is appropriate to the business requirements the query solves.
Syntax
The following are three basic syntax templates that can be used with the window
functions. Reading these syntax templates is easy. Just keep in mind keywords between
square brackets mean they are optional. The following is the first syntax template
available for the OVER() clause:
Syntax 1
Most of the window functions use this first syntax, and it is composed of three
main clauses, the PARTITION BY clause, the ORDER BY clause, and a ROWS or RANGE
specification. You can include one or more of these clauses or none. These combinations
will affect how the partition is defined. For example, if you do not include a PARTITION
BY clause, the entire data set is considered a one large partition. The expression is usually
one or more columns, but in the case of the PARTITION BY and ORDER BY clauses, it could
also be a subquery (refer to Appendix A).
The Window Function is one of the functions identified in Table 1-1.
This first syntax is pretty much the same for all functions except for the
PERCENTILE_DISC() and PERCENTILE_CONT() functions that use a slight variation:
Syntax 2
These functions are used to calculate the percentile discrete and percentile
continuous values in a data set column. The numeric literal can be a value like .25, .50,
or .75 that is used to specify the percentile you wish to calculate. Notice that the ORDER
BY clause is inserted between the parentheses of the WITHIN GROUP command and the
OVER() clause just includes the PARTITION BY clause.
Don’t worry about what this does for now. Examples will be given that make the
behavior of this code clear. For now, just understand that there are three basic syntax
templates to be aware of.
In our chapter examples, the expression will usually be a column or columns
separated by commas although you can use other data objects like queries. Please
refer to the Microsoft SQL Server documentation to check out the detailed syntax
specification or Appendix A.
Lastly, our third syntax template applies to SQL Server 2022 (release 16.x). The
window capability has been enhanced that allows you to specify window options in a
named window that appears at the end of the query:
Syntax 3
As of this writing, SQL Server 2022 is available for evaluation only. In Listing 1-2 is an
example TSQL query that uses this new feature.
SELECT OrderYear,OrderMonth,SalesAmount,
SUM(SalesAmount) OVER SalesWindow AS SQPRangeUPCR
FROM OverExample
WINDOW SalesWindow AS (
PARTITION BY OrderYear
ORDER BY OrderMonth
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
);
GO
The name of the window is SalesWindow, and it is used right after the OVER operator
instead of the PARTITION BY, ORDER BY, and RANGE clauses as used in the first syntax
template we discussed.
Probably a good feature in case you have multiple window functions in your SELECT
clause that need to use this partition and window frame configuration. This would avoid
repeating the partition code in each column of the SELECT clause.
The PARTITION BY, ORDER BY, and RANGE clauses are declared at the end of the
query between parentheses after the WINDOW keyword instead of right after the OVER
keyword.
If you want to play around with this, download and install the 2022 evaluation
license and try it out on the example code available with the book or on your own
queries. The setup and download are fast and simple. Make sure you get the latest
version of SSMS. These are available on Microsoft’s download website.
A picture is worth a thousand words, so let’s look at one now. Please refer to
Figure 1-1.
Figure 1-1. A simple data set with three partitions and an example frame
Here we have a simple data set composed of eight rows. There are three example
partitions in this data set. One can include all eight rows of the data set; the other two
include rows identified by the TYPE column. There are only two type values, type A and
type B, so each of these partitions will have four rows. By the way, you cannot include
multiple PARTITION BY clauses in an OVER() clause.
You can define only one partition per OVER() clause although you can have more
than one column in the SELECT clause of the query that uses a partition. You can specify
different column combinations to define the partitions.
Where the power of this architecture comes in is that we can create smaller window
frames against the partition by using the ROWS or RANGE operator. These will allow you to
specify how many rows before and/or after the current row being processed will be used
by the window function.
7
Chapter 1 Partitions, Frames, and the OVER( ) Clause
In our preceding example snapshot, the current row is row 3, and the window frame
is defined so it includes only the prior row, the current row, and the next row relative
to the current row. If we apply the SUM() function to this window frame and add all the
values, we get the result 60 (15 + 20 + 25). (Remember this is within the first partition,
which contains only four rows.)
If processing continues on the next row, row 4, only rows 3 and 4 are available to
the SUM() function, and the result is 45 (20 + 25). I neglected to mention that if we start
at row 1, then only rows 1 and 2 are available to the SUM()function because there is no
prior row. The function returns the value 25 (10 + 15).
How do we control this type of processing? All we need to do is add a ROWS or RANGE
specification to the query if required. We could also include an ORDER BY clause to
specify how to order the rows within the partition so that the window function is applied
as needed. For example, generate rolling totals by month, starting of course at month 1
(January) and ending at month 12 (December).
Sounds easy, but we need to be aware of a few scenarios around default processing
when we leave the ORDER BY clause and/or the PARTITION clause out. We will discuss
these shortly.
8
Chapter 1 Partitions, Frames, and the OVER( ) Clause
This clause tells the function to operate on the current row and all rows preceding
the current row if there are any in the partition. A simple diagram in Figure 1-2 makes it
all clear.
Figure 1-2. Include the current row and all preceding rows
If we start at row 1, since there are no prior rows in the partition before this row, the
SUM() function returns the value 10.
Moving on to row 2, the SUM() function will include the only available prior row
(row 1), so the result is 25 (10 + 15).
Next (shown in the preceding figure), the current row to be processed is row 3. The
SUM() function will evaluate row 3 plus rows 1 and 2 in order to generate the total. The
result is 45 (10 + 15 + 20).
Lastly, moving to row 4, the function will include the current row and all prior rows
in its calculation and return 70 (10 + 15 + 20 + 25). Processing now concludes for this
partition.
Moving to partition B, the processing repeats itself, and only rows from partition B
are included. All rows in partition A are ignored.
9
Chapter 1 Partitions, Frames, and the OVER( ) Clause
The first clause is legal but the second is not. It is not supported. One would think
that if there is a ROWS UNBOUNDED PRECEDING, there should be a ROWS UNBOUNDED
FOLLOWING, but it is not supported at this time. Go figure!
As stated earlier, this clause takes us in the opposite direction than the prior scenario
we just discussed. It will allow the aggregate or other window functions to include the
current row and all succeeding rows until all rows in the partition are exhausted. Our
next diagram in Figure 1-3 shows us how this works.
Figure 1-3. Process the current row and all succeeding rows
10
Chapter 1 Partitions, Frames, and the OVER( ) Clause
Next, if the current row being processed is row 2 as in the preceding example, then
the SUM() function will include rows 2–4 to generate the total value. It will generate a
result of 60 (15 + 20 + 25).
Moving on to row 3, it will only include rows 3 and 4 and generate a total value of 45
(20 + 25).
Once processing gets to row 4, only the current row is used as there are no more rows
available in the partition. The SUM() function calculates a total of 25.
When processing resumes at the next partition, the entire scenario is repeated.
What if we do not want to include all prior or following rows but only a few before or
after? The next window frame clause will accomplish the trick for limiting the number of
following rows:
Including this clause in the OVER() clause will allow us to control the number of
rows to include relative to the current row, that is, how many rows after the current row
are going to be used from the available partition rows. Let’s examine another simple
example where we want to include only one row following the current row in the
calculation.
Please refer to Figure 1-4.
11
Chapter 1 Partitions, Frames, and the OVER( ) Clause
Processing starts at row 1. I think by now you understand that only rows 1 and 2 are
used and the result is 25 (10 + 15).
Next, in the preceding example, row 2 is the current row. If only the next row is
included, then the SUM() function will return a total of 35 (15 + 20).
Moving to row 3 (the next current row), the SUM() function will return 45 as the sum
(20 + 25).
Finally, when processing gets to the last row in the partition, then only row 4 is used
and the SUM() function returns 25.
When processing continues to the next partition, the sequence repeats itself ignoring
the values from the prior partition.
We can now see how this process creates windows into the partition that change
as the rows are processed one by one by the window function. Remember, the order
in which the rows are processed is controlled by the ORDER BY clause used in the
OVER() clause.
12
Chapter 1 Partitions, Frames, and the OVER( ) Clause
The next example takes us in the opposite direction. We want to include the current
row and the prior two rows (if there are any) in the calculation.
The window frame clause is
The letter n represents an unsigned integer value specifying the number of rows. The
following example uses 2 as the number of rows.
Please refer to Figure 1-5.
Figure 1-5. Include two prior rows and the current row
Starting at row 1 (the current row), the SUM() function will only use row 1 as there are
no prior rows. The value returned by the window function is 10.
Moving to row 2, there is only one prior row so the SUM() function returns 25
(10 + 15).
13
Chapter 1 Partitions, Frames, and the OVER( ) Clause
Next, the current row is row 3, so the SUM() function uses the two prior rows and
returns a value of 45 (10 + 15 +20).
In the preceding figure, the current row being processed is row 4 and the preceding
two rows are included; the SUM() function when applied will return a total value of 60 (15
+ 20 + 25).
Since all rows of the first partition have been processed, we move to partition B, and
this time only row 5 is used as there are no prior rows. We are at the beginning of the
partition. The SUM() function returns a total value of 30.
Processing continues at row 6, so the SUM function processes rows 5 and 6 (the
current and prior rows). The total value calculated is 65 (30 + 35).
Next, the current row is 7 so the window function will include rows 5, 6, and 7. The
SUM() function returns 105 (30 + 35 + 40).
Finally, processing gets and ends at row 8 of the partition. The rows used by the
SUM() function are rows 6, 7, and 8. The SUM() function() returns 120 (35 + 40 + 45).
There are no more partitions so the processing ends.
What if we want to specify several rows before and after the current row? The next
clause does the trick. This was one of the example in the chapter, but let’s review it again
and make one change:
In this case we want to include the current row, the prior row, and two following
rows. We could well specify two rows preceding and three rows following or any
combination within the number of rows in the partition if we had more rows in the
partition.
Please refer to Figure 1-6.
14
Chapter 1 Partitions, Frames, and the OVER( ) Clause
Starting at row 1 of the partition, the SUM() function can only use rows 1, 2, and 3
as there are no rows in the partition prior to row 1. The result calculated by the SUM()
function is 45 (10 + 15 + 20).
Moving to the next row, row 2 of the partition, the SUM() function can use rows 1, 2
(the current row), 3, and 4. The result calculated by the SUM() function is 70 (10 + 15 +
20 + 25).
Next, the current row is row 3. This window frame uses row 2, 3, and only 4 so the
SUM() function will calculate a total sum of 60 (15 + 20 + 25).
Finally, moving to row 4 (last row of the partition), the SUM() function can only use
rows 3 and 4 as there are no more available rows following in the partition. The result is
45 (20 + 25).
Processing at the next partition continues in the same fashion, ignoring all rows from
the prior partition.
15
Chapter 1 Partitions, Frames, and the OVER( ) Clause
We have pretty much examined most of the configurations for the ROWS clause. Next,
we look at window frames defined by the RANGE clause, which works at a logical level. It
considers values instead of physical row position. Duplicate ORDER BY values will yield a
strange behavior.
16
Chapter 1 Partitions, Frames, and the OVER( ) Clause
When we get to row 3, the first of two duplicate months, the window function (SUM()
in this case) will include all prior rows and add the values in row 3 and the next row 4
to generate a total value of 600.00. This is displayed for both rows 3 and 4. Weird! One
would expect a running total value of 400.00 for row 3.
Let’s try another one. If we apply the following RANGE clause to calculate
running totals
or
we get interesting results. Check out the partial results in Figure 1-7.
Everything works as expected until we get to the duplicate rows with a value of 5
(May) for the month. The window frame for this partition includes rows 1–4 and the
current row 5, but we also have a duplicate month value in row 6, which calculates the
rolling total value 60. This value is displayed in both rows 5 and 6. Aggregations continue
for the rest of the months (not shown).
17
Chapter 1 Partitions, Frames, and the OVER( ) Clause
Let’s process rows in the opposite direction. Here is our next RANGE clause:
By the way, the second RANGE clause is not supported, but I included it so you are
aware that it will not work and generate an error. Please refer to Figure 1-8.
Rows 5 and 6 represent two sales figures for the month of May. Since these are
considered duplicates, row 5 displays a rolling value of 90 and row 6 also shows 90
instead of 80 as one would expect if they did not understand the behavior of this window
frame declaration.
Notice, by the way, how the rolling sum values decrease as the row processing
advances from row 1 onward. We started with 130 and ended with 10 – moving totals in
reverse.
Putting it all together, here is a conceptual view that illustrates the generation of
window frames as rows are processed when the following ROWS clause is used:
18
Chapter 1 Partitions, Frames, and the OVER( ) Clause
This was one of the example in the chapter. Only the partition for type A rows is
considered. Please refer to Figure 1-9.
Both partitions are shown so you can see how the frame resets when it gets to the
next partition.
The generated window frames, as processing goes row by row, show which values
are included for the window function to process within the partition. The first window
and last window only have two rows to process, but the second and third both have three
rows to process, the prior, current, and next rows. The same pattern repeats itself for the
second partition.
Let’s put our knowledge to the test by looking at a simple query based on the figures
we just examined.
Example 1
We will start by creating a small test temporary table for our analysis.
The following listing shows the test table CREATE TABLE DDL (data declaration
language) statement created as a temporary table and one INSERT statement to load all
the rows we wish to process.
19
Random documents with unrelated
content Scribd suggests to you:
Whether he was to be trusted to return or not was left an open
question. One thing was plain—he must leave India now. He
reached Bombay by easy stages, and completely restored by the
sea voyage, landed at Southampton a month later, after an absence
from England of nearly three years.
CHAPTER IV.
MONKSWOOD.
Our website is not just a platform for buying books, but a bridge
connecting readers to the timeless values of culture and wisdom. With
an elegant, user-friendly interface and an intelligent search system,
we are committed to providing a quick and convenient shopping
experience. Additionally, our special promotions and home delivery
services ensure that you save time and fully enjoy the joy of reading.
ebookmass.com