0
Posted on Monday, January 04, 2016 by 醉·醉·鱼 and labeled under
引用: http://www.brentozar.com/archive/2013/12/recompile-hints-and-execution-plan-caching/
http://www.sqlskills.com/blogs/kimberly/exec-and-sp_executesql-how-are-they-different/
http://www.sqlskills.com/blogs/kimberly/high-performance-procedures/



Kimberly Tripp 看上去建议使用Dynamic query来避免parameter sniffing的问题。这里来一个简化版了。

第一次执行SP的时候,query plan会被cache起来。第二次执行的时候,会被用到。很显然,第二次会遇到parameter sniffing问题。

ALTER PROCEDURE Get_OrderID_OrderQty
 @ProductID INT,
 @OrderId INT = NULL
AS 

SELECT SalesOrderDetailID, OrderQty
FROM Sales.SalesOrderDetail
WHERE ProductID = @ProductID 
AND (SalesOrderID = @OrderId OR @OrderId IS NULL)

GO

SET STATISTICS IO  ON

EXEC Get_OrderID_OrderQty @ProductID=870

EXEC Get_OrderID_OrderQty @ProductID=870, @OrderId= 51108

SET STATISTICS IO OFF

改成Dynamic query的方式,就好了。

ALTER PROCEDURE Get_OrderID_OrderQty
 @ProductID INT,
 @OrderId INT = NULL
AS 

DECLARE @sql NVARCHAR(MAX) = N'';

SET @sql = 'SELECT SalesOrderDetailID, OrderQty
FROM Sales.SalesOrderDetail
WHERE ProductID = @ProductID '

IF @OrderId IS NOT NULL
    SET @sql = @sql + 'AND SalesOrderID = @OrderId'

EXEC sys.sp_executesql @sql
    , N'@ProductID INT, @OrderId INT'
    , @ProductID, @OrderId

GO

SET STATISTICS IO  ON

EXEC Get_OrderID_OrderQty @ProductID=897

EXEC Get_OrderID_OrderQty @ProductID=897, @OrderId= 51108

SET STATISTICS IO OFF



这样就好了?那我们就too young too simple了。有一个明显的问题,就是tipping point。如果这个时候执行下面的query,你就会得到的一个糟糕的结果。实际上,IO由1246变成了14379,这是非常非常的糟糕的。最好参数里面能够有high selective的,否则还是得老老实实加上OPTION(RECOMPILE)。

SET STATISTICS IO  ON

EXEC Get_OrderID_OrderQty @ProductID=870

SELECT SalesOrderDetailID, OrderQty, SalesOrderID 
FROM Sales.SalesOrderDetail
WHERE ProductID = 870
SET STATISTICS IO OFF
0
Posted on Monday, January 04, 2016 by 醉·醉·鱼 and labeled under
引用: http://www.brentozar.com/archive/2013/06/the-elephant-and-the-mouse-or-parameter-sniffing-in-sql-server/
http://www.brentozar.com/archive/2013/12/recompile-hints-and-execution-plan-caching/
http://www.sqlpass.org/summit/2014/PASStv.aspx?watch=p-6ii2NiUI0


  1. SP_RECOMPILE 在不知道怎么改的时候,一般的临时方案就是sp_recompile. 
    EXEC sys.sp_recompile 'Get_OrderID_OrderQty' 
    GO
  2. WITH RECOMPILE 能够工作,但是这样做会没有cached query plan,也没有DMV data。
    ALTER PROCEDURE Get_OrderID_OrderQty
     @ProductID INT
     WITH RECOMPILE
    AS 
    
    SELECT SalesOrderDetailID, OrderQty
    FROM Sales.SalesOrderDetail
    WHERE ProductID = @ProductID;
  3. OPTION RECOMPILE 比上面稍微好点,能够cached 最后一次执行的query plan,DMV里面也能够看到数据了。
    ALTER PROCEDURE Get_OrderID_OrderQty
     @ProductID INT
    AS 
    
    SELECT SalesOrderDetailID, OrderQty
    FROM Sales.SalesOrderDetail
    WHERE ProductID = @ProductID OPTION(RECOMPILE);
  4. 核武器 - DBCC FREEPROCCACHE 世界一下清净了,所有的都需要重新compile和cache。但这只是短暂的,等下一次糟糕的query plan被cache起来以后,问题还是会出现。
  5. Dynamic query?这个看上去是个不错的选择。但实际上我还是发现,我需要加上OPTION(RECOMPILE),否则,第一次执行的query plan会被cache起来被第二次执行用掉,问题依旧。而加上以后,其实就是第三个方案一样了。
    ALTER PROCEDURE Get_OrderID_OrderQty
     @ProductID INT
    AS 
    
    DECLARE @sql NVARCHAR(MAX) = N''
    
    SET @sql = 'SELECT SalesOrderDetailID, OrderQty
    FROM Sales.SalesOrderDetail
    WHERE ProductID = @ProductID OPTION(RECOMPILE)'
    
    EXEC sys.sp_executesql @sql, N'@ProductId INT', @ProductID
    
    GO
附上方案5的IO和执行计划
SET STATISTICS IO  ON

