Serverless SQL Query Optimization in Azure Synapse AnalyticsΒΆ
Comparative positioning note
This document is written from the perspective of Microsoft Azure, Cloud Scale Analytics, and CSA Loom. Any description of third-party or competing products, services, pricing, or capabilities is derived from publicly available documentation and sources believed accurate at the time of writing, and is provided for general comparison only. We do not claim expertise in, or authority over, any non-Microsoft product or service; the respective vendor's official documentation is the authoritative source for their offerings, which may change over time. Nothing here is intended to disparage any vendor β where a competing product has genuine advantages, we aim to note them honestly. Verify all third-party details against the vendor's current official documentation before making decisions.
π Home > π» Code Examples > βοΈ Serverless SQL > π Query Optimization
This guide provides detailed examples for optimizing SQL queries in Azure Synapse Serverless SQL pools to improve performance and reduce costs.
Introduction to Serverless SQL OptimizationΒΆ
Azure Synapse Serverless SQL pools provide on-demand query processing for data in data lakes. Optimizing these queries is essential for reducing costs and improving query performance.
PrerequisitesΒΆ
- Azure Synapse Analytics workspace
- Storage account with data files (Parquet, CSV, JSON, etc.)
- Appropriate permissions to execute SQL queries
Query Optimization TechniquesΒΆ
1. File Format SelectionΒΆ
One of the most important optimization factors is choosing the right file format:
-- Query against Parquet (recommended) - most efficient
SELECT TOP 100 *
FROM OPENROWSET(
BULK 'https://synapseexampledata.blob.core.windows.net/data/parquet/sales_data/*.parquet',
FORMAT = 'PARQUET'
) AS [sales];
-- Query against CSV - less efficient
SELECT TOP 100 *
FROM OPENROWSET(
BULK 'https://synapseexampledata.blob.core.windows.net/data/csv/sales_data/*.csv',
FORMAT = 'CSV',
PARSER_VERSION = '2.0',
HEADER_ROW = TRUE
) AS [sales];
-- Query against JSON - least efficient for large datasets
SELECT TOP 100 *
FROM OPENROWSET(
BULK 'https://synapseexampledata.blob.core.windows.net/data/json/sales_data/*.json',
FORMAT = 'CSV',
FIELDTERMINATOR = '0x0b',
FIELDQUOTE = '0x0b',
ROWTERMINATOR = '0x0b'
) WITH (jsonDoc NVARCHAR(MAX)) AS [sales]
CROSS APPLY OPENJSON(jsonDoc)
WITH (
order_id INT,
customer_id INT,
product_id INT,
quantity INT,
price DECIMAL(10,2),
order_date DATE
);
2. Column PruningΒΆ
Only select the columns you need to reduce data scanning:
-- Inefficient - scans all columns
SELECT *
FROM OPENROWSET(
BULK 'https://synapseexampledata.blob.core.windows.net/data/parquet/sales_data/*.parquet',
FORMAT = 'PARQUET'
) AS [sales];
-- Optimized - only scans necessary columns
SELECT customer_id, SUM(price * quantity) AS total_spent
FROM OPENROWSET(
BULK 'https://synapseexampledata.blob.core.windows.net/data/parquet/sales_data/*.parquet',
FORMAT = 'PARQUET'
) AS [sales]
GROUP BY customer_id
ORDER BY total_spent DESC;
3. Predicate PushdownΒΆ
Utilize filter conditions that can be pushed down to storage:
-- Inefficient - filters after loading all data
SELECT *
FROM OPENROWSET(
BULK 'https://synapseexampledata.blob.core.windows.net/data/parquet/sales_data/*.parquet',
FORMAT = 'PARQUET'
) AS [sales]
WHERE YEAR(order_date) = 2023 AND MONTH(order_date) = 6;
-- Optimized - uses predicate pushdown
SELECT *
FROM OPENROWSET(
BULK 'https://synapseexampledata.blob.core.windows.net/data/parquet/sales_data/*.parquet',
FORMAT = 'PARQUET'
) AS [sales]
WHERE order_date BETWEEN '2023-06-01' AND '2023-06-30';
4. Partition EliminationΒΆ
Leverage partitioned data for efficient queries:
-- Query against partitioned data
-- Data is stored in a folder structure like: /year=2023/month=06/day=15/data.parquet
SELECT *
FROM OPENROWSET(
BULK 'https://synapseexampledata.blob.core.windows.net/data/parquet/sales_data/year=*/month=*/day=*/*.parquet',
FORMAT = 'PARQUET'
) WITH (
order_id INT,
customer_id INT,
product_id INT,
quantity INT,
price DECIMAL(10,2),
order_date DATE,
year INT,
month INT,
day INT
) AS [sales]
WHERE year = 2023 AND month = 6;
5. External Tables for Better PerformanceΒΆ
Create external tables with optimized statistics:
-- Create database for external tables
CREATE DATABASE SalesData;
GO
USE SalesData;
GO
-- Create external data source
CREATE EXTERNAL DATA SOURCE ExampleDataSource
WITH (
LOCATION = 'https://synapseexampledata.blob.core.windows.net/data/'
);
GO
-- Create file format
CREATE EXTERNAL FILE FORMAT ParquetFormat
WITH (
FORMAT_TYPE = PARQUET,
DATA_COMPRESSION = 'org.apache.hadoop.io.compress.SnappyCodec'
);
GO
-- Create external table
CREATE EXTERNAL TABLE SalesTable (
order_id INT,
customer_id INT,
product_id INT,
quantity INT,
price DECIMAL(10,2),
order_date DATE,
year INT,
month INT,
day INT
)
WITH (
LOCATION = '/parquet/sales_data/',
DATA_SOURCE = ExampleDataSource,
FILE_FORMAT = ParquetFormat
);
GO
-- Create statistics on the external table
CREATE STATISTICS stat_customer_id ON SalesTable(customer_id);
CREATE STATISTICS stat_order_date ON SalesTable(order_date);
CREATE STATISTICS stat_product_id ON SalesTable(product_id);
GO
-- Query the external table with statistics
SELECT
year,
month,
SUM(quantity * price) AS total_sales
FROM SalesTable
WHERE order_date BETWEEN '2023-01-01' AND '2023-12-31'
GROUP BY year, month
ORDER BY year, month;
Advanced Optimization TechniquesΒΆ
1. Query Plan AnalysisΒΆ
Use the EXPLAIN command to analyze query plans:
-- View the query execution plan
EXPLAIN
SELECT
customer_id,
SUM(price * quantity) AS total_spent
FROM OPENROWSET(
BULK 'https://synapseexampledata.blob.core.windows.net/data/parquet/sales_data/*.parquet',
FORMAT = 'PARQUET'
) AS [sales]
GROUP BY customer_id
ORDER BY total_spent DESC;
2. Optimizing JoinsΒΆ
Optimize joins by using the proper join type and join order:
-- Create customer external table
CREATE EXTERNAL TABLE CustomerTable (
customer_id INT,
customer_name NVARCHAR(100),
customer_segment NVARCHAR(50),
customer_region NVARCHAR(50)
)
WITH (
LOCATION = '/parquet/customer_data/',
DATA_SOURCE = ExampleDataSource,
FILE_FORMAT = ParquetFormat
);
-- Create statistics on join columns
CREATE STATISTICS stat_sales_customer_id ON SalesTable(customer_id);
CREATE STATISTICS stat_customer_customer_id ON CustomerTable(customer_id);
-- Inefficient join - larger table on left side
SELECT
c.customer_name,
SUM(s.price * s.quantity) AS total_spent
FROM SalesTable s
LEFT JOIN CustomerTable c ON s.customer_id = c.customer_id
GROUP BY c.customer_name
ORDER BY total_spent DESC;
-- Optimized join - smaller table on left side
SELECT
c.customer_name,
SUM(s.price * s.quantity) AS total_spent
FROM CustomerTable c
INNER JOIN SalesTable s ON c.customer_id = s.customer_id
WHERE s.year = 2023
GROUP BY c.customer_name
ORDER BY total_spent DESC;
3. Data Skew HandlingΒΆ
Address data skew with more granular partitioning or CETAS (Create External Table As Select):
-- Identify data skew
SELECT
product_id,
COUNT(*) as row_count
FROM SalesTable
GROUP BY product_id
ORDER BY row_count DESC;
-- Handle skew using CETAS for high-volume products
CREATE EXTERNAL TABLE HighVolumeProducts
WITH (
LOCATION = '/optimized/high_volume_products/',
DATA_SOURCE = ExampleDataSource,
FILE_FORMAT = ParquetFormat
)
AS
SELECT *
FROM SalesTable
WHERE product_id IN (101, 202, 303); -- High volume product IDs
-- Handle skew using CETAS for normal-volume products
CREATE EXTERNAL TABLE NormalVolumeProducts
WITH (
LOCATION = '/optimized/normal_volume_products/',
DATA_SOURCE = ExampleDataSource,
FILE_FORMAT = ParquetFormat
)
AS
SELECT *
FROM SalesTable
WHERE product_id NOT IN (101, 202, 303); -- Exclude high volume product IDs
-- Union the results when querying
SELECT * FROM HighVolumeProducts
UNION ALL
SELECT * FROM NormalVolumeProducts;
4. Caching with Materialized ViewsΒΆ
Use materialized views for frequently accessed aggregated data:
-- Create materialized view
CREATE EXTERNAL TABLE MonthlySalesSummary
WITH (
LOCATION = '/optimized/monthly_sales_summary/',
DATA_SOURCE = ExampleDataSource,
FILE_FORMAT = ParquetFormat
)
AS
SELECT
year,
month,
product_id,
SUM(quantity) AS total_quantity,
SUM(price * quantity) AS total_sales,
COUNT(DISTINCT order_id) AS order_count
FROM SalesTable
GROUP BY year, month, product_id;
-- Query the materialized view
SELECT
year,
month,
SUM(total_sales) AS monthly_revenue
FROM MonthlySalesSummary
WHERE year = 2023
GROUP BY year, month
ORDER BY year, month;
5. Result Set CachingΒΆ
Enable result set caching for repeated queries:
-- Enable result set caching
ALTER DATABASE SalesData
SET RESULT_SET_CACHING ON;
-- Run a query that will be cached
SELECT TOP 100 *
FROM SalesTable
WHERE year = 2023 AND month = 6;
-- Run the same query again - will use the cached results
SELECT TOP 100 *
FROM SalesTable
WHERE year = 2023 AND month = 6;
Working with Different File TypesΒΆ
1. CSV File OptimizationΒΆ
-- Create external table for CSV with optimal settings
CREATE EXTERNAL TABLE SalesCSV (
order_id INT,
customer_id INT,
product_id INT,
quantity INT,
price DECIMAL(10,2),
order_date DATE
)
WITH (
LOCATION = '/csv/sales_data/',
DATA_SOURCE = ExampleDataSource,
FILE_FORMAT = DELIMITED TEXT WITH (
FIELD_TERMINATOR = ',',
USE_TYPE_DEFAULT = TRUE,
STRING_DELIMITER = '"',
DATE_FORMAT = 'yyyy-MM-dd',
PARSER_VERSION = '2.0',
FIRST_ROW = 2 -- Skip header row
)
);
-- Query with optimal file handling
SELECT
YEAR(order_date) AS year,
MONTH(order_date) AS month,
SUM(price * quantity) AS total_sales
FROM SalesCSV
GROUP BY YEAR(order_date), MONTH(order_date)
ORDER BY year, month;
2. JSON File OptimizationΒΆ
-- Create external table for JSON with optimal settings
CREATE EXTERNAL TABLE SalesJSON
WITH (
LOCATION = '/json/sales_data/',
DATA_SOURCE = ExampleDataSource,
FILE_FORMAT = JSON
)
AS
SELECT
JSON_VALUE(jsonDoc, '$.order_id') AS order_id,
JSON_VALUE(jsonDoc, '$.customer_id') AS customer_id,
JSON_VALUE(jsonDoc, '$.product_id') AS product_id,
JSON_VALUE(jsonDoc, '$.quantity') AS quantity,
JSON_VALUE(jsonDoc, '$.price') AS price,
CONVERT(DATE, JSON_VALUE(jsonDoc, '$.order_date')) AS order_date
FROM OPENROWSET(
BULK 'https://synapseexampledata.blob.core.windows.net/data/json/sales_data/*.json',
FORMAT = 'CSV',
FIELDTERMINATOR = '0x0b',
FIELDQUOTE = '0x0b',
ROWTERMINATOR = '0x0b'
) WITH (jsonDoc NVARCHAR(MAX)) AS [sales];
-- Query the optimized JSON table
SELECT
YEAR(order_date) AS year,
MONTH(order_date) AS month,
SUM(CAST(quantity AS INT) * CAST(price AS DECIMAL(10,2))) AS total_sales
FROM SalesJSON
GROUP BY YEAR(order_date), MONTH(order_date)
ORDER BY year, month;
Resource Management and ConcurrencyΒΆ
1. Setting Appropriate DWUΒΆ
-- Check current resource utilization
SELECT * FROM sys.dm_exec_requests;
-- Check query resource consumption
SELECT
r.request_id,
r.total_elapsed_time,
r.cpu_time,
r.reads,
r.writes,
r.logical_reads,
t.text
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.session_id > 50 -- Filter out system sessions
ORDER BY r.total_elapsed_time DESC;
2. Optimizing for ConcurrencyΒΆ
Use query hints for better concurrency:
-- Add resource allocation hints
SELECT
year,
month,
SUM(quantity * price) AS total_sales
FROM SalesTable
WHERE order_date BETWEEN '2023-01-01' AND '2023-12-31'
GROUP BY year, month
ORDER BY year, month
OPTION (LABEL = 'Monthly Sales Report', MAXDOP 4);
Cost Optimization StrategiesΒΆ
1. Reduce Data ScanningΒΆ
-- Use partitioning and file filtering
SELECT *
FROM OPENROWSET(
BULK 'https://synapseexampledata.blob.core.windows.net/data/parquet/sales_data/year=2023/month=06/day=*/*.parquet',
FORMAT = 'PARQUET'
) AS [sales];
2. Query Monitoring for Cost ControlΒΆ
-- Monitor data processed by queries
SELECT
r.session_id,
r.request_id,
r.start_time,
r.end_time,
r.total_elapsed_time,
s.bytes_processed,
s.files_processed,
t.text
FROM sys.dm_exec_requests r
JOIN sys.dm_external_work_stats s ON r.request_id = s.request_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
ORDER BY s.bytes_processed DESC;
Best PracticesΒΆ
-
Use Parquet Format: Parquet provides the best performance for both storage and query efficiency.
-
Apply Column Pruning: Always select only the columns you need instead of using SELECT *.
-
Leverage Partitioning: Use partitioned data and partition elimination in your queries.
-
Create Statistics: Create statistics on external tables for better query optimization.
-
Use CETAS: Create External Table As Select (CETAS) to materialize intermediate results and optimize complex queries.
-
Regular Monitoring: Monitor query performance and data processed to identify optimization opportunities.
-
Proper File Sizes: Aim for file sizes between 100MB and 1GB for optimal performance.
-
Minimize File Count: Reduce the number of small files by using CETAS to combine them.
-
Enable Result Set Caching: For frequently executed identical queries.
-
Use WITH Clause: Simplify complex queries with common table expressions.
Common Issues and SolutionsΒΆ
Issue: Slow query performance on CSV filesΒΆ
Solution: Convert CSV to Parquet using CETAS for better performance.
Issue: Out of memory errorsΒΆ
Solution:
- Reduce the amount of data processed in a single query
- Implement proper partitioning
- Use CETAS for large intermediate results
Issue: High costs due to excessive data scanningΒΆ
Solution:
- Implement column pruning
- Use partitioning and partition elimination
- Convert to Parquet format
- Create smaller, focused external tables