Microsoft Access 掲示板

クエリを使って条件に当てはまる日付を抽出したい

10 コメント
views
4 フォロー

Accessで曜日、週、月などの条件で繰り返しを設定できるリマインダー機能を作成しようとしています。
指定の条件で日付を抽出するためのテーブルとクエリの構成で行き詰まってしまい、質問させていただきます。

こちらではじめてお世話になります。Accessのバージョンは2013,2016,office365です。
サンプルとしてできるだけ問題部分を単純にしたテーブルとクエリを作成しました。

テーブル構成

Tリマインダー

Tくり返し条件

T日付

※テーブルのフィールドと内容について画像を添付します。
画像1
 

作成したクエリ

Qクロス結合 … 「Tリマインダー」と「T日付」をクロスさせて、式で検索用フィールドを追加したクエリ

SELECT TリマインダーID.[リマインダーID], T日付.日付, Weekday([日付]) AS 曜日番号, (Day([日付])+6)\7 AS 週番号
FROM T日付, TリマインダーID;

Q抽出結果 … 「Qクロス結合」からリマインダーIDごとに条件に合う日付を抽出するクエリ

SELECT Qクロス結合.[リマインダーID], Qクロス結合.日付, Qクロス結合.曜日番号, Qクロス結合.週番号
FROM Qクロス結合
WHERE (((Qクロス結合.曜日番号) In (select 曜日番号 from T条件 where T条件.リマインダーID = Qクロス結合.リマインダーID)) AND ((Qクロス結合.週番号) In (select 週番号 from T条件 where T条件.リマインダーID = Qクロス結合.リマインダーID)))
ORDER BY Qクロス結合.[リマインダーID], Qクロス結合.日付;

結果

このクエリでは、リマインダーIDの1と2の条件については希望の結果が得られました。
しかし、リマインダーIDの3の条件のときは週番号の指定がないため何も抽出されませんでした。

質問したい内容

①「Q抽出結果」の各フィールドのwhere条件のIn句の中に抽出する条件が無いときに(または0のみのとき)
そのフィールドでのフィルタリングをせず、全件抽出するようにしたいのですが、どのような方法が考えられますか。

②Tリマインダーにあるような任意の条件で日付の抽出できるよりシンプルまたは負荷の少ない方法はありますか。
あるとすればどのような方法か教えて下さい。

(実際に組み込む際は曜日と週以外にも日付、月など同様の方法で条件を増やすことを想定しています。)

以上、やや個別的な内容になり申し訳有りませんが、ご教授いただきたくよろしくおねがいします。

名前なし
作成: 2019/10/25 (金) 16:34:03
通報 ...
1
rookjim 2019/10/25 (金) 20:47:54

質問者です。
テーブルの名前は
Tくり返し条件× → T条件○
でした。
大変失礼致しました。
よろしくお願い致します。

2

質問内容 1, 2 両方ともに関連することですが
Tくり返し条件 テーブルの
データ構成に問題があるでしょう。

一意のリマインダーIDを構成する要素は
  週番号 + 曜日番号
なのですから
週番号だけ とか、曜日番号だけ という
どちらか1つだけを表現した行を作ってはならないのです。

rookjimさんの例で申し上げるなら
リマインダーID:3 の「 毎週月曜と金曜 」というのは

  ・ 週番号は 1 または 2 または 3 または 4 または 5
 且つ
  ・ 曜日番号は 2 または 6

であるのに
週番号の入力値を 0 という非該当の数字で表現していることが
SQL文が複雑になり、思ったように抽出できない原因になっています。
 
T条件

条件IDリマインダーID曜日番号週番号
1172
2174
3213
4321
5322
6323
7324
8325
9361
10362
11363
12364
13365

 
このように
複数の条件( 週 + 曜日 )を全て網羅するようなデータ構成に変更すると
SQL文は以下のように簡潔に記述できて
抽出漏れも解消できます。

SELECT x.リマインダーID
     , y.日付
     , x.曜日番号
     , x.週番号
FROM T条件 x
   , T日付 y
WHERE x.曜日番号 = Weekday( y.日付 )
  AND x.週番号   = ( Day( y.日付 ) + 6 ) \ 7
ORDER BY y.日付
       , x.リマインダーID ;
4
rookjim 2019/10/26 (土) 22:20:07

