2021-04-26 20:07:00 +02:00
|
|
|
#coding:utf-8
|
|
|
|
#
|
|
|
|
# id: bugs.core_5393
|
|
|
|
# title: Bad optimization of some operations with views containing subqueries
|
|
|
|
# decription:
|
2021-12-31 12:06:51 +01:00
|
|
|
# https://github.com/FirebirdSQL/firebird/issues/5666
|
|
|
|
#
|
|
|
|
# Re-implemented 21.11.2021: avoid compare values of datediff(millisecond from ... to ...) because heavy concurrent
|
|
|
|
# workload can occur at the time when this test is running (encountered multiple cases of this for last year).
|
|
|
|
#
|
|
|
|
# CPU User Time values are used instead, package 'psutil' must be installed for this test.
|
|
|
|
#
|
|
|
|
# Two procedures are created in order to check issue, each with pair of UPDATE statements inside cursor loop:
|
|
|
|
# * sp_upd_using_primary_key_field - with 1st UPDATE that uses primary key;
|
|
|
|
# * sp_upd_using_current_of_cursor - with 1st UPDATE that uses 'WHERE CURRENT OF <C>' feature.
|
|
|
|
# (second UPDATE in both procedures always uses PK of view v_test2)
|
|
|
|
#
|
|
|
|
# We make several calls of each procedure (see N_MEASURES) and do invocation of psutil.Process(fb_pid).cpu_times()
|
|
|
|
# before and after each call, thus and obtain CPU User Time values for each iteration.
|
|
|
|
# Difference between them can be considered as much more accurate performance estimation.
|
|
|
|
#
|
|
|
|
# ::: NB :::
|
|
|
|
# It was decided NOT to analyze table statistics (via trace or get_table_access_stats() method of db_conn)
|
|
|
|
# because access paths can change in the future. Particularly, if HASH will be used for joining indexed columns
|
|
|
|
# and optimizer will choose it for one of statements then statistics can have big numbers of natural reads for
|
|
|
|
# if and still no nat. reads in another statement.
|
|
|
|
#
|
|
|
|
# Checked on Windows:
|
|
|
|
# 3.0.1.32609 (27-sep-2016, before fix):
|
|
|
|
# ratio_lst: [222.0, 110.5, 220.0, 220.0, 111.0, 222.0, 109.5, 123.5, 148.0, 133.0, 240.0, 228.0, 224.0, 221.0, 219.0, 217.0, 222.0, 222.0, 221.0, 222.0]
|
|
|
|
# median_ratio= 220.5
|
|
|
|
#
|
|
|
|
# 3.0.2.32703 (21-mar-2017, after fix):
|
|
|
|
# ratio_lst: [0.73, 0.73, 0.8, 0.68, 0.73, 0.8, 0.68, 0.94, 0.82, 0.75, 0.66, 0.75, 0.62, 0.65, 0.57, 0.8, 0.86, 0.63, 0.52, 0.72]
|
|
|
|
# median_ratio= 0.73
|
|
|
|
#
|
|
|
|
# recent versions (3.0.8.33535, 4.0.1.2660, 5.0.0.311):
|
|
|
|
# ratio_lst: [2.0, 0.5, 1.0, 2.0, 0.5, 1.0, 15625.0, 0.5, 1.0, 0.5, 0.5, 1.0, 3.2e-05, 1.0, 1.0, 0.5, 2.0, 0.5, 1.0, 1.0]
|
|
|
|
# median_ratio= 1.0
|
|
|
|
#
|
|
|
|
# 50sS, build 5.0.0.311 : 2.814s ; 50Cs, build 5.0.0.311 : 3.153s.
|
|
|
|
# 40sS, build 4.0.1.2660 : 2.818s ; 40Cs, build 4.0.1.2660 : 3.532s.
|
|
|
|
# 30sS, build 3.0.8.33535 : 2.548s ; 30Cs, build 3.0.8.33535 : 3.162s.
|
|
|
|
#
|
|
|
|
# 21.11.2021. Checked on Linux (after installing pakage psutil):
|
|
|
|
# 5.0.0.313 SS: 3.264s
|
|
|
|
# 4.0.1.2668 SS: 2.968s
|
|
|
|
# 3.0.8.33540 SS: 3.285s
|
|
|
|
#
|
|
|
|
# OLD COMMENTS:
|
|
|
|
# -------------
|
|
|
|
# Confirmed trouble on WI-V3.0.2.3262, ratio was ~146(!).
|
|
|
|
# Performance is OK on WI-V3.0.2.32629 and WI-T4.0.0.450
|
|
|
|
# Plans differ on 3.0.2 and 4.0 thus they are excluded from the output!
|
|
|
|
# Performance of 4.0 significantly _WORSE_ than of 3.0.2, sent letter to dimitr, 11.11.2016 13:47.
|
|
|
|
#
|
2021-04-26 20:07:00 +02:00
|
|
|
# tracker_id: CORE-5393
|
|
|
|
# min_versions: ['3.0.2']
|
|
|
|
# versions: 3.0.2
|
|
|
|
# qmid: None
|
|
|
|
|
|
|
|
import pytest
|
2021-12-31 12:06:51 +01:00
|
|
|
from firebird.qa import db_factory, python_act, Action
|
2021-04-26 20:07:00 +02:00
|
|
|
|
|
|
|
# version: 3.0.2
|
|
|
|
# resources: None
|
|
|
|
|
|
|
|
substitutions_1 = []
|
|
|
|
|
|
|
|
init_script_1 = """"""
|
|
|
|
|
|
|
|
db_1 = db_factory(sql_dialect=3, init=init_script_1)
|
|
|
|
|
2021-12-31 12:06:51 +01:00
|
|
|
# test_script_1
|
|
|
|
#---
|
|
|
|
#
|
|
|
|
# import os
|
|
|
|
# import subprocess
|
|
|
|
# import psutil
|
|
|
|
# import time
|
|
|
|
#
|
|
|
|
# os.environ["ISC_USER"] = user_name
|
|
|
|
# os.environ["ISC_PASSWORD"] = user_password
|
|
|
|
# engine = db_conn.engine_version
|
|
|
|
#
|
|
|
|
# #--------------------------------------------
|
|
|
|
#
|
|
|
|
# def median(lst):
|
|
|
|
# n = len(lst)
|
|
|
|
# s = sorted(lst)
|
|
|
|
# return (sum(s[n//2-1:n//2+1])/2.0, s[n//2])[n % 2] if n else None
|
|
|
|
#
|
|
|
|
# #--------------------------------------------
|
|
|
|
#
|
|
|
|
# def flush_and_close( file_handle ):
|
|
|
|
# # https://docs.python.org/2/library/os.html#os.fsync
|
|
|
|
# # If you're starting with a Python file object f,
|
|
|
|
# # first do f.flush(), and
|
|
|
|
# # then do os.fsync(f.fileno()), to ensure that all internal buffers associated with f are written to disk.
|
|
|
|
# global os
|
|
|
|
#
|
|
|
|
# file_handle.flush()
|
|
|
|
# if file_handle.mode not in ('r', 'rb') and file_handle.name != os.devnull:
|
|
|
|
# # otherwise: "OSError: [Errno 9] Bad file descriptor"!
|
|
|
|
# os.fsync(file_handle.fileno())
|
|
|
|
# file_handle.close()
|
|
|
|
#
|
|
|
|
# #--------------------------------------------
|
|
|
|
#
|
|
|
|
# def cleanup( f_names_list ):
|
|
|
|
# global os
|
|
|
|
# for i in range(len( f_names_list )):
|
|
|
|
# if type(f_names_list[i]) == file:
|
|
|
|
# del_name = f_names_list[i].name
|
|
|
|
# elif type(f_names_list[i]) == str:
|
|
|
|
# del_name = f_names_list[i]
|
|
|
|
# else:
|
|
|
|
# print('Unrecognized type of element:', f_names_list[i], ' - can not be treated as file.')
|
|
|
|
# print('type(f_names_list[i])=',type(f_names_list[i]))
|
|
|
|
# del_name = None
|
|
|
|
#
|
|
|
|
# if del_name and os.path.isfile( del_name ):
|
|
|
|
# os.remove( del_name )
|
|
|
|
#
|
|
|
|
# #--------------------------------------------
|
|
|
|
#
|
|
|
|
#
|
|
|
|
# ###########################
|
|
|
|
# ### S E T T I N G S ###
|
|
|
|
# ###########################
|
|
|
|
# # Number of PSQL calls:
|
|
|
|
# N_MEASURES = 20
|
|
|
|
#
|
|
|
|
# # How many rows must be inserted in the table:
|
|
|
|
# N_COUNT_PER_MEASURE = 1000
|
|
|
|
#
|
|
|
|
# # Maximal value for MEDIAN of ratios between CPU user time when comparison was made.
|
|
|
|
# #
|
|
|
|
# MEDIAN_TIME_MAX_RATIO = 1.10
|
|
|
|
# ############################
|
|
|
|
#
|
|
|
|
# sql_init = '''
|
|
|
|
# set bail on;
|
|
|
|
# set echo on;
|
|
|
|
# set term ^;
|
|
|
|
# recreate global temporary table test (id int primary key using index test_pk, col int) on commit delete rows
|
|
|
|
# --recreate table test (id int primary key using index test_pk, col int)
|
|
|
|
# ^
|
|
|
|
# -- view1, must contain a subquery:
|
|
|
|
# create or alter view v_test1 (id1, id2, col1, col2, dummy) as
|
|
|
|
# select t1.id, t2.id, t1.col, t2.col, (select 1 from rdb$database)
|
|
|
|
# from test t1
|
|
|
|
# join test t2 on t1.col = t2.id
|
|
|
|
# ^
|
|
|
|
#
|
|
|
|
# -- view2, must contain a subquery:
|
|
|
|
# create or alter view v_test2 (id1, id2, col1, col2, dummy) as
|
|
|
|
# select ta.id, tb.id, ta.col, tb.col, (select 1 from rdb$database)
|
|
|
|
# from test ta
|
|
|
|
# join test tb on ta.col = tb.id
|
|
|
|
# ^
|
|
|
|
#
|
|
|
|
# -- empty triggers to make both views updatable:
|
|
|
|
# create or alter trigger trg_vtest1_bu for v_test1 before update as
|
|
|
|
# begin
|
|
|
|
# end
|
|
|
|
# ^
|
|
|
|
# create or alter trigger trg_vtest2_bu for v_test2 before update as
|
|
|
|
# begin
|
|
|
|
# end
|
|
|
|
# ^
|
|
|
|
#
|
|
|
|
# create procedure sp_insert_rows( a_rows_cnt int ) as
|
|
|
|
# declare i int = 0;
|
|
|
|
# begin
|
|
|
|
# while ( i < a_rows_cnt ) do
|
|
|
|
# begin
|
|
|
|
# insert into test(id, col) values(:i, :i);
|
|
|
|
# i = i + 1;
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
# ^
|
|
|
|
#
|
|
|
|
# create procedure sp_upd_using_primary_key_field as
|
|
|
|
# begin
|
|
|
|
#
|
|
|
|
# for select id1 from v_test1 as cursor c do
|
|
|
|
# begin
|
|
|
|
# -- ####################################
|
|
|
|
# -- "Both updates utilize the primary key index for table T1. So far so good."
|
|
|
|
# -- ####################################
|
|
|
|
# update v_test1 x set x.col1 = 1
|
|
|
|
# where x.id1 = c.id1; ------------------ u s i n g t a b l e s P K
|
|
|
|
#
|
|
|
|
# update v_test2 y set y.col1 = 1
|
|
|
|
# where y.id1 = c.id1;
|
|
|
|
#
|
|
|
|
# -- execute statement ('update v_test2 set col1 = 1 where id1 = ?') (c.id1);
|
|
|
|
#
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
# ^
|
|
|
|
#
|
|
|
|
# create procedure sp_upd_using_current_of_cursor as
|
|
|
|
# begin
|
|
|
|
# for select id1 from v_test1 as cursor c do
|
|
|
|
# begin
|
|
|
|
#
|
|
|
|
# -- first update is not reported in the plan because it's based on the same cursor as the select itself
|
|
|
|
# update v_test1 u set u.col1 = 1
|
|
|
|
# where current of c; ------------------ u s i n g c u r r e n t o f
|
|
|
|
#
|
|
|
|
# -- ######################################################
|
|
|
|
# -- "... second update is unable to utilize the primary key index for table T1 anymore"
|
|
|
|
# -- ######################################################
|
|
|
|
# -- In the production database, this issue is causing 100x degradation in execution time.
|
|
|
|
# update v_test2 v set v.col1 = 1
|
|
|
|
# where v.id1 = c.id1;
|
|
|
|
#
|
|
|
|
# -- execute statement ('update v_test2 set col1 = 1 where id1 = ?') (c.id1);
|
|
|
|
#
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
# ^
|
|
|
|
# set term ;^
|
|
|
|
# commit;
|
|
|
|
# '''
|
|
|
|
#
|
|
|
|
#
|
|
|
|
# f_init_sql=open( os.path.join(context['temp_directory'],'tmp_5393_init.sql'), 'w')
|
|
|
|
# f_init_sql.write(sql_init)
|
|
|
|
# flush_and_close( f_init_sql )
|
|
|
|
#
|
|
|
|
# f_init_log=open( os.path.join(context['temp_directory'],'tmp_5393_init.log'), 'w')
|
|
|
|
# f_init_err=open( os.path.join(context['temp_directory'],'tmp_5393_init.err'), 'w')
|
|
|
|
# subprocess.call( [ context['isql_path'], dsn, '-q', '-i', f_init_sql.name], stdout=f_init_log, stderr=f_init_err )
|
|
|
|
# flush_and_close( f_init_log )
|
|
|
|
# flush_and_close( f_init_err )
|
|
|
|
#
|
|
|
|
# cur=db_conn.cursor()
|
|
|
|
#
|
|
|
|
# cur.execute("select rdb$relation_id from rdb$relations where rdb$relation_name = upper('test')")
|
|
|
|
# test_rel_id = int(cur.fetchone()[0])
|
|
|
|
#
|
|
|
|
# cur.execute('select mon$server_pid as p from mon$attachments where mon$attachment_id = current_connection')
|
|
|
|
# fb_pid = int(cur.fetchone()[0])
|
|
|
|
#
|
|
|
|
# name_suffixes = ( 'using_primary_key_field', 'using_current_of_cursor' )
|
|
|
|
# sp_time = {}
|
|
|
|
#
|
|
|
|
# for i in range(0, N_MEASURES):
|
|
|
|
# cur.callproc( 'sp_insert_rows', (N_COUNT_PER_MEASURE,) )
|
|
|
|
# for x_name in name_suffixes:
|
|
|
|
#
|
|
|
|
# tabstat1 = [ p for p in db_conn.get_table_access_stats() if p.table_id == test_rel_id ]
|
|
|
|
#
|
|
|
|
# fb_info_init = psutil.Process(fb_pid).cpu_times()
|
|
|
|
# cur.callproc( 'sp_upd_%(x_name)s' % locals() )
|
|
|
|
# fb_info_curr = psutil.Process(fb_pid).cpu_times()
|
|
|
|
#
|
|
|
|
# tabstat2 = [ p for p in db_conn.get_table_access_stats() if p.table_id == test_rel_id ]
|
|
|
|
#
|
|
|
|
# # [ ... 'backouts', 'deletes', 'expunges', 'indexed', 'inserts', 'purges', 'sequential', 'table_id', 'table_name', 'updates']
|
|
|
|
# #print('mode:',x_name)
|
|
|
|
# #print('stat before:')
|
|
|
|
# #for t in tabstat1:
|
|
|
|
# # print( t.table_id, t.sequential, t.indexed )
|
|
|
|
# #print('stat after:')
|
|
|
|
# #for t in tabstat2:
|
|
|
|
# # print( t.table_id, t.sequential, t.indexed )
|
|
|
|
#
|
|
|
|
# sp_time[ x_name, i ] = max(fb_info_curr.user - fb_info_init.user, 0.000001)
|
|
|
|
#
|
|
|
|
# # commit will truncate GTT, so we can insert again into empty table on next iteration:
|
|
|
|
# db_conn.commit()
|
|
|
|
#
|
|
|
|
#
|
|
|
|
# ratio_lst = []
|
|
|
|
# for i in range(0, N_MEASURES):
|
|
|
|
# ratio_lst.append( sp_time['using_current_of_cursor',i] / sp_time['using_primary_key_field',i] )
|
|
|
|
#
|
|
|
|
# median_ratio = median(ratio_lst)
|
|
|
|
#
|
|
|
|
# #for k,v in sorted(sp_time.items()):
|
|
|
|
# # print(k,':::',v)
|
|
|
|
# #print('ratio_lst:', [round(p,2) for p in ratio_lst] )
|
|
|
|
# #print('median_ratio=',median_ratio)
|
|
|
|
# msg = 'Median value of elapsed time ratios: '
|
|
|
|
# if median_ratio < MEDIAN_TIME_MAX_RATIO:
|
|
|
|
# msg += 'acceptable.'
|
|
|
|
# print(msg)
|
|
|
|
# else:
|
|
|
|
# msg += 'INACCEPTABLE, more than threshold = %5.2f' % MEDIAN_TIME_MAX_RATIO
|
|
|
|
# print(msg)
|
|
|
|
#
|
|
|
|
# print('Check values for %d measurements:' % N_MEASURES)
|
|
|
|
# for i,p in enumerate(ratio_lst):
|
|
|
|
# print( '%3d : %12.2f' % (i,p) )
|
|
|
|
#
|
|
|
|
# print('Check values of elapsed time:')
|
|
|
|
# for k,v in sorted(sp_time.items()):
|
|
|
|
# print('proc_name, measure: ',k,' - elapsed time, ms: ',v)
|
|
|
|
#
|
|
|
|
#
|
|
|
|
# cleanup( (f_init_sql, f_init_log,f_init_err) )
|
|
|
|
#
|
|
|
|
#---
|
2021-04-26 20:07:00 +02:00
|
|
|
|
2021-12-31 12:06:51 +01:00
|
|
|
act_1 = python_act('db_1', substitutions=substitutions_1)
|
2021-04-26 20:07:00 +02:00
|
|
|
|
|
|
|
expected_stdout_1 = """
|
2021-12-31 12:06:51 +01:00
|
|
|
Median value of elapsed time ratios: acceptable.
|
2021-12-22 20:23:11 +01:00
|
|
|
"""
|
2021-04-26 20:07:00 +02:00
|
|
|
|
|
|
|
@pytest.mark.version('>=3.0.2')
|
2021-12-31 12:06:51 +01:00
|
|
|
@pytest.mark.xfail
|
2021-04-28 12:42:11 +02:00
|
|
|
def test_1(act_1: Action):
|
2021-12-31 12:06:51 +01:00
|
|
|
pytest.fail("Test not IMPLEMENTED")
|