To make our lives easier migrating Qlik Replicate tasks from one environment to another; I am looking how to migrate Qlik Replicate schedules.
So far by GitLab pipelines we can migrate tasks and endpoint; but schedules we must do manually. Quite often we forget to manually create the schedules as they are not as obvious component compared to tasks and endpoints. If you move to a new environment and your task is not there, well that is a no brainer. If your endpoint is not there; your task will not migrate.
But schedules are something that lurks in the background; inconsistently created when we rush to put them in when we realise the Qlik task did not run when it was meant to.
Plan
Using the python API, we can export all the items from a Qlik Replicate server with the export_all method. In the returned Json file will be the schedule details like this:
The idea is to modify the Json for the schedule to match the environment we are migrating to; put the schedule into a blank server Json template and use the import_all
method to upload the new schedules to the destination server.
The Cron does not look right…
Looking over the exported Json; the cron syntax did not look right. It had six fields instead of the expected five.
"schedule": "0 1 * * * *"
This confused me for a while as I haven’t come across a cron like this before.
After coming up with no results searching Qlik’s documentation; I created some test schedules to try and determine what the sixth field is used for.
Ahh. After a short while I found the answer.
If the schedule is a “Once off” run; the sixth field is used for “Year”.
I think as Qlik Replicate developers; we have all been here before.
The testers berating you through MS Teams saying, “We have made a change, and we cannot see it downstream! It is all Qlik Replicate’s fault!”
Opening the task and checking the monitoring screen – I can see the change against the table. What are they going on about? Qlik has picked up something; why can’t they see it? (Sixty percent of the time; something downstream has failed, twenty percent of the time something upstream has failed, nineteen percent of the time they are looking in the wrong spot and the remaining one percent of the time – well we won’t mention that one percent.)
But looking at the monitoring screen – what does those numbers mean? Also, if you look in the analytics database; what does those figures mean?
Hopefully, this article will help you understand the monitoring and analytics numbers with some simple examples.
Filters – why does it have to be filters?
We have two main types of Qlik Replicate tasks. One type grabs all changes from a particular table and sends it to our Data Lake in micro batches of fifteen minutes.
The other type are our speed tasks; only grabbing changes on specific columns on a table. To limit QR to only picking up specific changes; we have filters like this:
($AR_H_OPERATION != 'UPDATE' AND $AR_H_OPERATION != 'DELETE') OR
(
($AR_H_OPERATION == 'UPDATE') AND
(
( $BI__FIELD_1 != $FIELD_1 ) OR ( $BI__FIELD_1 IS NULL AND $FIELD_1 IS NOT NULL ) OR ( $BI__FIELD_1 IS NOT NULL AND $FIELD_1 IS NULL ) OR
( $BI__FIELD_2 != $FIELD_2 ) OR ( $BI__FIELD_2 IS NULL AND $FIELD_2 IS NOT NULL ) OR ( $BI__FIELD_2 IS NOT NULL AND $FIELD_2 IS NULL ) OR
( $BI__FIELD_3 != $FIELD_3 ) OR ( $BI__FIELD_3 IS NULL AND $FIELD_3 IS NOT NULL ) OR ( $BI__FIELD_3 IS NOT NULL AND $FIELD_3 IS NULL )
)
)
So, the question is – if we made an update to $FIELD_4 meaning that QR will send the change downstream; what would the monitoring tab on the task display?
Let is create an example to find out.
An example
Here is a simple table -pre-populated with six record:
CREATE TABLE dbo.CDC_ANALYTICS_EXAMPLE
(
RECORD_ID INT IDENTITY(1,1) PRIMARY KEY,
DATA_INT INT,
DATA_VARCHAR VARCHAR(100),
INCLUDE_RECORD CHAR(1)
);
GO
INSERT INTO dbo.CDC_ANALYTICS_EXAMPLE VALUES(1, 'Record 1', 'Y');
INSERT INTO dbo.CDC_ANALYTICS_EXAMPLE VALUES(2, 'Record 2', 'Y');
INSERT INTO dbo.CDC_ANALYTICS_EXAMPLE VALUES(3, 'Record 3', 'Y');
INSERT INTO dbo.CDC_ANALYTICS_EXAMPLE VALUES(4, 'Record 4', 'N');
INSERT INTO dbo.CDC_ANALYTICS_EXAMPLE VALUES(5, 'Record 5', 'N');
INSERT INTO dbo.CDC_ANALYTICS_EXAMPLE VALUES(6, 'Record 6', 'N');
SELECT *
FROM dbo.CDC_ANALYTICS_EXAMPLE WITH (NOLOCK);
And we create a simple QR task with the following filters:
The first filter is the Fullload Passthru Filter. When the full load initially runs; only three of the six records will be brought across.
The second filter Record Selection Condition; should filter any data changes on when $INCLUDE_RECORD has a value of ‘Y’.
Results after running the QR task
Full load
After running the full load; qlik shows the following on the monitoring screen:
Three records – that’s what we expected out of the six.
Interesting under the “Total Completion” section; the distinction of the filter is quite clear. Three records brought across and three remaining.
Change Processing
Let’s make some changes to the data in our test table:
-- This will be captured
BEGIN TRANSACTION
DECLARE @IDENTITY INT;
INSERT INTO dbo.CDC_ANALYTICS_EXAMPLE VALUES(null, 'Record x', 'Y');
SET @IDENTITY = @@IDENTITY;
UPDATE dbo.CDC_ANALYTICS_EXAMPLE
SET DATA_INT = @IDENTITY,
DATA_VARCHAR = 'Record ' + CAST(@IDENTITY AS varchar)
WHERE RECORD_ID = @IDENTITY;
DELETE dbo.CDC_ANALYTICS_EXAMPLE WHERE RECORD_ID = @IDENTITY;
COMMIT;
GO
-- This will be excluded
BEGIN TRANSACTION
DECLARE @IDENTITY INT;
INSERT INTO dbo.CDC_ANALYTICS_EXAMPLE VALUES(null, 'Record x', 'N');
SET @IDENTITY = @@IDENTITY;
UPDATE dbo.CDC_ANALYTICS_EXAMPLE
SET DATA_INT = @IDENTITY,
DATA_VARCHAR = 'Record ' + CAST(@IDENTITY AS varchar)
WHERE RECORD_ID = @IDENTITY;
DELETE dbo.CDC_ANALYTICS_EXAMPLE WHERE RECORD_ID = @IDENTITY;
COMMIT;
This is what the monitoring screen shows:
Looking at the Recent activity screen; hmm – this could lead to some confusion.
The metrics shows the two blocks of changes; even though one block was filtered out. So this is indicating that this gives the results when there is a change in the table; not matter if it was subsequently filtered out.
If those testers in the opening paragraph created a record that was filtered out in QR; we could give them false direction if we said “it should be downstream”
If we look at the Aggregates screen; it paints a true picture of what was detected and transferred to the target:
We can see the three records in the “Total Changes Applied” column.
Analytics database
From the analytic database:
SELECT
*
FROM public.aem_taskv t
WHERE
t.task_name IN ('TEST_ANALYTICS')
ORDER BY
t.server_name, t.task_name, t.retrieval_time
Field
Value
id
110876201
retrieval_time
2025-12-03 12:02
server_id
5
task_name_id
914
task_state_id
3
task_stop_reason_id
1
task_profile_id
1
cdc_evt_applied_insert_count
1
cdc_evt_applied_update_count
1
cdc_evt_applied_delete_count
1
cdc_evt_applied_ddl_count
0
full_load_tables_completed_count
1
full_load_tables_loading_count
0
full_load_tables_queued_count
0
full_load_tables_with_error_count
0
full_load_total_records_transferred
3
full_load_est_records_count_for_all_tables
6
full_load_completed
1
full_load_start
2025-12-03 10:57
full_load_finish
2025-12-03 10:57
full_load_thrput_src_thrput_records_count
0
full_load_thrput_src_thrput_volume
0
full_load_thrput_trg_thrput_records_count
0
full_load_thrput_trg_thrput_volume
0
cdc_thrput_src_thrput_records_count
0
cdc_thrput_src_thrput_volume
0
cdc_thrput_trg_thrput_records_count
0
cdc_thrput_trg_thrput_volume
0
cdc_trans_read_rollback_count
0
cdc_trans_read_records_rollback_count
0
cdc_trans_rollback_change_volume
0
cdc_trans_applied_transactions_in_progress_count
0
cdc_trans_applied_records_in_progress_count
0
cdc_trans_applied_comitted_transaction_count
2
cdc_trans_applied_records_comitted_count
6
cdc_trans_applied_volume_committed
0
cdc_trans_read_memory_events_count
0
cdc_trans_read_swapped_events_count
0
cdc_trans_applied_memory_events_count
0
cdc_trans_applied_swap_events_count
0
cdc_source_latency
0
cdc_apply_latency
0
memory_usage_kb
0
disk_usage_kb
0
cpu_percentage
0
data_error_count
0
task_option_full_load_enabled
1
task_option_apply_changes_enabled
0
task_option_store_changes_enabled
1
task_option_audit_changes_enabled
0
task_option_recovery_enabled
0
cdc_trans_read_in_progress
2
server_cpu_percentage
0
machine_cpu_percentage
16
tasks_cpu_percentage
0
server_name
xxxx
task_name
TEST_ANALYTICS
server_with_task_name
xxxx::::TEST_ANALYTICS
A couple of take aways from the results:
Unfortunately the records in the the analytic database is grouped up at a task level; not at an individual table level. This makes it harder to determine if a change from a particular table was applied
cdc_trans_applied_records_comitted_count is a count of all the records read; but not necessarily applied
If you want the number of records flowing down to downstream; add together the fields cdc_evt_applied_insert_count, cdc_evt_applied_update_count and cdc_evt_applied_delete_count together
Final thoughts
The monitoring and analytics screen is useful tools for diagnosing QR tasks – and general ASMR; watching the numbers bounce up as transactions flow through the system.
Understanding the figures of what they translate to – whether changes getting detected or applied downstream is fundamental in diagnosing problems.
Nothing is better than getting access to the source and target systems and checking yourself to confirm if things are working correctly; but the monitoring screen is a good place to start investigating.
The formation of a Lutheran congregation at Castlemaine in Victoria, Australia (1861 – 1940)
The history of the Lutheran congregation in Castlemaine is strongly linked to the history of the Bendigo congregation. In 1856 a congregation of Lutherans was formed in Bendigo in communion with the Evangelical Lutheran Church of Victoria.
The beginnings of the Lutheran congregation in Castlemaine must have dated to a similar time, as in 1861 a Lutheran Church was constructed in Castlemaine at the corner of Hargraves and Parker Street. A notice in the Mount Alexander Mail, on 19th July 1861, recorded the laying of the foundation stone for the building.
Today no sign of the original church building remains, with residential dwellings located in the area where it once stood. It is understood that the building was destroyed by fire in 1919. The Castlemaine Art Museum records the existence of a photo of the church in its archives, and I have requested a digital copy of the photo. Considering the time period, it is likely that the building was a fairly modest one.
A news article, from the Mount Alexander Mail, dated 29th May 1875, indicates a manse was located at the same site. The original dwelling, although altered and fully renovated, still exists today at 99 Hargreaves St.
In 1862 Pastor Friedrich Munzel arrived in Australia and took up the role of Pastor for Castlemaine, Maldon and Yandoit. In the same year Pastor Munzel, who resided in Castlemaine, was requested to include Bendigo into his sphere of labour.
He preached every second week in Bendigo. It was recorded that because he was not living in Bendigo, Pastor Munzel was unable to make as much an impression on them as would a Pastor who was living amongst them. It appears that the Bendigonians were a tough crowd.
In 1864 (2 years from his arrival) a serious dissension between Pastor Munzel and the employed schoolteacher at Bendigo caused a storm and eventually lead to a split in the Bendigo congregation. With these events having taken a toll on his heath, Pastor Munzel resigned in 1868, completing a term of 6 years.
The congregations of Bendigo, Castlemaine and Maldon sent a call to Germany for a new minister, and the call was accepted by Pastor Friedrich Leypoldt, who took charge in 1869, and commenced work to heal the rifts. In those days a good many of the members were gold miners, living in tents, scattered over the hills, flats and back gullies, and it was often no easy matter to find their dwellings.
Pastor Leypoldt conducted services once a month in Castlemaine.
An article in the Mount Alexander Mail, on 11th June 1907, gave notice for a general meeting of the congregation to be held on the 12th of June at the German Lutheran Church in Barker St.
I have not been able to identify where this Church building, in Barker St, was located. One has to wonder if this was a typo error and meant to refer to Parker St.
Outbreak of World War I (1914 – 1918).
1914 saw the outbreak of World War I.
German Lutherans in Australia faced intense anti-German sentiment, leading to the closure of Lutheran schools, the banning of the German language in services and publications, and widespread social prejudice. German Lutherans, many of whom were Australian born, were subjected to harassment and negative attitudes from their fellow citizens.
In 1922, 4 years after the end of the war, Pastor Leypoldt moved to Sandringham and interestingly, the LCA archives (Sept 2023) record Castlemaine ceasing to be a preaching place in 1922. This is despite the fact that Pastor Leypoldt apparently travelled back to deliver services once a month in Castlemaine until 1928 and in Bendigo until 1930. This was an incredible term of service, spanning 61 years and demonstrated incredible dedication to his flock.
In 1929 the Victorian Synod asked Pastor Prove, of Grovedale, to take over from Pastor Leypoldt.
There was a steady decline in attendances during this period, however, it was noted that during the 1930’s some members of the Evangelical Lutheran Church of Australia (ELCA) moved to Bendigo.
During this extended period of vacancy, Pastor Prove visited the area on a monthly basis up until 1937 (a substantial period of 7 years).
In 1936 the Home Mission Board issued a number of calls for a pastor to move to the area. This was successful in 1937 when Pastor Peter Strelan arrived and commenced services in Bendigo and Echuca. At various times he also conducted services at neighbouring regional locations including Castlemaine, Maldon and Maryborough.
The outbreak of World War II (1939 – 1945)
Following the outbreak of war for a second time, German Lutherans in Australia faced a continuation of intense anti-German sentiment. Some Lutheran pastors and members were arrested and interned by the Australian government as enemy aliens.
LCA records show that one of the locations served by Pastor Strelan included the Tatura Internment Camp (spanning a period of 1941-1946).
The transition to English services (1910 – 1945)
German continued to be the language of many Lutheran homes for up to three or four generations. Similarly, German was the language of the Lutheran Church in its worship and its business. In the early 1900s, moves were made to introduce English, and this was hastened by the outbreak of World War I. There was a transition period in the 1920s and 1930s, and after World War II, only English was used.
A New Beginning (from 1952)
The LCA archives (Sept 2023) record Castlemaine as an Evangelical Lutheran Church preaching place from 1952 to the present day.
With the world wars behind them, by 1952, Lutheran services at Castlemaine had presumably become a regular feature on the calendar.
Pastor Strelan accepted a call to Unley, SA, in 1953 having served for a not inconsiderable 16 years.
Over the next 71 years (spanning from 1954 – 2025) a total of 13 different Pastors would faithfully serve the members of Castlemaine and its surrounding districts.
One notable Pastor was Stanley Mibus who served from 1954 – 1958 (4 years) and then missed the place so much he returned in 1961 for another 5 year stint. I guess there is just something special about this area.
Castlemaine’s membership during the 1960’s varied from the mid 30’s to the mid 40’s, but the average worship attendance was below 10. This was a time of significant immigration from the UK and Europe, with the government easing racial restrictions in 1966.
In 1967 the then serving Pastor, Pastor Harms reported “because of several transfers from the Castlemaine area, the attendances at the services there (two a month) have dropped back to less than half a dozen per service”.
The Chewton Era (~1978 – 2004)
Pastor Paul Stolz arrived in 1977 and around this time (perhaps 1978) worship services commenced at St Johns Anglican Church, 18 Fryers Rd, Chewton.
An unloved feature of the church property was the corrugated iron toilet structure, inhabited by spiders. After making do for 26 years, this shortcoming eventually led to the decision to find an alternative place of worship. The final service at Chewton is recorded as being held on the 18th of January 2004.
Castlemaine membership during the 1970’s hovered around the mid 30’s but declined in the 1980’s and 1990’s, dropping back to the mid 20’s. Average attendance was okay at around 15.
The Recent Era (2004 – 2025)
In 2004 worship services commenced at the Seventh Day Adventist Church, 252 Barker St, Castlemaine. The first worship service there is recorded as being held on the 1st of February 2004 and would have been conducted by Pastor Greg Lockwood.
This information was read by Craig Linke at the final Lutheran worship service in Castlemaine on Sunday the 5th of October 2025.
Records
Baptised membership at Castlemaine –
1963 – 33
1965 – 46
1978 – 35
1986 – 23
1983 – 23
1991 – 25
Average attendance at Castlemaine –
1967 – 4
1969 – 6
1975 – 20
1992 – 14
Figures obtained from the statistics held at Bendigo Church (2025).
We have been doing a bit of “Stress and Volume” testing in Qlik Replicate over the past few days; investigating how my latency is introduced to a MS SQL server task if we run it through a log stream.
If you’re not aware – you can get minute latecy from Qlik Replicate by turning the “Performance” logs up to “Trace” or higher:
This will result in messages getting produced in the task’s log file like:
I created a simple python script to parse a folder of log files and output it in a excel. Since the data ends up in a panda data frame; it would be easy to manipulate the data and output it in a specific way:
import os
import pandas as pd
from datetime import datetime
def strip_seconds(inString):
index = inString.find(" ")
returnString = float(inString[0:index])
return(returnString)
def format_timestamp(in_timestamp):
# Capture dates like "2025-08-01T10:42:36" and add a microsecond section
if len(in_timestamp) == 19:
in_timestamp = in_timestamp + ":000000"
date_format = "%Y-%m-%dT%H:%M:%S:%f"
# Converts date string to a date object
date_obj = datetime.strptime(in_timestamp, date_format)
return date_obj
def process_file(in_file_path):
return_array = []
with open(in_file_path, "r") as in_file:
for line in in_file:
upper_case = line.upper().strip()
if upper_case.find('[PERFORMANCE ]') >= 0:
timestamp_temp = upper_case[10:37]
timestamp = timestamp_temp.split(" ")[0]
split_string = upper_case.split("LATENCY ")
if len(split_string) == 4:
source_latency = strip_seconds(split_string[1])
target_latency = strip_seconds(split_string[2])
handling_latency = strip_seconds(split_string[3])
# Makes the date compatible with Excel
date_obj = format_timestamp(timestamp)
excel_datetime = date_obj.strftime("%Y-%m-%d %H:%M:%S")
# If you're outputting to standard out
#print(f"{in_file_path}\t{time_stamp}\t{source_latency}\t{target_latency}\t{handling_latency}\n")
return_array.append([in_file_path, timestamp, excel_datetime, source_latency, target_latency, handling_latency])
return return_array
if __name__ == '__main__':
log_folder = "/path/to/logfile/dir"
out_excel = "OutLatency.xlsx"
latency_data = []
# Loops through files in log_folder
for file_name in os.listdir(log_folder):
focus_file = os.path.join(log_folder, file_name )
if os.path.isfile(focus_file):
filename, file_extension = os.path.splitext(focus_file)
if file_extension.upper().endswith("LOG"):
print(f"Processing file: {focus_file}")
return_array = process_file(focus_file)
latency_data += return_array
df = pd.DataFrame(latency_data, columns=["File Name", "Time Stamp", "Excel Timestamp", "Source Latency", "Target Latency", "Handling Latency"])
df.info()
# Dump file to Excel; but you can dump to other formats like text etc
df.to_excel(out_excel)
And voilà – we have an output to Excel to quickly create statistics on latency for a given task(s).
What statistics to use?
Latency is something you can analyse in different ways, depending on what you’re trying to answer. It is also important to pair latency statistics with the change volume that is coming through.
Has the latency jumped at a specific time because the source database is processing a daily batch? Is there a spike of latency around Christmas time where there are more financial transactions compared to a benign day in February?
Generally, if the testers are processing the latency data they provide back:
Average target latency
90th percentile target latency
95th percentile target latency
Min target latency
Max target latency
This is what we use to compare two runs to each other when the source load is consistent and we’re changing a Qlik Replicate task
I am just writing a brief post about a conversation I was asked to join.
The question paraphrased:
For dates in an Microsoft SQL Server, when they get passed through Qlik Replicate to Kafka Avro – how are they interpreted? Epoch to utc? Or to the current time zone?
I didn’t know the answer so I created the following simple test:
This is a Mac + Cheese recipe that I learnt from the OMG BBQ course.
It is truly frightening how decadent it is; I have never seen so much cheese go into one recipe. And that’s not even considering the amount of cream to add to it.
This is the recipe dictated by the owner so I haven’t tried cooking it myself; but the results were the most gooey, artery clogging, heart stopping Mac n Cheese I have ever tasted.
The Legend behind the recipe
The legend where this recipe originates from is from a “gangster” living in America; involved in all sorts of violence and drugs. One day an epiphany came to him that he was heading towards a very early grave.
So he got out of crime, went to rehab to got clean and opened up a BBQ restaurant/food van.
But he was looking for ideas to expand his business.
Using his former life’s intuition and the opportunities that some US states has leagalised cannabis; he realised there was a market for good “munchies” food at cannabis festivals that are regularly held.
The problem with some food truck’s menu choice; hot chips are a bottleneck to the process. Quite often people are waiting for chips to be cooked.
He was searching for something that he can serve with BBQ meats, very quick and easy to make, not a bottleneck in a food truck business and satisfies the festival goers particular needs.
This is when he came up with this recipe for Mac and Cheese.
The Recipe
Guesstimate serves 8 people
Ingredients
500g Macaroni pasta shapes
1kg Tasty cheese – grated
50gm Parmesan cheese – grated
2 large eggs
1L Thicken cream
2 tsp ground Black pepper
2 tsp Garlic Powder (Guesstimate – chef did it by feel)
2 tsp Onion powder (Guesstimate – chef did it by feel)
2 tsp Smoked Paprika (Guesstimate – chef did it by feel)
Extra Paprika for dusting on top
Method
Pre-heat an oven to 160°c .
Cook the pasta to packet instructions. Drain the pasta and allow it to cool slightly.
Reserve a cup of grated cheese.
Meanwhile in a large bowl; add in remaining cheese, eggs, thickened cream and seasonings. Gently mix to combine.
Once the pasta is cool enough; add into the bowl and stir to combine everything.
Pour into a baking dish. Sprinkle over reserved cheese and dust evenly with paprika.
Place on a tray to catch the drippings and bake for 25min until lightly brown on top.
Rest for 5min and then serve
Notes
While I don’t think one’s cardiologist would recommend eating this every day; I do see some potential as using this recipe as a base. This would be a show stopping comfort food on a cold winter’s night; either served as a side, or as a main with some nice bread.
One thing I would change from the base recipe that we were taught is to under cook the pasta by a couple of minutes. This will prevent it from going too mushy in the final product.
Other things I was thinking:
Don’t use pre-grated cheese – the additives in it don’t make the cheese melt as well.
Add some creamy compatible vegetables like pumpkin, broccoli, mushrooms and/or corn to it.
Instead of smoked paprika, use nutmeg
Experiment with a different combination of cheeses to add a more complex flavour.
My introduction to my in-law’s Christmas traditions was “Nancy’s Noodle Salad”; a recipe handed down to my to-be wife that she would cook for an Australian Christmas lunch.
The whole trouble is with her excitement for Christmas; she would quite often cook way too much; and we’re eating “Nancy’s Noodle Salad” for a week afterwards.
But this salad is important; nostalgias of times gone by and remembering passed on love ones. And someone on this vast World Wide Web might pick up Nancy’s Christmas Noodle salad and start their own tradition with it.
Anyway – I can’t read the original recipe and I have to get my wife to interpret the hand writing.
The Recipe
Ingredients
Makes a side for about 4 – my wife quadruples it for Christmas
Main Salad component
250g of Short pasta shapes
1 stick of Celery – diced
Half a of a Red and Green capsicums – diced
Small can of corn
1/2 cp Sultanas
300g Bacon – diced
Dressing
1/2 cp Salad oil (I just use canola oil)
1/2 cp Sugar
1/2 cp Vinegar
1 or 2 tsp of Keen’s curry powder
Method
Combine all dressing ingredients into a jar with a tight lid. Shake to combine and then set aside
Cook the pasta to packet instructions. Once cook; rinse under cold water and drain well and place in a large serving bowl.
Add the bacon to a cold non stick frypan. Turn to medium-high heat, stir occasionally until while foam appears around the cooked bacon piece. Remove bacon from pan and drain on paper towels.
In the serving bowl, add in the diced celery, diced capsicums, the small can of corn, sultanas and bacon. This can be wrapped up and placed in the fridge ready to be served.
When ready to serve, mix the salad ingredients together. Drizzle over salad dressing to desired level and mix into the salad.
For over fifteen years I have worked with a Microsoft SQL Data Warehouse that is over twenty-five years old. Although it is ancient in this world of huge cloud-based Data Vaults – it churns out an abundant of value to the organisation I work for.
The problem with this value – it is opened to anyone who wants to access to build their queries off. This means we get a range of SQL queries running on the database. We have queries ranging from precise and efficient built queries that uses every trick in optimisation – all the way to “WTF are you trying to do?!” queries.
The second category is the one we have the most problem with. With limited resources of an OnPrem SQL server; some uses write atrocious queries without a perceived care for other people using the database. There is nothing more frustrating getting a support call out because of a failed data load; only to find the table is locked by someone running a query over the past six hours.
To counter this problem; I created a python script that checks what is running on the database every fifteen minutes. If a user has a query running for over half an hour, I get an alert to show what they are running.
This allows our team to:
Work out if someone is running a query that is blocking or taking resources away from our critical processes – resulting in timeouts
With our experience help users optimise poorly running queries so they have a better outcome
To stop any stupid queries from running
Since the queries are logged into a DB – I have four years of history of 14,000 unique queries running. This gives me a lot of learning experiences of what users are running and what problems they face.
From this learning – this is the Top 10 most horrible queries I see running on our database.
8. The “Discovery” Query
This query come from new and experience users exploring the data of the database. They will run something like this:
SELECT *
FROM dbo.A_VERY_LARGE_TABLE;
Look at the data, grab what they need and then just leave the query running in the background in their Server Management Studio. When we contact them an hour later; they act surprise that the query is still running in the background – returning millions upon millions of rows to their client.
This is an easy fix with training. Using the TOP command to limit the number of rows returned, use SP_HELP to return a schema of an object, provide a sandbox for people to explore in are all relatively simple fixes for this problem
7. The “Application Preview” Query
Our SAS application have a nasty default setting that if a user previews a data in a table; it will try and return the whole table – with the user completely unaware that this is happening.
Even worse; if the application locks up returning the data; the user kills the application from task manager, open it back up and performs the same steps. Since the first query was not killed, it will still be running on the database. So quite often you will see half a dozen of the same queries running on the database, all starting at different times.
To prevent this – it is important to assess new DB client software to see what it is trying to do in the background of the database. What does “preview” mean? Return 10, 100 or all the rows? Is there a setting that restricts the number of rows returned; or a timeout function that prevents complex views running forever just returning a small set of data?
If these features are available – it is important to either set this as default in the application; or part of the initial user setup / training program.
6. The “I am going to copy this to Excel and Analyse the Data” Query
This is like exploratory queries that come up. The user is running a query like:
SELECT *
FROM dbo.A_VERY_LARGE_TABLE;
“Why are you running this query?” I politely enquire. “Oh – I am going to copy the data into excel and search/pivot/do fancy things with it” “Ummm – this table has 8 billion rows in it…”
I can sympathise with queries like these.
The user thinks, “Why run the same query over and over again to get the data that I want to explore and present in different ways. Let just get one large cut and then manipulate it on the client.”
What users don’t realise that dealing with a huge amount of data on the client side is difficult. Exporting data out of the query client, into an application like excel can be frustrating with memory and CPU limitations compared to a spec’d-out server. Plus, Excel is not going to handle 8 billion rows.
The requirements of queries like these needs to be analysed – do the users need that atomic level data? Can strategic aggregation tables be provided to the user, so they have more manageable data to return. Can different technology like OLAP cubes be built on top of large tables to answer the user’s needs?
5. The “I got duplicates so I am going to get rid of them with a DISTINCT” Query
This is a pet peeve of mine.
The user has duplicate in their results and don’t know why. Instead of investigating why there is duplication (usually result of a poor join), they just slap a DISTINCT at the top of the query and call it done. Here is a simplified example that I regularly come across:
DECLARE @ACCOUNTS TABLE
(
ORG INT,
ACCOUNT_ID INT,
BRANCH INT,
BALANCE NUMERIC(18,2),
PRIMARY KEY (ORG, ACCOUNT_ID)
);
INSERT INTO @ACCOUNTS VALUES(1, 18, 60, 50);
INSERT INTO @ACCOUNTS VALUES(1, 19, 60, 150);
DECLARE @BRANCHES TABLE
(
ORG INT,
BRANCH INT,
BRANCH_NAME VARCHAR(20),
BRANCH_STATE VARCHAR(3),
PRIMARY KEY (ORG, BRANCH)
);
INSERT INTO @BRANCHES VALUES(1, 60, 'Branch of ORG 1', 'VIC');
INSERT INTO @BRANCHES VALUES(2, 60, 'Branch of ORG 2', 'VIC');
SELECT
A.ORG,
A.ACCOUNT_ID,
A.BRANCH,
A.BALANCE,
B.BRANCH_STATE
FROM @ACCOUNTS A
JOIN @BRANCHES B
ON -- B.ORG = A.ORG -- User forgot this predicate in the join
B.BRANCH = A.BRANCH;
Results:
ORG
ACCOUNT_ID
BRANCH
BALANCE
BRANCH_STATE
1
18
60
50.00
VIC
1
18
60
50.00
VIC
1
19
60
150.00
VIC
1
18
60
150.00
VIC
The user looks – “Oh Dear – I’ve got duplicates! Let’s get rid of them.”
SELECT DISTINCT
A.ORG,
A.ACCOUNT_ID,
A.BRANCH,
A.BALANCE,
B.BRANCH_STATE
FROM @ACCOUNTS A
JOIN @BRANCHES B
ON -- B.ORG = A.ORG -- User forgot this predicate in the join
B.BRANCH = A.BRANCH;
This poses many problems:
The Database must work harder using incomplete joins on indexes to bring across the data; and then work harder supressing the duplicates
If the field list change; then the distinct might not work anymore.
For example, with the above query – if the user brings in the column “BRANCH_NAME” then the duplicates return:
SELECT DISTINCT
A.ORG,
A.ACCOUNT_ID,
A.BRANCH,
A.BALANCE,
--------- Add in new column ---------
B.BRANCH_NAME,
-------------------------------------
B.BRANCH_STATE
FROM @ACCOUNTS A
JOIN @BRANCHES B
ON -- B.ORG = A.ORG -- User forgot this predicate in the join
B.BRANCH = A.BRANCH;
ORG
ACCOUNT_ID
BRANCH
BALANCE
BRANCH_NAME
BRANCH_STATE
1
18
60
50.00
Branch of ORG 1
VIC
1
18
60
50.00
Branch of ORG 2
VIC
1
19
60
150.00
Branch of ORG 1
VIC
1
18
60
150.00
Branch of ORG 2
VIC
Preventing queries like this comes down to user experience and training. If they see duplicates in their data – their initial thoughts should be “Why do I have duplicates?”
Duplicates might be legitimate and a DISINCT might be OK – but quite often it is because of a bad join. For an inexperience user – it might be overwhelming to break down a large table to find where the duplicates are coming from, and they might need help from a more experienced user. This is where internal data forums are useful – where people can help each other with their problems.
4. The “Ever lengthening” Query
This is a query I see scheduled daily for users getting trends over time for a BI tool:
SELECT *
FROM dbo.A_TABLE A
JOIN dbo.B_TABLE B
ON B.KEY_1 = A.KEY_1
WHERE
--------------- Start Date ---------------
A.BUSINESS_DATE >= '2025-01-01' AND
------------------------------------------
A.PREDICATE_1 = 'X' AND
B.PREDICATE_2 = 'Y';
Initially it starts off OK – running fast and the user is happy. But as time goes by; the query gets slower and slower; returning more and more data. Eventually it gets to the point the query goes off into the nether and never returns. Since it is always starting the from the same point; it is constantly re-querying the same data over an over again
There are a couple of options you can handle queries like these:
Really examine the requirements of the user’s needs. How much data is really relevant for their trending report? In 2027; will data from 2025 be useful for the observer of the report?
Create a table specific for this report and append a timeframe of data (eg daily) onto it. The report than can just query this specific table and not having to regenerate complex joins over and over again for previous timeframes.
3. The “Linked Database” Query
With the growth of our Data Warehouse, other database on different servers started building process flows off our database; sometimes without out knowledge that they are doing this until we see a linked server connection coming up.
What pattern they use our database is where the problems lie. One downstream database runs this query every day:
SELECT *
FROM REMOTE_SERVER.SOME_DATABASE.dbo.A_BIG_HISTORICAL_TABLE;
So, they are truncating a landing table on their database and grabbing the whole table. Hundreds of millions of rows transferring across a slow linked server; taking hours and hours. And with time; this will get slower and slower as the source table grows its history.
This is a tricky one to tackle with the downstream users. Odds are if they design a cumbersome process like this their appetite for change to a faster (yet more complex solution) might not be high; especially if their process ‘works’.
If you can get around the political hurdle and convince them the process must change; there are many options to improve the performance.
Use a ETL tool to transfer the data across. Even a simple python script to copy batch by bath files across will be quicker than using a linked server.
Assess the downstream use cases for the data. Do they need the whole table with the complete history to satisfy their requirements? Do they need all the columns; or can some be trimmed off to reduce the amount of data getting transferred?
If the source table a historical table; only bring across the period deltas. With the downstream process I am having trouble with now; they are bringing across twenty-one million rows daily. If they only bring across the deltas from the night loads it brings across a whopping five thousand rows. That is 0.02% of the total row count. Add in a row count check between the two systems to have confidence that the two tables are in sync and the process will be astronomically quicker.
2. The “I don’t know where that query comes from” Query.
We have a continuing problem from a Microsoft Power BI report that runs a query that looks at data from the beginning of the month to the current date. It was a poorly written query and therefore as the month went along its performance got worse and worse.
Since the username associated with the query was from the Power BI server’s account – we had no idea who the query belonged to as a couple of simple fixes could drastically improve the performance.
We contacted the team that manages Power BI server and asked them to find the query and update the sql code.
They said it was completed, and the report sql was updated.
But soon the sql was back – running for hours and hours.
We contacted the team again –
“Hey that query is back.”
They try updating the report again; but soon the query was back again.
So, either someone was constantly uploading an original copy – or another copy of the report was buried somewhere on the server that the admins could not find.
Since we do not have access to their system, it hard to determine what is happening.
The barbaric solution would be to block the Power BI user from our system; but goodness knows how many critical business processes that will disrupt.
The best solution at the minute we are doing is just constantly killing the running SQL code on the database with the hope that someone will identify the constantly failing report. This can be tricky as well if the Power BI server automatically tries rerunning failed reports.
1. The “I don’t listen to your advice” Query.
Disclaimer – this is a frustration rant that I have to get off my chest.
We have a user that constantly runs a long running crook query. As in when I collated all the long running queries in research for this post – his was at the top by a significant margin.
The problem with the query itself is a join using an uncommonly used business key in a table that is not indexed. The fix itself is quite simple fix; use the primary keys in join.
But he has been running the same code for months – locked our nightly loads several times and caused incidents.
We tried the carrot approach. “Hey here is optimised code. It will make your query run quicker and be less burden on our database.” Got indifferent replies.
More locked loads we included his manager into correspondence, but she did not seem to want to be involved.
With my carrot approach communication, I gained the impression that he had a self-righteous personality and thinks his role and report is above question.
Enough was enough – it was the stick approach time.
We raised an operational risk against his activity.
The Ops risk manager asks, “Do you have evidence that you attempted to reason and help?”
“Yep,” I replied, “Here is a zipped-up folder of dozens of emails and chat messages that I sent him.”
Senior managers were involved; with my senior manager commenting to my manager in the background “Is that guy all there?”
Anyway, my manager wrote a very terse email saying to correct their query and if they crash the loads again; they will get their access removed from the database.
No committal reply. No apology. And to this day they are still running the same query; but just under the radar that it is not locking our loads.
I am watching him like a hawk – waiting for the day that I can cut his access from the database.
It is a pity that our Database does not have a charge back capability of processes used. I bet if his manager got a bill of $$$ from one person running one query; she would be more proactive in this problem.
In retrospect, I would have campaigned to have his access cut right away and make him justify why he should have it back. When there is hundreds of other people doing the right thing on the database; it is not fair that their deliverables are getting impacted by the indifference attitude of one user.
My sister in-law runs a dog training business in Queensland. Part of her business is to track records of her canine clients – especially notes, vaccinations when they’re due, medical records and certificates.
In a previous job she had experience with Animal Shelter Manager (ASM3). She’s familiar with the features and interface to know it will cover her needs.
Her business is not big enough to justify the price of the SaaS version of ASM3; so being the tech savvy (debatable) one the family – it was my task getting it up and running for her.
This lead to several nights of struggling, annoyance and failure.
Fitting the pieces together
To start off I wanted to get a demo version running so I can see what I need to do to deploy it for her.
Apart from a domain name that was sure to set off all the “appropriate content” filters at work; with a few modifications I could get it to work. Looking at the instructions from the author Ræn; it is substantially different to the old Dockerfile and the instructions on the ASM3 home page.
Let’s build it
With my base working version – I cobbled up some Dockerfiles for ASM3 and postres and a docker compose file to tie them together:
(Note that this is not a production version and have to obscure passwords etc in the final version)
The containers build just fine and fire up with no problem.
Vising the website
http://localhost/
ASM3 redirects and builds the database – but then goes to a login page. I enter the username and password; but it loops back to the login page.
I think the problem lies with the base_url and service_url in asm3.conf; possibly with http-asm3.conf settings.
Anyway – I logged a issue with ASM3 see if it is something simple that I missed; or maybe I have to start pulling apart of source code to find what it is trying to do.
Wait – Performance problems writing to an AWS RDS Postgres database?
Haven’t we been here before?
Yes, we had, and I wrote quite a few posts of the trials and tribulations that we went through.
But this is a new problem that we came across that resulted in several messages backwards and forwards between us and Qlik before we worked out the problem.
Core System Migration.
Our organisation has several core systems; making maintenance of them expensive and holding us back in using the data in these systems in modern day tools like advance AI. Over the past several years – various projects are running to consolidate the systems together.
This is all fun and games for the downstream consumers – as they have lots of migration data coming down the pipelines. For instance, shell accounts getting created on the main system from the sacrificial system.
One downstream system wanted to exclude migration data from their downstream data and branch the data into another database so they can manipulate the migrated data to fit it into their pipeline.
I created the Qlik Replicate task to capture the migration data. It was a simple task to create. Unusually, the downstream users created their own tables that they want me top pipe the data into. In the past, we let Qlik Replicate create the table in the lower environment, copy the schema and use that schema going forwards.
Ready to go we fired up the task in testing to capture the test run through of the migration.
Slow. So Slow.
The test migration ran on the main core system, and we were ready to capture the changes under the user account running the migration process.
It was running slow. So slow.
We knew data was getting loaded as we were periodically running a SELECT COUNT(*) on the destination table. But we were running at less than 20tps.
Things we checked:
The source and target databases were not under duress.
The QR server (although a busy server) CPU and Memory wasn’t maxed out.
There were no critical errors in the error log.
Records were not getting written to the attrep_apply_exceptions table.
There were no triggers built off the landing table that might be slowing down the process
We knew from previous testing that we could get a higher tps.
I bumped up the logging on “Target Apply” to see if we can capture more details on the problem.
One by One.
After searching the log files, we came across an interesting message:
00007508: 2025-02-20T08:33:24:595067 [TARGET_APPLY ]I: Bulk apply operation failed. Trying to execute bulk statements in 'one-by-one' mode (bulk_apply.c:2430)
00007508: 2025-02-20T08:33:24:814779 [TARGET_APPLY ]I: Applying INSERTS one-by-one for table 'dbo'.'DESTINATION_TABLE' (4) (bulk_apply.c:4849)
For some reason instead of using a bulk load operation – QR was loading the records one by one. This accounted for the slow performance.
But why was it switching to this one-by-one mode? What caused the main import of bulk insert to fail – but one-by-still works.
Truncating data types
First, I suspected that the column names might be mismatched. I got the source and destination schema out and compared the two.
All the column names aligned correctly.
Turning up the logging we got the following message:
00003568: 2025-02-19T15:24:50 [TARGET_APPLY ]T: Error code 3 is identified as a data error (csv_target.c:1013)
00003568: 2025-02-19T15:24:50 [TARGET_APPLY ]T: Command failed to load data with exit error code 3, Command output: psql:C:/Program Files/Attunity/Replicate/data/tasks/MY_CDC_TASK_NAME/data_files/0/LOAD00000001.csv.sql:1: ERROR: value too long for type character varying(20)
CONTEXT: COPY attrep_changes472AD6934FE46504, line 1, column col12: "2014-08-10 18:33:52.883" [1020417] (csv_target.c:1087)
All the varchar fields between the source and target align correctly.
Then I noticed the column MODIFIED_DATE. On the source it is a datetime; while on the postgres target it is just a date.
My theory was that the bulk copy could not handle the conversion – but in the one-by-one; it could truncate the time component off the date and successfully load.
The downstream team changed the field from a date to a timestamp and I reloaded the data. With this fix the task went blindingly quick; from hours for just a couple of thousands of records to all done within minutes.
Conclusion
I suppose the main conclusion from this exercise is that “Qlik Replicate Knows best.”
Unless you have a very accurate mapping process from the source to the target; let QR create the destination table in a lower environment. Use this table as a source and build on it.
Websites store cookies to enhance functionality and personalise your experience. You can manage your preferences, but blocking some cookies may impact site performance and services.
Essential cookies enable basic functions and are necessary for the proper function of the website.
Name
Description
Duration
Cookie Preferences
This cookie is used to store the user's cookie consent preferences.
30 days
These cookies are needed for adding comments on this website.
Name
Description
Duration
comment_author
Used to track the user across multiple sessions.
Session
comment_author_email
Used to track the user across multiple sessions.
Session
comment_author_url
Used to track the user across multiple sessions.
Session
Statistics cookies collect information anonymously. This information helps us understand how visitors use our website.
Google Analytics is a powerful tool that tracks and analyzes website traffic for informed marketing decisions.
Contains information related to marketing campaigns of the user. These are shared with Google AdWords / Google Ads when the Google Ads and Google Analytics accounts are linked together.
90 days
__utma
ID used to identify users and sessions
2 years after last activity
__utmt
Used to monitor number of Google Analytics server requests
10 minutes
__utmb
Used to distinguish new sessions and visits. This cookie is set when the GA.js javascript library is loaded and there is no existing __utmb cookie. The cookie is updated every time data is sent to the Google Analytics server.
30 minutes after last activity
__utmc
Used only with old Urchin versions of Google Analytics and not with GA.js. Was used to distinguish between new sessions and visits at the end of a session.
End of session (browser)
__utmz
Contains information about the traffic source or campaign that directed user to the website. The cookie is set when the GA.js javascript is loaded and updated when data is sent to the Google Anaytics server
6 months after last activity
__utmv
Contains custom information set by the web developer via the _setCustomVar method in Google Analytics. This cookie is updated every time new data is sent to the Google Analytics server.
2 years after last activity
__utmx
Used to determine whether a user is included in an A / B or Multivariate test.
18 months
_ga
ID used to identify users
2 years
_gali
Used by Google Analytics to determine which links on a page are being clicked
30 seconds
_ga_
ID used to identify users
2 years
_gid
ID used to identify users for 24 hours after last activity
24 hours
_gat
Used to monitor number of Google Analytics server requests when using Google Tag Manager