mayuさん
つたない質問内容を読み取っていただき、丁寧な回答いただきありがとうございます。

ご指摘いただき、T条件のテーブルの記述の仕方が、望む抽出結果を得るために適さない形式であることがわかりました。

T条件のテーブルが1レコードごとに1条件の構成になっている理由は、ユーザーがくり返し条件を入力するためのフォームを下のように作成したためです。

画像1

ご回答いただいたように複数の条件を全て網羅するようにテーブル構成を変更すると、
「月」、「日付」等の並列する条件を追加した際に条件テーブルが長大なものになってしまうため➀については現在のテーブル構成のまま目的の抽出結果を得られる方法を求めております。引き続きご教授いただけますと幸いです。

または
・条件を網羅したテーブルのデータが作成できるようにフォームを再設計する
・T条件のデータを複数の条件を全て網羅するように変換するようクエリを作成する
等で解決できるかと思うのですが、こちらについても具体的な方法をアドバイスいただけますと幸いです。

検討いただき大変有り難く思っております。大変わがままな質問で恐縮ですが
何卒宜しくお願いします。

5
mayu 2019/10/27 (日) 08:15:03 修正 76b28@61ad5

> ご回答いただいたように複数の条件を全て網羅するようにテーブル構成を変更すると、
> 「月」、「日付」等の並列する条件を追加した際に
> 条件テーブルが長大なものになってしまうため

 
「月」や「日付」の条件をテーブルに追加して長大になるというのは
意味がわかりかねます。
前回の回答で、私はフィールド構成の変更を提案したわけではなく
データの持ち方を変更するよう、示唆したに過ぎません。

したがって、ご自身が考える「 長大にならない 」設計方針と
「月」や「日付」をどのような構成でデータ登録し、
週次や曜日毎に実施するデータと同居させる予定なのか
また、どのような SQL で抽出を行うつもりでいるのか、構想をお聞かせ願えますか。

> [1]については現在のテーブル構成のまま目的の抽出結果を得られる方法を求めております。

 
現状の構成は、以下の問題点が明白ですので

 ・ 第三者から見て、単一の行からテーブルのデータ構造が把握できない
 ・ 相関サブクエリというのは、莫大な演算回数が発生するパフォーマンスの悪い SQL である
 ・ 複雑なサブクエリを複数回発行しても、思ったようにデータが抽出できていない
 ・ 演算フィールドを左辺に指定して抽出条件を指定しているため、インデックスが効かない

運用も構築も この先 地獄を見る覚悟がおありということでしたら
一応、ご希望の回答はしておきます。

なお、週番号だけを指定するリマインダーが あるのかどうか不明ですが
とりあえず、どちらでもいいように冗長な SQL にしておきます。
以下の結果を ユニオンクエリで rookjimさん記載の SQL と結合すればいいでしょう。

SELECT x.リマインダーID
     , z.日付
     , y.曜日番号
     , y.週番号
FROM 
(
    SELECT リマインダーID
      FROM T条件
    GROUP BY リマインダーID
    HAVING Sum( 週番号 )   = 0
        OR Sum( 曜日番号 ) = 0 
)
         x
, T条件  y
, T日付  z
WHERE x.リマインダーID = y.リマインダーID
  AND ( 
         y.曜日番号 = Weekday( z.日付 )
             OR
         y.週番号   = ( Day( z.日付 ) + 6 ) \ 7
      )
ORDER BY 1, 2 ;

 

> または
> ・条件を網羅したテーブルのデータが作成できるようにフォームを再設計する
> ・T条件のデータを複数の条件を全て網羅するように変換するようクエリを作成する
> 等で解決できるかと思うのですが、
> こちらについても具体的な方法をアドバイスいただけますと幸いです。

 
具体的なアドバイスをご希望なら、まずは データベースソフトの扱いにおいて
rookjimさんの方針や考え方は、順番や方向性が真逆である
ということを理解いただかない限り、不毛なやりとりになるでしょう。

> ・条件を網羅したテーブルのデータが作成できるようにフォームを再設計する

のではなく、構築手順は

  1. 条件を網羅した適切な構造のテーブルを設計する
  2. パフォーマンスや見通しの良い SQL を考える
  3. VBAの利用やコントロールの配置は必要最小限に抑え、
    シンプルで保守性の高いインターフェース( フォーム・レポート )の構築を目指す

