Sqlserver
 sql >> Teknologi Basis Data >  >> RDS >> Sqlserver

Masalah pembulatan dalam fungsi LOG dan EXP

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.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Buat Hubungan di SQL Server 2017

  2. Dapatkan daftar dengan nilai awal dan akhir dari tabel datetimes

  3. Waktu format Eropa - Konversi string ke Datetime dalam SQL

  4. ClassNotFoundException - com.microsoft.jdbc.sqlserver.SQLServerDriver

  5. C#/SQL :backup dan restore dengan menyalin dan mengganti file database?