Redash 上で桁数が多い数値を表示すると発生する誤差について調べた

久しぶりの記事もやはり Redash 関連でした。

結論を先に(個人の感想・意見です)

  • Redash の UI 上で 9007199254740991 を超える数値を扱いたい場合は、精度が下がることを許容するか文字列として扱う
  • これは JavaScript の仕様によるものなので、当面は Redash のバージョンアップだけで対応される可能性は低い
  • Redash についての質問はぜひ Redash 日本語フォーラム で!

きっかけ

このツイートを目にしたことがきっかけでした。

いったん「redash じゃなくて Redash」については気にしないことにしましょう。

以前も似たようなツイートを見た覚えがあるのですが、ちょっと気になったので調べてみたくなりました。

再現してみる

環境について

データソースは SQLite 以外に MySQL 5.7 や PostgreSQL 9 でも確認し、同様の現象が発生することを確認していますが、この記事では手元の環境で検証したいので SQLite を使用して検証をした結果について書いていきます。

また、Redash についても 7.0.0, 8.0.0 で同様の現象が発生していることを確認していますが、こちらも手元の環境に合わせて 9.0.0-beta を使用しています。

再現手順

実行したクエリーは以下です。

select 12345678901234567890 foo;

クエリの実行結果を見ると、下3桁が違っていることに気づきます。

f:id:ariarijp:20201124222325p:plain

17桁にしてみても、まだ誤差がある。

select 12345678901234567 foo;

f:id:ariarijp:20201124222652p:plain

16桁にしてみると、どうやら問題なさそう。

select 1234567890123456 foo;

f:id:ariarijp:20201124223049p:plain

ツイートされていた現象は17桁以上の数値を扱おうとすると遭遇するようです。

原因を考える

どこで誤差が発生しているのかを考えて、ざっくり以下のようにわけてみました。

  • Redash のフロントエンド(HTML / JavaScript)
  • Redash のサーバーサイド/API(Python)
  • Redash のワーカー(Python)
  • データソース(接続先による)

ここまでの調査で以下の仮説があったので、フロント寄りをみていくことにします。

  • 複数のデータソースで現象を確認しているので、データソースよりもフロント寄りで起きていそう
  • 大きな値ではあるが、Python で扱えない桁数ではなさそう
    • (17桁でも起きるので Python の int の最大値 2**63 - 1 は関係なさそう)

調査してみる

調査については以下のクエリーを使っていきます。

select 12345678901234567 foo;

サーバーサイドをみる

Redash のサーバーサイドの処理はほとんどが REST(RESTish?) API になっているため、クエリーの結果も API が返します。まずはそのレスポンスをみてみました。

確認手順の詳細は割愛しますが、以下が API から取得できる JSON です。

{
    "query_result": {
        "id": 18,
        "query_hash": "13f6db1bd3a5470f7d0641d1a7a380eb",
        "query": "select 12345678901234567 foo;\n",
        "data": {
            "columns": [
                {
                    "name": "foo",
                    "friendly_name": "foo",
                    "type": "integer"
                }
            ],
            "rows": [
                {
                    "foo": 12345678901234567
                }
            ]
        },
        "data_source_id": 1,
        "runtime": 0.00493335723876953,
        "retrieved_at": "2020-11-24T13:44:31.532Z"
    }
}

API12345678901234567 という値を返してくれているようなので、API より手前側で何かが起きているというのは正しそうです。

フロントエンドをみる

ふと、12345678901234567JavaScript は数値として扱えるのかなと思ったので、開発者ツールのコンソールで試してみました。

f:id:ariarijp:20201124225000p:plain

入力に対し、結果として表示される値が変わっていますね。このあたりにヒントがありそうです。

もう少し調べてみる

MDN を参照してみたところ「Number の整数の範囲」という項目がありました。おそらくこれが原因を表しているのだと思います。

developer.mozilla.org

JavaScript で扱えるの最大値は 9007199254740991 ということと、JSON9007199254740991 を超える値をデシリアライズすると信頼できない値になることについて触れられているので、試してみましょう。

select
    9007199254740991 foo
    , 9007199254740992 bar
    , 9007199254740999 baz;

f:id:ariarijp:20201124230233p:plain

9007199254740992 にしても結果は変わらないようにみえますが、9007199254740999 は結果が 9007199254741000 となっていて 1 多いので、信頼できないといえそうです。

JavaScript の Number の最大値から離れれば離れるほど、かつ、Python の int の最大値を超えない範囲ではこの「信頼できない数値」問題が発生すると考えます。

対策

冒頭に結論を書きましたが、数値としてそのまま扱うことは今のところ難しく、MDN にも以下のような記載があります。

可能な回避策として、代わりに String を使用してください。

大きい数値は BigInt 型を用いて表すことができます。

文字列として扱うというのは「あるある」だと思いますが、Visualization で使う際にはどこかで数値にキャストされているようで、誤差のある状態の数値となってしまいます。

ある程度は誤差を許容して使うか、金額であれば千円や百万円単位にするなど、桁数をクエリ側で調整するなどの工夫も考えられます。

f:id:ariarijp:20201124231242p:plain

BigInt については、どうやら期待を満たしてくれそうではありますが、これは Redash 単体だけでは解決しない可能性が高く、すぐに解決するようなことではないと思いました。

f:id:ariarijp:20201124231554p:plain

まとめ

ふと目にしたツイートきっかけで、意外な事実を知ることができました。

Redash は 17桁を超えるような数値を扱うような環境でも使われている(?)ということに、Redash ファンとして嬉しさも感じつつ、調査を進められた気がします。

私は趣味で Redash エゴサをしていますが、なにか質問などあれば、ぜひフォーラムへ投稿をおねがいします。

Redash のフォーラムには日本語チャンネル(英語以外の言語チャンネルは日本語だけ!)があるので、日本語で質問できますし、私も知っている限りの範囲になりますが、フォーラムで質問に回答しています。

discuss.redash.io

Happy Querying!