Dalam LOG
T-SQL murni dan EXP
beroperasi dengan float
ketik (8 byte), yang hanya memiliki 15-17 digit signifikan
. Bahkan digit ke-15 terakhir bisa menjadi tidak akurat jika Anda menjumlahkan nilai yang cukup besar. Data Anda adalah numeric(22,6)
, jadi 15 digit signifikan tidak cukup.
POWER
dapat mengembalikan numeric
mengetik dengan presisi yang berpotensi lebih tinggi, tetapi tidak banyak berguna bagi kami, karena keduanya LOG
dan LOG10
hanya dapat mengembalikan float
bagaimanapun juga.
Untuk mendemonstrasikan masalah, saya akan mengubah jenis dalam contoh Anda menjadi numeric(15,0)
dan gunakan POWER
bukannya EXP
:
DECLARE @TEST TABLE
(
PAR_COLUMN INT,
PERIOD INT,
VALUE NUMERIC(15, 0)
);
INSERT INTO @TEST VALUES
(1,601,10 ),
(1,602,20 ),
(1,603,30 ),
(1,604,40 ),
(1,605,50 ),
(1,606,60 ),
(2,601,100),
(2,602,200),
(2,603,300),
(2,604,400),
(2,605,500),
(2,606,600);
SELECT *,
POWER(CAST(10 AS numeric(15,0)),
Sum(LOG10(
Abs(NULLIF(VALUE, 0))
))
OVER(PARTITION BY PAR_COLUMN ORDER BY PERIOD)) AS Mul
FROM @TEST;
Hasil
+------------+--------+-------+-----------------+
| PAR_COLUMN | PERIOD | VALUE | Mul |
+------------+--------+-------+-----------------+
| 1 | 601 | 10 | 10 |
| 1 | 602 | 20 | 200 |
| 1 | 603 | 30 | 6000 |
| 1 | 604 | 40 | 240000 |
| 1 | 605 | 50 | 12000000 |
| 1 | 606 | 60 | 720000000 |
| 2 | 601 | 100 | 100 |
| 2 | 602 | 200 | 20000 |
| 2 | 603 | 300 | 6000000 |
| 2 | 604 | 400 | 2400000000 |
| 2 | 605 | 500 | 1200000000000 |
| 2 | 606 | 600 | 720000000000001 |
+------------+--------+-------+-----------------+
Setiap langkah di sini kehilangan presisi. Menghitung LOG kehilangan presisi, SUM kehilangan presisi, EXP/POWER kehilangan presisi. Dengan fungsi bawaan ini, saya rasa Anda tidak dapat berbuat banyak.
Jadi, jawabannya adalah - gunakan CLR dengan C# decimal
ketik (bukan double
), yang mendukung presisi yang lebih tinggi (28-29 angka signifikan). Jenis SQL asli Anda numeric(22,6)
akan cocok dengannya. Dan Anda tidak perlu trik dengan LOG/EXP
.
Ups. Saya mencoba membuat agregat CLR yang menghitung Produk. Ini berfungsi dalam pengujian saya, tetapi hanya sebagai agregat sederhana, yaitu
Ini berfungsi:
SELECT T.PAR_COLUMN, [dbo].[Product](T.VALUE) AS P
FROM @TEST AS T
GROUP BY T.PAR_COLUMN;
Dan bahkan OVER (PARTITION BY)
bekerja:
SELECT *,
[dbo].[Product](T.VALUE)
OVER (PARTITION BY PAR_COLUMN) AS P
FROM @TEST AS T;
Tapi, menjalankan produk menggunakan OVER (PARTITION BY ... ORDER BY ...)
tidak berfungsi (diperiksa dengan SQL Server 2014 Express 12.0.2000.8):
SELECT *,
[dbo].[Product](T.VALUE)
OVER (PARTITION BY T.PAR_COLUMN ORDER BY T.PERIOD
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS CUM_MUL
FROM @TEST AS T;
Penelusuran menemukan menghubungkan item ini , yang ditutup sebagai "Tidak Akan Diperbaiki" dan pertanyaan .
Kode C#:
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.IO;
using System.Collections.Generic;
using System.Text;
namespace RunningProduct
{
[Serializable]
[SqlUserDefinedAggregate(
Format.UserDefined,
MaxByteSize = 17,
IsInvariantToNulls = true,
IsInvariantToDuplicates = false,
IsInvariantToOrder = true,
IsNullIfEmpty = true)]
public struct Product : IBinarySerialize
{
private bool m_bIsNull; // 1 byte storage
private decimal m_Product; // 16 bytes storage
public void Init()
{
this.m_bIsNull = true;
this.m_Product = 1;
}
public void Accumulate(
[SqlFacet(Precision = 22, Scale = 6)] SqlDecimal ParamValue)
{
if (ParamValue.IsNull) return;
this.m_bIsNull = false;
this.m_Product *= ParamValue.Value;
}
public void Merge(Product other)
{
SqlDecimal otherValue = other.Terminate();
this.Accumulate(otherValue);
}
[return: SqlFacet(Precision = 22, Scale = 6)]
public SqlDecimal Terminate()
{
if (m_bIsNull)
{
return SqlDecimal.Null;
}
else
{
return m_Product;
}
}
public void Read(BinaryReader r)
{
this.m_bIsNull = r.ReadBoolean();
this.m_Product = r.ReadDecimal();
}
public void Write(BinaryWriter w)
{
w.Write(this.m_bIsNull);
w.Write(this.m_Product);
}
}
}
Instal rakitan CLR:
-- Turn advanced options on
EXEC sys.sp_configure @configname = 'show advanced options', @configvalue = 1 ;
GO
RECONFIGURE WITH OVERRIDE ;
GO
-- Enable CLR
EXEC sys.sp_configure @configname = 'clr enabled', @configvalue = 1 ;
GO
RECONFIGURE WITH OVERRIDE ;
GO
CREATE ASSEMBLY [RunningProduct]
AUTHORIZATION [dbo]
FROM 'C:\RunningProduct\RunningProduct.dll'
WITH PERMISSION_SET = SAFE;
GO
CREATE AGGREGATE [dbo].[Product](@ParamValue numeric(22,6))
RETURNS numeric(22,6)
EXTERNAL NAME [RunningProduct].[RunningProduct.Product];
GO
pertanyaan ini membahas perhitungan SUM yang berjalan dengan sangat detail dan Paul White menunjukkan dalam jawabannya cara menulis fungsi CLR yang menghitung menjalankan SUM secara efisien. Ini akan menjadi awal yang baik untuk menulis fungsi yang menghitung Produk yang sedang berjalan.
Perhatikan, bahwa ia menggunakan pendekatan yang berbeda. Alih-alih membuat agregat custom khusus fungsi, Paul membuat fungsi yang mengembalikan tabel. Fungsi membaca data asli ke dalam memori dan melakukan semua perhitungan yang diperlukan.
Mungkin lebih mudah untuk mencapai efek yang diinginkan dengan menerapkan perhitungan ini di sisi klien Anda menggunakan bahasa pemrograman pilihan Anda. Cukup baca seluruh tabel dan hitung produk yang berjalan di klien. Membuat fungsi CLR masuk akal jika produk yang berjalan yang dihitung di server merupakan langkah perantara dalam perhitungan yang lebih kompleks yang akan menggabungkan data lebih lanjut.
Satu lagi ide yang muncul di pikiran.
Temukan perpustakaan matematika .NET pihak ketiga yang menawarkan Log
dan Exp
fungsi dengan presisi tinggi. Buat versi CLR dari skalar ini fungsi. Dan kemudian gunakan EXP + LOG + SUM() Over (Order by)
pendekatan, di mana SUM
adalah fungsi T-SQL bawaan, yang mendukung Over (Order by)
dan Exp
dan Log
adalah fungsi CLR khusus yang mengembalikan bukan float
, tetapi decimal
presisi tinggi .
Perhatikan, bahwa perhitungan presisi tinggi mungkin juga lambat. Dan menggunakan fungsi skalar CLR dalam kueri juga dapat membuatnya lambat.