EXEC Get_OrderID_OrderQty @ProductID=870

EXEC Get_OrderID_OrderQty @ProductID=897

SET STATISTICS IO OFF



事实上,这里涉及到SQL SERVER的tipping point了。所以,在这个例子里面,我们只能够加上OPTION(RECOMPILE)了。至少我现在还没有想出什么好方法。
0
Posted on Monday, January 04, 2016 by 醉·醉·鱼 and labeled under

引用 http://www.brentozar.com/archive/2013/06/the-elephant-and-the-mouse-or-parameter-sniffing-in-sql-server/

什么是parameter sniffing

来看个例子。
拿AdventureWorks2012为例,执行下面的query

可以看到每个query的IO和执行计划。



创建一个SP。


CREATE PROCEDURE Get_OrderID_OrderQty
 @ProductID INT
AS 

SELECT SalesOrderDetailID, OrderQty
FROM Sales.SalesOrderDetail
WHERE ProductID = @ProductID;

执行下面的query。第一次执行的query plan会被cache起来,并被第二个query引用。问题也就出在第二次query上面。可以从IO 和执行计划上面看出,SQL SERVER使用了一个不是很好,有时候可能会使很糟糕的执行计划。IO从之前的10跑到了1246,看执行计划也会发现,预估的是返回4688条记录,实际上只有2条记录,相差太大了。






实际上,Sales.SalesOrderDetail也只有1237个page。

DBCC IND ('AdventureWorks2012', 'Sales.SalesOrderDetail', 0);

这就是parameter sniffing,由于传入的参数的不同,会倒是返回的结果集差异太大,而SQL SERVER却不能够正确的选择最优的执行计划。

0
Posted on Tuesday, December 29, 2015 by 醉·醉·鱼 and labeled under
引用: https://technet.microsoft.com/en-us/library/ms181055(v=sql.105).aspx
https://www.youtube.com/watch?list=PLoGAcXKPcRvanix7u9eqg_qt1tp849rX3&v=p-6ii2NiUI0

MSDN上面有这么一段关于plan cache的文章。大致就是说

  1. 当没有cached query plan 的时候,执行query就会创建一个query plan并被cache起来。
  2. 下面这些操作可以导致cached plan被清掉
    • SERVER RESTART/DATABASE LEVEL CHANGES/CONFIGURATION CHANGES
    • DBCC FREEPROCCACHE/DBCC FREESYSTEMCACHE/SP_RECOMPILE
    • DROP & RECREATE SP
    • Object changes,包括view,table,statistics
    • 长时间没用
MSDN提到,SQL SERVER采用了基于COST的机制去清理query plan。如果当前没有内存压力,SQL SERVER 不会去清query plan。如果有内存压力,SQL SERVER回去验证每个query plan,并且降低他们的currect cost,直至0。一旦当它变成0并且内存压力来的时候,每次验证的同时就会清掉这个plan。如果在这之前这个query plan被用掉,那current cost又会被reset成最原来的cost,即不会被清掉。



0
Posted on Monday, October 12, 2015 by 醉·醉·鱼 and labeled under
用ruby写了一个定期发邮件的脚本,然后又写了个sh去调这个脚本,在cron里面添加一个job定期调用这个sh文件,然后他就能够正常工作了。但是最近突然发现有时候他居然不能够发邮件了。我以为又是gmail被墙了,但是我上次已经改过host了,应该不会有问题了。而且还加了30分钟内retry的机制。


64.233.161.108 smtp.gmail.com

百思不得其解。在遇到N次这事之后,我就去看log,看邮件,看自己job的日志。突然发现一件事,不知道什么时候,cron的PID 会变成一个很小的number,然后一路往上升,直到下次又变成一个很小的number。一旦PID变成很小的number,第一个被启动的job就不会正常工作。

一般来说,cron的job被启动,会在cron.log里面有记录,在系统邮件(/var/mail)里面也有记录。如下图,在10月7日早上10点,有一个脚本被启动了,但是在系统邮件里面却没法找到对应的记录。在这个job之前和之后的job在系统邮件和cron.log里面都有记录相对应,唯独这条没有。同时,也可以看到,大概4个小时前,PID number由64348降成352。

我又找了几个例子,都能够对应我上面的结论:“一旦PID变成很小的number,第一个被启动的job就不会正常工作。

现在我只能够在我需要跑的job前一分钟再添加一个job A,啥事都不做,只是单独占个坑,这样就算倒霉也就是这个job A没法正常启动。

这都是啥奇葩的事情。


0
Posted on Sunday, July 19, 2015 by 醉·醉·鱼 and labeled under ,
其实,网上已经有很多计算器了,比如新浪的计算器就算还能够过去,只是有两点

  1. 计算不够精确。其实这个不能够怪新浪的计算器。实际上每个银行在计算每年1月份利率调整的方法各不一样。比如这篇博客里面解释了工商银行的计算方式,而我按照他的计算方式去算结果总是会有一些偏差。
  2. 局限性。他只能够计算到3个利率变化的情况,一般5年以上的贷款利率变化很有可能超过3个。
  • 关于等额本息以及相关计算,请转百科和这篇博客
