How to update data periodically
The data reading algorithm looks like this:
- create tables in your DWH
- load historical data from version zero
- periodically read data from version N+1, where N is the latest version of data in your DWH
- get links to the necessary files and read the contents of the files
- put the data from the files into DWH, fixing the version of the data so that in the next iteration of reading, start reading from the version that has not yet been written to your DWH, and not read everything from the beginning
Example of tables and views for Clickhouse
Tables for raw data
Balance changes
CREATE TABLE "BonusPointChanges_raw" (
"id" Int,
"kindSystemName" Nullable(String),
"mechanicsInternalId" Nullable(String),
"balanceInternalId" Nullable(String),
"changeAmount" Decimal64 (5),
"availableFromDateTimeUtc" Nullable(String),
"expirationDateTimeUtc" Nullable(String),
"dateTimeUtc" Nullable(String),
"unmergedCustomerId" Nullable(String),
"orderId" Nullable(String),
"comments" Nullable(String),
"pointOfContactInternalId" Nullable(String),
"brandInternalId" Nullable(String),
"_isDeleted" Nullable(String),
"_rowversion_ts" DateTime,
"_data_version" Int
)
ENGINE MergeTree()
ORDER BY tuple()
SETTINGS index_granularity = 8192 SETTINGS flatten_nested=0Mechanics of points balance changes
CREATE TABLE "BonusPointsMechanics_raw" (
"id" Int,
"internalId" String,
"discriminator" Nullable(String),
"name" Nullable(String),
"ownerId" Nullable(String),
"ownerType" Nullable(String),
"_isDeleted" Nullable(String),
"_rowversion_ts" DateTime,
"_data_version" Int
)
ENGINE MergeTree()
ORDER BY tuple()
SETTINGS index_granularity = 8192 SETTINGS flatten_nested=0Points accounts
CREATE TABLE "Balances_raw" (
"internalId" String,
"id" Nullable(Int),
"name" Nullable(String),
"systemName" Nullable(String),
"description" Nullable(String),
"_isDeleted" Nullable(String),
"_rowversion_ts" DateTime,
"_data_version" Int
)
ENGINE MergeTree()
ORDER BY tuple()
SETTINGS index_granularity = 8192 SETTINGS flatten_nested=0Links between write-offs and accruals
CREATE TABLE "NegativeCustomerBalanceChangeDetails_raw" (
"id" Int,
"negativeCustomerBalanceChangeId" Nullable(Int),
"positiveCustomerBalanceChangeId" Nullable(Int),
"spentAmount" Decimal64 (5),
"_isDeleted" Nullable(String),
"_rowversion_ts" DateTime,
"_data_version" Int
)
ENGINE MergeTree()
ORDER BY tuple()
SETTINGS index_granularity = 8192 SETTINGS flatten_nested=0Orders
CREATE TABLE "Orders_raw" (
"id" String,
"unmergedCustomerId" Nullable(Int64),
"firstDateTimeUtc" Nullable(DateTime),
"firstPointOfContactInternalId" Nullable(String),
"firstBrandInternalId" Nullable(String),
"price" Nullable(Decimal64 (5)),
"priceWithDiscounts" Nullable(Decimal64 (5)),
"deliveryPrice" Nullable(Decimal64 (5)),
"deliveryPriceWithDiscounts" Nullable(Decimal64 (5)),
"paidAmount" Nullable(Decimal64 (5)),
"pointOfContactInternalId" Nullable(String),
"_isDeleted" Nullable(String),
"_rowversion_ts" DateTime,
"_data_version" Int
)
ENGINE MergeTree()
ORDER BY tuple()
SETTINGS index_granularity = 8192 SETTINGS flatten_nested=0Touchpoints
CREATE TABLE "PointsOfContact_raw" (
"id" Int64,
"internalId" String,
"externalId" Nullable(String),
"name" String,
"systemName" String,
"parentId" Nullable(String),
"_isDeleted" Nullable(String),
"_rowversion_ts" DateTime,
"_data_version" Int32
)
ENGINE = MergeTree()
ORDER BY tuple()
SETTINGS index_granularity = 8192Purchase statuses dictionary
CREATE TABLE "PurchaseStatuses_raw" (
"internalId" String,
"name" Nullable(String),
"externalId" Nullable(String),
"categorySystemName" Nullable(String),
"_isDeleted" Nullable(String),
"_rowversion_ts" DateTime,
"_data_version" Int32
)
ENGINE = MergeTree()
ORDER BY tuple()
SETTINGS index_granularity = 8192Purchases
The purchase record does not have a _isDeleted field because it cannot be deleted - only the entire order can be deleted.
CREATE TABLE "Purchases_raw" (
"orderId" String,
"pricePerItem" Decimal64 (5),
"priceOfLine" Decimal64 (5),
"quantity" Float,
"quantityType" String,
"lineId" Nullable(String),
"lineNumber" Int,
"statusInternalId" String,
"productInternalId" String,
"_rowversion_ts" DateTime,
"_data_version" Int32
)
ENGINE = MergeTree()
ORDER BY tuple()
SETTINGS index_granularity = 8192Mailings dictionary
CREATE TABLE "Mailings_raw" (
"id" String,
"name" String,
"systemName" String,
"type" String,
"channel" String,
"creationDateTimeUtc" DateTime,
"lastUpdateDateTimeUtc" DateTime,
"folderInternalId" Nullable(String),
"subscriptionTopicInternalId" Nullable(String),
"brandInternalId" String,
"utmSource" Nullable(String),
"utmMedium" Nullable(String),
"utmCampaign" Nullable(String),
"utmContent" Nullable(String),
"utmTerm" Nullable(String),
"_isDeleted" Nullable(String),
"_rowversion_ts" DateTime,
"_data_version" Int32
)
ENGINE = MergeTree()
ORDER BY tuple()
SETTINGS index_granularity = 8192Subscription topics dictionary
CREATE TABLE "SubscriptionTopics_raw"
(
"internalId" String,
"systemName" String,
"name" String,
"brandInternalId" String,
"_isDeleted" Nullable(String),
"_rowversion_ts" DateTime,
"_data_version" Int32
)
ENGINE = MergeTree()
ORDER BY tuple()
SETTINGS index_granularity = 8192Mailings statuses
CREATE TABLE "CustomerMessagesStatuses_raw"
(
"messageId" Int64,
"messageStatusId" String,
"mailingStatusSystemName" String,
"dateTimeUtc" DateTime,
"unmergedCustomerId" Int64,
"mailingInternalId" String,
"mailingVariantNum" Nullable(String),
"mailingLink" Nullable(String),
"mailingSourceEntityType" Nullable(String),
"mailingSourceEntityId" Nullable(String),
"notSentSystemName" Nullable(String),
"notDeliveredReasonSystemName" Nullable(String),
"_isDeleted" Nullable(String),
"_rowversion_ts" DateTime,
"_data_version" Int32
)
ENGINE = MergeTree()
ORDER BY tuple()
SETTINGS index_granularity = 8192Customer merges
CREATE TABLE "MergedCustomers_raw"
(
"unmergedCustomerId" Int64,
"mergedCustomerId" Int64,
"dateTimeUtc" DateTime,
"_isDeleted" Nullable(String),
"_rowversion_ts" DateTime,
"_data_version" Int32
)
ENGINE = MergeTree()
ORDER BY tuple()
SETTINGS index_granularity = 8192Folders
CREATE TABLE "Folders_raw"
(
"internalId" String,
"systemName" String,
"name" String,
"parentInternalId" Nullable(String),
"_isDeleted" Nullable(String),
"_rowversion_ts" DateTime,
"_data_version" Int32
)
ENGINE = MergeTree()
ORDER BY tuple()
SETTINGS index_granularity = 8192Datamarts with the current state of data
These views will only contain current data without manually deleted data and without duplicates - only the latest states of facts and entities
Balance changes
CREATE VIEW BonusPointChanges_dm AS
SELECT * FROM (
SELECT *
FROM
BonusPointChanges_raw
ORDER BY
BonusPointChanges_raw._rowversion_ts DESC
LIMIT 1 BY BonusPointChanges_raw.id
) AS dm
WHERE
empty (dm._isDeleted) OR dm._isDeleted = 'false'Mechanics of points balance changes
CREATE VIEW BonusPointsMechanics_dm AS
SELECT * FROM (
SELECT *
FROM
BonusPointsMechanics_raw
ORDER BY
BonusPointsMechanics_raw._rowversion_ts DESC
LIMIT 1 BY BonusPointsMechanics_raw.id
) AS dm
WHERE
empty (dm._isDeleted) OR dm._isDeleted = 'false'Points accounts
CREATE VIEW Balances_dm AS
SELECT * FROM (
SELECT *
FROM
Balances_raw
ORDER BY
Balances_raw._rowversion_ts DESC
LIMIT 1 BY Balances_raw.id
) AS dm
WHERE
empty (dm._isDeleted) OR dm._isDeleted = 'false'Links between write-offs and accruals
CREATE VIEW NegativeCustomerBalanceChangeDetails_dm AS
SELECT * FROM (
SELECT *
FROM
NegativeCustomerBalanceChangeDetails_raw
ORDER BY
NegativeCustomerBalanceChangeDetails_raw._rowversion_ts DESC
LIMIT 1 BY NegativeCustomerBalanceChangeDetails_raw.id
) AS dm
WHERE
empty (dm._isDeleted) OR dm._isDeleted = 'false'Orders
CREATE VIEW Orders_dm AS
SELECT * FROM (
SELECT *
FROM
Orders_raw
ORDER BY
Orders_raw._rowversion_ts DESC
LIMIT 1 BY Orders_raw.id
) AS dm
WHERE
empty (dm._isDeleted) OR dm._isDeleted = 'false'Touchpoints
CREATE VIEW PointsOfContact_dm AS
SELECT * FROM (
SELECT *
FROM
PointsOfContact_raw
ORDER BY
PointsOfContact_raw._rowversion_ts DESC
LIMIT 1 BY PointsOfContact_raw.id
) AS dm
WHERE
empty (dm._isDeleted) OR dm._isDeleted = 'false'Purchase statuses dictionary
CREATE VIEW PurchaseStatuses_dm AS
SELECT * FROM
(
SELECT *
FROM PurchaseStatuses_raw
ORDER BY PurchaseStatuses_raw._rowversion_ts DESC
LIMIT 1 BY PurchaseStatuses_raw.internalId
) AS dm
WHERE empty(dm._isDeleted) OR (dm._isDeleted = 'false')Purchases
CREATE VIEW Purchases_dm AS
SELECT *
FROM Purchases_raw
ORDER BY Purchases_raw._rowversion_ts DESC
LIMIT 1 BY Purchases_raw.orderId, Purchases_raw.lineId Mailings dictionary
CREATE VIEW Mailings_dm AS
SELECT *
FROM
(
SELECT *
FROM Mailings.Mailings_raw
ORDER BY Mailings_raw._rowversion_ts DESC
LIMIT 1 BY Mailings_raw.id
) AS dm
WHERE empty(dm._isDeleted) OR (dm._isDeleted = 'false')Subscription topics
CREATE VIEW SubscriptionTopics_dm AS
SELECT *
FROM
(
SELECT *
FROM SubscriptionTopics_raw
ORDER BY SubscriptionTopics_raw._rowversion_ts DESC
LIMIT 1 BY SubscriptionTopics_raw.internalId
) AS dm
WHERE empty(dm._isDeleted) OR (dm._isDeleted = 'false')Mailing statuses
CREATE VIEW CustomerMessagesStatuses_dm AS
SELECT *
FROM
(
SELECT *
FROM CustomerMessagesStatuses_raw
ORDER BY CustomerMessagesStatuses_raw._rowversion_ts DESC
LIMIT 1 BY CustomerMessagesStatuses_raw.messageStatusId
) AS dm
WHERE empty(dm._isDeleted) OR (dm._isDeleted = 'false')Customer merges
CREATE VIEW MergedCustomers_dm AS
SELECT *
FROM
(
SELECT *
FROM MergedCustomers_raw
ORDER BY MergedCustomers_raw._rowversion_ts DESC
LIMIT 1 BY
MergedCustomers_raw.unmergedCustomerId,
MergedCustomers_raw.mergedCustomerId
) AS dm
WHERE empty(dm._isDeleted) OR (dm._isDeleted = 'false')Folders
CREATE VIEW Folders_dm AS
SELECT *
FROM
(
SELECT *
FROM Folders_raw
ORDER BY Folders_raw._rowversion_ts DESC
LIMIT 1 BY Folders_raw.internalId
) AS dm
WHERE empty(dm._isDeleted) OR (dm._isDeleted = 'false')Example of reading data using python
Using a script below read data and write it to Clickhouse. This script can then be called daily to write updates to Clickhouse.
To work with Clickhouse, the example uses pandahouse library. If necessary, you will need to install this library using pip3 install pandahouse
Before you begin, install the delta_sharing library using pip3 install delta_sharing
import delta_sharing
import pandahouse as ph
from delta_sharing.rest_client import DataSharingRestClient
from delta_sharing.protocol import DeltaSharingProfile
from delta_sharing.rest_client import DataSharingRestClient, HTTPError
from delta_sharing.protocol import CdfOptions
import pathlib
import pyarrow.parquet as pq
import requests
import io
from datetime import datetime
import urllib3
import pandas
### Connection settings for your DWH ###
connection = dict(database='DB in your DWH',
host='URL of your DWH',
user='Service user login',
password='Service user password')
class Tables():
def __init__(self, database, schema, source, target):
self.database = database # Source DB
self.schema = schema # Source schema
self.source = source # Source table
self.target = target # Destination table in your DWH
tables = []
tables.append ( Tables('exports', 'ProcessingOrders', 'BonusPointChanges', 'BonusPointChanges_raw') )
tables.append ( Tables('exports', 'ProcessingOrders', 'BalanceChangeKinds', 'BalanceChangeKinds_raw') )
tables.append ( Tables('exports', 'ProcessingOrders', 'BonusPointsMechanics', 'BonusPointsMechanics_raw') )
tables.append ( Tables('exports', 'ProcessingOrders', 'Balances', 'Balances_raw') )
tables.append ( Tables('exports', 'ProcessingOrders', 'NegativeCustomerBalanceChangeDetails', 'NegativeCustomerBalanceChangeDetails_raw') )
tables.append ( Tables('exports', 'ProcessingOrders', 'Orders', 'Orders_raw') )
tables.append ( Tables('exports', 'ProcessingOrders', 'Purchases', 'Purchases_raw') )
tables.append ( Tables('exports', 'ProcessingOrders', 'PurchaseStatuses', 'PurchaseStatuses_raw') )
tables.append ( Tables('exports', 'Mailings', 'Mailings', 'Mailings_raw') )
tables.append ( Tables('exports', 'Mailings', 'SubscriptionTopics', 'SubscriptionTopics_raw') )
tables.append ( Tables('exports', 'Mailings', 'CustomerMessagesStatuses', 'CustomerMessagesStatuses_raw') )
tables.append ( Tables('exports', 'CDP', 'MergedCustomers', 'MergedCustomers_raw') )
tables.append ( Tables('exports', 'CDP', 'Folders', 'Folders_raw') )
### Authentication ###
share_file_path = str(pathlib.Path().resolve()) + "/Profile.json" # Authentication file path
# Batch size for reading - it can be edited to fit into RAM
batch_size = 1_000_000
# Number of data file versions read at a time
cdf_batch_size = 10
# Variables fo statistics and logs
total_len = 0
start_datetime = datetime.now()
# For each table new versions of data are checked and copied to the DWH
for t in tables:
'''Paste your code below to check the last read version of the data that was copied to your DWH'''
# Getting the latest version of data in DVH - we will read everything from DS that has a higher version
df = ph.read_clickhouse('SELECT _data_version FROM ' + t.target + ' ORDER BY _data_version DESC LIMIT 1', connection=connection)
'''Paste your code above to check the last read version of the data that was copied to your DWH'''
# Checking if there is data in the table, if not, then reading is performed from version 0
if df.empty:
latest_version = 0
else:
latest_version = df.iat[0,0] + 1
# Reading from the table
# Creating a client
profile = DeltaSharingProfile.read_from_file(share_file_path)
rest_client = DataSharingRestClient(profile)
table = delta_sharing.Table(share=t.database, schema=t.schema, name=t.source)
# Getting the latest versions of data in a table - versions that are greater than latest_version
ver = latest_version
# Reading in chunks of cdf_batch_size versions at a time due to limit on number of requested versions
while True: # Reading so far no error that there are no versions available
i = 0
while True and i < 5: # 5 read attempts in case the service is unavailable
i += 1
try:
print (f"Reading data from table {t.source} versions from {ver} to {ver+cdf_batch_size-1}.")
res = rest_client.list_table_changes(
table,
cdfOptions=CdfOptions(starting_version=ver, ending_version=ver+cdf_batch_size-1)
)
except (delta_sharing.rest_client.HTTPError) as err: # error may occur if no new data is loaded or more than 10 versions are requested at once
print (f"Did not receive updated data for versions from {ver} to {ver+cdf_batch_size-1} {i} times. Reason: {err.response.text}")
continue
break
# if failed to read the data after 5 attempts, then try to read from the next table
if i == 5:
break
# If new data is received, we write it to Clickhouse
print (f"Writing data of versions from {ver} to {ver+cdf_batch_size-1} to table {t.target}")
# Reading data in parts by batch_size rows
for cdcFile in res.actions:
signed_url = cdcFile.url
def read_parquet_from_signed_url_in_batches(signed_url, batch_size):
# Getting data file
response = requests.get(signed_url, stream=True)
response.raise_for_status()
buffer = io.BytesIO()
# Downoading a file
for chunk in response.iter_content(chunk_size=1024):
buffer.write(chunk)
buffer.seek(0)
# Opening a file
parquet_file = pq.ParquetFile(buffer)
# Reading a file in batches
for batch in parquet_file.iter_batches(batch_size=batch_size):
yield batch
# Processing parquet file in batches
for batch in read_parquet_from_signed_url_in_batches(signed_url, batch_size):
# Convert batch to pandas dataframe
batch_df = batch.to_pandas()
# Adding a column with a data version to a dataframe
batch_df['_data_version'] = cdcFile.version
if '_tenant' in batch_df.columns:
batch_df = batch_df.drop('_tenant', axis=1) # Removing the column with the project name as it is not needed for analytics
# Writing to a target table in ClickHouse
i = 0
while True and i < 5: # 5 attempts to write - Clickhouse may not respond sometimes
i += 1
try:
'''Paste the code to write the dataframe to your DWH below'''
ph.to_clickhouse(batch_df, t.target, index=False, chunksize=batch_size, connection=connection)
'''Paste the code to write the dataframe to your DWH above'''
# Statistics
current_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
message = f"{len(batch_df)} rows written in {i} attempts ({current_datetime})"
print(message)
total_len += len(batch_df)
except (urllib3.exceptions.ConnectionError or urllib3.exceptions.HTTPError) as err:
print (err.response.text)
continue
break
ver += cdf_batch_size # next iteration of reading cdf_batch_size versions
# Output of information about the volume of lines read, the script execution time and the average data writing speed
current_datetime = datetime.now()
print(f"Written rows: {total_len} - in: {(current_datetime - start_datetime).total_seconds()} seconds. Average read and write speed: {total_len / (current_datetime - start_datetime).total_seconds()} rows per second")How to read changes over a specific period
Using function get_table_version you can also obtain data that was changed over a specific period.
Function get_table_version has 2 parameters:
- *table url - string in format
{share_file_path}#{database}.{schema}.{table} - date in format
YYYY-MM-DDThh:mm:ssZ- you will get first version after that date
For example 2024/10/10 11:45PM 138 version of Orders table was uploaded and 2024/10/11 11:45PM 139 version of Orders table was uploaded. If you callget_table_versionwith parameters{share_file_path}#exports.ProcessingOrders.Ordersand2024-10-11T00:00:00Zit will return 139 version - the nearest version after given date.
Second parameter is optional - if you leave it empty, you will get maximum version of the table
Below you could find an example of reading data for specific period
import delta_sharing
import pandahouse as ph
from delta_sharing.rest_client import DataSharingRestClient
from delta_sharing.protocol import DeltaSharingProfile
from delta_sharing.rest_client import DataSharingRestClient, HTTPError
from delta_sharing.protocol import CdfOptions
import pathlib
import pyarrow.parquet as pq
import pyarrow as pa
import requests
import io
from datetime import datetime
import urllib3
import pandas
import numpy as np
### Connection settings for your DWH ###
connection = dict(database='DB in your DWH',
host='URL of your DWH',
user='Service user login',
password='Service user password')
# Period boundary dates
date_start = '2024-10-29T00:00:00Z'
date_end = '2024-10-30T00:00:00Z'
class Tables():
def __init__(self, database, schema, source, target):
self.database = database # Source DB
self.schema = schema # Source schema
self.source = source # Source table
self.target = target # Destination table in your DWH
tables = []
tables.append ( Tables('exports', 'ProcessingOrders', 'Orders', 'Orders_raw') )
### Authentication ###
share_file_path = str(pathlib.Path().resolve()) + "/Profile.json" # Authentication file path
# Batch size for reading - it can be edited to fit into RAM
batch_size = 1_000_000
# Number of data file versions read at a time
cdf_batch_size = 1
# Variables fo statistics and logs
total_len = 0
start_datetime = datetime.now()
# For each table versions of data over specific period are copied to the DWH
for t in tables:
print (f"Reading from table {t.source}")
# Reading from the table
# Creating a client
table_url = f"{share_file_path}#{t.database}.{t.schema}.{t.source}"
profile = DeltaSharingProfile.read_from_file(share_file_path)
rest_client = DataSharingRestClient(profile)
table = delta_sharing.Table(share=t.database, schema=t.schema, name=t.source)
# Finding a date that matches the start of a period
version_start = delta_sharing.get_table_version (table_url, date_start)
print (f"Version corresponding to the date {date_start} for a table {t.source} is {version_start}")
# Finding a date that matches the end of a period
try:
version_end = delta_sharing.get_table_version (table_url, date_end)
except (delta_sharing.rest_client.HTTPError) as err: # If a date is after the latest data in a table - we will take the maximum version
print (f"Date {date_end} is after the latest date in a table - getting the maximum version")
version_end = delta_sharing.get_table_version (table_url)
print (f"Version corresponding to the date {date_end} for a table {t.source} is {version_end}")
ver = version_start
while ver <= version_end: #
try:
print (f"Reading data from table {t.source} versions from {ver} to {ver+cdf_batch_size-1}.")
res = rest_client.list_table_changes(
table,
cdfOptions=CdfOptions(starting_version=ver, ending_version=ver+cdf_batch_size-1)
)
print(res)
except (delta_sharing.rest_client.HTTPError) as err:
print (f"Did not receive updated data for versions from {ver} to {ver+cdf_batch_size-1} {i} times. Reason: {err.response.text}")
break
# If new data is received, we write it to Clickhouse
print (f"Writing data of versions from {ver} to {ver+cdf_batch_size-1} to table {t.target}")
# Reading data in parts by batch_size rows
for cdcFile in res.actions:
signed_url = cdcFile.url
def read_parquet_from_signed_url_in_batches(signed_url, batch_size):
# Getting data file
response = requests.get(signed_url, stream=True)
response.raise_for_status()
buffer = io.BytesIO()
# Downoading a file
for chunk in response.iter_content(chunk_size=1024):
buffer.write(chunk)
buffer.seek(0)
# Opening a file
parquet_file = pq.ParquetFile(buffer)
# Reading a file in batches
for batch in parquet_file.iter_batches(batch_size=batch_size):
yield batch
# Processing parquet file in batches
for batch in read_parquet_from_signed_url_in_batches(signed_url, batch_size):
# Convert batch to pandas dataframe
batch_df = batch.to_pandas()
# Adding a column with a data version to a dataframe
batch_df['_data_version'] = cdcFile.version
if '_tenant' in batch_df.columns:
batch_df = batch_df.drop('_tenant', axis=1) # Removing the column with the project name as it is not needed for analytics
# Writing to a target table in ClickHouse
i = 0
while True and i < 5: # 5 attempts to write - Clickhouse may not respond sometimes
i += 1
try:
'''Paste the code to write the dataframe to your DWH below'''
ph.to_clickhouse(batch_df, t.target, index=False, chunksize=batch_size, connection=connection)
'''Paste the code to write the dataframe to your DWH above'''
# Statistics
current_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
message = f"{len(batch_df)} rows written in {i} attempts ({current_datetime})"
print(message)
total_len += len(batch_df)
except (urllib3.exceptions.ConnectionError or urllib3.exceptions.HTTPError) as err:
print (err.response.text)
continue
break
ver += cdf_batch_size # next iteration of reading cdf_batch_size versions
# Output of information about the volume of lines read, the script execution time and the average data writing speed
current_datetime = datetime.now()
print(f"Written rows: {total_len} - in: {(current_datetime - start_datetime).total_seconds()} seconds. Average read and write speed: {total_len / (current_datetime - start_datetime).total_seconds()} rows per second")Example of reading data using spark
Before you begin, install the delta_sharing library using pip3 install delta_sharing
Version of connectorThe example uses the latest version of the connector for Apache Spark 3.2.0 at the time of writing. If you have problems with the version, you can check the current one here
import delta_sharing
from pyspark.sql import SparkSession
import pathlib
import delta_sharing
import pyarrow.parquet as pq
import requests
import io
from datetime import datetime
import urllib3
class Tables():
def __init__(self, database, schema, source, target):
self.database = database # Source DB
self.schema = schema # Source schema
self.source = source # Source table
self.target = target # Destination table in your DWH
tables = []
tables.append ( Tables('exports', 'ProcessingOrders', 'BonusPointChanges', 'BonusPointChanges_raw') )
tables.append ( Tables('exports', 'ProcessingOrders', 'BalanceChangeKinds', 'BalanceChangeKinds_raw') )
tables.append ( Tables('exports', 'ProcessingOrders', 'BonusPointsMechanics', 'BonusPointsMechanics_raw') )
tables.append ( Tables('exports', 'ProcessingOrders', 'Balances', 'Balances_raw') )
tables.append ( Tables('exports', 'ProcessingOrders', 'NegativeCustomerBalanceChangeDetails', 'NegativeCustomerBalanceChangeDetails_raw') )
tables.append ( Tables('exports', 'ProcessingOrders', 'Orders', 'Orders_raw') )
tables.append ( Tables('exports', 'ProcessingOrders', 'Purchases', 'Purchases_raw') )
tables.append ( Tables('exports', 'ProcessingOrders', 'PurchaseStatuses', 'PurchaseStatuses_raw') )
tables.append ( Tables('exports', 'Mailings', 'Mailings', 'Mailings_raw') )
tables.append ( Tables('exports', 'Mailings', 'SubscriptionTopics', 'SubscriptionTopics_raw') )
tables.append ( Tables('exports', 'Mailings', 'CustomerMessagesStatuses', 'CustomerMessagesStatuses_raw') )
tables.append ( Tables('exports', 'CDP', 'MergedCustomers', 'MergedCustomers_raw') )
tables.append ( Tables('exports', 'CDP', 'Folders', 'Folders_raw') )
# Creating SparkSession
spark = (
SparkSession.builder.config(
"spark.jars.packages",
"org.apache.hadoop:hadoop-azure:3.3.1,io.delta:delta-core_2.12:2.2.0,io.delta:delta-sharing-spark_2.12:3.2.0",
)
.config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension")
.config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog")
.getOrCreate()
)
### Authentication ###
share_file_path = str(pathlib.Path().resolve()) + "/Profile.json" # Authentication file path
# Number of data file versions read at a time
cdf_batch_size = 10
for t in tables:
'''Paste your code below to check the last read version of the data that was copied to your DWH'''
latest_version = #Place here script for obtaining latest version of data in your dwh
'''Paste your code above to check the last read version of the data that was copied to your DWH'''
table_url = share_file_path + f"#{t.database}.{t.schema}.{t.source}"
ver = latest_version
final_version = delta_sharing.get_table_version (table_url) # getting the maximum version in the table
# Reading in chunks of cdf_batch_size versions at a time due to limit on number of requested versions
while ver <= final_version: # Reading till the maximum version
i = 0
while True and i < 5: # 5 read attempts in case the service is unavailable
i += 1
try:
print (f"Reading data from table {t.source} versions from {ver} to {ver+cdf_batch_size-1}.")
res = (
spark.read.format("deltaSharing")
.option("readChangeFeed", "true")
.option("startingVersion", ver)
.option("endingVersion", ver+cdf_batch_size-1)
.load(table_url)
)
except (delta_sharing.rest_client.HTTPError) as err: # The request may fail if there is no new data available or if more than 10 versions are requested in a single call.
print (f"Did not receive updated data for versions from {ver} to {ver+cdf_batch_size-1} {i} times. Reason: {err.response.text}")
continue
break
# if failed to read the data after 5 attempts, then try to read from the next table
if i == 5:
break
'''Paste the code to write the dataframe to your DWH below'''
print (f"Writing data from version from {ver} to {ver+cdf_batch_size-1} into table {t.target}")
'''Paste the code to write the dataframe to your DWH above'''
ver += cdf_batch_size #next iterationWhat are the rowversion_ts, isDeleted, _data_version fields?
_rowversion_ts is a timestamp when the data was changed. For example, the upload may include 2 changes to the order status: first it was placed, then paid for. To select the current order status, we need to select the record with the maximum _rowversion_ts value
_isDeleted is a flag that the record was manually deleted from the system if its value is true. Auto-deleted records will be exported like regular data without the _isDeleted = true flag
_data_version is the version of the uploaded data, the value of this field will be increased by 1 with each new upload. We use it to check if there is new data on the server.
Updated 4 months ago