になります。

添付画像における リマインダーID: 1 の内容においても
発生条件が「 第2、第4土曜日 」ですから
フォーム上で [ 指定日 ] [ 日 ] といった項目を ユーザが指定する必要は無いように思いますが
代替値を入力しない限り、テーブル上でそれらの項目は Null になります。
これらの項目を SQLでどう扱うか、見通しが立っていないのでしたら
テーブルの設計から見直す必要があり、当然、フォームも改変を余儀なくされます。

まずは、テーブル設計からやり直す気が有るのか否か、意思表示をお願いします。

6
rookjim 2019/10/27 (日) 22:11:41

mayuさん

大変に熱のこもったご回答をいただきありがとうございます。

ひとまず、中段にご提示いただいた、元のクエリで抽出されないレコードを集計クエリで別途抽出しユニオンクエリで結合するという考え方を参考にさせていただき、
実際の動作を検証してみたいと思います。

また、独学なため実例を見る機会があまりなく、テーブル設計含めより適した方法を引き続き検討していきたいと思いますので、機会がありましたらぜひよろしくお願い致します。以上、ありがとうございました。

7

> テーブル設計含めより適した方法を引き続き検討していきたいと思いますので、
> 機会がありましたらぜひよろしくお願い致します。

テーブルの構造を変更する場合のサンプルを以下に載せておきます。

まず、リマインダーは

  ・ 日 単位で発生するもの
  ・ それ以外のもの( 週・曜日 )

の2種に大別できるでしょう。
( 両立しない条件のため )

よって、リマインダの定義テーブルに
種別を管理するフィールドが必要になります。( ※1 )

次に、リマインダーの発生周期は
月、週、曜日、日の組み合わせによることから
条件テーブルに、これらの値を格納するフィールドが必要 ( ※2 )

条件テーブルへデータを登録するにあたり、
行数が増えることに漠然とした不安を感じておられるようなので
行数を抑制できるフィールド構成にします ( ※3 )

特定の日に1度きりで実施されるリマインダと
特定の周期で何度も実施されるリマインダは
入力項目を共通化して統一し、Nullや意味不明の値は許可しない ( ※4 )

※1 ~ ※4 を網羅したテーブル定義

CREATE TABLE T_リマインダー
(
      リマインダーID   INT            NOT NULL
    , リマインダー内容 VARCHAR( 100 ) NOT NULL
    , リマインダー種別 SMALLINT       NOT NULL
    , 開始年           SMALLINT       NOT NULL
    , 終了年           SMALLINT       NOT NULL
    , CONSTRAINT pk_reminder PRIMARY KEY ( リマインダーID )
);

 

CREATE TABLE T_条件
(
      条件ID          AUTOINCREMENT PRIMARY KEY
    , リマインダーID  INT           NOT NULL
    , 対象月_from     SMALLINT      NOT NULL
    , 対象月_to       SMALLINT      NOT NULL
    , 週番号_from     SMALLINT      NOT NULL
    , 週番号_to       SMALLINT      NOT NULL
    , 曜日番号_from   SMALLINT      NOT NULL
    , 曜日番号_to     SMALLINT      NOT NULL
    , 日番号          SMALLINT      NOT NULL
);

 

CREATE TABLE T_日付
(
      日付 DATETIME NOT NULL PRIMARY KEY 
);
8

※ 続き

データ例

< T_リマインダー >

ID内容種別開始年終了年
1第2、第4土曜日220182027
2第3日曜日220172026
3毎週 月曜日と金曜日220112050
4毎月10日と月末日120112025
53月と4月の第1、第2月曜日220132030
6即位の礼120192019

 
リマインダーの種別において
 ・ 日 単位で発生するもの    は 1 を入力
 ・ それ以外のもの ( 週・曜日 ) は 2 を入力
というルールにします

 
< T_条件 >

F1 ---> 条件ID      F5 ---> 週番号_from
F2 ---> リマインダーID  F6 ---> 週番号_to
F3 ---> 対象月_from    F7 ---> 曜日番号_from
F4 ---> 対象月_to     F8 ---> 曜日番号_to
              F9 ---> 日番号

F1F2F3F4F5F6F7F8F9
1111222770
2111244770
3211233110
4311215220
5311215660
64112151710
7411215170
853412220
961010443322
 