我按照我自己的案例,写了以下脚本,去看农行是如何计算1月份的本息和的。结果发现,农行在计算一月份利息的时候是按照通用分段计息的方法,但是本金却是继续延续上一轮利率计算的本金。比如,12月份的剩余本金是10000块,那么1月份的本金是按照上一年的利率算,利息又是分段计算,然后加起来算出本息和。此后2月份就正常了。

反正每年1月份都是算的31天的利率,基本上1月份都要比平时还得多。


require 'date'

# 贷款总额
total_loan_money = 50 * 10000
# 贷款年限
total_loan_month = 20 * 12
# 初始利率
initial_interest_rate = 4.7
# 第一次还贷时间
initial_pay_dt = '2011-05-08'
# 利率调整时间表
interest_rate_changes = [] 
interest_rate_changes << [initial_pay_dt, initial_interest_rate]
interest_rate_changes << ['2012-01-01', 4.9]
interest_rate_changes << ['2013-01-01', 4.5]
interest_rate_changes << ['2015-01-01', 4.25]
interest_rate_changes << ['2016-01-01', 3.5]

p interest_rate_changes

def averageInterest(n, money, rate, ini_pay_dt)
 list = []
 money_to_pay_per_month = (money * rate * (1 + rate) ** n /((1 + rate) ** n - 1)).round(2)

 n.times do |i|
  remaining_load_money = (i==0) ? money : list[i-1][4]
  interest_to_pay = (remaining_load_money * rate).round(2)
  load_money_to_pay = (money_to_pay_per_month - interest_to_pay).round(2)
  # [date, 本期利息,本期本金,本期本息,剩余本金]
  list << [ini_pay_dt.strftime('%Y-%m-%d'), interest_to_pay, load_money_to_pay, 
money_to_pay_per_month, (remaining_load_money - load_money_to_pay).round(2)]

  ini_pay_dt = ini_pay_dt.next_month
 end
 list
end

def averageInterestAdjust(list, date, total_loan_month, rate1, rate2)
 tmp_list = []
 last_list = list[-2]
 money = last_list[4]
 rate1_per_day = rate1 / 30
 rate2_per_day = rate2 / 30
 ini_pay_dt = Date.parse(last_list[0])

 rate1_days = date - ini_pay_dt
 rate2_days = ini_pay_dt.next_month - date

 # 本月利息
 interest_to_pay = (money * (rate1_per_day * rate1_days + 
rate2_per_day * rate2_days)).round(2)

 # 本月应还金额
 # 农业银行特色 应还本金还是按照上一年的利率计算的,但是利息是分段计算的
 money_to_pay = (interest_to_pay + list[-1][2]).round(2)

 tmp_list = [ini_pay_dt.strftime('%Y-%m-%d'), interest_to_pay, 
(money_to_pay - interest_to_pay), money_to_pay, 
money-(money_to_pay - interest_to_pay)]

 list[-1][1] = tmp_list[1].round(2)
 list[-1][2] = tmp_list[2].round(2)
 list[-1][3] = tmp_list[3].round(2)
 list[-1][4] = tmp_list[4].round(2)

 list
end

list = []
i = 0
ini_pay_dt = Date.parse(initial_pay_dt)

interest_rate_changes.each do |t|

 date = Date.parse(t[0])

 if ini_pay_dt == date
  i += 1
  next
 else
  diff = (date.year - ini_pay_dt.year) * 12 + (date.month - ini_pay_dt.month)
 end

 list += averageInterest(total_loan_month, total_loan_money, 
    interest_rate_changes[i-1][1]/100.0/12.0, ini_pay_dt).slice(0, diff+1)

 total_loan_month -= (diff) 

 # 每年1月1日利率调整,按日计息
 list = averageInterestAdjust(list, date, total_loan_month, 
    interest_rate_changes[i-1][1]/100.0/12.0, interest_rate_changes[i][1]/100.0/12.0)

 total_loan_month -= 1
 ini_pay_dt = Date.parse(list[-1][0]).next_month
 total_loan_money = list[-1][4]
 i += 1
end

list += averageInterest(total_loan_month, total_loan_money, 
    interest_rate_changes[-1][1]/100.0/12.0, ini_pay_dt)

total_interest = 0

list.each do |t|
 p t
 total_interest += t[1]
end

puts total_interest

0
Posted on Wednesday, July 08, 2015 by 醉·醉·鱼 and labeled under , ,
最近在玩ActiveRecord和Sql server,发现ActiveRecord居然支持preparedstatement了。但有些功能还是不能支持,比如NOT EXISTS和CROSS APPLY。

比如,下面这段SQL就没有办法转换成为ActiveRecord

只是有些接近的方法

生成的SQL实际上是

用到了LEFT JOIN,而不是EXISTS。不过,还好是prepared statement。

或者直接用SQL

实际上面生成的SQL就不是prepared过的了。这样就不好了,就不好的利用cache plan了。比起LEFT JOIN 引起的side effect,我更加倾向用LEFT JOIN方式,至少能够cache plan,不用每次compile。