※1
掲示板の文字数制限上、F1 や F2 という、表のフィールド名は適宜 読み替えて下さい。

※2
月、週、曜日において
連続した値( 毎月、毎週、火~金、4月~6月 など )は
連続した範囲として1行で表現し、
連続しない場合は行を分け、データを個別に登録します。
この部分が、データ登録における一番の難関だと思いますが
データサンプルをよくご覧になれば、構造を理解できるでしょう。

※3
日単位で発生するリマインダにおいて
日番号フィールドに入力する値は、月末は 0
                それ以外は 日の数字
※4
曜日や週単位で発生するリマインダにおいて
日番号フィールドに入力する値は 0

9

※ 続き

SELECT x.リマインダーID
     , z.日付
     , Weekday( z.日付 )         As 曜日番号
     , ( Day( z.日付 ) + 6 ) \ 7 As 週番号
FROM T_リマインダー  x
   , T_条件          y
   , T_日付          z
WHERE x.リマインダーID  =  y.リマインダーID
  AND x.開始年         <=  Year( z.日付 )
  AND x.終了年         >=  Year( z.日付 )
  AND y.対象月_from    <=  Month( z.日付 ) 
  AND y.対象月_to      >=  Month( z.日付 )
  AND y.週番号_from    <=  ( Day( z.日付 ) + 6 ) \ 7
  AND y.週番号_to      >=  ( Day( z.日付 ) + 6 ) \ 7
  AND y.曜日番号_from  <=  Weekday( z.日付 )
  AND y.曜日番号_to    >=  Weekday( z.日付 )
  AND (
           ( x.リマインダー種別 = 2 AND y.日番号 = 0 )
        OR
           ( x.リマインダー種別 = 1 
               AND
             z.日付 = 
                 DateSerial( Year( z.日付 - y.日番号 ), 
                             Month( z.日付 - y.日番号 ) + 1, y.日番号 ) 
           )
      )
ORDER BY 1, 2 ;

結果 ( 一部抜粋 )

リマインダーID日付曜日番号週番号
12019/01/1272
12019/01/2674
22019/01/2013
22019/02/1713
32019/01/0461
32019/01/0721
42019/01/1052
42019/01/3155
42019/02/1012
42019/02/2854
52019/03/0421
52019/03/1122
52019/04/0121
52019/04/0822
62019/10/2234
10
rookjim 2019/10/28 (月) 12:17:26

mayuさん

テーブル設計を見直したサンプルのご提示ありがとうございます。
素朴な質問のつもりでしたが、ここまで懇切丁寧に回答いただき、
情報量にただただ圧倒される次第です。

また、その前段にてmayuさんよりご提示いただいた、
SUM集計関数を用いて元のクエリで抽出できなかったレコードを一括で抽出し
別途対処するというアイディアを参考にして下のクエリを作成しました。
こちらで一応望む結果を得ることができましたので、あまりスマートな方法では
ないと自覚していますが、ひとまずこの質問は解決にしようと思います。

SELECT 
 Qクロス結合.[リマインダーID],  Qクロス結合.[リマインダー内容],  Qクロス結合.日付,  Qクロス結合.曜日, 
 IIf( [曜日番号合計] = 0, 0, [曜日番号] ) AS 曜日条件, 
 IIf( [週番号合計] = 0, 0, [週番号] ) AS 週条件 
FROM 
 Qクロス結合  LEFT JOIN ( SELECT  [リマインダーID],  sum(曜日番号) AS 曜日番号合計,  sum(週番号) AS 週番号合計  FROM  T条件  GROUP BY  [リマインダーID] ) AS 番号合計 ON Qクロス結合.[リマインダーID] = 番号合計.[リマインダーID] 
WHERE 
 ( ( ( IIf( [曜日番号合計] = 0, 0, [曜日番号] ) ) In ( select  曜日番号  from  T条件  where  T条件.リマインダーID = Qクロス結合.リマインダーID ) ) 
 AND ( ( IIf( [週番号合計] = 0, 0, [週番号] ) ) In ( select  週番号  from  T条件  where  T条件.リマインダーID = Qクロス結合.リマインダーID ) ) ) 
ORDER BY 
 Qクロス結合.[リマインダーID], 
 Qクロス結合.日付;

以上、大変参考になるご回答数々いただき感謝致します。ありがとうございました。